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
|
-- :name get-latest-strike-price :? :1
|
||||||
-- :doc Get the most recent Strike ticker snapshot
|
-- :doc Get the most recent Strike ticker snapshot
|
||||||
SELECT rates, recorded_at FROM strike_price ORDER BY id DESC LIMIT 1
|
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}
|
{:query-fn #ig/ref :db.sql/query-fn}
|
||||||
|
|
||||||
:frankfurter/rates
|
: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
|
:strike/ticker
|
||||||
{:query-fn #ig/ref :db.sql/query-fn
|
{:query-fn #ig/ref :db.sql/query-fn
|
||||||
|
|||||||
@@ -6,7 +6,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 Instant]))
|
[java.time Instant LocalDate]))
|
||||||
|
|
||||||
(defn- fetch-rates
|
(defn- fetch-rates
|
||||||
"GET /latest?from=USD&to=EUR,DKK from Frankfurter. Returns {:EUR x :DKK y}."
|
"GET /latest?from=USD&to=EUR,DKK from Frankfurter. Returns {:EUR x :DKK y}."
|
||||||
@@ -22,23 +22,31 @@
|
|||||||
(:rates body)))
|
(:rates body)))
|
||||||
|
|
||||||
(defn- poll!
|
(defn- poll!
|
||||||
"Fetch rates and update the atom."
|
"Fetch rates, update the atom, and persist to DB."
|
||||||
[client base-url rates-atom]
|
[client base-url rates-atom query-fn]
|
||||||
(let [rates (fetch-rates client base-url)]
|
(let [rates (fetch-rates client base-url)]
|
||||||
(when (and (:EUR rates) (:DKK rates))
|
(when (and (:EUR rates) (: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
|
(reset! rates-atom
|
||||||
{:eur (bigdec (str (:EUR rates)))
|
{:eur eur
|
||||||
:dkk (bigdec (str (:DKK rates)))
|
:dkk dkk
|
||||||
:updated-at (Instant/now)})
|
:updated-at (Instant/now)})
|
||||||
(log/info "Frankfurter rates — EUR:" (:EUR rates) "DKK:" (:DKK rates)))))
|
(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!
|
(defn- start-poll-loop!
|
||||||
"Fetch immediately, then poll every 60 minutes."
|
"Fetch immediately, then poll every 60 minutes."
|
||||||
[client base-url rates-atom running?]
|
[client base-url rates-atom running? query-fn]
|
||||||
(future
|
(future
|
||||||
(loop [retries 12]
|
(loop [retries 12]
|
||||||
(let [ok? (try
|
(let [ok? (try
|
||||||
(poll! client base-url rates-atom)
|
(poll! client base-url rates-atom query-fn)
|
||||||
true
|
true
|
||||||
(catch Exception e
|
(catch Exception e
|
||||||
(log/error e (str "Frankfurter fetch failed, retrying in 10s (" retries " left)"))
|
(log/error e (str "Frankfurter fetch failed, retrying in 10s (" retries " left)"))
|
||||||
@@ -50,20 +58,34 @@
|
|||||||
(Thread/sleep 3600000)
|
(Thread/sleep 3600000)
|
||||||
(when @running?
|
(when @running?
|
||||||
(try
|
(try
|
||||||
(poll! client base-url rates-atom)
|
(poll! client base-url rates-atom query-fn)
|
||||||
(catch Exception e
|
(catch Exception e
|
||||||
(log/error e "Frankfurter poll failed")))))))
|
(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
|
(defmethod ig/init-key :frankfurter/rates
|
||||||
[_ {:keys [url]}]
|
[_ {:keys [url query-fn]}]
|
||||||
(log/info "Starting Frankfurter rates poller:" url)
|
(log/info "Starting Frankfurter rates poller:" url)
|
||||||
(let [client (HttpClient/newHttpClient)
|
(let [client (HttpClient/newHttpClient)
|
||||||
rates (atom nil)
|
rates (atom nil)
|
||||||
running? (atom true)
|
running? (atom true)]
|
||||||
fut (start-poll-loop! client url rates running?)]
|
(seed-from-db! rates query-fn)
|
||||||
|
(let [fut (start-poll-loop! client url rates running? query-fn)]
|
||||||
{:rates rates
|
{:rates rates
|
||||||
:running? running?
|
:running? running?
|
||||||
:future fut}))
|
:future fut})))
|
||||||
|
|
||||||
(defmethod ig/halt-key! :frankfurter/rates
|
(defmethod ig/halt-key! :frankfurter/rates
|
||||||
[_ {:keys [running? future]}]
|
[_ {:keys [running? future]}]
|
||||||
|
|||||||
Reference in New Issue
Block a user