Files
microdao-daarion/services/senpai-md-consumer/tests/test_models_parse.py
Apple 09dee24342 feat: MD pipeline — market-data-service hardening + SenpAI NATS consumer
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>
2026-02-09 11:46:15 -08:00

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