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.

TransportHow the key is sent
HTTPX-API-Key: <OHLC_SERVICE_API_KEY> request header
WebSocket?key=<OHLC_SERVICE_API_KEY> query parameter on upgrade

Environment variables:

ServiceVariableRole
mi-casaOHLC_SERVICE_URLBase URL of ohlc-service
mi-casaOHLC_SERVICE_API_KEYSecret sent with every request
ohlc-serviceOHLC_API_KEYExpected 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.

MethodPathQuery paramsDescription
GET/healthcheckLiveness (no auth)
GET/symbolsTracked symbols and timeframes
GET/candlessymbol, timeframe, from, toCandles by time range
GET/candles/latestsymbol, timeframe, limitLatest N candles
GET/candles/fromsymbol, timeframe, fromAll candles from a given date
GET/exportNDJSON bulk export
POST/backfillbody: { symbols?, timeframes?, source?: 'auto' | 'ctrader' }Trigger backfill, SSE progress stream
GET/statusWS 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 dev

Set 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.