Persist Frankfurter EUR/DKK rates in currencies table
Store daily exchange rates in PostgreSQL with EUR/DKK cross rate. Seed rates atom from DB on startup so WebSocket works immediately even if Frankfurter is unreachable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
DROP TABLE currencies;
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE currencies (
|
||||
rate_date DATE PRIMARY KEY,
|
||||
eur NUMERIC(12,6) NOT NULL,
|
||||
dkk NUMERIC(12,6) NOT NULL,
|
||||
eur_dkk NUMERIC(12,6) NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
@@ -50,3 +50,14 @@ INSERT INTO strike_price (rates, quote_sats) VALUES (:rates, :quote-sats)
|
||||
-- :name get-latest-strike-price :? :1
|
||||
-- :doc Get the most recent Strike ticker snapshot
|
||||
SELECT rates, recorded_at FROM strike_price ORDER BY id DESC LIMIT 1
|
||||
|
||||
-- :name upsert-currency-rates! :! :n
|
||||
-- :doc Upsert daily currency rates from Frankfurter
|
||||
INSERT INTO currencies (rate_date, eur, dkk, eur_dkk, updated_at)
|
||||
VALUES (:rate-date, :eur, :dkk, :eur-dkk, NOW())
|
||||
ON CONFLICT (rate_date) DO UPDATE
|
||||
SET eur = EXCLUDED.eur, dkk = EXCLUDED.dkk, eur_dkk = EXCLUDED.eur_dkk, updated_at = NOW()
|
||||
|
||||
-- :name get-latest-currency-rates :? :1
|
||||
-- :doc Get the most recent currency rates
|
||||
SELECT eur, dkk, eur_dkk, updated_at FROM currencies ORDER BY rate_date DESC LIMIT 1
|
||||
|
||||
@@ -61,7 +61,8 @@
|
||||
{:query-fn #ig/ref :db.sql/query-fn}
|
||||
|
||||
:frankfurter/rates
|
||||
{:url #or [#env FRANKFURTER_URL "http://localhost:8080"]}
|
||||
{:url #or [#env FRANKFURTER_URL "http://localhost:8080"]
|
||||
:query-fn #ig/ref :db.sql/query-fn}
|
||||
|
||||
:strike/ticker
|
||||
{:query-fn #ig/ref :db.sql/query-fn
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
(:import
|
||||
[java.net URI]
|
||||
[java.net.http HttpClient HttpRequest HttpResponse$BodyHandlers]
|
||||
[java.time Instant]))
|
||||
[java.time Instant LocalDate]))
|
||||
|
||||
(defn- fetch-rates
|
||||
"GET /latest?from=USD&to=EUR,DKK from Frankfurter. Returns {:EUR x :DKK y}."
|
||||
@@ -22,23 +22,31 @@
|
||||
(:rates body)))
|
||||
|
||||
(defn- poll!
|
||||
"Fetch rates and update the atom."
|
||||
[client base-url rates-atom]
|
||||
"Fetch rates, update the atom, and persist to DB."
|
||||
[client base-url rates-atom query-fn]
|
||||
(let [rates (fetch-rates client base-url)]
|
||||
(when (and (:EUR rates) (:DKK rates))
|
||||
(reset! rates-atom
|
||||
{:eur (bigdec (str (:EUR rates)))
|
||||
:dkk (bigdec (str (:DKK rates)))
|
||||
:updated-at (Instant/now)})
|
||||
(log/info "Frankfurter rates — EUR:" (:EUR rates) "DKK:" (:DKK rates)))))
|
||||
(let [eur (bigdec (str (:EUR rates)))
|
||||
dkk (bigdec (str (:DKK rates)))
|
||||
eur-dkk (.divide dkk eur 6 java.math.RoundingMode/HALF_UP)]
|
||||
(reset! rates-atom
|
||||
{:eur eur
|
||||
:dkk dkk
|
||||
:updated-at (Instant/now)})
|
||||
(query-fn :upsert-currency-rates!
|
||||
{:rate-date (LocalDate/now)
|
||||
:eur eur
|
||||
:dkk dkk
|
||||
:eur-dkk eur-dkk})
|
||||
(log/info "Frankfurter rates — EUR:" eur "DKK:" dkk "EUR/DKK:" eur-dkk)))))
|
||||
|
||||
(defn- start-poll-loop!
|
||||
"Fetch immediately, then poll every 60 minutes."
|
||||
[client base-url rates-atom running?]
|
||||
[client base-url rates-atom running? query-fn]
|
||||
(future
|
||||
(loop [retries 12]
|
||||
(let [ok? (try
|
||||
(poll! client base-url rates-atom)
|
||||
(poll! client base-url rates-atom query-fn)
|
||||
true
|
||||
(catch Exception e
|
||||
(log/error e (str "Frankfurter fetch failed, retrying in 10s (" retries " left)"))
|
||||
@@ -50,20 +58,34 @@
|
||||
(Thread/sleep 3600000)
|
||||
(when @running?
|
||||
(try
|
||||
(poll! client base-url rates-atom)
|
||||
(poll! client base-url rates-atom query-fn)
|
||||
(catch Exception e
|
||||
(log/error e "Frankfurter poll failed")))))))
|
||||
|
||||
(defn- seed-from-db!
|
||||
"Load latest rates from DB into the atom so we have data immediately."
|
||||
[rates-atom query-fn]
|
||||
(try
|
||||
(when-let [row (query-fn :get-latest-currency-rates {})]
|
||||
(reset! rates-atom
|
||||
{:eur (:eur row)
|
||||
:dkk (:dkk row)
|
||||
:updated-at (:updated_at row)})
|
||||
(log/info "Seeded Frankfurter rates from DB — EUR:" (:eur row) "DKK:" (:dkk row)))
|
||||
(catch Exception e
|
||||
(log/warn e "Could not seed rates from DB"))))
|
||||
|
||||
(defmethod ig/init-key :frankfurter/rates
|
||||
[_ {:keys [url]}]
|
||||
[_ {:keys [url query-fn]}]
|
||||
(log/info "Starting Frankfurter rates poller:" url)
|
||||
(let [client (HttpClient/newHttpClient)
|
||||
rates (atom nil)
|
||||
running? (atom true)
|
||||
fut (start-poll-loop! client url rates running?)]
|
||||
{:rates rates
|
||||
:running? running?
|
||||
:future fut}))
|
||||
running? (atom true)]
|
||||
(seed-from-db! rates query-fn)
|
||||
(let [fut (start-poll-loop! client url rates running? query-fn)]
|
||||
{:rates rates
|
||||
:running? running?
|
||||
:future fut})))
|
||||
|
||||
(defmethod ig/halt-key! :frankfurter/rates
|
||||
[_ {:keys [running? future]}]
|
||||
|
||||
Reference in New Issue
Block a user