""" Trade data transformation with safety limits. This module handles the transformation of trade data while enforcing safety limits to prevent errors and protect against edge cases. """ import logging from datetime import datetime, timezone from decimal import Decimal, InvalidOperation from typing import Dict, List, Optional, Any from ..data_types import StandardizedTrade from .time_utils import timestamp_to_datetime from .numeric_utils import safe_decimal_conversion from .normalization import normalize_trade_side, validate_symbol_format, normalize_exchange_name from .safety import ( validate_trade_size, validate_trade_price, TradeLimits, get_trade_limits ) # Create a logger for this module logger = logging.getLogger(__name__) def create_standardized_trade( symbol: str, trade_id: str, price: Any, size: Any, side: str, timestamp: Any, exchange: str, raw_data: Optional[Dict[str, Any]] = None, is_milliseconds: bool = True ) -> StandardizedTrade: """ Utility function to create StandardizedTrade with proper validation. Args: symbol: Trading symbol trade_id: Trade identifier price: Trade price (any numeric type) size: Trade size (any numeric type) side: Trade side ('buy' or 'sell') timestamp: Trade timestamp exchange: Exchange name raw_data: Original raw data is_milliseconds: True if timestamp is in milliseconds Returns: StandardizedTrade object Raises: ValueError: If data is invalid """ # Validate symbol if not symbol or not isinstance(symbol, str): raise ValueError(f"Invalid symbol: {symbol}") # Validate trade_id if not trade_id: raise ValueError(f"Invalid trade_id: {trade_id}") # Convert timestamp try: if isinstance(timestamp, (int, float, str)): dt = timestamp_to_datetime(timestamp, is_milliseconds) elif isinstance(timestamp, datetime): dt = timestamp if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) else: raise ValueError(f"Invalid timestamp type: {type(timestamp)}") except Exception as e: raise ValueError(f"Invalid timestamp: {timestamp}") from e # Convert price and size to Decimal try: if not price or not size: raise ValueError("Price and size must not be empty") decimal_price = safe_decimal_conversion(price, "price") decimal_size = safe_decimal_conversion(size, "size") if decimal_price is None or decimal_size is None: raise ValueError("Invalid price or size format") if decimal_price <= 0: raise ValueError(f"Price must be positive: {price}") if decimal_size <= 0: raise ValueError(f"Size must be positive: {size}") except (InvalidOperation, TypeError, ValueError) as e: raise ValueError(f"Invalid price or size: {e}") # Normalize side with strict validation try: if not side or not isinstance(side, str): raise ValueError(f"Invalid trade side: {side}") normalized_side = normalize_trade_side(side, logger=logger) except ValueError as e: logger.error(f"Trade side validation failed: {e}") raise ValueError(f"Invalid trade side: {side}") # Normalize symbol and exchange try: normalized_symbol = validate_symbol_format(symbol) normalized_exchange = normalize_exchange_name(exchange) except ValueError as e: raise ValueError(str(e)) return StandardizedTrade( symbol=normalized_symbol, trade_id=str(trade_id), price=decimal_price, size=decimal_size, side=normalized_side, timestamp=dt, exchange=normalized_exchange, raw_data=raw_data ) def batch_create_standardized_trades( raw_trades: List[Dict[str, Any]], symbol: str, exchange: str, field_mapping: Dict[str, str], is_milliseconds: bool = True ) -> List[StandardizedTrade]: """ Batch create standardized trades from raw data. Args: raw_trades: List of raw trade dictionaries symbol: Trading symbol exchange: Exchange name field_mapping: Mapping of StandardizedTrade fields to raw data fields is_milliseconds: True if timestamps are in milliseconds Returns: List of successfully created StandardizedTrade objects Example field_mapping: { 'trade_id': 'id', 'price': 'px', 'size': 'sz', 'side': 'side', 'timestamp': 'ts' } """ trades = [] for raw_trade in raw_trades: try: trade = create_standardized_trade( symbol=symbol, trade_id=raw_trade[field_mapping['trade_id']], price=raw_trade[field_mapping['price']], size=raw_trade[field_mapping['size']], side=raw_trade[field_mapping['side']], timestamp=raw_trade[field_mapping['timestamp']], exchange=exchange, raw_data=raw_trade, is_milliseconds=is_milliseconds ) trades.append(trade) except Exception as e: # Log error but continue processing print(f"Failed to transform trade: {e}") return trades class TradeTransformer: """Transform trade data with safety checks.""" VALID_SIDES = {'buy', 'sell'} def __init__(self, market_data_provider: Optional[Any] = None): """ Initialize transformer. Args: market_data_provider: Optional provider of market data for price validation """ self.market_data_provider = market_data_provider def normalize_trade_side(self, side: str) -> str: """ Normalize trade side to standard format. Args: side: Trade side indicator Returns: Normalized trade side ('buy' or 'sell') Raises: ValueError: If side is invalid """ side_lower = str(side).lower().strip() # Handle common variations if side_lower in {'buy', 'bid', 'long', '1', 'true'}: return 'buy' elif side_lower in {'sell', 'ask', 'short', '0', 'false'}: return 'sell' raise ValueError(f"Invalid trade side: {side}") def normalize_trade_size( self, size: Any, price: Any, symbol: str ) -> Decimal: """ Normalize and validate trade size. Args: size: Raw trade size price: Trade price for notional calculations symbol: Trading pair symbol Returns: Normalized trade size as Decimal Raises: ValueError: If size is invalid or violates limits """ try: size_decimal = Decimal(str(size)) price_decimal = Decimal(str(price)) except (TypeError, ValueError) as e: raise ValueError(f"Invalid trade size or price format: {e}") if size_decimal <= 0: raise ValueError(f"Trade size must be positive: {size}") # Get limits and validate limits = get_trade_limits(symbol) # Round to appropriate precision size_decimal = round(size_decimal, limits.size_precision) # Validate against limits validate_trade_size( size_decimal, price_decimal, symbol, logger ) return size_decimal def normalize_trade_price( self, price: Any, symbol: str ) -> Decimal: """ Normalize and validate trade price. Args: price: Raw trade price symbol: Trading pair symbol Returns: Normalized price as Decimal Raises: ValueError: If price is invalid or violates limits """ try: price_decimal = Decimal(str(price)) except (TypeError, ValueError) as e: raise ValueError(f"Invalid price format: {e}") if price_decimal <= 0: raise ValueError(f"Price must be positive: {price}") # Get limits and round to appropriate precision limits = get_trade_limits(symbol) price_decimal = round(price_decimal, limits.price_precision) # Get market price if available market_price = None if self.market_data_provider is not None: try: market_price = self.market_data_provider.get_price(symbol) except Exception as e: logger.warning(f"Failed to get market price for {symbol}: {e}") # Validate against limits and market price validate_trade_price( price_decimal, market_price, symbol, logger ) return price_decimal def transform_trade( self, trade_data: Dict[str, Any] ) -> Dict[str, Any]: """ Transform trade data with safety checks. Args: trade_data: Raw trade data Returns: Transformed trade data with normalized values Raises: ValueError: If any validation fails """ if not isinstance(trade_data, dict): raise ValueError(f"Trade data must be a dictionary: {trade_data}") # Required fields required = {'symbol', 'side', 'size', 'price'} missing = required - set(trade_data.keys()) if missing: raise ValueError(f"Missing required fields: {missing}") # Validate and normalize symbol symbol = str(trade_data['symbol']).upper() validate_symbol_format(symbol, logger) # Transform with safety checks transformed = { 'symbol': symbol, 'side': self.normalize_trade_side(trade_data['side']), 'size': self.normalize_trade_size( trade_data['size'], trade_data['price'], symbol ), 'price': self.normalize_trade_price( trade_data['price'], symbol ) } # Copy any additional fields for key, value in trade_data.items(): if key not in transformed: transformed[key] = value return transformed