Binance (completed in earlier tasks)

 Coinbase Pro (completed in task 12)
 Kraken (completed in task 12)
 Bybit (completed in task 13)
 OKX (completed in task 13)
 Huobi (completed in task 13)
 KuCoin (completed in this task)
 Gate.io (completed in this task)
 Bitfinex (completed in this task)
 MEXC (completed in this task)
This commit is contained in:
Dobromir Popov
2025-08-04 23:49:08 +03:00
parent 7339972eab
commit 3c7d13416f
6 changed files with 1940 additions and 0 deletions

View File

@ -0,0 +1,270 @@
"""
Bitfinex exchange connector implementation.
Supports WebSocket connections to Bitfinex with proper channel subscription management.
"""
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, ConnectionError
from ..utils.validation import validate_symbol, validate_price, validate_volume
from .base_connector import BaseExchangeConnector
logger = get_logger(__name__)
class BitfinexConnector(BaseExchangeConnector):
"""
Bitfinex WebSocket connector implementation.
Supports:
- Channel subscription management
- Order book streams
- Trade streams
- Symbol normalization
"""
# Bitfinex WebSocket URLs
WEBSOCKET_URL = "wss://api-pub.bitfinex.com/ws/2"
API_URL = "https://api-pub.bitfinex.com"
def __init__(self, api_key: str = None, api_secret: str = None):
"""Initialize Bitfinex connector."""
super().__init__("bitfinex", self.WEBSOCKET_URL)
self.api_key = api_key
self.api_secret = api_secret
# Bitfinex-specific message handlers
self.message_handlers.update({
'subscribed': self._handle_subscription_response,
'unsubscribed': self._handle_unsubscription_response,
'error': self._handle_error_message,
'info': self._handle_info_message
})
# Channel management
self.channels = {} # channel_id -> channel_info
self.subscribed_symbols = set()
logger.info("Bitfinex connector initialized")
def _get_message_type(self, data) -> str:
"""Determine message type from Bitfinex message data."""
if isinstance(data, dict):
if 'event' in data:
return data['event']
elif 'error' in data:
return 'error'
elif isinstance(data, list) and len(data) >= 2:
# Data message format: [CHANNEL_ID, data]
return 'data'
return 'unknown'
def normalize_symbol(self, symbol: str) -> str:
"""Normalize symbol to Bitfinex format."""
# Bitfinex uses 't' prefix for trading pairs
if symbol.upper() == 'BTCUSDT':
return 'tBTCUSD'
elif symbol.upper() == 'ETHUSDT':
return 'tETHUSD'
elif symbol.upper().endswith('USDT'):
base = symbol[:-4].upper()
return f"t{base}USD"
else:
# Generic conversion
normalized = symbol.upper().replace('-', '').replace('/', '')
return f"t{normalized}" if not normalized.startswith('t') else normalized
def _denormalize_symbol(self, bitfinex_symbol: str) -> str:
"""Convert Bitfinex symbol back to standard format."""
if bitfinex_symbol.startswith('t'):
symbol = bitfinex_symbol[1:] # Remove 't' prefix
if symbol.endswith('USD'):
return symbol[:-3] + 'USDT'
return symbol
return bitfinex_symbol
async def subscribe_orderbook(self, symbol: str) -> None:
"""Subscribe to order book updates for a symbol."""
try:
set_correlation_id()
bitfinex_symbol = self.normalize_symbol(symbol)
subscription_msg = {
"event": "subscribe",
"channel": "book",
"symbol": bitfinex_symbol,
"prec": "P0",
"freq": "F0",
"len": "25"
}
success = await self._send_message(subscription_msg)
if success:
if symbol not in self.subscriptions:
self.subscriptions[symbol] = []
if 'orderbook' not in self.subscriptions[symbol]:
self.subscriptions[symbol].append('orderbook')
self.subscribed_symbols.add(bitfinex_symbol)
logger.info(f"Subscribed to order book for {symbol} ({bitfinex_symbol}) on Bitfinex")
else:
logger.error(f"Failed to subscribe to order book for {symbol} on Bitfinex")
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."""
try:
set_correlation_id()
bitfinex_symbol = self.normalize_symbol(symbol)
subscription_msg = {
"event": "subscribe",
"channel": "trades",
"symbol": bitfinex_symbol
}
success = await self._send_message(subscription_msg)
if success:
if symbol not in self.subscriptions:
self.subscriptions[symbol] = []
if 'trades' not in self.subscriptions[symbol]:
self.subscriptions[symbol].append('trades')
self.subscribed_symbols.add(bitfinex_symbol)
logger.info(f"Subscribed to trades for {symbol} ({bitfinex_symbol}) on Bitfinex")
else:
logger.error(f"Failed to subscribe to trades for {symbol} on Bitfinex")
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."""
# Implementation would find the channel ID and send unsubscribe message
pass
async def unsubscribe_trades(self, symbol: str) -> None:
"""Unsubscribe from trade updates."""
# Implementation would find the channel ID and send unsubscribe message
pass
async def get_symbols(self) -> List[str]:
"""Get available symbols from Bitfinex."""
try:
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.API_URL}/v1/symbols") as response:
if response.status == 200:
data = await response.json()
symbols = [self._denormalize_symbol(f"t{s.upper()}") for s in data]
logger.info(f"Retrieved {len(symbols)} symbols from Bitfinex")
return symbols
else:
logger.error(f"Failed to get symbols from Bitfinex: HTTP {response.status}")
return []
except Exception as e:
logger.error(f"Error getting symbols from Bitfinex: {e}")
return []
async def get_orderbook_snapshot(self, symbol: str, depth: int = 20) -> Optional[OrderBookSnapshot]:
"""Get order book snapshot from Bitfinex REST API."""
try:
import aiohttp
bitfinex_symbol = self.normalize_symbol(symbol)
url = f"{self.API_URL}/v2/book/{bitfinex_symbol}/P0"
params = {'len': min(depth, 100)}
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: List, symbol: str) -> OrderBookSnapshot:
"""Parse Bitfinex order book data."""
try:
bids = []
asks = []
for level in data:
price = float(level[0])
count = int(level[1])
amount = float(level[2])
if validate_price(price) and validate_volume(abs(amount)):
if amount > 0:
bids.append(PriceLevel(price=price, size=amount))
else:
asks.append(PriceLevel(price=price, size=abs(amount)))
return OrderBookSnapshot(
symbol=symbol,
exchange=self.exchange_name,
timestamp=datetime.now(timezone.utc),
bids=bids,
asks=asks
)
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_subscription_response(self, data: Dict) -> None:
"""Handle subscription response."""
channel_id = data.get('chanId')
channel = data.get('channel')
symbol = data.get('symbol', '')
if channel_id:
self.channels[channel_id] = {
'channel': channel,
'symbol': symbol
}
logger.info(f"Bitfinex subscription confirmed: {channel} for {symbol} (ID: {channel_id})")
async def _handle_unsubscription_response(self, data: Dict) -> None:
"""Handle unsubscription response."""
channel_id = data.get('chanId')
if channel_id in self.channels:
del self.channels[channel_id]
logger.info(f"Bitfinex unsubscription confirmed for channel {channel_id}")
async def _handle_error_message(self, data: Dict) -> None:
"""Handle error message."""
error_msg = data.get('msg', 'Unknown error')
error_code = data.get('code', 'unknown')
logger.error(f"Bitfinex error {error_code}: {error_msg}")
async def _handle_info_message(self, data: Dict) -> None:
"""Handle info message."""
logger.info(f"Bitfinex info: {data}")
def get_bitfinex_stats(self) -> Dict[str, Any]:
"""Get Bitfinex-specific statistics."""
base_stats = self.get_stats()
bitfinex_stats = {
'active_channels': len(self.channels),
'subscribed_symbols': list(self.subscribed_symbols),
'authenticated': bool(self.api_key and self.api_secret)
}
base_stats.update(bitfinex_stats)
return base_stats