mexc interface integrations REST API fixes
This commit is contained in:
3184
NN/exchanges/mexc/mexc_postman_dump.json
Normal file
3184
NN/exchanges/mexc/mexc_postman_dump.json
Normal file
File diff suppressed because it is too large
Load Diff
170
NN/exchanges/mexc/test_live_trading.py
Normal file
170
NN/exchanges/mexc/test_live_trading.py
Normal file
@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test Live Trading - Verify MEXC Connection and Trading
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
# Add project root to path
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from core.trading_executor import TradingExecutor
|
||||
from core.config import get_config
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def test_live_trading():
|
||||
"""Test live trading functionality"""
|
||||
try:
|
||||
logger.info("=== LIVE TRADING TEST ===")
|
||||
logger.info("Testing MEXC connection and account balance reading")
|
||||
|
||||
# Initialize trading executor
|
||||
logger.info("Initializing Trading Executor...")
|
||||
executor = TradingExecutor("config.yaml")
|
||||
|
||||
# Check trading mode
|
||||
logger.info(f"Trading Mode: {executor.trading_mode}")
|
||||
logger.info(f"Simulation Mode: {executor.simulation_mode}")
|
||||
logger.info(f"Trading Enabled: {executor.trading_enabled}")
|
||||
|
||||
if executor.simulation_mode:
|
||||
logger.warning("WARNING: Still in simulation mode. Check config.yaml")
|
||||
return
|
||||
|
||||
# Test 1: Get account balance
|
||||
logger.info("\n=== TEST 1: ACCOUNT BALANCE ===")
|
||||
try:
|
||||
balances = executor.get_account_balance()
|
||||
logger.info("Account Balances:")
|
||||
|
||||
total_value = 0.0
|
||||
for asset, balance_info in balances.items():
|
||||
if balance_info['total'] > 0:
|
||||
logger.info(f" {asset}: {balance_info['total']:.6f} ({balance_info['type']})")
|
||||
if asset in ['USDT', 'USDC', 'USD']:
|
||||
total_value += balance_info['total']
|
||||
|
||||
logger.info(f"Total USD Value: ${total_value:.2f}")
|
||||
|
||||
if total_value < 25:
|
||||
logger.warning(f"Account balance ${total_value:.2f} may be insufficient for testing")
|
||||
else:
|
||||
logger.info(f"Account balance ${total_value:.2f} looks good for testing")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting account balance: {e}")
|
||||
return
|
||||
|
||||
# Test 2: Get current ETH price
|
||||
logger.info("\n=== TEST 2: MARKET DATA ===")
|
||||
try:
|
||||
# Test getting current price for ETH/USDT
|
||||
if executor.exchange:
|
||||
ticker = executor.exchange.get_ticker("ETH/USDT")
|
||||
if ticker and 'last' in ticker:
|
||||
current_price = ticker['last']
|
||||
logger.info(f"Current ETH/USDT Price: ${current_price:.2f}")
|
||||
else:
|
||||
logger.error("Failed to get ETH/USDT ticker data")
|
||||
return
|
||||
else:
|
||||
logger.error("Exchange interface not available")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting market data: {e}")
|
||||
return
|
||||
|
||||
# Test 3: Calculate position sizing
|
||||
logger.info("\n=== TEST 3: POSITION SIZING ===")
|
||||
try:
|
||||
# Test position size calculation with different confidence levels
|
||||
test_confidences = [0.3, 0.5, 0.7, 0.9]
|
||||
|
||||
for confidence in test_confidences:
|
||||
position_size = executor._calculate_position_size(confidence, current_price)
|
||||
quantity = position_size / current_price
|
||||
logger.info(f"Confidence {confidence:.1f}: ${position_size:.2f} = {quantity:.6f} ETH")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating position sizes: {e}")
|
||||
return
|
||||
|
||||
# Test 4: Small test trade (optional - requires confirmation)
|
||||
logger.info("\n=== TEST 4: TEST TRADE (OPTIONAL) ===")
|
||||
|
||||
user_input = input("Do you want to execute a SMALL test trade? (type 'YES' to confirm): ")
|
||||
if user_input.upper() == 'YES':
|
||||
try:
|
||||
logger.info("Executing SMALL test BUY order...")
|
||||
|
||||
# Execute a very small buy order with low confidence (minimum position size)
|
||||
success = executor.execute_signal(
|
||||
symbol="ETH/USDT",
|
||||
action="BUY",
|
||||
confidence=0.3, # Low confidence = minimum position size
|
||||
current_price=current_price
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("✅ Test BUY order executed successfully!")
|
||||
|
||||
# Wait a moment, then try to sell
|
||||
await asyncio.sleep(2)
|
||||
|
||||
logger.info("Executing corresponding SELL order...")
|
||||
success = executor.execute_signal(
|
||||
symbol="ETH/USDT",
|
||||
action="SELL",
|
||||
confidence=0.9, # High confidence to ensure execution
|
||||
current_price=current_price
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info("✅ Test SELL order executed successfully!")
|
||||
logger.info("✅ Full test trade cycle completed!")
|
||||
else:
|
||||
logger.warning("❌ Test SELL order failed")
|
||||
else:
|
||||
logger.warning("❌ Test BUY order failed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing test trade: {e}")
|
||||
else:
|
||||
logger.info("Test trade skipped")
|
||||
|
||||
# Test 5: Position and trade history
|
||||
logger.info("\n=== TEST 5: POSITIONS AND HISTORY ===")
|
||||
try:
|
||||
positions = executor.get_positions()
|
||||
trade_history = executor.get_trade_history()
|
||||
|
||||
logger.info(f"Current Positions: {len(positions)}")
|
||||
for symbol, position in positions.items():
|
||||
logger.info(f" {symbol}: {position.side} {position.quantity:.6f} @ ${position.entry_price:.2f}")
|
||||
|
||||
logger.info(f"Trade History: {len(trade_history)} trades")
|
||||
for trade in trade_history[-5:]: # Last 5 trades
|
||||
pnl_str = f"${trade.pnl:+.2f}" if trade.pnl else "$0.00"
|
||||
logger.info(f" {trade.symbol} {trade.side}: {pnl_str}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting positions/history: {e}")
|
||||
|
||||
logger.info("\n=== LIVE TRADING TEST COMPLETED ===")
|
||||
logger.info("If all tests passed, live trading is ready!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in live trading test: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_live_trading())
|
@ -65,45 +65,48 @@ class MEXCInterface(ExchangeInterface):
|
||||
return False
|
||||
|
||||
def _format_spot_symbol(self, symbol: str) -> str:
|
||||
"""Formats a symbol to MEXC spot API standard (e.g., 'ETH/USDT' -> 'ETHUSDC')."""
|
||||
"""Formats a symbol to MEXC spot API standard and converts USDT to USDC for execution."""
|
||||
if '/' in symbol:
|
||||
base, quote = symbol.split('/')
|
||||
# Convert USDT to USDC for MEXC spot trading
|
||||
# Convert USDT to USDC for MEXC execution (MEXC API only supports USDC pairs)
|
||||
if quote.upper() == 'USDT':
|
||||
quote = 'USDC'
|
||||
return f"{base.upper()}{quote.upper()}"
|
||||
else:
|
||||
# Convert USDT to USDC for symbols like ETHUSDT
|
||||
symbol = symbol.upper()
|
||||
if symbol.endswith('USDT'):
|
||||
symbol = symbol.replace('USDT', 'USDC')
|
||||
return symbol
|
||||
# Convert USDT to USDC for symbols like ETHUSDT -> ETHUSDC
|
||||
if symbol.upper().endswith('USDT'):
|
||||
symbol = symbol.upper().replace('USDT', 'USDC')
|
||||
return symbol.upper()
|
||||
|
||||
def _format_futures_symbol(self, symbol: str) -> str:
|
||||
"""Formats a symbol to MEXC futures API standard (e.g., 'ETH/USDT' -> 'ETH_USDT')."""
|
||||
# This method is included for completeness but should not be used for spot trading
|
||||
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']
|
||||
def _generate_signature(self, params: Dict[str, Any]) -> str:
|
||||
"""Generate signature for private API calls using MEXC's parameter ordering"""
|
||||
# MEXC uses specific parameter ordering for signature generation
|
||||
# Based on working Postman collection: symbol, side, type, quantity, price, timestamp, recvWindow, then others
|
||||
|
||||
# Remove signature if present
|
||||
clean_params = {k: v for k, v in params.items() if k != 'signature'}
|
||||
|
||||
# MEXC parameter order (from working Postman collection)
|
||||
mexc_order = ['symbol', 'side', 'type', 'quantity', 'price', '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]}")
|
||||
for param_name in mexc_order:
|
||||
if param_name in clean_params:
|
||||
ordered_params.append(f"{param_name}={clean_params[param_name]}")
|
||||
del clean_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]}")
|
||||
# Add any remaining parameters in alphabetical order
|
||||
for key in sorted(clean_params.keys()):
|
||||
ordered_params.append(f"{key}={clean_params[key]}")
|
||||
|
||||
# Create query string (MEXC doesn't use the api_key + timestamp prefix)
|
||||
# Create query string
|
||||
query_string = '&'.join(ordered_params)
|
||||
|
||||
logger.debug(f"MEXC signature query string: {query_string}")
|
||||
@ -118,7 +121,7 @@ class MEXCInterface(ExchangeInterface):
|
||||
logger.debug(f"MEXC signature: {signature}")
|
||||
return signature
|
||||
|
||||
def _send_public_request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
def _send_public_request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
||||
"""Send a public API request to MEXC."""
|
||||
if params is None:
|
||||
params = {}
|
||||
@ -145,46 +148,52 @@ 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 = {}
|
||||
|
||||
timestamp = str(int(time.time() * 1000))
|
||||
|
||||
# Add timestamp and recvWindow to params for signature and request
|
||||
params['timestamp'] = timestamp
|
||||
params['recvWindow'] = self.recv_window
|
||||
signature = self._generate_signature(timestamp, method, endpoint, params)
|
||||
params['recvWindow'] = str(self.recv_window)
|
||||
|
||||
# Generate signature with all parameters
|
||||
signature = self._generate_signature(params)
|
||||
params['signature'] = signature
|
||||
|
||||
headers = {
|
||||
"X-MEXC-APIKEY": self.api_key,
|
||||
"Request-Time": timestamp
|
||||
"X-MEXC-APIKEY": self.api_key
|
||||
}
|
||||
|
||||
# For spot API, use the correct endpoint format
|
||||
if not endpoint.startswith('api/v3/'):
|
||||
endpoint = f"api/v3/{endpoint}"
|
||||
url = f"{self.base_url}/{endpoint}"
|
||||
|
||||
try:
|
||||
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
|
||||
# For POST requests, MEXC expects parameters as query parameters, not form data
|
||||
# Based on Postman collection: Content-Type header is disabled
|
||||
response = self.session.post(url, headers=headers, params=params, timeout=10)
|
||||
elif method.upper() == "DELETE":
|
||||
response = self.session.delete(url, headers=headers, params=params, timeout=10)
|
||||
else:
|
||||
logger.error(f"Unsupported method: {method}")
|
||||
return None
|
||||
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
# For successful responses, return the data directly
|
||||
# MEXC doesn't always use 'success' field for successful operations
|
||||
logger.debug(f"Request URL: {response.url}")
|
||||
logger.debug(f"Response status: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
return data
|
||||
return response.json()
|
||||
else:
|
||||
logger.error(f"API error: Status Code: {response.status_code}, Response: {response.text}")
|
||||
return None
|
||||
|
||||
except requests.exceptions.HTTPError as http_err:
|
||||
logger.error(f"HTTP error for {endpoint}: Status Code: {response.status_code}, Response: {response.text}")
|
||||
logger.error(f"HTTP error details: {http_err}")
|
||||
@ -223,7 +232,11 @@ class MEXCInterface(ExchangeInterface):
|
||||
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)
|
||||
found_ticker = None
|
||||
for item in response:
|
||||
if isinstance(item, dict) and item.get('symbol') == formatted_symbol:
|
||||
found_ticker = item
|
||||
break
|
||||
if found_ticker:
|
||||
ticker_data = found_ticker
|
||||
else:
|
||||
@ -293,38 +306,66 @@ class MEXCInterface(ExchangeInterface):
|
||||
logger.info(f"Supported symbols include: {supported_symbols[:10]}...") # Show first 10
|
||||
return {}
|
||||
|
||||
endpoint = "order"
|
||||
# Round quantity to MEXC precision requirements and ensure minimum order value
|
||||
# MEXC ETHUSDC requires precision based on baseAssetPrecision (5 decimals for ETH)
|
||||
if 'ETH' in formatted_symbol:
|
||||
quantity = round(quantity, 5) # MEXC ETHUSDC precision: 5 decimals
|
||||
# Ensure minimum order value (typically $10+ for MEXC)
|
||||
if price and quantity * price < 10.0:
|
||||
quantity = round(10.0 / price, 5) # Adjust to minimum $10 order
|
||||
elif 'BTC' in formatted_symbol:
|
||||
quantity = round(quantity, 6) # MEXC BTCUSDC precision: 6 decimals
|
||||
if price and quantity * price < 10.0:
|
||||
quantity = round(10.0 / price, 6) # Adjust to minimum $10 order
|
||||
else:
|
||||
quantity = round(quantity, 5) # Default precision for MEXC
|
||||
if price and quantity * price < 10.0:
|
||||
quantity = round(10.0 / price, 5) # Adjust to minimum $10 order
|
||||
|
||||
params: Dict[str, Any] = {
|
||||
# MEXC doesn't support MARKET orders for many pairs - use LIMIT orders instead
|
||||
if order_type.upper() == 'MARKET':
|
||||
# Convert market order to limit order with aggressive pricing for immediate execution
|
||||
if price is None:
|
||||
ticker = self.get_ticker(symbol)
|
||||
if ticker and 'last' in ticker:
|
||||
current_price = float(ticker['last'])
|
||||
# For buy orders, use slightly above market to ensure immediate execution
|
||||
# For sell orders, use slightly below market to ensure immediate execution
|
||||
if side.upper() == 'BUY':
|
||||
price = current_price * 1.002 # 0.2% premium for immediate buy execution
|
||||
else:
|
||||
price = current_price * 0.998 # 0.2% discount for immediate sell execution
|
||||
else:
|
||||
logger.error("Cannot get current price for market order conversion")
|
||||
return {}
|
||||
|
||||
# Convert to limit order with immediate execution pricing
|
||||
order_type = 'LIMIT'
|
||||
logger.info(f"MEXC: Converting MARKET to aggressive LIMIT order at ${price:.2f} for immediate execution")
|
||||
|
||||
# Prepare order parameters
|
||||
params = {
|
||||
'symbol': formatted_symbol,
|
||||
'side': side.upper(),
|
||||
'type': order_type.upper(),
|
||||
'quantity': str(quantity) # Quantity must be a string
|
||||
}
|
||||
|
||||
if price is not None:
|
||||
params['price'] = str(price) # Price must be a string for limit orders
|
||||
# Format price to remove unnecessary decimal places (e.g., 2900.0 -> 2900)
|
||||
params['price'] = str(int(price)) if price == int(price) else str(price)
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
# MEXC API endpoint for placing orders is /api/v3/order (POST)
|
||||
order_result = self._send_private_request('POST', endpoint, params)
|
||||
if order_result:
|
||||
logger.info(f"MEXC: Order placed successfully: {order_result}")
|
||||
return order_result
|
||||
else:
|
||||
logger.error(f"MEXC: Error placing order: {order_result}")
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.error(f"MEXC: Exception placing order: {e}")
|
||||
# Use the standard private request method which handles timestamp and signature
|
||||
endpoint = "order"
|
||||
result = self._send_private_request("POST", endpoint, params)
|
||||
|
||||
if result:
|
||||
logger.info(f"MEXC: Order placed successfully: {result}")
|
||||
return result
|
||||
else:
|
||||
logger.error(f"MEXC: Failed to place order")
|
||||
return {}
|
||||
|
||||
def cancel_order(self, symbol: str, order_id: str) -> Dict[str, Any]:
|
||||
|
Reference in New Issue
Block a user