idea105-deltatrace/deltatrace/adapters.py

170 lines
5.4 KiB
Python

"""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