OHLC Service — Candle Data Flow
This document covers how live candle data flows through ohlc-service and into mi-casa, how historical backfill works, and how the two services authenticate with each other.
Data source: ohlc-service now uses the cTrader Open API (IC Markets EU / Spotware) for both live ticks and historical backfill. See Broker — IC Markets EU + cTrader Open API for the full integration details.
See ADR-008 for the rationale behind the microservice extraction.
Architecture at a Glance
cTrader Open API WS
│ live ticks
▼
ohlc-service
├─ marketDataWebSocket ← builds candles in memory
├─ TimescaleDB ← persists closed candles (ohlc_candles hypertable)
└─ /ws/ticks WebSocket ← broadcasts candle:update / candle:closed
│
│ X-API-Key / ?key=
▼
mi-casa backend
├─ marketDataWebSocket (WS client) ← forwards events to browser clients
└─ ohlcCandleRepository (HTTP) ← fetches candles on demand
│
▼
PostgreSQL (main DB)
└─ ohlcPatterns, ohlcWatchlist, ohlcPatternDetectionEvents
Live Feed Flow
cTrader Open API WebSocket
│ raw price ticks (bid/ask + symbol)
▼
marketDataWebSocket (ohlc-service)
│ accumulates ticks into candle buckets per timeframe
│
├─ on tick received
│ → emit candle:update (in-progress candle state)
│ → broadcast via tickBroadcaster to /ws/ticks subscribers
│
└─ on candle close (minute/5m/15m/1h/4h/1d boundary)
→ persist to TimescaleDB ohlc_candles
→ emit candle:closed
→ broadcast via tickBroadcaster to /ws/ticks subscribers
/ws/ticks subscribers (mi-casa backend)
│
▼
marketDataWebSocket (mi-casa)
│ forwards candle:update + candle:closed
▼
browser WebSocket clients
│
▼
frontend ohlcSlice / MarketsChart
The mi-casa WebSocket client reconnects automatically when ohlc-service restarts. startWebSocket() in backend/ws/ is a no-op when OHLC_SERVICE_URL is unset, allowing mi-casa to run in environments where ohlc-service is not deployed (e.g. in CI or on machines without the markets feature).
Backfill Flow
Admin triggers POST /api/ohlc/backfill (mi-casa)
│
▼
mi-casa proxies SSE stream → POST /backfill (ohlc-service)
│
▼
ohlc-service backfillService
├─ iterates tracked symbols × timeframes
├─ for each: fetch from cTrader
│ → upsert into TimescaleDB ohlc_candles (conflict on (symbol, timeframe, time))
└─ streams SSE progress events back to caller
{ symbol, timeframe, fetched, inserted, source, error? }
mi-casa streams SSE events directly to the browser client
(response body piped; no buffering)
Backfill is idempotent — rows that already exist in ohlc_candles are skipped via ON CONFLICT DO NOTHING. A full backfill from scratch takes 2–5 minutes for 5 symbols × 6 timeframes.
Service-to-Service Authentication
All communication between mi-casa and ohlc-service uses a static pre-shared API key.
| Transport | How the key is sent |
|---|---|
| HTTP | X-API-Key: <OHLC_SERVICE_API_KEY> request header |
| WebSocket | ?key=<OHLC_SERVICE_API_KEY> query parameter on upgrade |
Environment variables:
| Service | Variable | Role |
|---|---|---|
| mi-casa | OHLC_SERVICE_URL | Base URL of ohlc-service |
| mi-casa | OHLC_SERVICE_API_KEY | Secret sent with every request |
| ohlc-service | OHLC_API_KEY | Expected secret; validates inbound requests |
The key is a server-side secret. It is never forwarded to browser clients. The browser-facing mi-casa WebSocket connection is authenticated by the standard JWT session; ohlc-service credentials are not visible to the frontend.
When OHLC_SERVICE_URL is absent in mi-casa (e.g. local dev without the service running), startWebSocket() is a no-op and all /api/ohlc/candles* endpoints return a 503. The rest of mi-casa — auth, family features, etc. — is unaffected.
REST Endpoints (ohlc-service)
All endpoints require X-API-Key except /healthcheck.
| Method | Path | Query params | Description |
|---|---|---|---|
| GET | /healthcheck | — | Liveness (no auth) |
| GET | /symbols | — | Tracked symbols and timeframes |
| GET | /candles | symbol, timeframe, from, to | Candles by time range |
| GET | /candles/latest | symbol, timeframe, limit | Latest N candles |
| GET | /candles/from | symbol, timeframe, from | All candles from a given date |
| GET | /export | — | NDJSON bulk export |
| POST | /backfill | body: { symbols?, timeframes?, source?: 'auto' | 'ctrader' } | Trigger backfill, SSE progress stream |
| GET | /status | — | WS state, DB health, candle counts |
| WS | /ws/ticks | ?key=<OHLC_API_KEY> | Live candle broadcast |
The /status response is proxied by mi-casa at GET /api/ohlc/service-status (admin-only).
Local Development
# Start only the OHLC stack
docker compose up ohlc-service timescale-db
# Trigger a full backfill (streams progress to stdout)
curl -N -X POST http://localhost:3871/backfill \
-H "X-API-Key: <your OHLC_API_KEY>"
# ohlc-service is now serving candles; start mi-casa normally
bun devSet OHLC_SERVICE_URL=http://localhost:3871 and OHLC_SERVICE_API_KEY=<same key> in backend/.env. Once set, mi-casa’s markets feature is fully functional with cTrader as the sole market data source.
Related
- ADR-008 — rationale and tradeoffs
- Architecture Overview — full stack and request flow
- Database Schema —
ohlcPatterns,ohlcWatchlist,ohlcPatternDetectionEventstables (now on main Postgres)