170 lines
5.4 KiB
Python
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
|