COBY : specs + task 1
This commit is contained in:
31
COBY/models/__init__.py
Normal file
31
COBY/models/__init__.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""
|
||||
Data models for the multi-exchange data aggregation system.
|
||||
"""
|
||||
|
||||
from .core import (
|
||||
OrderBookSnapshot,
|
||||
PriceLevel,
|
||||
TradeEvent,
|
||||
PriceBuckets,
|
||||
HeatmapData,
|
||||
HeatmapPoint,
|
||||
ConnectionStatus,
|
||||
OrderBookMetrics,
|
||||
ImbalanceMetrics,
|
||||
ConsolidatedOrderBook,
|
||||
ReplayStatus
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'OrderBookSnapshot',
|
||||
'PriceLevel',
|
||||
'TradeEvent',
|
||||
'PriceBuckets',
|
||||
'HeatmapData',
|
||||
'HeatmapPoint',
|
||||
'ConnectionStatus',
|
||||
'OrderBookMetrics',
|
||||
'ImbalanceMetrics',
|
||||
'ConsolidatedOrderBook',
|
||||
'ReplayStatus'
|
||||
]
|
324
COBY/models/core.py
Normal file
324
COBY/models/core.py
Normal file
@ -0,0 +1,324 @@
|
||||
"""
|
||||
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")
|
Reference in New Issue
Block a user