Producer (market-data-service):
- Backpressure: smart drop policy (heartbeats→quotes→trades preserved)
- Heartbeat monitor: synthetic HeartbeatEvent on provider silence
- Graceful shutdown: WS→bus→storage→DB engine cleanup sequence
- Bybit V5 public WS provider (backup for Binance, no API key needed)
- FailoverManager: health-based provider switching with recovery
- NATS output adapter: md.events.{type}.{symbol} for SenpAI
- /bus-stats endpoint for backpressure monitoring
- Dockerfile + docker-compose.node1.yml integration
- 36 tests (parsing + bus + failover), requirements.lock
Consumer (senpai-md-consumer):
- NATSConsumer: subscribe md.events.>, queue group senpai-md, backpressure
- State store: LatestState + RollingWindow (deque, 60s)
- Feature engine: 11 features (mid, spread, VWAP, return, vol, latency)
- Rule-based signals: long/short on return+volume+spread conditions
- Publisher: rate-limited features + signals + alerts to NATS
- HTTP API: /health, /metrics, /state/latest, /features/latest, /stats
- 10 Prometheus metrics
- Dockerfile + docker-compose.senpai.yml
- 41 tests (parsing + state + features + rate-limit), requirements.lock
CI: ruff + pytest + smoke import for both services
Tests: 77 total passed, lint clean
Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
4.3 KiB
Python
155 lines
4.3 KiB
Python
"""
|
|
Test event parsing from JSON payloads (mirrors market-data-service contracts).
|
|
"""
|
|
import json
|
|
|
|
|
|
from senpai.md_consumer.models import (
|
|
EventType,
|
|
TradeEvent,
|
|
QuoteEvent,
|
|
HeartbeatEvent,
|
|
parse_event,
|
|
)
|
|
|
|
|
|
# ── Trade events ───────────────────────────────────────────────────────
|
|
|
|
|
|
def test_parse_trade_basic():
|
|
data = {
|
|
"event_type": "trade",
|
|
"provider": "binance",
|
|
"symbol": "BTCUSDT",
|
|
"price": 70500.0,
|
|
"size": 1.5,
|
|
"ts_recv": "2026-02-09T12:00:00+00:00",
|
|
}
|
|
event = parse_event(data)
|
|
assert event is not None
|
|
assert isinstance(event, TradeEvent)
|
|
assert event.event_type == EventType.TRADE
|
|
assert event.symbol == "BTCUSDT"
|
|
assert event.price == 70500.0
|
|
assert event.size == 1.5
|
|
assert event.provider == "binance"
|
|
|
|
|
|
def test_parse_trade_with_extra_fields():
|
|
"""Unknown fields should be silently ignored (tolerant parsing)."""
|
|
data = {
|
|
"event_type": "trade",
|
|
"provider": "bybit",
|
|
"symbol": "ETHUSDT",
|
|
"price": 2100.0,
|
|
"size": 10.0,
|
|
"ts_recv": "2026-02-09T12:00:00+00:00",
|
|
"unknown_field": "should_be_ignored",
|
|
"another_extra": 42,
|
|
}
|
|
event = parse_event(data)
|
|
assert event is not None
|
|
assert event.symbol == "ETHUSDT"
|
|
|
|
|
|
def test_parse_trade_with_side_and_exchange_ts():
|
|
data = {
|
|
"event_type": "trade",
|
|
"provider": "binance",
|
|
"symbol": "BTCUSDT",
|
|
"price": 70000.0,
|
|
"size": 0.5,
|
|
"side": "buy",
|
|
"ts_exchange": "2026-02-09T12:00:00+00:00",
|
|
"ts_recv": "2026-02-09T12:00:00.100+00:00",
|
|
"trade_id": "t12345",
|
|
}
|
|
event = parse_event(data)
|
|
assert event.side == "buy"
|
|
assert event.trade_id == "t12345"
|
|
assert event.ts_exchange is not None
|
|
|
|
|
|
# ── Quote events ───────────────────────────────────────────────────────
|
|
|
|
|
|
def test_parse_quote_basic():
|
|
data = {
|
|
"event_type": "quote",
|
|
"provider": "binance",
|
|
"symbol": "BTCUSDT",
|
|
"bid": 70000.0,
|
|
"ask": 70001.0,
|
|
"bid_size": 5.0,
|
|
"ask_size": 3.0,
|
|
"ts_recv": "2026-02-09T12:00:00+00:00",
|
|
}
|
|
event = parse_event(data)
|
|
assert isinstance(event, QuoteEvent)
|
|
assert event.bid == 70000.0
|
|
assert event.ask == 70001.0
|
|
|
|
|
|
def test_parse_quote_zero_values():
|
|
data = {
|
|
"event_type": "quote",
|
|
"provider": "binance",
|
|
"symbol": "BTCUSDT",
|
|
"bid": 0.0,
|
|
"ask": 0.0,
|
|
"bid_size": 0.0,
|
|
"ask_size": 0.0,
|
|
}
|
|
event = parse_event(data)
|
|
assert event is not None
|
|
assert event.bid == 0.0
|
|
|
|
|
|
# ── Heartbeat events ──────────────────────────────────────────────────
|
|
|
|
|
|
def test_parse_heartbeat():
|
|
data = {
|
|
"event_type": "heartbeat",
|
|
"provider": "alpaca",
|
|
"ts_recv": "2026-02-09T12:00:00+00:00",
|
|
}
|
|
event = parse_event(data)
|
|
assert isinstance(event, HeartbeatEvent)
|
|
assert event.provider == "alpaca"
|
|
|
|
|
|
# ── Edge cases ─────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_parse_unknown_type():
|
|
data = {"event_type": "unknown_type", "provider": "test"}
|
|
event = parse_event(data)
|
|
assert event is None
|
|
|
|
|
|
def test_parse_missing_type():
|
|
data = {"provider": "test", "symbol": "BTC"}
|
|
event = parse_event(data)
|
|
assert event is None
|
|
|
|
|
|
def test_parse_invalid_data():
|
|
data = {"event_type": "trade"} # missing required fields
|
|
event = parse_event(data)
|
|
assert event is None
|
|
|
|
|
|
def test_parse_empty_dict():
|
|
event = parse_event({})
|
|
assert event is None
|
|
|
|
|
|
def test_parse_from_json_bytes():
|
|
"""Simulate actual NATS message deserialization."""
|
|
raw = b'{"event_type":"trade","provider":"binance","symbol":"BTCUSDT","price":70500.0,"size":1.5}'
|
|
data = json.loads(raw)
|
|
event = parse_event(data)
|
|
assert event is not None
|
|
assert event.price == 70500.0
|