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>
60 lines
1.6 KiB
Python
60 lines
1.6 KiB
Python
"""
|
|
Market data provider interface and registry.
|
|
|
|
To add a new provider:
|
|
1. Create providers/your_provider.py
|
|
2. Subclass MarketDataProvider
|
|
3. Register in PROVIDER_REGISTRY below
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from typing import AsyncIterator
|
|
|
|
from app.domain.events import Event
|
|
|
|
|
|
class MarketDataProvider(ABC):
|
|
"""
|
|
Base class for all market-data feed adapters.
|
|
|
|
Lifecycle: connect() → subscribe() → stream() → close()
|
|
"""
|
|
|
|
name: str = "unknown"
|
|
|
|
@abstractmethod
|
|
async def connect(self) -> None:
|
|
"""Establish connection to the data source."""
|
|
|
|
@abstractmethod
|
|
async def subscribe(self, symbols: list[str]) -> None:
|
|
"""Subscribe to symbols. May be called after reconnect."""
|
|
|
|
@abstractmethod
|
|
async def stream(self) -> AsyncIterator[Event]:
|
|
"""Yield normalized domain events. Must handle reconnect internally."""
|
|
yield # type: ignore
|
|
|
|
@abstractmethod
|
|
async def close(self) -> None:
|
|
"""Graceful shutdown."""
|
|
|
|
|
|
def get_provider(name: str) -> MarketDataProvider:
|
|
"""Factory: instantiate provider by name."""
|
|
from app.providers.binance import BinanceProvider
|
|
from app.providers.alpaca import AlpacaProvider
|
|
from app.providers.bybit import BybitProvider
|
|
|
|
registry: dict[str, type[MarketDataProvider]] = {
|
|
"binance": BinanceProvider,
|
|
"alpaca": AlpacaProvider,
|
|
"bybit": BybitProvider,
|
|
}
|
|
cls = registry.get(name.lower())
|
|
if cls is None:
|
|
available = ", ".join(registry.keys())
|
|
raise ValueError(f"Unknown provider '{name}'. Available: {available}")
|
|
return cls()
|