Files
Apple c50843933f feat: market-data-service for SenpAI trading agent
New service: real-time market data collection with unified event model.

Architecture:
- Domain events: TradeEvent, QuoteEvent, BookL2Event, HeartbeatEvent
- Provider interface: MarketDataProvider ABC with connect/subscribe/stream/close
- Async EventBus with fan-out to multiple consumers

Providers:
- BinanceProvider: public WebSocket (trades + bookTicker), no API key needed,
  auto-reconnect with exponential backoff, heartbeat timeout detection
- AlpacaProvider: IEX real-time data + paper trading auth,
  dry-run mode when no keys configured (heartbeats only)

Consumers:
- StorageConsumer: SQLite (via SQLAlchemy async) + JSONL append-only log
- MetricsConsumer: Prometheus counters, latency histograms, events/sec gauge
- PrintConsumer: sampled structured logging (1/100 events)

CLI: python -m app run --provider binance --symbols BTCUSDT,ETHUSDT
HTTP: /health, /metrics (Prometheus), /latest?symbol=XXX

Tests: 19/19 passed (Binance parse, Alpaca parse, bus smoke tests)

Config: pydantic-settings + .env, all secrets via environment variables.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-09 11:19:00 -08:00

108 lines
3.1 KiB
Python

"""
Unit tests for Alpaca raw → domain event parsing.
"""
import pytest
from app.providers.alpaca import AlpacaProvider
from app.domain.events import EventType
@pytest.fixture
def provider():
return AlpacaProvider()
# ── Trade parsing ──────────────────────────────────────────────────────
ALPACA_TRADE_RAW = {
"T": "t",
"S": "AAPL",
"p": 185.50,
"s": 100,
"t": "2024-01-15T14:30:00.123456Z",
"i": 12345,
"x": "V",
"z": "C",
}
def test_parse_trade_basic(provider):
event = provider._parse(ALPACA_TRADE_RAW)
assert event is not None
assert event.event_type == EventType.TRADE
assert event.symbol == "AAPL"
assert event.price == 185.50
assert event.size == 100
assert event.trade_id == "12345"
assert event.provider == "alpaca"
def test_parse_trade_timestamp(provider):
event = provider._parse(ALPACA_TRADE_RAW)
assert event.ts_exchange is not None
assert event.ts_exchange.year == 2024
assert event.ts_exchange.month == 1
# ── Quote parsing ──────────────────────────────────────────────────────
ALPACA_QUOTE_RAW = {
"T": "q",
"S": "TSLA",
"bp": 250.10,
"bs": 200,
"ap": 250.25,
"as": 150,
"t": "2024-01-15T14:30:01.456789Z",
"x": "V",
"z": "C",
}
def test_parse_quote_basic(provider):
event = provider._parse(ALPACA_QUOTE_RAW)
assert event is not None
assert event.event_type == EventType.QUOTE
assert event.symbol == "TSLA"
assert event.bid == 250.10
assert event.ask == 250.25
assert event.bid_size == 200
assert event.ask_size == 150
assert event.provider == "alpaca"
# ── Control messages ───────────────────────────────────────────────────
def test_parse_success_message(provider):
raw = {"T": "success", "msg": "connected"}
event = provider._parse(raw)
assert event is None
def test_parse_subscription_message(provider):
raw = {"T": "subscription", "trades": ["AAPL"], "quotes": ["AAPL"]}
event = provider._parse(raw)
assert event is None
def test_parse_error_message(provider):
raw = {"T": "error", "code": 402, "msg": "auth failed"}
event = provider._parse(raw)
assert event is None
# ── Edge cases ─────────────────────────────────────────────────────────
def test_parse_trade_missing_timestamp(provider):
raw = {"T": "t", "S": "AAPL", "p": 100, "s": 10, "i": 1}
event = provider._parse(raw)
assert event is not None
assert event.ts_exchange is None
def test_parse_quote_zero_values(provider):
raw = {"T": "q", "S": "SPY", "bp": 0, "bs": 0, "ap": 0, "as": 0}
event = provider._parse(raw)
assert event is not None
assert event.bid == 0
assert event.ask == 0