Started: 2026-06-02
- ✅ Done
- 🔄 In Progress
- ⬜ Not Started
- ❌ Blocked / Incomplete
- ✅
streamSender— buffered per-stream channel (cap 2048), drain goroutine; actor never blocks on gRPC write - ✅ Redis pub/sub fan-out:
depth:{SYM},ticker:{SYM}publish inner proto bytes;order:{userID}publishes full EngineEvent bytes - ✅
event.Symbolstamped on all events in actor loop before broadcast/publish - ✅
REDIS_URLenv — non-fatal if missing (WAL/Kafka unaffected) - ✅
PublishEngineEvent— goroutine-dispatched, non-blocking to actor loop
- ✅ Kafka consumer (reads trade events)
- ✅ Worker pool + router
- ✅ In-memory CandleStore (L1) — OHLCV per symbol per timeframe, auto-closes candles
- ✅ Redis pub/sub publish on every tick (
PublishCandleEventToRedis) — proto-marshalled bytes - ✅ Redis L2 storage —
PushCandlecalled on candle close, LTrim 5000 cap - ✅ gRPC server —
GetCandleswith L1 → L2 (Redis) → L3 (ClickHouse) fallthrough - ✅ ClickHouse L3 —
FetchCandlesaggregates OHLCV fromtradestable on-the-fly (no separate candles table) - ✅
onCandleClosedcomposite: PushCandle (L2) + Kafka publish (no CH write — trades table is source of truth)
- ✅ Kafka consumer client
- ✅
TradeBatcher— size + time-based flush (500 msgs or 50ms) - ✅ ClickHouse batch insert (
InsertTrades) — marks Kafka offsets only on success - ✅
EnsureTradesSchema— createstradestable on startup - ✅ Consumer handler wired to batcher — no immediate MarkMessage
- Subscribe to Redis pub/sub candle channel (
candles:<SYMBOL>:<tf>) - Compute indicators on new candle (RSI, EMA, MACD, Bollinger — pick what's needed)
- Store latest indicator values in Redis (L2) and in-memory (L1)
- Publish indicator updates to Redis pub/sub for WS consumers
- ✅ Candle stream — Redis pub/sub per (symbol, timeframe), fan-out to subscribers
- ✅
subscribe_candles/unsubscribe_candlesmessage routing - ✅ Depth/ticker stream — Redis pub/sub per symbol (
depth:{SYM}+ticker:{SYM}), fan-out to subscribers - ✅
subscribe_depth/unsubscribe_depthmessage routing - ✅ Order/trade stream — per-user Redis sub (
order:{userID}) started on connect, stopped on disconnect - ✅ Auto-reconnect on Redis drop (only if users still subscribed / connected)
- ✅ Pre-encode JSON once at fan-out time (zero per-user marshal work)
- ✅ Refactored:
fanoutStreamgeneric struct instream.goreplaces duplicate depth/ticker/candle stream files — one Redis sub per key, fan-out to N users, auto-reconnect - ✅ Non-blocking
writePumpvia buffered per-user channel (1024 cap, drop with warn on full) - ❌ No indicator stream (indicator-service not built yet)
- ✅ gRPC engine connection removed — order/depth events now via Redis (see matching-engine below)
- ✅ RS256 asymmetric JWT — private key in auth-service only, public key distributed to api-gateway + websocket-server for local verification (no auth-service call on hot path)
- ✅ Access token (15 min) + refresh token (7 days) with rotation and family-based theft detection
- ✅ Redis blacklist:
bl:{jti}TTL = remaining token life — immediate logout invalidation - ✅ auth-service — gRPC server (port 50054), Prisma users table, bcrypt cost 12
- ✅ auth-service gRPC methods: Register, Login, Refresh, Logout, Me
- ✅ api-gateway —
/auth/*routes proxy to auth-service via gRPC client;authMiddlewareverifies RS256 + blacklist check - ✅ websocket-server — optional
?token=query param; public streams (depth/ticker/candle) allow anon; order stream requires valid JWT - ✅
authRequiredActionsmap in websocket-server — single place to define which WS actions need auth
- Add / withdraw balance (dummy — no real payment gateway)
- Hold / release margin
- gRPC interface for order-service to call
- In-memory + DB persistence
- Initial margin = (price × quantity) / leverage
- Available margin check before order placement
- Call ledger-service to hold margin on order placement
- Release margin on cancel / fill
- Per-position leverage (isolated or cross)
- Liquidation price calculation
- Liquidation trigger — background monitor watching mark price vs liquidation price
- Force-close position via order-service when triggered
- New order types: STOP_LOSS, TAKE_PROFIT
- Trigger monitor (compare mark price against SL/TP)
- Place market order when triggered
- Cancel on position close
- Current: matching engine publishes to Kafka synchronously (blocks engine loop)
- Goal: async — engine writes to internal channel, separate goroutine flushes to Kafka
- WAL checkpoint file already exists (
wal/*/checkpoint.meta) — track publish offset there - Candle-service: already async (consumer-side), no change needed
- Risk: on crash between WAL write and Kafka publish, replay from checkpoint offset
- https://chatgpt.com/s/t_69ee8613950881918281d91d6e8a6849
Migration path:
- Add internal
kafkaQueue chan []byteto engine - Flush goroutine: dequeue → produce → update checkpoint offset
- On startup: read checkpoint, seek Kafka consumer to that offset for replay
- Trading chart (TradingView Lightweight Charts or similar)
- Real-time candle feed via WS
- Orderbook / depth view
- Order placement form
- Portfolio / positions panel
- Auth pages (login, register)
| Service | Language | Status | Transport |
|---|---|---|---|
| matching-engine | Go | ✅ Core done | gRPC (orders) + Redis pub/sub (events) |
| candle-service | Go | ✅ 100% | gRPC + Kafka + Redis + ClickHouse |
| trade-ingestor-service | Go | ✅ 100% | Kafka → ClickHouse |
| indicator-service | - | ⬜ 0% | Redis pub/sub |
| websocket-server | Go | ✅ ~90% | WS + Redis pub/sub (depth/ticker/order/candle) |
| order-service | TS | 🔄 basic | gRPC |
| order-trade-store-service | TS | 🔄 basic | Kafka → DB |
| api-gateway | TS | 🔄 basic | HTTP |
| market-maker | TS | ⬜ stub | - |
| ledger-service | - | ⬜ 0% | gRPC |
| web | TS/React | ⬜ scaffold | WS + HTTP |