bucket aggregation
This commit is contained in:
489
COBY/connectors/binance_connector.py
Normal file
489
COBY/connectors/binance_connector.py
Normal file
@ -0,0 +1,489 @@
|
||||
"""
|
||||
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
|
Reference in New Issue
Block a user