sync fees from API. usdc. trade works

This commit is contained in:
Dobromir Popov
2025-05-28 11:18:07 +03:00
parent dd86d21854
commit d6a71c2b1a
15 changed files with 2638 additions and 109 deletions

View File

@ -295,15 +295,31 @@ class MEXCInterface(ExchangeInterface):
return False
def _generate_signature(self, params: Dict[str, Any]) -> str:
"""Generate signature for authenticated requests."""
# Sort parameters by key for consistent signature generation
"""Generate HMAC SHA256 signature for MEXC API.
The signature is generated by creating a query string from all parameters
(excluding the signature itself), then using HMAC SHA256 with the secret key.
"""
if not self.api_secret:
raise ValueError("API secret is required for generating signatures")
# Sort parameters by key to ensure consistent ordering
# This is crucial for MEXC API signature validation
sorted_params = sorted(params.items())
query_string = urlencode(sorted_params)
# Create query string
query_string = '&'.join([f"{key}={value}" for key, value in sorted_params])
# Generate HMAC SHA256 signature
signature = hmac.new(
self.api_secret.encode('utf-8'),
query_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
logger.debug(f"MEXC signature query string: {query_string}")
logger.debug(f"MEXC signature: {signature}")
return signature
def _send_public_request(self, method: str, endpoint: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
@ -330,49 +346,95 @@ class MEXCInterface(ExchangeInterface):
if params is None:
params = {}
# Add timestamp and recvWindow as required by MEXC
# Use server time for better synchronization
# Add timestamp using server time for better synchronization
try:
server_time_response = self._send_public_request('GET', 'time')
params['timestamp'] = server_time_response['serverTime']
except:
server_time = server_time_response['serverTime']
params['timestamp'] = server_time
except Exception as e:
logger.warning(f"Failed to get server time, using local time: {e}")
params['timestamp'] = int(time.time() * 1000)
if 'recvWindow' not in params:
params['recvWindow'] = 10000 # Increased receive window
# Generate signature using the exact format from MEXC documentation
# For order placement, the query string should be in this specific order:
# symbol=X&side=X&type=X&quantity=X&timestamp=X (for market orders)
# symbol=X&side=X&type=X&quantity=X&price=X&timeInForce=X&timestamp=X (for limit orders)
# Generate signature using the correct MEXC format
signature = self._generate_signature(params)
if endpoint == 'order' and method == 'POST':
# Special handling for order placement - use exact MEXC documentation format
query_parts = []
# Required parameters in exact order per MEXC docs
if 'symbol' in params:
query_parts.append(f"symbol={params['symbol']}")
if 'side' in params:
query_parts.append(f"side={params['side']}")
if 'type' in params:
query_parts.append(f"type={params['type']}")
if 'quantity' in params:
query_parts.append(f"quantity={params['quantity']}")
if 'price' in params:
query_parts.append(f"price={params['price']}")
if 'timeInForce' in params:
query_parts.append(f"timeInForce={params['timeInForce']}")
if 'timestamp' in params:
query_parts.append(f"timestamp={params['timestamp']}")
query_string = '&'.join(query_parts)
else:
# For other endpoints, use sorted parameters (original working method)
sorted_params = sorted(params.items())
query_string = urlencode(sorted_params)
# Generate signature
signature = hmac.new(
self.api_secret.encode('utf-8'),
query_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Add signature to parameters
params['signature'] = signature
# Set headers as required by MEXC documentation
# Prepare request
url = f"{self.base_url}/api/v3/{endpoint}"
headers = {
'X-MEXC-APIKEY': self.api_key
}
url = f"{self.base_url}/{self.api_version}/{endpoint}"
# Do not add Content-Type - let requests handle it automatically
# Log request details for debugging
logger.debug(f"MEXC {method} request to {endpoint}")
logger.debug(f"Query string for signature: {query_string}")
logger.debug(f"Signature: {signature}")
try:
if method.upper() == 'GET':
# 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
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
response = requests.delete(url, params=params, headers=headers)
if method == 'GET':
response = requests.get(url, params=params, headers=headers, timeout=30)
elif method == 'POST':
response = requests.post(url, params=params, headers=headers, timeout=30)
elif method == 'DELETE':
response = requests.delete(url, params=params, headers=headers, timeout=30)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
response.raise_for_status()
return response.json()
logger.debug(f"MEXC API response status: {response.status_code}")
if response.status_code == 200:
return response.json()
else:
logger.error(f"Error in private request to {endpoint}: {response.status_code} {response.reason}")
logger.error(f"Response status: {response.status_code}")
logger.error(f"Response content: {response.text}")
response.raise_for_status()
except requests.exceptions.RequestException as e:
logger.error(f"Network error in private request to {endpoint}: {str(e)}")
raise
except Exception as e:
logger.error(f"Error in private request to {endpoint}: {str(e)}")
if hasattr(e, 'response') and e.response is not None:
logger.error(f"Response status: {e.response.status_code}")
logger.error(f"Response content: {e.response.text}")
logger.error(f"Unexpected error in private request to {endpoint}: {str(e)}")
raise
def get_server_time(self) -> Dict[str, Any]:
@ -385,7 +447,7 @@ class MEXCInterface(ExchangeInterface):
def get_account_info(self) -> Dict[str, Any]:
"""Get account information."""
params = {'recvWindow': 5000}
params = {} # recvWindow will be set by _send_private_request
return self._send_private_request('GET', 'account', params)
def get_balance(self, asset: str) -> float:
@ -398,7 +460,7 @@ class MEXCInterface(ExchangeInterface):
float: Available balance of the asset
"""
try:
params = {'recvWindow': 5000}
params = {} # recvWindow will be set by _send_private_request
account_info = self._send_private_request('GET', 'account', params)
balances = account_info.get('balances', [])
@ -493,7 +555,7 @@ class MEXCInterface(ExchangeInterface):
# Validate we have a valid price
if result['last'] > 0:
logger.info(f"MEXC: Got ticker from {endpoint} for {symbol}: ${result['last']:.2f}")
logger.info(f"MEXC: Got ticker from {endpoint} for {symbol}: ${result['last']:.2f}")
return result
except Exception as e:
@ -520,20 +582,35 @@ class MEXCInterface(ExchangeInterface):
"""
mexc_symbol = symbol.replace('/', '')
# Prepare order parameters according to MEXC API
# Prepare order parameters according to MEXC API specification
# Parameters must be in specific order for proper signature generation
params = {
'symbol': mexc_symbol,
'side': side.upper(),
'type': order_type.upper(),
'quantity': str(quantity), # MEXC expects string format
'recvWindow': 5000
'type': order_type.upper()
}
# Format quantity properly - respect MEXC precision requirements
# ETH has 5 decimal places max on MEXC, most other symbols have 6-8
if 'ETH' in mexc_symbol:
# ETH pairs: 5 decimal places maximum
quantity_str = f"{quantity:.5f}".rstrip('0').rstrip('.')
else:
# Other pairs: 6 decimal places (conservative)
quantity_str = f"{quantity:.6f}".rstrip('0').rstrip('.')
params['quantity'] = quantity_str
# Add price and timeInForce for limit orders
if order_type.upper() == 'LIMIT':
if price is None:
raise ValueError("Price is required for LIMIT orders")
params['price'] = str(price)
# Format price properly - respect MEXC precision requirements
# USDC pairs typically have 2 decimal places, USDT pairs may have more
if 'USDC' in mexc_symbol:
price_str = f"{price:.2f}".rstrip('0').rstrip('.')
else:
price_str = f"{price:.6f}".rstrip('0').rstrip('.')
params['price'] = price_str
params['timeInForce'] = 'GTC' # Good Till Cancelled
try:
@ -609,4 +686,96 @@ class MEXCInterface(ExchangeInterface):
return open_orders
except Exception as e:
logger.error(f"Error getting open orders: {str(e)}")
return []
return []
def get_trading_fees(self) -> Dict[str, Any]:
"""Get current trading fee rates from MEXC API
Returns:
dict: Trading fee information including maker/taker rates
"""
try:
# MEXC API endpoint for account commission rates
account_info = self._send_private_request('GET', 'account', {})
# Extract commission rates from account info
# MEXC typically returns commission rates in the account response
maker_commission = account_info.get('makerCommission', 0)
taker_commission = account_info.get('takerCommission', 0)
# Convert from basis points to decimal (MEXC uses basis points: 10 = 0.001%)
maker_rate = maker_commission / 100000 # Convert from basis points
taker_rate = taker_commission / 100000
logger.info(f"MEXC: Retrieved trading fees - Maker: {maker_rate*100:.3f}%, Taker: {taker_rate*100:.3f}%")
return {
'maker_rate': maker_rate,
'taker_rate': taker_rate,
'maker_commission': maker_commission,
'taker_commission': taker_commission,
'source': 'mexc_api',
'timestamp': int(time.time())
}
except Exception as e:
logger.error(f"Error getting MEXC trading fees: {e}")
# Return fallback values
return {
'maker_rate': 0.0000, # 0.00% fallback
'taker_rate': 0.0005, # 0.05% fallback
'source': 'fallback',
'error': str(e),
'timestamp': int(time.time())
}
def get_symbol_trading_fees(self, symbol: str) -> Dict[str, Any]:
"""Get trading fees for a specific symbol
Args:
symbol: Trading symbol (e.g., 'ETH/USDT')
Returns:
dict: Symbol-specific trading fee information
"""
try:
mexc_symbol = symbol.replace('/', '')
# Try to get symbol-specific fee info from exchange info
exchange_info_response = self._send_public_request('GET', 'exchangeInfo', {})
if exchange_info_response and 'symbols' in exchange_info_response:
symbol_info = None
for sym in exchange_info_response['symbols']:
if sym.get('symbol') == mexc_symbol:
symbol_info = sym
break
if symbol_info:
# Some exchanges provide symbol-specific fees in exchange info
logger.info(f"MEXC: Found symbol info for {symbol}")
# For now, use account-level fees as symbol-specific fees
# This can be enhanced if MEXC provides symbol-specific fee endpoints
account_fees = self.get_trading_fees()
account_fees['symbol'] = symbol
account_fees['symbol_specific'] = False
return account_fees
# Fallback to account-level fees
account_fees = self.get_trading_fees()
account_fees['symbol'] = symbol
account_fees['symbol_specific'] = False
return account_fees
except Exception as e:
logger.error(f"Error getting symbol trading fees for {symbol}: {e}")
return {
'symbol': symbol,
'maker_rate': 0.0000,
'taker_rate': 0.0005,
'source': 'fallback',
'symbol_specific': False,
'error': str(e),
'timestamp': int(time.time())
}