position history with fees

This commit is contained in:
Dobromir Popov
2025-05-28 11:17:41 +03:00
parent f8681447e3
commit dd86d21854
11 changed files with 880 additions and 58 deletions

View File

@ -1,17 +1,23 @@
import logging
import time
from typing import Dict, Any, List, Optional
import asyncio
import json
import websockets
from typing import Dict, Any, List, Optional, Callable
import requests
import hmac
import hashlib
from urllib.parse import urlencode
from datetime import datetime
from threading import Thread, Lock
from collections import deque
from .exchange_interface import ExchangeInterface
logger = logging.getLogger(__name__)
class MEXCInterface(ExchangeInterface):
"""MEXC Exchange API Interface"""
"""MEXC Exchange API Interface with WebSocket support"""
def __init__(self, api_key: str = None, api_secret: str = None, test_mode: bool = True):
"""Initialize MEXC exchange interface.
@ -25,6 +31,247 @@ class MEXCInterface(ExchangeInterface):
self.base_url = "https://api.mexc.com"
self.api_version = "api/v3"
# WebSocket configuration
self.ws_base_url = "wss://wbs.mexc.com/ws"
self.websocket_tasks = {}
self.is_streaming = False
self.stream_lock = Lock()
self.tick_callbacks = []
self.ticker_callbacks = []
# Data buffers for reliability
self.recent_ticks = {} # {symbol: deque}
self.current_prices = {} # {symbol: price}
self.buffer_size = 1000
def add_tick_callback(self, callback: Callable[[Dict[str, Any]], None]):
"""Add callback for real-time tick data"""
self.tick_callbacks.append(callback)
logger.info(f"Added MEXC tick callback: {len(self.tick_callbacks)} total")
def add_ticker_callback(self, callback: Callable[[Dict[str, Any]], None]):
"""Add callback for real-time ticker data"""
self.ticker_callbacks.append(callback)
logger.info(f"Added MEXC ticker callback: {len(self.ticker_callbacks)} total")
def _notify_tick_callbacks(self, tick_data: Dict[str, Any]):
"""Notify all tick callbacks with new data"""
for callback in self.tick_callbacks:
try:
callback(tick_data)
except Exception as e:
logger.error(f"Error in MEXC tick callback: {e}")
def _notify_ticker_callbacks(self, ticker_data: Dict[str, Any]):
"""Notify all ticker callbacks with new data"""
for callback in self.ticker_callbacks:
try:
callback(ticker_data)
except Exception as e:
logger.error(f"Error in MEXC ticker callback: {e}")
async def start_websocket_streams(self, symbols: List[str], stream_types: List[str] = None):
"""Start WebSocket streams for multiple symbols
Args:
symbols: List of symbols in 'BTC/USDT' format
stream_types: List of stream types ['trade', 'ticker', 'depth'] (default: ['trade', 'ticker'])
"""
if stream_types is None:
stream_types = ['trade', 'ticker']
self.is_streaming = True
logger.info(f"Starting MEXC WebSocket streams for {symbols} with types {stream_types}")
# Initialize buffers for symbols
for symbol in symbols:
mexc_symbol = symbol.replace('/', '').upper()
self.recent_ticks[mexc_symbol] = deque(maxlen=self.buffer_size)
# Start streams for each symbol and stream type combination
for symbol in symbols:
for stream_type in stream_types:
task = asyncio.create_task(self._websocket_stream(symbol, stream_type))
task_key = f"{symbol}_{stream_type}"
self.websocket_tasks[task_key] = task
async def stop_websocket_streams(self):
"""Stop all WebSocket streams"""
logger.info("Stopping MEXC WebSocket streams")
self.is_streaming = False
# Cancel all tasks
for task_key, task in self.websocket_tasks.items():
if not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
self.websocket_tasks.clear()
async def _websocket_stream(self, symbol: str, stream_type: str):
"""Individual WebSocket stream for a symbol and stream type"""
mexc_symbol = symbol.replace('/', '').upper()
# MEXC WebSocket stream naming convention
if stream_type == 'trade':
stream_name = f"{mexc_symbol}@trade"
elif stream_type == 'ticker':
stream_name = f"{mexc_symbol}@ticker"
elif stream_type == 'depth':
stream_name = f"{mexc_symbol}@depth"
else:
logger.error(f"Unsupported MEXC stream type: {stream_type}")
return
url = f"{self.ws_base_url}"
while self.is_streaming:
try:
logger.info(f"Connecting to MEXC WebSocket: {stream_name}")
async with websockets.connect(url) as websocket:
# Subscribe to the stream
subscribe_msg = {
"method": "SUBSCRIPTION",
"params": [stream_name]
}
await websocket.send(json.dumps(subscribe_msg))
logger.info(f"Subscribed to MEXC stream: {stream_name}")
async for message in websocket:
if not self.is_streaming:
break
try:
await self._process_websocket_message(mexc_symbol, stream_type, message)
except Exception as e:
logger.warning(f"Error processing MEXC message for {stream_name}: {e}")
except Exception as e:
logger.error(f"MEXC WebSocket error for {stream_name}: {e}")
if self.is_streaming:
logger.info(f"Reconnecting MEXC WebSocket for {stream_name} in 5 seconds...")
await asyncio.sleep(5)
async def _process_websocket_message(self, symbol: str, stream_type: str, message: str):
"""Process incoming WebSocket message"""
try:
data = json.loads(message)
# Handle subscription confirmation
if data.get('id') is not None:
logger.info(f"MEXC WebSocket subscription confirmed for {symbol} {stream_type}")
return
# Process data based on stream type
if stream_type == 'trade' and 'data' in data:
await self._process_trade_data(symbol, data['data'])
elif stream_type == 'ticker' and 'data' in data:
await self._process_ticker_data(symbol, data['data'])
elif stream_type == 'depth' and 'data' in data:
await self._process_depth_data(symbol, data['data'])
except Exception as e:
logger.error(f"Error processing MEXC WebSocket message: {e}")
async def _process_trade_data(self, symbol: str, trade_data: Dict[str, Any]):
"""Process trade data from WebSocket"""
try:
# MEXC trade data format
price = float(trade_data.get('p', 0))
quantity = float(trade_data.get('q', 0))
timestamp = datetime.fromtimestamp(int(trade_data.get('t', 0)) / 1000)
is_buyer_maker = trade_data.get('m', False)
trade_id = trade_data.get('i', '')
# Create standardized tick
tick = {
'symbol': symbol,
'timestamp': timestamp,
'price': price,
'volume': price * quantity, # Volume in quote currency
'quantity': quantity,
'side': 'sell' if is_buyer_maker else 'buy',
'trade_id': str(trade_id),
'is_buyer_maker': is_buyer_maker,
'exchange': 'MEXC',
'raw_data': trade_data
}
# Update buffers
self.recent_ticks[symbol].append(tick)
self.current_prices[symbol] = price
# Notify callbacks
self._notify_tick_callbacks(tick)
except Exception as e:
logger.error(f"Error processing MEXC trade data: {e}")
async def _process_ticker_data(self, symbol: str, ticker_data: Dict[str, Any]):
"""Process ticker data from WebSocket"""
try:
# MEXC ticker data format
ticker = {
'symbol': symbol,
'timestamp': datetime.now(),
'price': float(ticker_data.get('c', 0)), # Current price
'bid': float(ticker_data.get('b', 0)), # Best bid
'ask': float(ticker_data.get('a', 0)), # Best ask
'volume': float(ticker_data.get('v', 0)), # Volume
'high': float(ticker_data.get('h', 0)), # 24h high
'low': float(ticker_data.get('l', 0)), # 24h low
'change': float(ticker_data.get('P', 0)), # Price change %
'exchange': 'MEXC',
'raw_data': ticker_data
}
# Update current price
self.current_prices[symbol] = ticker['price']
# Notify callbacks
self._notify_ticker_callbacks(ticker)
except Exception as e:
logger.error(f"Error processing MEXC ticker data: {e}")
async def _process_depth_data(self, symbol: str, depth_data: Dict[str, Any]):
"""Process order book depth data from WebSocket"""
try:
# Process depth data if needed for future features
logger.debug(f"MEXC depth data received for {symbol}")
except Exception as e:
logger.error(f"Error processing MEXC depth data: {e}")
def get_current_price(self, symbol: str) -> Optional[float]:
"""Get current price for a symbol from WebSocket data or REST API fallback"""
mexc_symbol = symbol.replace('/', '').upper()
# Try from WebSocket data first
if mexc_symbol in self.current_prices:
return self.current_prices[mexc_symbol]
# Fallback to REST API
try:
ticker = self.get_ticker(symbol)
if ticker and 'price' in ticker:
return float(ticker['price'])
except Exception as e:
logger.warning(f"Failed to get current price for {symbol} from MEXC: {e}")
return None
def get_recent_ticks(self, symbol: str, count: int = 100) -> List[Dict[str, Any]]:
"""Get recent ticks for a symbol"""
mexc_symbol = symbol.replace('/', '').upper()
if mexc_symbol in self.recent_ticks:
return list(self.recent_ticks[mexc_symbol])[-count:]
return []
def connect(self) -> bool:
"""Connect to MEXC API."""
if not self.api_key or not self.api_secret:
@ -84,9 +331,15 @@ class MEXCInterface(ExchangeInterface):
params = {}
# Add timestamp and recvWindow as required by MEXC
params['timestamp'] = int(time.time() * 1000)
# Use server time for better synchronization
try:
server_time_response = self._send_public_request('GET', 'time')
params['timestamp'] = server_time_response['serverTime']
except:
params['timestamp'] = int(time.time() * 1000)
if 'recvWindow' not in params:
params['recvWindow'] = 5000
params['recvWindow'] = 10000 # Increased receive window
# Generate signature using the correct MEXC format
signature = self._generate_signature(params)
@ -94,18 +347,18 @@ class MEXCInterface(ExchangeInterface):
# Set headers as required by MEXC documentation
headers = {
'X-MEXC-APIKEY': self.api_key,
'Content-Type': 'application/x-www-form-urlencoded'
'X-MEXC-APIKEY': self.api_key
}
url = f"{self.base_url}/{self.api_version}/{endpoint}"
try:
if method.upper() == 'GET':
# For GET requests, send parameters as query string
# For GET requests with authentication, signature goes in query string
response = requests.get(url, params=params, headers=headers)
elif method.upper() == 'POST':
# For POST requests, send as form data in request body per MEXC documentation
# For POST requests, send as form data in request body
headers['Content-Type'] = 'application/x-www-form-urlencoded'
response = requests.post(url, data=params, headers=headers)
elif method.upper() == 'DELETE':
# For DELETE requests, send parameters as query string