From 30aa52cdc79e8ade68ee003b26df510974be2110 Mon Sep 17 00:00:00 2001 From: Per Magnus Petersen Date: Tue, 17 Feb 2026 17:40:36 +0100 Subject: [PATCH] Schedule Kraken OHLC polls at HH:01 UTC instead of fixed 5-min interval Hourly candles only finalize on the hour, so polling every 5 minutes was wasteful. Now the poller sleeps until 1 minute past each hour, fetches immediately on first run when no data exists, and logs time to next fetch on startup. Co-Authored-By: Claude Opus 4.6 --- resources/system.edn | 3 +- src/clj/pmagnus/btcprice/kraken/ohlc.clj | 45 ++++++++++++++++++------ 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/resources/system.edn b/resources/system.edn index 11673ab..1c17688 100644 --- a/resources/system.edn +++ b/resources/system.edn @@ -59,5 +59,4 @@ :uri "wss://stream.binance.com:9443/ws/btcusdt@trade"} :kraken/ohlc - {:query-fn #ig/ref :db.sql/query-fn - :interval-ms 300000}} + {:query-fn #ig/ref :db.sql/query-fn}} diff --git a/src/clj/pmagnus/btcprice/kraken/ohlc.clj b/src/clj/pmagnus/btcprice/kraken/ohlc.clj index b38620b..df2b36d 100644 --- a/src/clj/pmagnus/btcprice/kraken/ohlc.clj +++ b/src/clj/pmagnus/btcprice/kraken/ohlc.clj @@ -70,26 +70,49 @@ (log/info "Fetched" (count candles) "completed Kraken hourly candles")) (count candles))) +(defn- ms-until-next-poll + "Milliseconds from now until next HH:01:00 UTC." + [] + (let [now (java.time.ZonedDateTime/now java.time.ZoneOffset/UTC) + next (-> now + (.truncatedTo java.time.temporal.ChronoUnit/HOURS) + (.plusMinutes 1)) + target (if (.isAfter now next) + (.plusHours next 1) + next)] + (.toMillis (java.time.Duration/between now target)))) + (defn- start-poll-loop! - "Start a background future that polls Kraken every `interval-ms`. - Returns the future." - [client query-fn since-atom running? interval-ms] + "Start a background future that polls Kraken aligned to HH:01:00 UTC. + When `has-data?` is false, fetches immediately to backfill." + [client query-fn since-atom running? has-data?] (future - (while @running? + (when-not has-data? (try (poll! client query-fn since-atom) (catch Exception e - (log/error e "Kraken OHLC poll failed"))) - (Thread/sleep interval-ms)))) + (log/error e "Kraken OHLC initial poll failed")))) + (while @running? + (let [wait (ms-until-next-poll)] + (log/info "Next Kraken OHLC poll in" (quot wait 60000) "minutes") + (Thread/sleep wait) + (when @running? + (try + (poll! client query-fn since-atom) + (catch Exception e + (log/error e "Kraken OHLC poll failed")))))))) (defmethod ig/init-key :kraken/ohlc - [_ {:keys [query-fn interval-ms]}] - (log/info "Starting Kraken OHLC poller, interval:" interval-ms "ms") + [_ {:keys [query-fn]}] (let [client (HttpClient/newHttpClient) - since (atom (seed-since-from-db query-fn)) + db-since (seed-since-from-db query-fn) + since (atom db-since) running? (atom true) - fut (start-poll-loop! client query-fn since running? - (or interval-ms 300000))] + has-data? (some? db-since) + fut (start-poll-loop! client query-fn since running? has-data?)] + (if has-data? + (log/info "Starting Kraken OHLC poller, next fetch in" (quot (ms-until-next-poll) 60000) "minutes") + (log/info "Starting Kraken OHLC poller, fetching immediately (no data)")) {:running? running? :future fut :since since}))