324 lines
10 KiB
Python
324 lines
10 KiB
Python
"""
|
|
Core data models for the multi-exchange data aggregation system.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from typing import List, Dict, Optional, Any
|
|
from enum import Enum
|
|
|
|
|
|
class ConnectionStatus(Enum):
|
|
"""Exchange connection status"""
|
|
DISCONNECTED = "disconnected"
|
|
CONNECTING = "connecting"
|
|
CONNECTED = "connected"
|
|
RECONNECTING = "reconnecting"
|
|
ERROR = "error"
|
|
|
|
|
|
class ReplayStatus(Enum):
|
|
"""Replay session status"""
|
|
CREATED = "created"
|
|
RUNNING = "running"
|
|
PAUSED = "paused"
|
|
STOPPED = "stopped"
|
|
COMPLETED = "completed"
|
|
ERROR = "error"
|
|
|
|
|
|
@dataclass
|
|
class PriceLevel:
|
|
"""Individual price level in order book"""
|
|
price: float
|
|
size: float
|
|
count: Optional[int] = None
|
|
|
|
def __post_init__(self):
|
|
"""Validate price level data"""
|
|
if self.price <= 0:
|
|
raise ValueError("Price must be positive")
|
|
if self.size < 0:
|
|
raise ValueError("Size cannot be negative")
|
|
|
|
|
|
@dataclass
|
|
class OrderBookSnapshot:
|
|
"""Standardized order book snapshot"""
|
|
symbol: str
|
|
exchange: str
|
|
timestamp: datetime
|
|
bids: List[PriceLevel]
|
|
asks: List[PriceLevel]
|
|
sequence_id: Optional[int] = None
|
|
|
|
def __post_init__(self):
|
|
"""Validate and sort order book data"""
|
|
if not self.symbol:
|
|
raise ValueError("Symbol cannot be empty")
|
|
if not self.exchange:
|
|
raise ValueError("Exchange cannot be empty")
|
|
|
|
# Sort bids descending (highest price first)
|
|
self.bids.sort(key=lambda x: x.price, reverse=True)
|
|
# Sort asks ascending (lowest price first)
|
|
self.asks.sort(key=lambda x: x.price)
|
|
|
|
@property
|
|
def mid_price(self) -> Optional[float]:
|
|
"""Calculate mid price"""
|
|
if self.bids and self.asks:
|
|
return (self.bids[0].price + self.asks[0].price) / 2
|
|
return None
|
|
|
|
@property
|
|
def spread(self) -> Optional[float]:
|
|
"""Calculate bid-ask spread"""
|
|
if self.bids and self.asks:
|
|
return self.asks[0].price - self.bids[0].price
|
|
return None
|
|
|
|
@property
|
|
def bid_volume(self) -> float:
|
|
"""Total bid volume"""
|
|
return sum(level.size for level in self.bids)
|
|
|
|
@property
|
|
def ask_volume(self) -> float:
|
|
"""Total ask volume"""
|
|
return sum(level.size for level in self.asks)
|
|
|
|
|
|
@dataclass
|
|
class TradeEvent:
|
|
"""Standardized trade event"""
|
|
symbol: str
|
|
exchange: str
|
|
timestamp: datetime
|
|
price: float
|
|
size: float
|
|
side: str # 'buy' or 'sell'
|
|
trade_id: str
|
|
|
|
def __post_init__(self):
|
|
"""Validate trade event data"""
|
|
if not self.symbol:
|
|
raise ValueError("Symbol cannot be empty")
|
|
if not self.exchange:
|
|
raise ValueError("Exchange cannot be empty")
|
|
if self.price <= 0:
|
|
raise ValueError("Price must be positive")
|
|
if self.size <= 0:
|
|
raise ValueError("Size must be positive")
|
|
if self.side not in ['buy', 'sell']:
|
|
raise ValueError("Side must be 'buy' or 'sell'")
|
|
if not self.trade_id:
|
|
raise ValueError("Trade ID cannot be empty")
|
|
|
|
|
|
@dataclass
|
|
class PriceBuckets:
|
|
"""Aggregated price buckets for heatmap"""
|
|
symbol: str
|
|
timestamp: datetime
|
|
bucket_size: float
|
|
bid_buckets: Dict[float, float] = field(default_factory=dict) # price -> volume
|
|
ask_buckets: Dict[float, float] = field(default_factory=dict) # price -> volume
|
|
|
|
def __post_init__(self):
|
|
"""Validate price buckets"""
|
|
if self.bucket_size <= 0:
|
|
raise ValueError("Bucket size must be positive")
|
|
|
|
def get_bucket_price(self, price: float) -> float:
|
|
"""Get bucket price for a given price"""
|
|
return round(price / self.bucket_size) * self.bucket_size
|
|
|
|
def add_bid(self, price: float, volume: float):
|
|
"""Add bid volume to appropriate bucket"""
|
|
bucket_price = self.get_bucket_price(price)
|
|
self.bid_buckets[bucket_price] = self.bid_buckets.get(bucket_price, 0) + volume
|
|
|
|
def add_ask(self, price: float, volume: float):
|
|
"""Add ask volume to appropriate bucket"""
|
|
bucket_price = self.get_bucket_price(price)
|
|
self.ask_buckets[bucket_price] = self.ask_buckets.get(bucket_price, 0) + volume
|
|
|
|
|
|
@dataclass
|
|
class HeatmapPoint:
|
|
"""Individual heatmap data point"""
|
|
price: float
|
|
volume: float
|
|
intensity: float # 0.0 to 1.0
|
|
side: str # 'bid' or 'ask'
|
|
|
|
def __post_init__(self):
|
|
"""Validate heatmap point"""
|
|
if self.price <= 0:
|
|
raise ValueError("Price must be positive")
|
|
if self.volume < 0:
|
|
raise ValueError("Volume cannot be negative")
|
|
if not 0 <= self.intensity <= 1:
|
|
raise ValueError("Intensity must be between 0 and 1")
|
|
if self.side not in ['bid', 'ask']:
|
|
raise ValueError("Side must be 'bid' or 'ask'")
|
|
|
|
|
|
@dataclass
|
|
class HeatmapData:
|
|
"""Heatmap visualization data"""
|
|
symbol: str
|
|
timestamp: datetime
|
|
bucket_size: float
|
|
data: List[HeatmapPoint] = field(default_factory=list)
|
|
|
|
def __post_init__(self):
|
|
"""Validate heatmap data"""
|
|
if self.bucket_size <= 0:
|
|
raise ValueError("Bucket size must be positive")
|
|
|
|
def add_point(self, price: float, volume: float, side: str, max_volume: float = None):
|
|
"""Add a heatmap point with calculated intensity"""
|
|
if max_volume is None:
|
|
max_volume = max((point.volume for point in self.data), default=volume)
|
|
|
|
intensity = min(volume / max_volume, 1.0) if max_volume > 0 else 0.0
|
|
point = HeatmapPoint(price=price, volume=volume, intensity=intensity, side=side)
|
|
self.data.append(point)
|
|
|
|
def get_bids(self) -> List[HeatmapPoint]:
|
|
"""Get bid points sorted by price descending"""
|
|
bids = [point for point in self.data if point.side == 'bid']
|
|
return sorted(bids, key=lambda x: x.price, reverse=True)
|
|
|
|
def get_asks(self) -> List[HeatmapPoint]:
|
|
"""Get ask points sorted by price ascending"""
|
|
asks = [point for point in self.data if point.side == 'ask']
|
|
return sorted(asks, key=lambda x: x.price)
|
|
|
|
|
|
@dataclass
|
|
class OrderBookMetrics:
|
|
"""Order book analysis metrics"""
|
|
symbol: str
|
|
exchange: str
|
|
timestamp: datetime
|
|
mid_price: float
|
|
spread: float
|
|
spread_percentage: float
|
|
bid_volume: float
|
|
ask_volume: float
|
|
volume_imbalance: float # (bid_volume - ask_volume) / (bid_volume + ask_volume)
|
|
depth_10: float # Volume within 10 price levels
|
|
depth_50: float # Volume within 50 price levels
|
|
|
|
def __post_init__(self):
|
|
"""Validate metrics"""
|
|
if self.mid_price <= 0:
|
|
raise ValueError("Mid price must be positive")
|
|
if self.spread < 0:
|
|
raise ValueError("Spread cannot be negative")
|
|
|
|
|
|
@dataclass
|
|
class ImbalanceMetrics:
|
|
"""Order book imbalance metrics"""
|
|
symbol: str
|
|
timestamp: datetime
|
|
volume_imbalance: float
|
|
price_imbalance: float
|
|
depth_imbalance: float
|
|
momentum_score: float # Derived from recent imbalance changes
|
|
|
|
def __post_init__(self):
|
|
"""Validate imbalance metrics"""
|
|
if not -1 <= self.volume_imbalance <= 1:
|
|
raise ValueError("Volume imbalance must be between -1 and 1")
|
|
|
|
|
|
@dataclass
|
|
class ConsolidatedOrderBook:
|
|
"""Consolidated order book from multiple exchanges"""
|
|
symbol: str
|
|
timestamp: datetime
|
|
exchanges: List[str]
|
|
bids: List[PriceLevel]
|
|
asks: List[PriceLevel]
|
|
weighted_mid_price: float
|
|
total_bid_volume: float
|
|
total_ask_volume: float
|
|
exchange_weights: Dict[str, float] = field(default_factory=dict)
|
|
|
|
def __post_init__(self):
|
|
"""Validate consolidated order book"""
|
|
if not self.exchanges:
|
|
raise ValueError("At least one exchange must be specified")
|
|
if self.weighted_mid_price <= 0:
|
|
raise ValueError("Weighted mid price must be positive")
|
|
|
|
|
|
@dataclass
|
|
class ExchangeStatus:
|
|
"""Exchange connection and health status"""
|
|
exchange: str
|
|
status: ConnectionStatus
|
|
last_message_time: Optional[datetime] = None
|
|
error_message: Optional[str] = None
|
|
connection_count: int = 0
|
|
uptime_percentage: float = 0.0
|
|
message_rate: float = 0.0 # Messages per second
|
|
|
|
def __post_init__(self):
|
|
"""Validate exchange status"""
|
|
if not self.exchange:
|
|
raise ValueError("Exchange name cannot be empty")
|
|
if not 0 <= self.uptime_percentage <= 100:
|
|
raise ValueError("Uptime percentage must be between 0 and 100")
|
|
|
|
|
|
@dataclass
|
|
class SystemMetrics:
|
|
"""System performance metrics"""
|
|
timestamp: datetime
|
|
cpu_usage: float
|
|
memory_usage: float
|
|
disk_usage: float
|
|
network_io: Dict[str, float] = field(default_factory=dict)
|
|
database_connections: int = 0
|
|
redis_connections: int = 0
|
|
active_websockets: int = 0
|
|
messages_per_second: float = 0.0
|
|
processing_latency: float = 0.0 # Milliseconds
|
|
|
|
def __post_init__(self):
|
|
"""Validate system metrics"""
|
|
if not 0 <= self.cpu_usage <= 100:
|
|
raise ValueError("CPU usage must be between 0 and 100")
|
|
if not 0 <= self.memory_usage <= 100:
|
|
raise ValueError("Memory usage must be between 0 and 100")
|
|
|
|
|
|
@dataclass
|
|
class ReplaySession:
|
|
"""Historical data replay session"""
|
|
session_id: str
|
|
start_time: datetime
|
|
end_time: datetime
|
|
speed: float # Playback speed multiplier
|
|
status: ReplayStatus
|
|
current_time: Optional[datetime] = None
|
|
progress: float = 0.0 # 0.0 to 1.0
|
|
symbols: List[str] = field(default_factory=list)
|
|
exchanges: List[str] = field(default_factory=list)
|
|
|
|
def __post_init__(self):
|
|
"""Validate replay session"""
|
|
if not self.session_id:
|
|
raise ValueError("Session ID cannot be empty")
|
|
if self.start_time >= self.end_time:
|
|
raise ValueError("Start time must be before end time")
|
|
if self.speed <= 0:
|
|
raise ValueError("Speed must be positive")
|
|
if not 0 <= self.progress <= 1:
|
|
raise ValueError("Progress must be between 0 and 1") |