"""Vendor-agnostic adapters for market data feeds and exchange gateways. Provides a pluggable adapter interface with two starter implementations: - FIXFeedAdapter: Parses FIX-protocol market data messages - SandboxExchangeAdapter: Simulates an exchange gateway for testing """ from __future__ import annotations import abc import random import time from typing import Optional from .event_graph import Event, EventGraph, EventType class BaseAdapter(abc.ABC): """Base adapter interface for market data sources.""" @property @abc.abstractmethod def name(self) -> str: ... @abc.abstractmethod def ingest(self, raw_data: dict) -> list[Event]: """Parse raw data into typed events.""" ... class FIXFeedAdapter(BaseAdapter): """Adapter for FIX-protocol market data feeds. Parses FIX-like message dicts into MDTick events. Supports tags: 35 (MsgType), 55 (Symbol), 44 (Price), 38 (OrderQty), 54 (Side), 60 (TransactTime). """ @property def name(self) -> str: return "fix_feed" def ingest(self, raw_data: dict) -> list[Event]: events = [] msg_type = raw_data.get("35", "") if msg_type == "W": # Market Data Snapshot event = Event.create( event_type=EventType.MD_TICK, payload={ "symbol": raw_data.get("55", ""), "bid": float(raw_data.get("bid", 0)), "ask": float(raw_data.get("ask", 0)), "last": float(raw_data.get("44", 0)), "volume": int(raw_data.get("38", 0)), }, source_adapter=self.name, ) events.append(event) elif msg_type == "8": # Execution Report event = Event.create( event_type=EventType.FILL, payload={ "symbol": raw_data.get("55", ""), "side": raw_data.get("54", ""), "price": float(raw_data.get("44", 0)), "qty": int(raw_data.get("38", 0)), "exec_type": raw_data.get("150", ""), }, source_adapter=self.name, ) events.append(event) elif msg_type == "D": # New Order Single event = Event.create( event_type=EventType.ORDER, payload={ "symbol": raw_data.get("55", ""), "side": raw_data.get("54", ""), "price": float(raw_data.get("44", 0)), "qty": int(raw_data.get("38", 0)), }, source_adapter=self.name, ) events.append(event) return events class SandboxExchangeAdapter(BaseAdapter): """Simulated exchange gateway for testing and validation. Generates synthetic market data, accepts orders, and produces fills with configurable latency and fill rates. """ def __init__(self, fill_rate: float = 0.8, latency_ms: float = 1.0) -> None: self._fill_rate = fill_rate self._latency_ms = latency_ms @property def name(self) -> str: return "sandbox_exchange" def ingest(self, raw_data: dict) -> list[Event]: msg_type = raw_data.get("type", "") events = [] if msg_type == "tick": event = Event.create( event_type=EventType.MD_TICK, payload={ "symbol": raw_data.get("symbol", "TEST"), "bid": raw_data.get("bid", 100.0), "ask": raw_data.get("ask", 100.05), "last": raw_data.get("last", 100.02), }, source_adapter=self.name, ) events.append(event) elif msg_type == "order": order = Event.create( event_type=EventType.ORDER, payload=raw_data, source_adapter=self.name, ) events.append(order) # Simulate fill with configured probability if random.random() < self._fill_rate: time.sleep(self._latency_ms / 1000) fill = Event.create( event_type=EventType.FILL, payload={ "order_ref": order.id, "symbol": raw_data.get("symbol", "TEST"), "price": raw_data.get("price", 100.0), "qty": raw_data.get("qty", 100), "status": "filled", }, source_adapter=self.name, ) events.append(fill) return events def generate_ticks(self, symbol: str, count: int, base_price: float = 100.0) -> list[Event]: """Generate synthetic tick data for testing.""" events = [] price = base_price for _ in range(count): delta = random.gauss(0, 0.05) price += delta spread = abs(random.gauss(0.03, 0.01)) event = Event.create( event_type=EventType.MD_TICK, payload={ "symbol": symbol, "bid": round(price - spread / 2, 4), "ask": round(price + spread / 2, 4), "last": round(price, 4), }, source_adapter=self.name, ) events.append(event) return events