fix sim and wip fix live
This commit is contained in:
@ -5,6 +5,7 @@ import requests
|
||||
import hmac
|
||||
import hashlib
|
||||
from urllib.parse import urlencode, quote_plus
|
||||
import json # Added for json.dumps
|
||||
|
||||
from .exchange_interface import ExchangeInterface
|
||||
|
||||
@ -85,37 +86,40 @@ class MEXCInterface(ExchangeInterface):
|
||||
return symbol.replace('/', '_').upper()
|
||||
|
||||
def _generate_signature(self, timestamp: str, method: str, endpoint: str, params: Dict[str, Any]) -> str:
|
||||
"""Generate signature for private API calls using MEXC's expected parameter order"""
|
||||
# MEXC requires specific parameter ordering, not alphabetical
|
||||
# Based on successful test: symbol, side, type, quantity, timestamp, then other params
|
||||
mexc_param_order = ['symbol', 'side', 'type', 'quantity', 'timestamp', 'recvWindow']
|
||||
|
||||
# Build ordered parameter list
|
||||
ordered_params = []
|
||||
|
||||
# Add parameters in MEXC's expected order
|
||||
for param_name in mexc_param_order:
|
||||
if param_name in params and param_name != 'signature':
|
||||
ordered_params.append(f"{param_name}={params[param_name]}")
|
||||
|
||||
# Add any remaining parameters not in the standard order (alphabetically)
|
||||
remaining_params = {k: v for k, v in params.items() if k not in mexc_param_order and k != 'signature'}
|
||||
for key in sorted(remaining_params.keys()):
|
||||
ordered_params.append(f"{key}={remaining_params[key]}")
|
||||
|
||||
# Create query string (MEXC doesn't use the api_key + timestamp prefix)
|
||||
query_string = '&'.join(ordered_params)
|
||||
|
||||
logger.debug(f"MEXC signature query string: {query_string}")
|
||||
|
||||
"""Generate signature for private API calls using MEXC's official method"""
|
||||
# MEXC signature format varies by method:
|
||||
# For GET/DELETE: URL-encoded query string of alphabetically sorted parameters.
|
||||
# For POST: JSON string of parameters (no sorting needed).
|
||||
# The API-Secret is used as the HMAC SHA256 key.
|
||||
|
||||
# Remove signature from params to avoid circular inclusion
|
||||
clean_params = {k: v for k, v in params.items() if k != 'signature'}
|
||||
|
||||
parameter_string: str
|
||||
|
||||
if method.upper() == "POST":
|
||||
# For POST requests, the signature parameter is a JSON string
|
||||
# Ensure sorting keys for consistent JSON string generation across runs
|
||||
# even though MEXC says sorting is not required for POST params, it's good practice.
|
||||
parameter_string = json.dumps(clean_params, sort_keys=True, separators=(',', ':'))
|
||||
else:
|
||||
# For GET/DELETE requests, parameters are spliced in dictionary order with & interval
|
||||
sorted_params = sorted(clean_params.items())
|
||||
parameter_string = '&'.join(f"{key}={str(value)}" for key, value in sorted_params)
|
||||
|
||||
# The string to be signed is: accessKey + timestamp + obtained parameter string.
|
||||
string_to_sign = f"{self.api_key}{timestamp}{parameter_string}"
|
||||
|
||||
logger.debug(f"MEXC string to sign (method {method}): {string_to_sign}")
|
||||
|
||||
# Generate HMAC SHA256 signature
|
||||
signature = hmac.new(
|
||||
self.api_secret.encode('utf-8'),
|
||||
query_string.encode('utf-8'),
|
||||
string_to_sign.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
logger.debug(f"MEXC signature: {signature}")
|
||||
|
||||
logger.debug(f"MEXC generated signature: {signature}")
|
||||
return signature
|
||||
|
||||
def _send_public_request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
@ -145,7 +149,7 @@ class MEXCInterface(ExchangeInterface):
|
||||
logger.error(f"Error in public request to {endpoint}: {e}")
|
||||
return {}
|
||||
|
||||
def _send_private_request(self, method: str, endpoint: str, params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]:
|
||||
def _send_private_request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Send a private request to the exchange with proper signature"""
|
||||
if params is None:
|
||||
params = {}
|
||||
@ -170,8 +174,11 @@ class MEXCInterface(ExchangeInterface):
|
||||
if method.upper() == "GET":
|
||||
response = self.session.get(url, headers=headers, params=params, timeout=10)
|
||||
elif method.upper() == "POST":
|
||||
# MEXC expects POST parameters as query string, not in body
|
||||
response = self.session.post(url, headers=headers, params=params, timeout=10)
|
||||
# MEXC expects POST parameters as JSON in the request body, not as query string
|
||||
# The signature is generated from the JSON string of parameters.
|
||||
# We need to exclude 'signature' from the JSON body sent, as it's for the header.
|
||||
params_for_body = {k: v for k, v in params.items() if k != 'signature'}
|
||||
response = self.session.post(url, headers=headers, json=params_for_body, timeout=10)
|
||||
else:
|
||||
logger.error(f"Unsupported method: {method}")
|
||||
return None
|
||||
@ -217,48 +224,46 @@ class MEXCInterface(ExchangeInterface):
|
||||
|
||||
response = self._send_public_request('GET', endpoint, params)
|
||||
|
||||
if response:
|
||||
# MEXC ticker returns a dictionary if single symbol, list if all symbols
|
||||
if isinstance(response, dict):
|
||||
ticker_data = response
|
||||
elif isinstance(response, list) and len(response) > 0:
|
||||
# If the response is a list, try to find the specific symbol
|
||||
found_ticker = next((item for item in response if item.get('symbol') == formatted_symbol), None)
|
||||
if found_ticker:
|
||||
ticker_data = found_ticker
|
||||
else:
|
||||
logger.error(f"Ticker data for {formatted_symbol} not found in response list.")
|
||||
return None
|
||||
if isinstance(response, dict):
|
||||
ticker_data: Dict[str, Any] = response
|
||||
elif isinstance(response, list) and len(response) > 0:
|
||||
found_ticker = next((item for item in response if item.get('symbol') == formatted_symbol), None)
|
||||
if found_ticker:
|
||||
ticker_data = found_ticker
|
||||
else:
|
||||
logger.error(f"Unexpected ticker response format: {response}")
|
||||
logger.error(f"Ticker data for {formatted_symbol} not found in response list.")
|
||||
return None
|
||||
else:
|
||||
logger.error(f"Unexpected ticker response format: {response}")
|
||||
return None
|
||||
|
||||
# Extract relevant info and format for universal use
|
||||
last_price = float(ticker_data.get('lastPrice', 0))
|
||||
bid_price = float(ticker_data.get('bidPrice', 0))
|
||||
ask_price = float(ticker_data.get('askPrice', 0))
|
||||
volume = float(ticker_data.get('volume', 0)) # Base asset volume
|
||||
# At this point, ticker_data is guaranteed to be a Dict[str, Any] due to the above logic
|
||||
# If it was None, we would have returned early.
|
||||
|
||||
# Determine price change and percent change
|
||||
price_change = float(ticker_data.get('priceChange', 0))
|
||||
price_change_percent = float(ticker_data.get('priceChangePercent', 0))
|
||||
# Extract relevant info and format for universal use
|
||||
last_price = float(ticker_data.get('lastPrice', 0))
|
||||
bid_price = float(ticker_data.get('bidPrice', 0))
|
||||
ask_price = float(ticker_data.get('askPrice', 0))
|
||||
volume = float(ticker_data.get('volume', 0)) # Base asset volume
|
||||
|
||||
logger.info(f"MEXC: Got ticker from {endpoint} for {symbol}: ${last_price:.2f}")
|
||||
|
||||
return {
|
||||
'symbol': formatted_symbol,
|
||||
'last': last_price,
|
||||
'bid': bid_price,
|
||||
'ask': ask_price,
|
||||
'volume': volume,
|
||||
'high': float(ticker_data.get('highPrice', 0)),
|
||||
'low': float(ticker_data.get('lowPrice', 0)),
|
||||
'change': price_change_percent, # This is usually priceChangePercent
|
||||
'exchange': 'MEXC',
|
||||
'raw_data': ticker_data
|
||||
}
|
||||
logger.error(f"Failed to get ticker for {symbol}")
|
||||
return None
|
||||
# Determine price change and percent change
|
||||
price_change = float(ticker_data.get('priceChange', 0))
|
||||
price_change_percent = float(ticker_data.get('priceChangePercent', 0))
|
||||
|
||||
logger.info(f"MEXC: Got ticker from {endpoint} for {symbol}: ${last_price:.2f}")
|
||||
|
||||
return {
|
||||
'symbol': formatted_symbol,
|
||||
'last': last_price,
|
||||
'bid': bid_price,
|
||||
'ask': ask_price,
|
||||
'volume': volume,
|
||||
'high': float(ticker_data.get('highPrice', 0)),
|
||||
'low': float(ticker_data.get('lowPrice', 0)),
|
||||
'change': price_change_percent, # This is usually priceChangePercent
|
||||
'exchange': 'MEXC',
|
||||
'raw_data': ticker_data
|
||||
}
|
||||
|
||||
def get_api_symbols(self) -> List[str]:
|
||||
"""Get list of symbols supported for API trading"""
|
||||
@ -293,39 +298,89 @@ class MEXCInterface(ExchangeInterface):
|
||||
logger.info(f"Supported symbols include: {supported_symbols[:10]}...") # Show first 10
|
||||
return {}
|
||||
|
||||
# Format quantity according to symbol precision requirements
|
||||
formatted_quantity = self._format_quantity_for_symbol(formatted_symbol, quantity)
|
||||
if formatted_quantity is None:
|
||||
logger.error(f"MEXC: Failed to format quantity {quantity} for {formatted_symbol}")
|
||||
return {}
|
||||
|
||||
# Handle order type restrictions for specific symbols
|
||||
final_order_type = self._adjust_order_type_for_symbol(formatted_symbol, order_type.upper())
|
||||
|
||||
# Get price for limit orders
|
||||
final_price = price
|
||||
if final_order_type == 'LIMIT' and price is None:
|
||||
# Get current market price
|
||||
ticker = self.get_ticker(symbol)
|
||||
if ticker and 'last' in ticker:
|
||||
final_price = ticker['last']
|
||||
logger.info(f"MEXC: Using market price ${final_price:.2f} for LIMIT order")
|
||||
else:
|
||||
logger.error(f"MEXC: Could not get market price for LIMIT order on {formatted_symbol}")
|
||||
return {}
|
||||
|
||||
endpoint = "order"
|
||||
|
||||
params: Dict[str, Any] = {
|
||||
'symbol': formatted_symbol,
|
||||
'side': side.upper(),
|
||||
'type': order_type.upper(),
|
||||
'quantity': str(quantity) # Quantity must be a string
|
||||
'type': final_order_type,
|
||||
'quantity': str(formatted_quantity) # Quantity must be a string
|
||||
}
|
||||
if price is not None:
|
||||
params['price'] = str(price) # Price must be a string for limit orders
|
||||
if final_price is not None:
|
||||
params['price'] = str(final_price) # Price must be a string for limit orders
|
||||
|
||||
logger.info(f"MEXC: Placing {side.upper()} {order_type.upper()} order for {quantity} {formatted_symbol} at price {price}")
|
||||
|
||||
# For market orders, some parameters might be optional or handled differently.
|
||||
# Check MEXC API docs for market order specifics (e.g., quoteOrderQty for buy market orders)
|
||||
if order_type.upper() == 'MARKET' and side.upper() == 'BUY':
|
||||
# If it's a market buy order, MEXC often expects quoteOrderQty instead of quantity
|
||||
# Assuming quantity here refers to the base asset, if quoteOrderQty is needed, adjust.
|
||||
# For now, we will stick to quantity and let MEXC handle the conversion if possible
|
||||
pass # No specific change needed based on the current params structure
|
||||
logger.info(f"MEXC: Placing {side.upper()} {final_order_type} order for {formatted_quantity} {formatted_symbol} at price {final_price}")
|
||||
|
||||
try:
|
||||
# MEXC API endpoint for placing orders is /api/v3/order (POST)
|
||||
order_result = self._send_private_request('POST', endpoint, params)
|
||||
if order_result:
|
||||
if order_result is not None:
|
||||
logger.info(f"MEXC: Order placed successfully: {order_result}")
|
||||
return order_result
|
||||
else:
|
||||
logger.error(f"MEXC: Error placing order: {order_result}")
|
||||
logger.error(f"MEXC: Error placing order: request returned None")
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"MEXC: Exception placing order: {e}")
|
||||
return {}
|
||||
|
||||
def _format_quantity_for_symbol(self, formatted_symbol: str, quantity: float) -> Optional[float]:
|
||||
"""Format quantity according to symbol precision requirements"""
|
||||
try:
|
||||
# Symbol-specific precision rules
|
||||
if formatted_symbol == 'ETHUSDC':
|
||||
# ETHUSDC requires max 5 decimal places, step size 0.000001
|
||||
formatted_qty = round(quantity, 5)
|
||||
# Ensure it meets minimum step size
|
||||
step_size = 0.000001
|
||||
formatted_qty = round(formatted_qty / step_size) * step_size
|
||||
# Round again to remove floating point errors
|
||||
formatted_qty = round(formatted_qty, 6)
|
||||
logger.info(f"MEXC: Formatted ETHUSDC quantity {quantity} -> {formatted_qty}")
|
||||
return formatted_qty
|
||||
elif formatted_symbol == 'BTCUSDC':
|
||||
# Assume similar precision for BTC
|
||||
formatted_qty = round(quantity, 6)
|
||||
step_size = 0.000001
|
||||
formatted_qty = round(formatted_qty / step_size) * step_size
|
||||
formatted_qty = round(formatted_qty, 6)
|
||||
return formatted_qty
|
||||
else:
|
||||
# Default formatting - 6 decimal places
|
||||
return round(quantity, 6)
|
||||
except Exception as e:
|
||||
logger.error(f"Error formatting quantity for {formatted_symbol}: {e}")
|
||||
return None
|
||||
|
||||
def _adjust_order_type_for_symbol(self, formatted_symbol: str, order_type: str) -> str:
|
||||
"""Adjust order type based on symbol restrictions"""
|
||||
if formatted_symbol == 'ETHUSDC':
|
||||
# ETHUSDC only supports LIMIT and LIMIT_MAKER orders
|
||||
if order_type == 'MARKET':
|
||||
logger.info(f"MEXC: Converting MARKET order to LIMIT for {formatted_symbol} (MARKET not supported)")
|
||||
return 'LIMIT'
|
||||
return order_type
|
||||
|
||||
def cancel_order(self, symbol: str, order_id: str) -> Dict[str, Any]:
|
||||
"""Cancel an existing order on MEXC."""
|
||||
|
Reference in New Issue
Block a user