""" Binance exchange connector implementation. """ import json from typing import Dict, List, Optional, Any from datetime import datetime, timezone from ..models.core import OrderBookSnapshot, TradeEvent, PriceLevel from ..utils.logging import get_logger, set_correlation_id from ..utils.exceptions import ValidationError from ..utils.validation import validate_symbol, validate_price, validate_volume from .base_connector import BaseExchangeConnector logger = get_logger(__name__) class BinanceConnector(BaseExchangeConnector): """ Binance WebSocket connector implementation. Supports: - Order book depth streams - Trade streams - Symbol normalization - Real-time data processing """ # Binance WebSocket URLs WEBSOCKET_URL = "wss://stream.binance.com:9443/ws" API_URL = "https://api.binance.com/api/v3" def __init__(self): """Initialize Binance connector""" super().__init__("binance", self.WEBSOCKET_URL) # Binance-specific message handlers self.message_handlers.update({ 'depthUpdate': self._handle_orderbook_update, 'trade': self._handle_trade_update, 'error': self._handle_error_message }) # Stream management self.active_streams: List[str] = [] self.stream_id = 1 logger.info("Binance connector initialized") def _get_message_type(self, data: Dict) -> str: """ Determine message type from Binance message data. Args: data: Parsed message data Returns: str: Message type identifier """ # Binance uses 'e' field for event type if 'e' in data: return data['e'] # Handle error messages if 'error' in data: return 'error' # Handle subscription confirmations if 'result' in data and 'id' in data: return 'subscription_response' return 'unknown' def normalize_symbol(self, symbol: str) -> str: """ Normalize symbol to Binance format. Args: symbol: Standard symbol format (e.g., 'BTCUSDT') Returns: str: Binance symbol format (e.g., 'BTCUSDT') """ # Binance uses uppercase symbols without separators normalized = symbol.upper().replace('-', '').replace('/', '') # Validate symbol format if not validate_symbol(normalized): raise ValidationError(f"Invalid symbol format: {symbol}", "INVALID_SYMBOL") return normalized async def subscribe_orderbook(self, symbol: str) -> None: """ Subscribe to order book depth updates for a symbol. Args: symbol: Trading symbol (e.g., 'BTCUSDT') """ try: set_correlation_id() normalized_symbol = self.normalize_symbol(symbol) stream_name = f"{normalized_symbol.lower()}@depth@100ms" # Create subscription message subscription_msg = { "method": "SUBSCRIBE", "params": [stream_name], "id": self.stream_id } # Send subscription success = await self._send_message(subscription_msg) if success: # Track subscription if symbol not in self.subscriptions: self.subscriptions[symbol] = [] if 'orderbook' not in self.subscriptions[symbol]: self.subscriptions[symbol].append('orderbook') self.active_streams.append(stream_name) self.stream_id += 1 logger.info(f"Subscribed to order book for {symbol} on Binance") else: logger.error(f"Failed to subscribe to order book for {symbol} on Binance") except Exception as e: logger.error(f"Error subscribing to order book for {symbol}: {e}") raise async def subscribe_trades(self, symbol: str) -> None: """ Subscribe to trade updates for a symbol. Args: symbol: Trading symbol (e.g., 'BTCUSDT') """ try: set_correlation_id() normalized_symbol = self.normalize_symbol(symbol) stream_name = f"{normalized_symbol.lower()}@trade" # Create subscription message subscription_msg = { "method": "SUBSCRIBE", "params": [stream_name], "id": self.stream_id } # Send subscription success = await self._send_message(subscription_msg) if success: # Track subscription if symbol not in self.subscriptions: self.subscriptions[symbol] = [] if 'trades' not in self.subscriptions[symbol]: self.subscriptions[symbol].append('trades') self.active_streams.append(stream_name) self.stream_id += 1 logger.info(f"Subscribed to trades for {symbol} on Binance") else: logger.error(f"Failed to subscribe to trades for {symbol} on Binance") except Exception as e: logger.error(f"Error subscribing to trades for {symbol}: {e}") raise async def unsubscribe_orderbook(self, symbol: str) -> None: """ Unsubscribe from order book updates for a symbol. Args: symbol: Trading symbol (e.g., 'BTCUSDT') """ try: normalized_symbol = self.normalize_symbol(symbol) stream_name = f"{normalized_symbol.lower()}@depth@100ms" # Create unsubscription message unsubscription_msg = { "method": "UNSUBSCRIBE", "params": [stream_name], "id": self.stream_id } # Send unsubscription success = await self._send_message(unsubscription_msg) if success: # Remove from tracking if symbol in self.subscriptions and 'orderbook' in self.subscriptions[symbol]: self.subscriptions[symbol].remove('orderbook') if not self.subscriptions[symbol]: del self.subscriptions[symbol] if stream_name in self.active_streams: self.active_streams.remove(stream_name) self.stream_id += 1 logger.info(f"Unsubscribed from order book for {symbol} on Binance") else: logger.error(f"Failed to unsubscribe from order book for {symbol} on Binance") except Exception as e: logger.error(f"Error unsubscribing from order book for {symbol}: {e}") raise async def unsubscribe_trades(self, symbol: str) -> None: """ Unsubscribe from trade updates for a symbol. Args: symbol: Trading symbol (e.g., 'BTCUSDT') """ try: normalized_symbol = self.normalize_symbol(symbol) stream_name = f"{normalized_symbol.lower()}@trade" # Create unsubscription message unsubscription_msg = { "method": "UNSUBSCRIBE", "params": [stream_name], "id": self.stream_id } # Send unsubscription success = await self._send_message(unsubscription_msg) if success: # Remove from tracking if symbol in self.subscriptions and 'trades' in self.subscriptions[symbol]: self.subscriptions[symbol].remove('trades') if not self.subscriptions[symbol]: del self.subscriptions[symbol] if stream_name in self.active_streams: self.active_streams.remove(stream_name) self.stream_id += 1 logger.info(f"Unsubscribed from trades for {symbol} on Binance") else: logger.error(f"Failed to unsubscribe from trades for {symbol} on Binance") except Exception as e: logger.error(f"Error unsubscribing from trades for {symbol}: {e}") raise async def get_symbols(self) -> List[str]: """ Get list of available trading symbols from Binance. Returns: List[str]: List of available symbols """ try: import aiohttp async with aiohttp.ClientSession() as session: async with session.get(f"{self.API_URL}/exchangeInfo") as response: if response.status == 200: data = await response.json() symbols = [ symbol_info['symbol'] for symbol_info in data.get('symbols', []) if symbol_info.get('status') == 'TRADING' ] logger.info(f"Retrieved {len(symbols)} symbols from Binance") return symbols else: logger.error(f"Failed to get symbols from Binance: HTTP {response.status}") return [] except Exception as e: logger.error(f"Error getting symbols from Binance: {e}") return [] async def get_orderbook_snapshot(self, symbol: str, depth: int = 20) -> Optional[OrderBookSnapshot]: """ Get current order book snapshot from Binance REST API. Args: symbol: Trading symbol depth: Number of price levels to retrieve Returns: OrderBookSnapshot: Current order book or None if unavailable """ try: import aiohttp normalized_symbol = self.normalize_symbol(symbol) # Binance supports depths: 5, 10, 20, 50, 100, 500, 1000, 5000 valid_depths = [5, 10, 20, 50, 100, 500, 1000, 5000] api_depth = min(valid_depths, key=lambda x: abs(x - depth)) url = f"{self.API_URL}/depth" params = { 'symbol': normalized_symbol, 'limit': api_depth } async with aiohttp.ClientSession() as session: async with session.get(url, params=params) as response: if response.status == 200: data = await response.json() return self._parse_orderbook_snapshot(data, symbol) else: logger.error(f"Failed to get order book for {symbol}: HTTP {response.status}") return None except Exception as e: logger.error(f"Error getting order book snapshot for {symbol}: {e}") return None def _parse_orderbook_snapshot(self, data: Dict, symbol: str) -> OrderBookSnapshot: """ Parse Binance order book data into OrderBookSnapshot. Args: data: Raw Binance order book data symbol: Trading symbol Returns: OrderBookSnapshot: Parsed order book """ try: # Parse bids and asks bids = [] for bid_data in data.get('bids', []): price = float(bid_data[0]) size = float(bid_data[1]) if validate_price(price) and validate_volume(size): bids.append(PriceLevel(price=price, size=size)) asks = [] for ask_data in data.get('asks', []): price = float(ask_data[0]) size = float(ask_data[1]) if validate_price(price) and validate_volume(size): asks.append(PriceLevel(price=price, size=size)) # Create order book snapshot orderbook = OrderBookSnapshot( symbol=symbol, exchange=self.exchange_name, timestamp=datetime.now(timezone.utc), bids=bids, asks=asks, sequence_id=data.get('lastUpdateId') ) return orderbook except Exception as e: logger.error(f"Error parsing order book snapshot: {e}") raise ValidationError(f"Invalid order book data: {e}", "PARSE_ERROR") async def _handle_orderbook_update(self, data: Dict) -> None: """ Handle order book depth update from Binance. Args: data: Order book update data """ try: set_correlation_id() # Extract symbol from stream name stream = data.get('s', '').upper() if not stream: logger.warning("Order book update missing symbol") return # Parse bids and asks bids = [] for bid_data in data.get('b', []): price = float(bid_data[0]) size = float(bid_data[1]) if validate_price(price) and validate_volume(size): bids.append(PriceLevel(price=price, size=size)) asks = [] for ask_data in data.get('a', []): price = float(ask_data[0]) size = float(ask_data[1]) if validate_price(price) and validate_volume(size): asks.append(PriceLevel(price=price, size=size)) # Create order book snapshot orderbook = OrderBookSnapshot( symbol=stream, exchange=self.exchange_name, timestamp=datetime.fromtimestamp(data.get('E', 0) / 1000, tz=timezone.utc), bids=bids, asks=asks, sequence_id=data.get('u') # Final update ID ) # Notify callbacks self._notify_data_callbacks(orderbook) logger.debug(f"Processed order book update for {stream}") except Exception as e: logger.error(f"Error handling order book update: {e}") async def _handle_trade_update(self, data: Dict) -> None: """ Handle trade update from Binance. Args: data: Trade update data """ try: set_correlation_id() # Extract trade data symbol = data.get('s', '').upper() if not symbol: logger.warning("Trade update missing symbol") return price = float(data.get('p', 0)) size = float(data.get('q', 0)) # Validate data if not validate_price(price) or not validate_volume(size): logger.warning(f"Invalid trade data: price={price}, size={size}") return # Determine side (Binance uses 'm' field - true if buyer is market maker) is_buyer_maker = data.get('m', False) side = 'sell' if is_buyer_maker else 'buy' # Create trade event trade = TradeEvent( symbol=symbol, exchange=self.exchange_name, timestamp=datetime.fromtimestamp(data.get('T', 0) / 1000, tz=timezone.utc), price=price, size=size, side=side, trade_id=str(data.get('t', '')) ) # Notify callbacks self._notify_data_callbacks(trade) logger.debug(f"Processed trade for {symbol}: {side} {size} @ {price}") except Exception as e: logger.error(f"Error handling trade update: {e}") async def _handle_error_message(self, data: Dict) -> None: """ Handle error message from Binance. Args: data: Error message data """ error_code = data.get('code', 'unknown') error_msg = data.get('msg', 'Unknown error') logger.error(f"Binance error {error_code}: {error_msg}") # Handle specific error codes if error_code == -1121: # Invalid symbol logger.error("Invalid symbol error - check symbol format") elif error_code == -1130: # Invalid listen key logger.error("Invalid listen key - may need to reconnect") def get_binance_stats(self) -> Dict[str, Any]: """Get Binance-specific statistics""" base_stats = self.get_stats() binance_stats = { 'active_streams': len(self.active_streams), 'stream_list': self.active_streams.copy(), 'next_stream_id': self.stream_id } base_stats.update(binance_stats) return base_stats