ในโลกของการเทรดคริปโตเชิงปริมาณ (Quantitative Trading) การเข้าถึงข้อมูล L2 Order Book คุณภาพสูงเป็นรากฐานสำคัญของระบบทุกตัว ไม่ว่าจะเป็น Market Making, Arbitrage Bot หรือ Alpha Research บทความนี้จะพาคุณสร้างระบบดึงข้อมูล Order Book จาก 3 Exchange ยอดนิยมอย่าง Binance, OKX และ Hyperliquid ผ่าน Tardis.dev API แบบ Unified โดยใช้ Python
ทำไมต้อง Tardis.dev?
Tardis.dev เป็น Data Aggregation Layer ที่รวม Historical Data จาก Exchange หลายตัวเข้าด้วยกันผ่าน API เดียว ข้อดีหลักคือ:
- Unified API — ใช้โค้ดเดียวดึงข้อมูลจาก Exchange หลายตัว
- Normalized Data Format — ข้อมูลถูกมาตรฐานให้เหมือนกันทุก Exchange
- Replay Mode — สามารถ Replay Historical Data ได้แบบ Real-time
- WebSocket + REST — รองรับทั้ง 2 โปรโตคอล
- High Availability — Uptime สูง ใช้ใน Production ได้เลย
สถาปัตยกรรมระบบ
ก่อนเข้าสู่โค้ด มาดูภาพรวมสถาปัตยกรรมที่เราจะสร้างกัน:
+------------------+ +------------------+ +------------------+
| Binance | | OKX | | Hyperliquid |
| L2 Order Book | | L2 Order Book | | L2 Order Book |
+--------+---------+ +--------+---------+ +--------+---------+
| | |
v v v
+--------+---------+ +--------+---------+ +--------+---------+
| Tardis.dev |<----| Tardis.dev |<----| Tardis.dev |
| Exchange API | | Exchange API | | Exchange API |
+--------+---------+ +--------+---------+ +--------+---------+
| | |
+------------------------+------------------------+
|
v
+-------------------+
| Python Consumer |
| - Async/Await |
| - Concurrency |
| - Buffer Queue |
+-------------------+
|
v
+-------------------+
| Data Processor |
| - Normalization |
| - Enrichment |
| - Storage |
+-------------------+
การติดตั้งและ Setup
# ติดตั้ง dependencies
pip install tardis-dev aiohttp asyncpg redis-python pandas numpy
หรือใช้ requirements.txt
tardis-dev>=2.0.0
aiohttp>=3.9.0
pandas>=2.0.0
numpy>=1.24.0
asyncpg>=0.29.0
redis>=5.0.0
python-dotenv>=1.0.0
โครงสร้างโปรเจกต์
# project structure
tardis_unified_consumer/
├── config/
│ ├── __init__.py
│ └── settings.py
├── clients/
│ ├── __init__.py
│ ├── base_client.py
│ ├── binance_client.py
│ ├── okx_client.py
│ └── hyperliquid_client.py
├── models/
│ ├── __init__.py
│ └── orderbook.py
├── storage/
│ ├── __init__.py
│ └── postgres_writer.py
├── main.py
├── requirements.txt
└── .env
Configuration และ Environment
# config/settings.py
import os
from dataclasses import dataclass
from typing import Dict, List
@dataclass
class ExchangeConfig:
exchange: str
symbols: List[str]
channels: List[str]
api_key: str
@dataclass
class DatabaseConfig:
host: str
port: int
database: str
user: str
password: str
@dataclass
class RedisConfig:
host: str
port: int
db: int
password: str = None
HolySheep AI - สำหรับ Real-time Analytics
@dataclass
class LLMConfig:
base_url: str = "https://api.holysheep.ai/v1" # ประหยัด 85%+ vs OpenAI
api_key: str = "YOUR_HOLYSHEEP_API_KEY"
model: str = "gpt-4.1" # $8/MTok vs $15 ที่อื่น
max_tokens: int = 1000
temperature: float = 0.3
class Settings:
# Tardis.dev API Token
TARDIS_API_KEY: str = os.getenv("TARDIS_API_KEY", "your_tardis_api_key")
# Exchange Configurations
EXCHANGES: Dict[str, ExchangeConfig] = {
"binance": ExchangeConfig(
exchange="binance",
symbols=["BTCUSDT", "ETHUSDT", "SOLUSDT"],
channels=["orderbook"],
api_key=os.getenv("TARDIS_API_KEY")
),
"okx": ExchangeConfig(
exchange="okx",
symbols=["BTC-USDT", "ETH-USDT", "SOL-USDT"],
channels=["books"],
api_key=os.getenv("TARDIS_API_KEY")
),
"hyperliquid": ExchangeConfig(
exchange="hyperliquid",
symbols=["BTC", "ETH", "SOL"],
channels=["orderbook"],
api_key=os.getenv("TARDIS_API_KEY")
)
}
# Database
DATABASE: DatabaseConfig = DatabaseConfig(
host=os.getenv("DB_HOST", "localhost"),
port=int(os.getenv("DB_PORT", "5432")),
database=os.getenv("DB_NAME", "orderbook_db"),
user=os.getenv("DB_USER", "postgres"),
password=os.getenv("DB_PASSWORD", "password")
)
# Redis for caching
REDIS: RedisConfig = RedisConfig(
host=os.getenv("REDIS_HOST", "localhost"),
port=int(os.getenv("REDIS_PORT", "6379")),
db=int(os.getenv("REDIS_DB", "0"))
)
# LLM for analytics (HolySheep AI)
LLM: LLMConfig = LLMConfig()
# Performance settings
BUFFER_SIZE: int = 1000
FLUSH_INTERVAL_SEC: int = 5
MAX_CONCURRENT_STREAMS: int = 10
@classmethod
def get_tardis_url(cls, exchange: str) -> str:
"""Generate Tardis.dev WebSocket URL for exchange"""
return f"wss://tardis.dev/v1/stream/{exchange}?token={cls.TARDIS_API_KEY}"
@classmethod
def get_tardis_rest_url(cls, exchange: str, symbol: str, channel: str) -> str:
"""Generate Tardis.dev REST API URL"""
return f"https://tardis.dev/v1/history/{exchange}/{symbol}?channel={channel}&token={cls.TARDIS_API_KEY}"
settings = Settings()
Unified Order Book Model
# models/orderbook.py
from dataclasses import dataclass, field
from typing import Dict, List, Tuple, Optional
from datetime import datetime
from decimal import Decimal
import json
@dataclass
class PriceLevel:
"""Single price level in order book"""
price: float
size: float
order_count: int = 0
def to_dict(self) -> dict:
return {
"price": self.price,
"size": self.size,
"order_count": self.order_count
}
@dataclass
class OrderBook:
"""Unified Order Book model across all exchanges"""
exchange: str
symbol: str
timestamp: datetime
local_timestamp: datetime = field(default_factory=datetime.utcnow)
# Bid side (buy orders) - sorted high to low
bids: List[PriceLevel] = field(default_factory=list)
# Ask side (sell orders) - sorted low to high
asks: List[PriceLevel] = field(default_factory=list)
# Sequence number for ordering
sequence: int = 0
# Exchange-specific raw data
raw_data: Optional[dict] = None
# Derived metrics
@property
def best_bid(self) -> Optional[PriceLevel]:
return self.bids[0] if self.bids else None
@property
def best_ask(self) -> Optional[PriceLevel]:
return self.asks[0] if self.asks else None
@property
def spread(self) -> Optional[float]:
if self.best_bid and self.best_ask:
return self.best_ask.price - self.best_bid.price
return None
@property
def spread_bps(self) -> Optional[float]:
"""Spread in basis points"""
if self.spread and self.mid_price:
return (self.spread / self.mid_price) * 10000
return None
@property
def mid_price(self) -> Optional[float]:
if self.best_bid and self.best_ask:
return (self.best_bid.price + self.best_ask.price) / 2
return None
@property
def bid_depth(self) -> float:
"""Total bid size"""
return sum(level.size for level in self.bids)
@property
def ask_depth(self) -> float:
"""Total ask size"""
return sum(level.size for level in self.asks)
@property
def imbalance(self) -> Optional[float]:
"""Order book imbalance: (bid - ask) / (bid + ask)"""
total = self.bid_depth + self.ask_depth
if total > 0:
return (self.bid_depth - self.ask_depth) / total
return None
def to_dict(self) -> dict:
return {
"exchange": self.exchange,
"symbol": self.symbol,
"timestamp": self.timestamp.isoformat(),
"local_timestamp": self.local_timestamp.isoformat(),
"bids": [level.to_dict() for level in self.bids],
"asks": [level.to_dict() for level in self.asks],
"best_bid": self.best_bid.to_dict() if self.best_bid else None,
"best_ask": self.best_ask.to_dict() if self.best_ask else None,
"spread": self.spread,
"spread_bps": self.spread_bps,
"mid_price": self.mid_price,
"bid_depth": self.bid_depth,
"ask_depth": self.ask_depth,
"imbalance": self.imbalance,
"sequence": self.sequence
}
def to_sql_tuple(self) -> Tuple:
return (
self.exchange,
self.symbol,
self.timestamp,
self.local_timestamp,
json.dumps(self.bids[:10]), # Top 10 levels
json.dumps(self.asks[:10]),
self.best_bid.price if self.best_bid else None,
self.best_bid.size if self.best_bid else None,
self.best_ask.price if self.best_ask else None,
self.best_ask.size if self.best_ask else None,
self.spread,
self.spread_bps,
self.mid_price,
self.bid_depth,
self.ask_depth,
self.imbalance,
self.sequence
)
@staticmethod
def normalize_symbol(exchange: str, symbol: str) -> str:
"""Normalize symbol format across exchanges"""
# Remove common separators and convert to uppercase
normalized = symbol.replace("-", "").replace("_", "").replace("/", "").upper()
# Exchange-specific mappings
if exchange == "binance":
if normalized.endswith("USDT"):
return normalized
elif exchange == "okx":
if normalized.endswith("USDT"):
return normalized.replace("USDT", "USDT") # OKX uses BTC-USDT format
elif exchange == "hyperliquid":
# Hyperliquid uses BTC, ETH without suffix for perpetuals
return normalized
return normalized
class OrderBookNormalizer:
"""Normalize order book data from different exchanges to unified format"""
@staticmethod
def normalize_binance(data: dict) -> OrderBook:
"""Normalize Binance order book data"""
# Binance timestamp is in milliseconds
ts = datetime.utcfromtimestamp(data.get("timestamp", 0) / 1000)
bids = [
PriceLevel(price=float(bid[0]), size=float(bid[1]), order_count=int(bid[2]) if len(bid) > 2 else 0)
for bid in data.get("bids", [])[:20]
]
asks = [
PriceLevel(price=float(ask[0]), size=float(ask[1]), order_count=int(ask[2]) if len(ask) > 2 else 0)
for ask in data.get("asks", [])[:20]
]
return OrderBook(
exchange="binance",
symbol=data.get("symbol", "").upper(),
timestamp=ts,
bids=bids,
asks=asks,
sequence=data.get("sequenceId", 0),
raw_data=data
)
@staticmethod
def normalize_okx(data: dict) -> OrderBook:
"""Normalize OKX order book data"""
# OKX timestamp format: "2024-01-15T10:30:00.000Z"
ts = datetime.fromisoformat(data.get("timestamp", "2024-01-01T00:00:00.000Z").replace("Z", "+00:00"))
bids = [
PriceLevel(price=float(bid[0]), size=float(bid[1]), order_count=int(bid[2]) if len(bid) > 2 else 0)
for bid in data.get("bids", [])[:20]
]
asks = [
PriceLevel(price=float(ask[0]), size=float(ask[1]), order_count=int(ask[2]) if len(ask) > 2 else 0)
for ask in data.get("asks", [])[:20]
]
return OrderBook(
exchange="okx",
symbol=data.get("symbol", "").replace("-", "").upper(),
timestamp=ts,
bids=bids,
asks=asks,
sequence=data.get("sequenceId", 0),
raw_data=data
)
@staticmethod
def normalize_hyperliquid(data: dict) -> OrderBook:
"""Normalize Hyperliquid order book data"""
# Hyperliquid uses different format
ts = datetime.utcnow()
# Extract levels from Hyperliquid format
book = data.get("data", {}).get("orderbook", {})
bids = [
PriceLevel(price=float(bid[0]), size=float(bid[1]))
for bid in book.get("bids", [])[:20]
]
asks = [
PriceLevel(price=float(ask[0]), size=float(ask[1]))
for ask in book.get("asks", [])[:20]
]
return OrderBook(
exchange="hyperliquid",
symbol=data.get("data", {}).get("symbol", "").upper(),
timestamp=ts,
bids=bids,
asks=asks,
sequence=data.get("data", {}).get("seqNum", 0),
raw_data=data
)
Base Client และ Async Architecture
# clients/base_client.py
import asyncio
import json
import logging
from abc import ABC, abstractmethod
from typing import Dict, List, Callable, Optional, Any
from datetime import datetime
import aiohttp
from dataclasses import dataclass, field
from collections import deque
import signal
import sys
from models.orderbook import OrderBook, OrderBookNormalizer
from config.settings import settings
logger = logging.getLogger(__name__)
@dataclass
class StreamStats:
"""Statistics for a stream"""
messages_received: int = 0
messages_processed: int = 0
errors: int = 0
last_message_time: Optional[datetime] = None
latency_ms: deque = field(default_factory=lambda: deque(maxlen=1000))
@property
def avg_latency_ms(self) -> float:
return sum(self.latency_ms) / len(self.latency_ms) if self.latency_ms else 0
@property
def p99_latency_ms(self) -> float:
if not self.latency_ms:
return 0
sorted_latencies = sorted(self.latency_ms)
idx = int(len(sorted_latencies) * 0.99)
return sorted_latencies[idx]
class BaseTardisClient(ABC):
"""Base class for Tardis.dev unified stream consumer"""
def __init__(
self,
exchange: str,
symbols: List[str],
channels: List[str],
on_message: Optional[Callable[[OrderBook], None]] = None,
buffer_size: int = 1000,
flush_interval: int = 5
):
self.exchange = exchange
self.symbols = symbols
self.channels = channels
self.on_message = on_message
self.buffer_size = buffer_size
self.flush_interval = flush_interval
self.websocket_url = settings.get_tardis_url(exchange)
self.ws: Optional[aiohttp.ClientWebSocketResponse] = None
self.session: Optional[aiohttp.ClientSession] = None
self.buffer: deque = deque(maxlen=buffer_size)
self.stats = StreamStats()
self._running = False
self._subscribed = False
# Setup graceful shutdown
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
def _signal_handler(self, signum, frame):
"""Handle shutdown signals gracefully"""
logger.info(f"Received signal {signum}, initiating graceful shutdown...")
self._running = False
async def connect(self) -> bool:
"""Establish WebSocket connection to Tardis.dev"""
try:
self.session = aiohttp.ClientSession()
# Build subscription message
subscribe_msg = {
"type": "subscribe",
"exchange": self.exchange,
"symbols": self.symbols,
"channels": self.channels
}
# Connect with headers
headers = {
"Authorization": f"Bearer {settings.TARDIS_API_KEY}"
}
logger.info(f"Connecting to {self.websocket_url}")
self.ws = await self.session.ws_connect(
self.websocket_url,
headers=headers,
timeout=aiohttp.ClientTimeout(total=30)
)
# Send subscription
await self.ws.send_json(subscribe_msg)
logger.info(f"Subscribed to {self.exchange}: {self.symbols}")
self._subscribed = True
return True
except aiohttp.ClientError as e:
logger.error(f"Connection failed: {e}")
return False
except Exception as e:
logger.error(f"Unexpected error during connection: {e}")
return False
async def disconnect(self):
"""Close WebSocket connection"""
self._running = False
if self.ws:
await self.ws.close()
logger.info(f"Disconnected from {self.exchange}")
if self.session:
await self.session.close()
async def consume(self):
"""Main consume loop"""
self._running = True
while self._running:
try:
if not self.ws or self.ws.closed:
connected = await self.connect()
if not connected:
await asyncio.sleep(5) # Retry after 5 seconds
continue
msg = await self.ws.receive()
if msg.type == aiohttp.WSMsgType.TEXT:
await self._handle_message(msg.data)
elif msg.type == aiohttp.WSMsgType.ERROR:
logger.error(f"WebSocket error: {msg.data}")
self.stats.errors += 1
elif msg.type == aiohttp.WSMsgType.CLOSED:
logger.warning("WebSocket closed by server")
break
except aiohttp.ClientError as e:
logger.error(f"Client error: {e}")
self.stats.errors += 1
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Error in consume loop: {e}")
self.stats.errors += 1
async def _handle_message(self, data: str):
"""Process incoming message"""
try:
self.stats.messages_received += 1
# Parse JSON
parsed = json.loads(data)
# Calculate latency
if "timestamp" in parsed:
msg_time = datetime.fromisoformat(
parsed["timestamp"].replace("Z", "+00:00")
) if isinstance(parsed["timestamp"], str) else datetime.utcfromtimestamp(parsed["timestamp"] / 1000)
latency = (datetime.utcnow() - msg_time).total_seconds() * 1000
self.stats.latency_ms.append(latency)
# Normalize order book
orderbook = self._normalize_message(parsed)
if orderbook:
self.stats.messages_processed += 1
self.stats.last_message_time = datetime.utcnow()
# Add to buffer
self.buffer.append(orderbook)
# Call callback
if self.on_message:
await self._safe_callback(orderbook)
# Auto flush if buffer full
if len(self.buffer) >= self.buffer_size:
await self._flush_buffer()
except json.JSONDecodeError as e:
logger.warning(f"Invalid JSON: {e}")
except Exception as e:
logger.error(f"Error processing message: {e}")
self.stats.errors += 1
async def _safe_callback(self, orderbook: OrderBook):
"""Safely execute callback"""
try:
if asyncio.iscoroutinefunction(self.on_message):
await self.on_message(orderbook)
else:
self.on_message(orderbook)
except Exception as e:
logger.error(f"Callback error: {e}")
@abstractmethod
def _normalize_message(self, data: dict) -> Optional[OrderBook]:
"""Normalize exchange-specific message to OrderBook"""
pass
async def _flush_buffer(self):
"""Flush buffer to storage"""
if self.buffer:
logger.info(f"Flushing {len(self.buffer)} messages")
self.buffer.clear()
def get_stats(self) -> dict:
"""Get stream statistics"""
return {
"exchange": self.exchange,
"messages_received": self.stats.messages_received,
"messages_processed": self.stats.messages_processed,
"errors": self.stats.errors,
"last_message_time": self.stats.last_message_time.isoformat() if self.stats.last_message_time else None,
"avg_latency_ms": round(self.stats.avg_latency_ms, 2),
"p99_latency_ms": round(self.stats.p99_latency_ms, 2),
"buffer_size": len(self.buffer)
}
class BinanceClient(BaseTardisClient):
"""Binance-specific Tardis.dev client"""
def __init__(self, symbols: List[str], **kwargs):
super().__init__(exchange="binance", symbols=symbols, channels=["orderbook"], **kwargs)
def _normalize_message(self, data: dict) -> Optional[OrderBook]:
if data.get("channel") == "orderbook":
return OrderBookNormalizer.normalize_binance(data)
return None
class OKXClient(BaseTardisClient):
"""OKX-specific Tardis.dev client"""
def __init__(self, symbols: List[str], **kwargs):
super().__init__(exchange="okx", symbols=symbols, channels=["books"], **kwargs)
def _normalize_message(self, data: dict) -> Optional[OrderBook]:
if data.get("channel") == "books" or data.get("channel") == "books-l2-tbt":
return OrderBookNormalizer.normalize_okx(data)
return None
class HyperliquidClient(BaseTardisClient):
"""Hyperliquid-specific Tardis.dev client"""
def __init__(self, symbols: List[str], **kwargs):
super().__init__(exchange="hyperliquid", symbols=symbols, channels=["orderbook"], **kwargs)
def _normalize_message(self, data: dict) -> Optional[OrderBook]:
if data.get("type") == "orderbook":
return OrderBookNormalizer.normalize_hyperliquid(data)
return None
Unified Consumer พร้อม Concurrency
# clients/unified_consumer.py
import asyncio
import logging
from typing import List, Dict, Optional, Callable
from datetime import datetime
import json
from collections import defaultdict
from clients.base_client import BaseTardisClient, BinanceClient, OKXClient, HyperliquidClient
from clients.binance_client import BinanceClient
from clients.okx_client import OKXClient
from clients.hyperliquid_client import HyperliquidClient
from models.orderbook import OrderBook
from config.settings import settings
logger = logging.getLogger(__name__)
class UnifiedOrderBookConsumer:
"""
Unified consumer for multiple exchange order books.
Handles concurrent connections with proper resource management.
"""
def __init__(
self,
exchanges: Dict[str, List[str]],
on_orderbook: Optional[Callable[[OrderBook], None]] = None,
on_error: Optional[Callable[[str, Exception], None]] = None,
max_concurrent: int = 10
):
"""
Initialize unified consumer.
Args:
exchanges: Dict mapping exchange names to list of symbols
{"binance": ["BTCUSDT", "ETHUSDT"], "okx": ["BTC-USDT"]}
on_orderbook: Callback for each order book update
on_error: Callback for errors
max_concurrent: Max concurrent stream connections
"""
self.exchanges = exchanges
self.on_orderbook = on_orderbook
self.on_error = on_error
self.max_concurrent = max_concurrent
self.clients: Dict[str, BaseTardisClient] = {}
self.tasks: List[asyncio.Task] = []
self._running = False
# Aggregate statistics
self.aggregate_stats = {
"total_messages": 0,
"total_errors": 0,
"by_exchange": defaultdict(lambda: {"messages": 0, "errors": 0})
}
self._init_clients()
def _init_clients(self):
"""Initialize clients for each exchange"""
exchange_configs = settings.EXCHANGES
for exchange_name, symbols in self.exchanges.items():
if exchange_name not in exchange_configs:
logger.warning(f"Unknown exchange: {exchange_name}")
continue
config = exchange_configs[exchange_name]
if exchange_name == "binance":
client = BinanceClient(
symbols=symbols,
on_message=self._wrap_callback(exchange_name),
buffer_size=settings.BUFFER_SIZE
)
elif exchange_name == "okx":
client = OKXClient(
symbols=symbols,
on_message=self._wrap_callback(exchange_name),
buffer_size=settings.BUFFER_SIZE
)
elif exchange_name == "hyperliquid":
client = HyperliquidClient(
symbols=symbols,
on_message=self._wrap_callback(exchange_name),
buffer_size=settings.BUFFER_SIZE
)
else:
continue
self.clients[exchange_name] = client
logger.info(f"Initialized client for {exchange_name} with symbols: {symbols}")
def _wrap_callback(self, exchange: str) -> Callable:
"""Wrap callback with error handling and stats tracking"""
async def wrapped_callback(orderbook: OrderBook):
try:
self.aggregate_stats["total_messages"] += 1
self.aggregate_stats["by_exchange"][exchange]["messages"] += 1
if self.on_orderbook:
await self._safe_callback(self.on_orderbook, orderbook)
except Exception as e:
logger.error(f"Callback error for {exchange}: {e}")
self.aggregate_stats["total_errors"] += 1
self.aggregate_stats["by_exchange"][exchange]["errors"] += 1
if self.on_error:
self.on_error(exchange, e)
return wrapped_callback
async def _safe_callback(self, callback: Callable, *args, **kwargs):
"""Safely execute callback"""
try:
if asyncio.iscoroutinefunction(callback):
await callback(*args, **kwargs)
else:
callback(*args, **kwargs)
except Exception as e:
logger.error(f"Safe callback error: {e}")
async def start(self):
"""Start all client streams concurrently"""
self._running = True
# Use semaphore to limit concurrent connections
semaphore = asyncio.Semaphore(self.max_concurrent)
async def bounded_consume(client: BaseTardisClient, exchange: str):
async with semaphore:
logger.info(f"Starting consumer for {exchange}")
try:
await client.consume()
except asyncio.CancelledError:
logger.info(f"Consumer for {exchange} cancelled")
except Exception as e:
logger.error(f"Consumer error for {exchange}: {e}")
if self.on_error:
self.on_error(exchange, e)
finally:
await client.disconnect()
# Create tasks for all clients
for exchange, client in self.clients.items():
task = asyncio.create_task(bounded_consume(client, exchange))
self.tasks.append(task)
# Wait for all tasks
await asyncio.gather(*self.tasks, return_exceptions=True)
async def stop(self):
"""Stop all streams gracefully"""
logger.info("Stopping unified consumer...")
self._running = False
# Cancel all tasks
for task in self.tasks:
if not task.done():
task.cancel()
# Wait for tasks to finish
await asyncio.gather(*self.tasks, return_exceptions=True)
# Disconnect all clients
for exchange, client in self.clients.items():
await client.disconnect()
logger.info("All consumers stopped")
def get_all_stats(self) -> dict:
"""Get statistics from all clients"""
return {
"aggregate": dict(self.aggregate_stats),
"clients": {
exchange: client.get_stats()
for exchange, client in self.clients.items()
}
}
class CrossExchangeArbitrageDetector:
"""
Detect arbitrage opportunities across exchanges using
unified order book data.
"""
def __init__(self, min_spread_bps: float = 5.0, min_size: float = 0.1):
self.min_spread_bps = min_spread_bps
self.min_size = min_size
# Store latest order books by symbol
self.order_books: Dict[str, Dict[str, OrderBook]] = defaultdict(dict)
async def on_orderbook(self, orderbook: OrderBook):
"""Process incoming order book"""
key = f"{orderbook.exchange}:{orderbook.symbol}"
self.order_books[orderbook.symbol][orderbook.exchange] = orderbook
# Check for arbitrage
await self._check_arbitrage(orderbook.symbol)
async def _check_arbitrage(self, symbol: str):
"""Check for cross-exchange arbitrage opportunities"""
if symbol not in self.order_books:
return
books = self.order_books[symbol]
if len(books) < 2:
return
opportunities = []
exchanges = list(books.keys())
for i, buy_exchange in enumerate(exchanges):
for sell_exchange in exchanges[i+1:]: