Files
microdao-daarion/services/market-data-service/tests/test_bybit_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

152 lines
4.4 KiB
Python

"""
Unit tests for Bybit provider — raw JSON → domain event parsing.
"""
import pytest
from app.domain.events import EventType
from app.providers.bybit import BybitProvider
@pytest.fixture
def provider():
return BybitProvider()
# ── Trade parsing ──────────────────────────────────────────────────────
def test_parse_trade_basic(provider):
"""Basic publicTrade parsing."""
raw = {
"topic": "publicTrade.BTCUSDT",
"data": [
{
"s": "BTCUSDT",
"S": "Buy",
"v": "0.001",
"p": "70500.5",
"T": 1672515782136,
"i": "trade123",
}
],
}
event = provider._parse(raw)
assert event is not None
assert event.event_type == EventType.TRADE
assert event.symbol == "BTCUSDT"
assert event.price == 70500.5
assert event.size == 0.001
assert event.side == "buy"
assert event.trade_id == "trade123"
assert event.provider == "bybit"
def test_parse_trade_sell_side(provider):
"""Sell side trade."""
raw = {
"topic": "publicTrade.ETHUSDT",
"data": [
{
"s": "ETHUSDT",
"S": "Sell",
"v": "10.5",
"p": "2100.00",
"T": 1672515782136,
"i": "t456",
}
],
}
event = provider._parse(raw)
assert event.side == "sell"
assert event.symbol == "ETHUSDT"
def test_parse_trade_batch_takes_last(provider):
"""Multiple trades in a batch — takes the last one."""
raw = {
"topic": "publicTrade.BTCUSDT",
"data": [
{"s": "BTCUSDT", "S": "Buy", "v": "0.001", "p": "70000.0", "T": 100, "i": "first"},
{"s": "BTCUSDT", "S": "Sell", "v": "0.01", "p": "70100.0", "T": 200, "i": "last"},
],
}
event = provider._parse(raw)
assert event.trade_id == "last"
assert event.price == 70100.0
def test_parse_trade_timestamp(provider):
"""Exchange timestamp is correctly parsed."""
raw = {
"topic": "publicTrade.BTCUSDT",
"data": [
{"s": "BTCUSDT", "S": "Buy", "v": "1", "p": "70000", "T": 1672515782136, "i": "x"},
],
}
event = provider._parse(raw)
assert event.ts_exchange is not None
assert event.ts_exchange.year >= 2022
# ── Ticker (quote) parsing ─────────────────────────────────────────────
def test_parse_ticker_basic(provider):
"""Bybit tickers → QuoteEvent."""
raw = {
"topic": "tickers.BTCUSDT",
"data": {
"symbol": "BTCUSDT",
"bid1Price": "70000.5",
"bid1Size": "1.5",
"ask1Price": "70001.0",
"ask1Size": "2.0",
"ts": "1672515782136",
},
}
event = provider._parse(raw)
assert event is not None
assert event.event_type == EventType.QUOTE
assert event.symbol == "BTCUSDT"
assert event.bid == 70000.5
assert event.ask == 70001.0
assert event.bid_size == 1.5
assert event.ask_size == 2.0
assert event.provider == "bybit"
def test_parse_ticker_missing_bid(provider):
"""Ticker without bid → returns None."""
raw = {
"topic": "tickers.BTCUSDT",
"data": {"symbol": "BTCUSDT"},
}
event = provider._parse(raw)
assert event is None
# ── Edge cases ─────────────────────────────────────────────────────────
def test_parse_unknown_topic(provider):
"""Unknown topic → None."""
raw = {"topic": "some_unknown.BTCUSDT", "data": {}}
event = provider._parse(raw)
assert event is None
def test_parse_pong_skipped(provider):
"""Pong/subscribe messages are not events."""
raw = {"op": "pong", "success": True}
# _parse would not be called for op messages (handled in stream()),
# but let's verify _parse returns None for incomplete data
event = provider._parse(raw)
assert event is None
def test_parse_empty_trade_data(provider):
"""Empty trade data array → None."""
raw = {"topic": "publicTrade.BTCUSDT", "data": []}
event = provider._parse(raw)
assert event is None