Files
microdao-daarion/services/market-data-service/app/db/schema.py
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

78 lines
3.1 KiB
Python

"""
SQLAlchemy async models for market data storage.
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, Float, Index, Integer, String, Text
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from app.config import settings
class Base(AsyncAttrs, DeclarativeBase):
pass
class TradeRecord(Base):
__tablename__ = "trades"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
provider: Mapped[str] = mapped_column(String(32), nullable=False)
symbol: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
price: Mapped[float] = mapped_column(Float, nullable=False)
size: Mapped[float] = mapped_column(Float, nullable=False)
side: Mapped[str | None] = mapped_column(String(8), nullable=True)
trade_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
ts_exchange: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
ts_recv: Mapped[datetime] = mapped_column(DateTime, nullable=False)
__table_args__ = (
Index("ix_trades_symbol_ts", "symbol", "ts_recv"),
)
class QuoteRecord(Base):
__tablename__ = "quotes"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
provider: Mapped[str] = mapped_column(String(32), nullable=False)
symbol: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
bid: Mapped[float] = mapped_column(Float, nullable=False)
ask: Mapped[float] = mapped_column(Float, nullable=False)
bid_size: Mapped[float] = mapped_column(Float, nullable=False)
ask_size: Mapped[float] = mapped_column(Float, nullable=False)
ts_exchange: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
ts_recv: Mapped[datetime] = mapped_column(DateTime, nullable=False)
__table_args__ = (
Index("ix_quotes_symbol_ts", "symbol", "ts_recv"),
)
class BookSnapshotRecord(Base):
__tablename__ = "book_snapshots"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
provider: Mapped[str] = mapped_column(String(32), nullable=False)
symbol: Mapped[str] = mapped_column(String(32), nullable=False, index=True)
bids_json: Mapped[str] = mapped_column(Text, nullable=False) # JSON
asks_json: Mapped[str] = mapped_column(Text, nullable=False) # JSON
depth: Mapped[int] = mapped_column(Integer, nullable=False)
ts_exchange: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
ts_recv: Mapped[datetime] = mapped_column(DateTime, nullable=False)
# ── Engine & Session factory ──────────────────────────────────────────
engine = create_async_engine(settings.sqlite_url, echo=False)
async_session = async_sessionmaker(engine, expire_on_commit=False)
async def init_db() -> None:
"""Create all tables."""
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)