Add Strike quote endpoint for real sats price with fee-adjusted amount
Use POST /v1/currency-exchange-quotes to get actual buy price including spread, matching the Strike app. Fee is cached daily to calculate the pre-fee amount from the configured total (default 55 EUR). Store quote_sats in the strike_price DB table. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE strike_price DROP COLUMN quote_sats;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE strike_price ADD COLUMN quote_sats BIGINT;
|
||||||
@@ -45,7 +45,7 @@ SELECT ts FROM kraken_day ORDER BY ts DESC LIMIT 1
|
|||||||
|
|
||||||
-- :name insert-strike-price! :! :n
|
-- :name insert-strike-price! :! :n
|
||||||
-- :doc Insert a Strike ticker snapshot
|
-- :doc Insert a Strike ticker snapshot
|
||||||
INSERT INTO strike_price (rates) VALUES (:rates)
|
INSERT INTO strike_price (rates, quote_sats) VALUES (:rates, :quote-sats)
|
||||||
|
|
||||||
-- :name get-latest-strike-price :? :1
|
-- :name get-latest-strike-price :? :1
|
||||||
-- :doc Get the most recent Strike ticker snapshot
|
-- :doc Get the most recent Strike ticker snapshot
|
||||||
|
|||||||
@@ -65,4 +65,5 @@
|
|||||||
|
|
||||||
:strike/ticker
|
:strike/ticker
|
||||||
{:query-fn #ig/ref :db.sql/query-fn
|
{:query-fn #ig/ref :db.sql/query-fn
|
||||||
:api-key #env STRIKE_API_KEY}}
|
:api-key #env STRIKE_API_KEY
|
||||||
|
:sats-eur-amount #or [#env SATS_EUR_AMOUNT "55"]}}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
(:import
|
(:import
|
||||||
[java.net URI]
|
[java.net URI]
|
||||||
[java.net.http HttpClient HttpRequest HttpResponse$BodyHandlers]
|
[java.net.http HttpClient HttpRequest HttpResponse$BodyHandlers]
|
||||||
|
[java.time Duration Instant]
|
||||||
[org.postgresql.util PGobject]))
|
[org.postgresql.util PGobject]))
|
||||||
|
|
||||||
(defn- ->jsonb
|
(defn- ->jsonb
|
||||||
@@ -29,6 +30,58 @@
|
|||||||
body (.body resp)]
|
body (.body resp)]
|
||||||
body))
|
body))
|
||||||
|
|
||||||
|
(defn- post-quote
|
||||||
|
"POST /v1/currency-exchange-quotes. Returns parsed response map or nil."
|
||||||
|
[^HttpClient client ^String api-key ^String eur-amount]
|
||||||
|
(let [body-str (json/write-str {:sell "EUR" :buy "BTC"
|
||||||
|
:amount {:amount eur-amount :currency "EUR"}})
|
||||||
|
request (-> (HttpRequest/newBuilder)
|
||||||
|
(.uri (URI. "https://api.strike.me/v1/currency-exchange-quotes"))
|
||||||
|
(.header "Accept" "application/json")
|
||||||
|
(.header "Content-Type" "application/json")
|
||||||
|
(.header "Authorization" (str "Bearer " api-key))
|
||||||
|
(.POST (java.net.http.HttpRequest$BodyPublishers/ofString body-str))
|
||||||
|
(.build))
|
||||||
|
resp (.send client request (HttpResponse$BodyHandlers/ofString))
|
||||||
|
status (.statusCode resp)
|
||||||
|
body (.body resp)]
|
||||||
|
(if (= 200 status)
|
||||||
|
(json/read-str body :key-fn keyword)
|
||||||
|
(do (log/warn "Strike quote HTTP" status body)
|
||||||
|
nil))))
|
||||||
|
|
||||||
|
(def ^:private fee-max-age (Duration/ofHours 24))
|
||||||
|
|
||||||
|
(defn- refresh-fee!
|
||||||
|
"Fetch fee for sats-eur-amount and cache it. Returns the fee BigDecimal or nil."
|
||||||
|
[client api-key sats-eur-amount fee-cache]
|
||||||
|
(when-let [parsed (post-quote client api-key sats-eur-amount)]
|
||||||
|
(let [fee (bigdec (get-in parsed [:fee :amount]))]
|
||||||
|
(log/info "Strike fee for" sats-eur-amount "EUR:" fee "EUR")
|
||||||
|
(reset! fee-cache {:fee fee :fetched-at (Instant/now)})
|
||||||
|
fee)))
|
||||||
|
|
||||||
|
(defn- cached-fee
|
||||||
|
"Return cached fee if fresh, otherwise refresh. Returns BigDecimal or nil."
|
||||||
|
[client api-key sats-eur-amount fee-cache]
|
||||||
|
(let [{:keys [fee fetched-at]} @fee-cache]
|
||||||
|
(if (and fee fetched-at
|
||||||
|
(.isBefore (Instant/now) (.plus ^Instant fetched-at fee-max-age)))
|
||||||
|
fee
|
||||||
|
(refresh-fee! client api-key sats-eur-amount fee-cache))))
|
||||||
|
|
||||||
|
(defn- fetch-quote-sats
|
||||||
|
"Get sats for a total EUR amount (including fee). Uses cached fee to calculate
|
||||||
|
pre-fee amount, then quotes that. Returns sats as long or nil."
|
||||||
|
[client api-key sats-eur-amount fee-cache]
|
||||||
|
(let [total (bigdec sats-eur-amount)
|
||||||
|
fee (cached-fee client api-key sats-eur-amount fee-cache)
|
||||||
|
pre-fee (if fee (.toPlainString (.subtract total fee)) sats-eur-amount)]
|
||||||
|
(when-let [parsed (post-quote client api-key pre-fee)]
|
||||||
|
(let [btc-amt (get-in parsed [:target :amount])]
|
||||||
|
(when btc-amt
|
||||||
|
(long (* (bigdec btc-amt) 100000000)))))))
|
||||||
|
|
||||||
(defn- format-rates
|
(defn- format-rates
|
||||||
"Build a compact log string from the parsed rates array."
|
"Build a compact log string from the parsed rates array."
|
||||||
[rates]
|
[rates]
|
||||||
@@ -47,36 +100,49 @@
|
|||||||
|
|
||||||
(defn- poll!
|
(defn- poll!
|
||||||
"Fetch rates and insert into the database. Updates latest-atom."
|
"Fetch rates and insert into the database. Updates latest-atom."
|
||||||
[client api-key query-fn latest-atom]
|
[client api-key query-fn latest-atom sats-eur-amount fee-cache]
|
||||||
(let [body (fetch-rates client api-key)
|
(let [body (fetch-rates client api-key)
|
||||||
rates (json/read-str body :key-fn keyword)]
|
rates (json/read-str body :key-fn keyword)
|
||||||
(query-fn :insert-strike-price! {:rates (->jsonb body)})
|
rmap (rates->map rates)
|
||||||
(reset! latest-atom (rates->map rates))
|
quote-sats (when sats-eur-amount
|
||||||
|
(try
|
||||||
|
(let [sats (fetch-quote-sats client api-key sats-eur-amount fee-cache)]
|
||||||
|
(when sats
|
||||||
|
(log/info "Strike quote:" sats-eur-amount "EUR ->" sats "sats"))
|
||||||
|
sats)
|
||||||
|
(catch Exception e
|
||||||
|
(log/error e "Strike quote fetch failed")
|
||||||
|
nil)))
|
||||||
|
rmap (if quote-sats (assoc rmap "quote-sats" quote-sats) rmap)]
|
||||||
|
(query-fn :insert-strike-price! {:rates (->jsonb body)
|
||||||
|
:quote-sats quote-sats})
|
||||||
|
(reset! latest-atom rmap)
|
||||||
(log/info "Strike:" (format-rates rates))))
|
(log/info "Strike:" (format-rates rates))))
|
||||||
|
|
||||||
(defn- start-poll-loop!
|
(defn- start-poll-loop!
|
||||||
"Fetch immediately, then poll every 10 seconds."
|
"Fetch immediately, then poll every 10 seconds."
|
||||||
[client api-key query-fn running? latest-atom]
|
[client api-key query-fn running? latest-atom sats-eur-amount fee-cache]
|
||||||
(future
|
(future
|
||||||
(try
|
(try
|
||||||
(poll! client api-key query-fn latest-atom)
|
(poll! client api-key query-fn latest-atom sats-eur-amount fee-cache)
|
||||||
(catch Exception e
|
(catch Exception e
|
||||||
(log/error e "Strike initial fetch failed")))
|
(log/error e "Strike initial fetch failed")))
|
||||||
(while @running?
|
(while @running?
|
||||||
(Thread/sleep 10000)
|
(Thread/sleep 10000)
|
||||||
(when @running?
|
(when @running?
|
||||||
(try
|
(try
|
||||||
(poll! client api-key query-fn latest-atom)
|
(poll! client api-key query-fn latest-atom sats-eur-amount fee-cache)
|
||||||
(catch Exception e
|
(catch Exception e
|
||||||
(log/error e "Strike poll failed")))))))
|
(log/error e "Strike poll failed")))))))
|
||||||
|
|
||||||
(defmethod ig/init-key :strike/ticker
|
(defmethod ig/init-key :strike/ticker
|
||||||
[_ {:keys [query-fn api-key]}]
|
[_ {:keys [query-fn api-key sats-eur-amount]}]
|
||||||
(log/info "Starting Strike ticker poller")
|
(log/info "Starting Strike ticker poller")
|
||||||
(let [client (HttpClient/newHttpClient)
|
(let [client (HttpClient/newHttpClient)
|
||||||
running? (atom true)
|
running? (atom true)
|
||||||
latest-rates (atom nil)
|
latest-rates (atom nil)
|
||||||
fut (start-poll-loop! client api-key query-fn running? latest-rates)]
|
fee-cache (atom nil)
|
||||||
|
fut (start-poll-loop! client api-key query-fn running? latest-rates sats-eur-amount fee-cache)]
|
||||||
{:running? running?
|
{:running? running?
|
||||||
:future fut
|
:future fut
|
||||||
:latest-rates latest-rates}))
|
:latest-rates latest-rates}))
|
||||||
|
|||||||
Reference in New Issue
Block a user