diff --git a/CLAUDE.md b/CLAUDE.md index 369bff0..2d243bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,6 +8,16 @@ Bitcoin data backend — Clojure Kit application. Fetches BTC prices from Binanc - `make repl` — Start nREPL - `make test` — Run tests - `make uberjar` — Build production JAR +- `docker compose up -d` — Start btcdata container + +## Docker + +Docker runs alongside local dev on different ports and databases: + +| | Local dev | Docker | +|--|----------------|----------------| +| **Port** | localhost:4100 | localhost:4101 | +| **Database** | btcprice | btcprod | ## REPL Commands diff --git a/README.md b/README.md new file mode 100644 index 0000000..64e2d08 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# btcdata + +Bitcoin data backend service. Fetches BTC prices from Binance (WebSocket) and Kraken (HTTP polling), stores them in PostgreSQL, and exposes a JSON API with Server-Sent Events for live price streaming. + +## API + +| Endpoint | Description | +|---|---| +| `GET /api/price/stream` | SSE stream — emits `price-update` events with JSON `{price, prev_price, recorded_at}` | +| `GET /api/price/latest` | Latest price as JSON | +| `GET /api/health` | Health check | + +## Quick Start + +```bash +# Local dev (requires PostgreSQL) +cp .env.example .env # edit JDBC_URL +make run # starts on port 4100 + +# Docker +docker compose up -d # starts on port 4101 +``` + +## Environment Variables + +| Variable | Default | Description | +|---|---|---| +| `BTCDATA_PORT` | `4100` | HTTP server port | +| `JDBC_URL` | — | PostgreSQL connection string | +| `CORS_ORIGIN` | `*` | Allowed origin for CORS headers | + +## Data Sources + +- **Binance** — WebSocket trade stream (`btcusdt@trade`), throttled to 5s writes +- **Kraken hourly** — OHLC candles polled at HH:01:00 UTC +- **Kraken daily** — OHLC candles polled at 00:01:00 UTC + +## Tech Stack + +Clojure · Kit · Undertow · Reitit · PostgreSQL · conman · Migratus diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..440888b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + btcdata: + build: . + restart: unless-stopped + ports: + - "4101:4100" + environment: + BTCDATA_PORT: "4100" + JDBC_URL: "jdbc:postgresql://postgres:5432/btcprod?user=postgres&password=ratata,123" + CORS_ORIGIN: "http://localhost:4041" + extra_hosts: + - "postgres:host-gateway" diff --git a/src/clj/pmagnus/btcdata/web/routes/api.clj b/src/clj/pmagnus/btcdata/web/routes/api.clj index 09cb11c..d09d767 100644 --- a/src/clj/pmagnus/btcdata/web/routes/api.clj +++ b/src/clj/pmagnus/btcdata/web/routes/api.clj @@ -28,11 +28,8 @@ :prev_price (when prev-price (str prev-price)) :recorded_at (str recorded-at)})) -(defn- price-stream-handler [{:keys [binance]} req] - (let [^HttpServerExchange exchange (:server-exchange req) - price-atom (:latest-price binance) - headers (.getResponseHeaders exchange) - cors-origin (or (System/getenv "CORS_ORIGIN") "*")] +(defn- run-sse-loop! [^HttpServerExchange exchange price-atom cors-origin] + (let [headers (.getResponseHeaders exchange)] (.setStatusCode exchange 200) (.put headers (HttpString. "Content-Type") "text/event-stream") (.put headers (HttpString. "Cache-Control") "no-cache") @@ -59,7 +56,21 @@ (catch Exception _) (finally (remove-watch price-atom wkey) - (try (.close writer) (catch Exception _))))) + (try (.close writer) (catch Exception _)) + (when-not (.isComplete exchange) + (.endExchange exchange))))))) + +(defn- price-stream-handler [{:keys [binance]} req] + (let [^HttpServerExchange exchange (:server-exchange req) + price-atom (:latest-price binance) + cors-origin (or (System/getenv "CORS_ORIGIN") "*")] + ;; Dispatch to a worker thread so the Ring adapter never processes + ;; the return value — Undertow handles the exchange lifecycle directly. + (.dispatch exchange + (reify io.undertow.server.HttpHandler + (handleRequest [_ ex] + (run-sse-loop! ex price-atom cors-origin)))) + ;; Return nil; exchange is dispatched so adapter won't touch it. nil)) (defn- latest-price-handler [{:keys [binance]} _req]