Add Frankfurter EUR/DKK rate polling and include rates in WebSocket stream
Polls self-hosted Frankfurter API hourly for USD-based EUR and DKK exchange rates. Both the WebSocket HTML fragments and the /api/price/latest JSON endpoint now include the currency rates alongside BTC price. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,15 +18,17 @@
|
||||
[java.time.format DateTimeFormatter]
|
||||
[java.util.concurrent LinkedBlockingQueue TimeUnit]))
|
||||
|
||||
(defn- price->json [{:keys [price prev-price recorded-at]}]
|
||||
(defn- price->json [{:keys [price prev-price recorded-at]} rates]
|
||||
(json/write-str
|
||||
{:price (str price)
|
||||
:prev_price (when prev-price (str prev-price))
|
||||
:recorded_at (str recorded-at)}))
|
||||
(cond-> {:price (str price)
|
||||
:prev_price (when prev-price (str prev-price))
|
||||
:recorded_at (str recorded-at)}
|
||||
rates (assoc :eur (str (:eur rates))
|
||||
:dkk (str (:dkk rates))))))
|
||||
|
||||
(def ^:private ts-fmt (DateTimeFormatter/ofPattern "dd-MM HH:mm:ss"))
|
||||
|
||||
(defn- price->html [{:keys [price prev-price recorded-at]}]
|
||||
(defn- price->html [{:keys [price prev-price recorded-at]} rates]
|
||||
(let [p (double (bigdec price))
|
||||
pp (when prev-price (double (bigdec prev-price)))
|
||||
color (cond
|
||||
@@ -39,50 +41,64 @@
|
||||
"<div class=\"bg-white rounded-2xl shadow-sm border border-gray-200 p-6\">"
|
||||
"<div class=\"text-center\">"
|
||||
"<p class=\"text-4xl font-bold " color "\">$" (format "%,.2f" p) "</p>"
|
||||
(when rates
|
||||
(let [eur (double (:eur rates))
|
||||
dkk (double (:dkk rates))]
|
||||
(str "<div class=\"flex justify-center gap-4 mt-3\">"
|
||||
"<span class=\"text-sm text-gray-600\">EUR " (format "%.4f" eur) "</span>"
|
||||
"<span class=\"text-sm text-gray-600\">DKK " (format "%.4f" dkk) "</span>"
|
||||
"</div>")))
|
||||
"<p class=\"text-xs text-gray-400 mt-2\">Updated " ts "</p>"
|
||||
"</div></div></div>")))
|
||||
|
||||
(defn- start-ws-send-loop! [^WebSocketChannel channel price-atom]
|
||||
(defn- start-ws-send-loop! [^WebSocketChannel channel price-atom rates-atom]
|
||||
(let [queue (LinkedBlockingQueue.)
|
||||
wkey (keyword (gensym "ws-"))]
|
||||
wkey (keyword (gensym "ws-"))
|
||||
rkey (keyword (gensym "ws-r-"))]
|
||||
(.set (.getReceiveSetter channel)
|
||||
(proxy [AbstractReceiveListener] []))
|
||||
(.resumeReceives channel)
|
||||
(add-watch price-atom wkey
|
||||
(fn [_ _ _ v] (.offer queue v)))
|
||||
(when-let [v @price-atom]
|
||||
(.offer queue v))
|
||||
(fn [_ _ _ _] (.offer queue :update)))
|
||||
(add-watch rates-atom rkey
|
||||
(fn [_ _ _ _] (.offer queue :update)))
|
||||
(when @price-atom
|
||||
(.offer queue :update))
|
||||
(future
|
||||
(try
|
||||
(loop []
|
||||
(when (.isOpen channel)
|
||||
(if-let [v (.poll queue 15 TimeUnit/SECONDS)]
|
||||
(WebSockets/sendTextBlocking (price->html v) channel)
|
||||
(if (.poll queue 15 TimeUnit/SECONDS)
|
||||
(when-let [v @price-atom]
|
||||
(WebSockets/sendTextBlocking (price->html v @rates-atom) channel))
|
||||
(WebSockets/sendTextBlocking "" channel))
|
||||
(recur)))
|
||||
(catch Exception _)
|
||||
(finally
|
||||
(remove-watch price-atom wkey)
|
||||
(remove-watch rates-atom rkey)
|
||||
(when (.isOpen channel)
|
||||
(try (.close channel) (catch Exception _))))))))
|
||||
|
||||
(defn- ws-price-handler [{:keys [binance]} req]
|
||||
(defn- ws-price-handler [{:keys [binance frankfurter]} req]
|
||||
(let [^HttpServerExchange exchange (:server-exchange req)
|
||||
price-atom (:latest-price binance)
|
||||
rates-atom (:rates frankfurter)
|
||||
callback (proxy [WebSocketConnectionCallback] []
|
||||
(onConnect [_ws-exchange channel]
|
||||
(start-ws-send-loop! channel price-atom)))
|
||||
(start-ws-send-loop! channel price-atom rates-atom)))
|
||||
handler (WebSocketProtocolHandshakeHandler. callback)]
|
||||
(.handleRequest handler exchange)
|
||||
nil))
|
||||
|
||||
(defn- latest-price-handler [{:keys [binance]} _req]
|
||||
(defn- latest-price-handler [{:keys [binance frankfurter]} _req]
|
||||
(let [price-atom (:latest-price binance)
|
||||
rates-atom (:rates frankfurter)
|
||||
v @price-atom]
|
||||
(if v
|
||||
{:status 200
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body (price->json v)}
|
||||
:body (price->json v @rates-atom)}
|
||||
{:status 503
|
||||
:headers {"Content-Type" "application/json"}
|
||||
:body (json/write-str {:error "No price data yet"})})))
|
||||
|
||||
Reference in New Issue
Block a user