Add buys query table from exchange_to_wallet events

Denormalized read table with id, event_id, occurred_at, wallet, sats,
fee_sats, and amount_eur. Projected on event creation and rebuildable
via POST /api/buys/rebuild. Fiat amounts converted to EUR using
Frankfurter rates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 12:44:44 +01:00
co-authored by Claude Opus 4.6
parent 93073820a2
commit 0caa8db0b3
7 changed files with 106 additions and 0 deletions
@@ -0,0 +1 @@
DROP TABLE IF EXISTS buys;
@@ -0,0 +1,7 @@
CREATE TABLE buys (
id BIGSERIAL PRIMARY KEY,
event_id BIGINT NOT NULL REFERENCES events(id) UNIQUE,
occurred_at DATE NOT NULL,
wallet TEXT NOT NULL,
amount_eur NUMERIC(18,2) NOT NULL
);
@@ -0,0 +1,3 @@
ALTER TABLE buys DROP COLUMN fee_sats;
--;;
ALTER TABLE buys DROP COLUMN sats;
@@ -0,0 +1,3 @@
ALTER TABLE buys ADD COLUMN sats BIGINT NOT NULL DEFAULT 0;
--;;
ALTER TABLE buys ADD COLUMN fee_sats BIGINT;
+24
View File
@@ -155,6 +155,30 @@ SELECT id, event_id, occurred_at, exchange, fiat_amount, fiat_currency,
FROM deposits
ORDER BY occurred_at DESC, id DESC
-- :name truncate-buys! :! :n
-- :doc Delete all rows from the buys read table
TRUNCATE buys
-- :name get-buy-events :? :*
-- :doc Get exchange_to_wallet events with wallet name, oldest first
SELECT e.id, e.occurred_at, e.sats, e.fee_sats, e.fiat_amount, e.fiat_currency,
e.to_wallet_id, tw.name AS wallet
FROM events e
LEFT JOIN wallets tw ON tw.id = e.to_wallet_id
WHERE e.event_type = 'exchange_to_wallet'
ORDER BY e.occurred_at ASC, e.id ASC
-- :name insert-buy! :! :n
-- :doc Insert a projected buy row
INSERT INTO buys (event_id, occurred_at, wallet, sats, fee_sats, amount_eur)
VALUES (:event-id, :occurred-at, :wallet, :sats, :fee-sats, :amount-eur)
-- :name get-all-buys :? :*
-- :doc Get all projected buys ordered by date descending
SELECT id, event_id, occurred_at, wallet, sats, fee_sats, amount_eur
FROM buys
ORDER BY occurred_at DESC, id DESC
-- :name get-kraken-hour-latest-24 :? :*
-- :doc Get the 24 most recent Kraken hourly candles
SELECT ts, open, high, low, close, vwap, volume, trade_count, created_at
@@ -69,6 +69,29 @@
(catch Exception e
(log/error e "Failed to project deposit for event" event-id))))
(defn- project-buy!
"Project an exchange_to_wallet event into the buys read table."
[{:keys [query-fn frankfurter]} event-id occurred-at to-wallet-id sats fee-sats fiat-amount fiat-currency]
(try
(let [wallet-name (or (:name (query-fn :get-wallet-by-id {:id to-wallet-id})) "Unknown")
amount-eur (if (= fiat-currency "EUR")
(bigdec fiat-amount)
(let [rates (or (query-fn :get-currency-rates-by-date {:rate-date occurred-at})
(rates/fetch-and-persist-for-date!
(:client frankfurter) (:url frankfurter)
query-fn occurred-at))]
(:amount-eur (convert-amount fiat-amount fiat-currency rates))))]
(when amount-eur
(query-fn :insert-buy!
{:event-id event-id
:occurred-at occurred-at
:wallet wallet-name
:sats (or sats 0)
:fee-sats fee-sats
:amount-eur amount-eur})))
(catch Exception e
(log/error e "Failed to project buy for event" event-id))))
(defn create-event! [{:keys [query-fn] :as opts} req]
(let [params (:body-params req)
event-type (:event_type params)]
@@ -93,6 +116,13 @@
(:fiat_amount params)
(:fiat_currency params)
(:note params)))
(when (and (= event-type "exchange_to_wallet") (:fiat_amount params))
(project-buy! opts (:id result) occurred-at
(some-> (:to_wallet_id params) UUID/fromString)
(:sats params)
(:fee_sats params)
(:fiat_amount params)
(:fiat_currency params)))
(response/created "/api/events" {:status "ok"})))))
(defn list-deposits [{:keys [query-fn]} _req]
@@ -128,6 +158,40 @@
0 events)]
(response/ok {:rebuilt n})))
(defn list-buys [{:keys [query-fn]} _req]
(response/ok (query-fn :get-all-buys {})))
(defn rebuild-buys! [{:keys [query-fn frankfurter]} _req]
(query-fn :truncate-buys! {})
(let [events (query-fn :get-buy-events {})
n (reduce
(fn [cnt {:keys [id occurred_at sats fee_sats fiat_amount fiat_currency wallet]}]
(if-not fiat_amount
cnt
(try
(let [amount-eur
(if (= fiat_currency "EUR")
(bigdec fiat_amount)
(let [rates (or (query-fn :get-currency-rates-by-date {:rate-date occurred_at})
(rates/fetch-and-persist-for-date!
(:client frankfurter) (:url frankfurter)
query-fn occurred_at))]
(:amount-eur (convert-amount fiat_amount fiat_currency rates))))]
(when amount-eur
(query-fn :insert-buy!
{:event-id id
:occurred-at occurred_at
:wallet (or wallet "Unknown")
:sats (or sats 0)
:fee-sats fee_sats
:amount-eur amount-eur}))
(inc cnt))
(catch Exception e
(log/error e "Failed to rebuild buy for event" id)
cnt))))
0 events)]
(response/ok {:rebuilt n})))
(defn list-events [{:keys [query-fn]} req]
(let [event-type (get-in req [:query-params "type"])]
(if event-type
@@ -202,6 +202,10 @@
{:get (fn [req] (tx/list-deposits opts req))}]
["/deposits/rebuild"
{:post (fn [req] (tx/rebuild-deposits! opts req))}]
["/buys"
{:get (fn [req] (tx/list-buys opts req))}]
["/buys/rebuild"
{:post (fn [req] (tx/rebuild-buys! opts req))}]
["/kraken-hour"
{:get (fn [req] (kraken-hour-handler opts req))}]
["/kraken-minute"