sync fees from API. usdc. trade works
This commit is contained in:
@ -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×tamp=X (for market orders)
|
||||
# symbol=X&side=X&type=X&quantity=X&price=X&timeInForce=X×tamp=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())
|
||||
}
|
Reference in New Issue
Block a user