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