Files
gogo2/COBY/connectors/mexc_connector.py
2025-08-05 00:13:38 +03:00

420 lines
16 KiB
Python

"""
MEXC exchange connector implementation.
Supports WebSocket connections to MEXC with their WebSocket streams.
"""
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 MEXCConnector(BaseExchangeConnector):
"""
MEXC WebSocket connector implementation.
Supports:
- Order book streams
- Trade streams
- Symbol normalization
"""
# MEXC WebSocket URLs
WEBSOCKET_URL = "wss://wbs.mexc.com/ws"
API_URL = "https://api.mexc.com"
def __init__(self, api_key: str = None, api_secret: str = None):
"""Initialize MEXC connector."""
super().__init__("mexc", self.WEBSOCKET_URL)
self.api_key = api_key
self.api_secret = api_secret
# MEXC-specific message handlers
self.message_handlers.update({
'spot@public.deals.v3.api': self._handle_trade_update,
'spot@public.increase.depth.v3.api': self._handle_orderbook_update,
'spot@public.limit.depth.v3.api': self._handle_orderbook_snapshot,
'pong': self._handle_pong
})
# Subscription tracking
self.subscribed_streams = set()
self.request_id = 1
logger.info("MEXC connector initialized")
def _get_message_type(self, data: Dict) -> str:
"""Determine message type from MEXC message data."""
if 'c' in data: # Channel
return data['c']
elif 'msg' in data:
return 'message'
elif 'pong' in data:
return 'pong'
return 'unknown'
def normalize_symbol(self, symbol: str) -> str:
"""Normalize symbol to MEXC format."""
# MEXC uses uppercase without separators (same as Binance)
normalized = symbol.upper().replace('-', '').replace('/', '')
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 updates for a symbol."""
try:
set_correlation_id()
mexc_symbol = self.normalize_symbol(symbol)
subscription_msg = {
"method": "SUBSCRIPTION",
"params": [f"spot@public.limit.depth.v3.api@{mexc_symbol}@20"]
}
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_streams.add(f"spot@public.limit.depth.v3.api@{mexc_symbol}@20")
logger.info(f"Subscribed to order book for {symbol} ({mexc_symbol}) on MEXC")
else:
logger.error(f"Failed to subscribe to order book for {symbol} on MEXC")
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()
mexc_symbol = self.normalize_symbol(symbol)
subscription_msg = {
"method": "SUBSCRIPTION",
"params": [f"spot@public.deals.v3.api@{mexc_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_streams.add(f"spot@public.deals.v3.api@{mexc_symbol}")
logger.info(f"Subscribed to trades for {symbol} ({mexc_symbol}) on MEXC")
else:
logger.error(f"Failed to subscribe to trades for {symbol} on MEXC")
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."""
try:
mexc_symbol = self.normalize_symbol(symbol)
unsubscription_msg = {
"method": "UNSUBSCRIPTION",
"params": [f"spot@public.limit.depth.v3.api@{mexc_symbol}@20"]
}
success = await self._send_message(unsubscription_msg)
if success:
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]
self.subscribed_streams.discard(f"spot@public.limit.depth.v3.api@{mexc_symbol}@20")
logger.info(f"Unsubscribed from order book for {symbol} on MEXC")
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."""
try:
mexc_symbol = self.normalize_symbol(symbol)
unsubscription_msg = {
"method": "UNSUBSCRIPTION",
"params": [f"spot@public.deals.v3.api@{mexc_symbol}"]
}
success = await self._send_message(unsubscription_msg)
if success:
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]
self.subscribed_streams.discard(f"spot@public.deals.v3.api@{mexc_symbol}")
logger.info(f"Unsubscribed from trades for {symbol} on MEXC")
except Exception as e:
logger.error(f"Error unsubscribing from trades for {symbol}: {e}")
raise
async def get_symbols(self) -> List[str]:
"""Get available symbols from MEXC."""
try:
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(f"{self.API_URL}/api/v3/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 MEXC")
return symbols
else:
logger.error(f"Failed to get symbols from MEXC: HTTP {response.status}")
return []
except Exception as e:
logger.error(f"Error getting symbols from MEXC: {e}")
return []
async def get_orderbook_snapshot(self, symbol: str, depth: int = 20) -> Optional[OrderBookSnapshot]:
"""Get order book snapshot from MEXC REST API."""
try:
import aiohttp
mexc_symbol = self.normalize_symbol(symbol)
url = f"{self.API_URL}/api/v3/depth"
params = {'symbol': mexc_symbol, 'limit': min(depth, 5000)}
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 MEXC order book data."""
try:
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))
return OrderBookSnapshot(
symbol=symbol,
exchange=self.exchange_name,
timestamp=datetime.now(timezone.utc),
bids=bids,
asks=asks,
sequence_id=data.get('lastUpdateId')
)
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 update from MEXC."""
try:
set_correlation_id()
symbol_data = data.get('s', '') # Symbol
if not symbol_data:
logger.warning("Order book update missing symbol")
return
symbol = symbol_data # Already in standard format
order_data = data.get('d', {})
# Parse bids and asks
bids = []
for bid_data in order_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 order_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.fromtimestamp(int(data.get('t', 0)) / 1000, tz=timezone.utc),
bids=bids,
asks=asks,
sequence_id=order_data.get('lastUpdateId')
)
# Notify callbacks
self._notify_data_callbacks(orderbook)
logger.debug(f"Processed order book update for {symbol}")
except Exception as e:
logger.error(f"Error handling order book update: {e}")
async def _handle_orderbook_snapshot(self, data: Dict) -> None:
"""Handle order book snapshot from MEXC."""
try:
set_correlation_id()
symbol_data = data.get('s', '') # Symbol
if not symbol_data:
logger.warning("Order book snapshot missing symbol")
return
symbol = symbol_data # Already in standard format
order_data = data.get('d', {})
# Parse bids and asks
bids = []
for bid_data in order_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 order_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.fromtimestamp(int(data.get('t', 0)) / 1000, tz=timezone.utc),
bids=bids,
asks=asks,
sequence_id=order_data.get('lastUpdateId')
)
# Notify callbacks
self._notify_data_callbacks(orderbook)
logger.debug(f"Processed order book snapshot for {symbol}")
except Exception as e:
logger.error(f"Error handling order book snapshot: {e}")
async def _handle_trade_update(self, data: Dict) -> None:
"""Handle trade update from MEXC."""
try:
set_correlation_id()
symbol_data = data.get('s', '') # Symbol
if not symbol_data:
logger.warning("Trade update missing symbol")
return
symbol = symbol_data # Already in standard format
trade_data = data.get('d', {})
# MEXC trade data format
trades = trade_data.get('deals', [])
for trade_info in trades:
price = float(trade_info.get('p', 0))
quantity = float(trade_info.get('v', 0))
# Validate data
if not validate_price(price) or not validate_volume(quantity):
logger.warning(f"Invalid trade data: price={price}, quantity={quantity}")
continue
# Determine side (MEXC uses 'S' field: 1=buy, 2=sell)
side_code = trade_info.get('S', 0)
side = 'buy' if side_code == 1 else 'sell'
# Create trade event
trade = TradeEvent(
symbol=symbol,
exchange=self.exchange_name,
timestamp=datetime.fromtimestamp(int(trade_info.get('t', 0)) / 1000, tz=timezone.utc),
price=price,
size=quantity,
side=side,
trade_id=str(trade_info.get('i', ''))
)
# Notify callbacks
self._notify_data_callbacks(trade)
logger.debug(f"Processed trade for {symbol}: {side} {quantity} @ {price}")
except Exception as e:
logger.error(f"Error handling trade update: {e}")
async def _handle_pong(self, data: Dict) -> None:
"""Handle pong response from MEXC."""
logger.debug("Received MEXC pong")
async def _send_ping(self) -> None:
"""Send ping to keep connection alive."""
try:
ping_msg = {"method": "PING"}
await self._send_message(ping_msg)
logger.debug("Sent ping to MEXC")
except Exception as e:
logger.error(f"Error sending ping: {e}")
def get_mexc_stats(self) -> Dict[str, Any]:
"""Get MEXC-specific statistics."""
base_stats = self.get_stats()
mexc_stats = {
'subscribed_streams': list(self.subscribed_streams),
'authenticated': bool(self.api_key and self.api_secret),
'next_request_id': self.request_id
}
base_stats.update(mexc_stats)
return base_stats