diff --git a/NN/exchanges/mexc_interface.py b/NN/exchanges/mexc_interface.py index 2bb7e64..1596ede 100644 --- a/NN/exchanges/mexc_interface.py +++ b/NN/exchanges/mexc_interface.py @@ -23,15 +23,15 @@ class MEXCInterface(ExchangeInterface): """ super().__init__(api_key, api_secret, test_mode) self.base_url = "https://api.mexc.com" - self.api_version = "v3" + self.api_version = "api/v3" def connect(self) -> bool: """Connect to MEXC API.""" if not self.api_key or not self.api_secret: logger.warning("MEXC API credentials not provided. Running in read-only mode.") try: - # Test public API connection by getting ticker data for BTC/USDT - self.get_ticker("BTC/USDT") + # Test public API connection by getting server time (ping) + self.get_server_time() logger.info("Successfully connected to MEXC API in read-only mode") return True except Exception as e: @@ -41,7 +41,7 @@ class MEXCInterface(ExchangeInterface): try: # Test connection by getting account info self.get_account_info() - logger.info("Successfully connected to MEXC API") + logger.info("Successfully connected to MEXC API with authentication") return True except Exception as e: logger.error(f"Failed to connect to MEXC API: {str(e)}") @@ -49,7 +49,9 @@ class MEXCInterface(ExchangeInterface): def _generate_signature(self, params: Dict[str, Any]) -> str: """Generate signature for authenticated requests.""" - query_string = urlencode(params) + # Sort parameters by key for consistent signature generation + sorted_params = sorted(params.items()) + query_string = urlencode(sorted_params) signature = hmac.new( self.api_secret.encode('utf-8'), query_string.encode('utf-8'), @@ -81,26 +83,32 @@ class MEXCInterface(ExchangeInterface): if params is None: params = {} - # Add timestamp + # Add timestamp and recvWindow as required by MEXC params['timestamp'] = int(time.time() * 1000) + if 'recvWindow' not in params: + params['recvWindow'] = 5000 - # Generate signature + # Generate signature using the correct MEXC format signature = self._generate_signature(params) params['signature'] = signature - # Set headers + # Set headers as required by MEXC documentation headers = { - 'X-MEXC-APIKEY': self.api_key + 'X-MEXC-APIKEY': self.api_key, + 'Content-Type': 'application/x-www-form-urlencoded' } url = f"{self.base_url}/{self.api_version}/{endpoint}" try: if method.upper() == 'GET': + # For GET requests, send parameters as query string response = requests.get(url, params=params, headers=headers) elif method.upper() == 'POST': - response = requests.post(url, json=params, headers=headers) + # For POST requests, send as form data in request body per MEXC documentation + 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) else: raise ValueError(f"Unsupported HTTP method: {method}") @@ -109,11 +117,23 @@ class MEXCInterface(ExchangeInterface): return response.json() 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}") raise + def get_server_time(self) -> Dict[str, Any]: + """Get server time (ping test).""" + return self._send_public_request('GET', 'time') + + def ping(self) -> Dict[str, Any]: + """Test connectivity to the Rest API.""" + return self._send_public_request('GET', 'ping') + def get_account_info(self) -> Dict[str, Any]: """Get account information.""" - return self._send_private_request('GET', 'account') + params = {'recvWindow': 5000} + return self._send_private_request('GET', 'account', params) def get_balance(self, asset: str) -> float: """Get balance of a specific asset. @@ -125,7 +145,8 @@ class MEXCInterface(ExchangeInterface): float: Available balance of the asset """ try: - account_info = self._send_private_request('GET', 'account') + params = {'recvWindow': 5000} + account_info = self._send_private_request('GET', 'account', params) balances = account_info.get('balances', []) for balance in balances: @@ -148,61 +169,86 @@ class MEXCInterface(ExchangeInterface): dict: Ticker data including price information """ mexc_symbol = symbol.replace('/', '') + + # Use official MEXC API endpoints from documentation endpoints_to_try = [ - ('ticker/price', {'symbol': mexc_symbol}), - ('ticker', {'symbol': mexc_symbol}), - ('ticker/24hr', {'symbol': mexc_symbol}), - ('ticker/bookTicker', {'symbol': mexc_symbol}), - ('market/ticker', {'symbol': mexc_symbol}) + ('ticker/price', {'symbol': mexc_symbol}), # Symbol Price Ticker + ('ticker/24hr', {'symbol': mexc_symbol}), # 24hr Ticker Price Change Statistics + ('ticker/bookTicker', {'symbol': mexc_symbol}), # Symbol Order Book Ticker ] for endpoint, params in endpoints_to_try: try: - logger.info(f"Trying to get ticker from endpoint: {endpoint}") + logger.debug(f"Trying MEXC endpoint: {endpoint} for {mexc_symbol}") response = self._send_public_request('GET', endpoint, params) + if not response: + continue + # Handle the response based on structure if isinstance(response, dict): - # Single ticker response ticker = response elif isinstance(response, list) and len(response) > 0: - # List of tickers, find the one we want + # Find the specific symbol in list response ticker = None for t in response: if t.get('symbol') == mexc_symbol: ticker = t break if ticker is None: - continue # Try next endpoint if not found + continue else: - continue # Try next endpoint if unexpected response + continue - # Convert to a standardized format with defaults for missing fields + # Convert to standardized format based on MEXC API response current_time = int(time.time() * 1000) - result = { - 'symbol': symbol, - 'bid': float(ticker.get('bidPrice', ticker.get('bid', 0))), - 'ask': float(ticker.get('askPrice', ticker.get('ask', 0))), - 'last': float(ticker.get('price', ticker.get('lastPrice', ticker.get('last', 0)))), - 'volume': float(ticker.get('volume', ticker.get('quoteVolume', 0))), - 'timestamp': int(ticker.get('time', ticker.get('closeTime', current_time))) - } - # Ensure we have at least a price + # Handle different response formats from different endpoints + if 'price' in ticker: + # ticker/price endpoint + price = float(ticker['price']) + result = { + 'symbol': symbol, + 'bid': price, # Use price as fallback + 'ask': price, # Use price as fallback + 'last': price, + 'volume': 0, # Not available in price endpoint + 'timestamp': current_time + } + elif 'lastPrice' in ticker: + # ticker/24hr endpoint + result = { + 'symbol': symbol, + 'bid': float(ticker.get('bidPrice', ticker.get('lastPrice', 0))), + 'ask': float(ticker.get('askPrice', ticker.get('lastPrice', 0))), + 'last': float(ticker.get('lastPrice', 0)), + 'volume': float(ticker.get('volume', ticker.get('quoteVolume', 0))), + 'timestamp': int(ticker.get('closeTime', current_time)) + } + elif 'bidPrice' in ticker: + # ticker/bookTicker endpoint + result = { + 'symbol': symbol, + 'bid': float(ticker.get('bidPrice', 0)), + 'ask': float(ticker.get('askPrice', 0)), + 'last': float(ticker.get('bidPrice', 0)), # Use bid as fallback for last + 'volume': 0, # Not available in book ticker + 'timestamp': current_time + } + else: + continue + + # Validate we have a valid price if result['last'] > 0: - logger.info(f"Successfully got ticker from {endpoint} for {symbol}: {result['last']}") + logger.info(f"✅ MEXC: Got ticker from {endpoint} for {symbol}: ${result['last']:.2f}") return result except Exception as e: - logger.error(f"❌ CRITICAL: Failed to get ticker for {symbol}: {e}") - logger.error("❌ NO DUMMY DATA FALLBACK - Real market data required") - # Return None instead of dummy data - let calling code handle the failure - return None + logger.warning(f"MEXC endpoint {endpoint} failed for {symbol}: {e}") + continue - # If we get here, all endpoints failed - logger.error(f"All ticker endpoints failed for {symbol}") - - # Return None instead of dummy data - let calling code handle the failure + # All endpoints failed + logger.error(f"❌ MEXC: All ticker endpoints failed for {symbol}") return None def place_order(self, symbol: str, side: str, order_type: str, @@ -211,8 +257,8 @@ class MEXCInterface(ExchangeInterface): Args: symbol: Trading symbol (e.g., 'BTC/USDT') - side: Order side ('buy' or 'sell') - order_type: Order type ('market', 'limit', etc.) + side: Order side ('BUY' or 'SELL') + order_type: Order type ('MARKET', 'LIMIT', etc.) quantity: Order quantity price: Order price (for limit orders) @@ -220,22 +266,30 @@ class MEXCInterface(ExchangeInterface): dict: Order information including order ID """ mexc_symbol = symbol.replace('/', '') + + # Prepare order parameters according to MEXC API params = { 'symbol': mexc_symbol, 'side': side.upper(), 'type': order_type.upper(), - 'quantity': quantity, + 'quantity': str(quantity), # MEXC expects string format + 'recvWindow': 5000 } - if order_type.lower() == 'limit' and price is not None: - params['price'] = price + # 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) params['timeInForce'] = 'GTC' # Good Till Cancelled try: + logger.info(f"MEXC: Placing {side} {order_type} order for {symbol}: {quantity} @ {price}") order_result = self._send_private_request('POST', 'order', params) + logger.info(f"MEXC: Order placed successfully: {order_result.get('orderId', 'N/A')}") return order_result except Exception as e: - logger.error(f"Error placing {side} {order_type} order for {symbol}: {str(e)}") + logger.error(f"MEXC: Error placing {side} {order_type} order for {symbol}: {str(e)}") raise def cancel_order(self, symbol: str, order_id: str) -> bool: diff --git a/config.yaml b/config.yaml index 10c16cf..e61da89 100644 --- a/config.yaml +++ b/config.yaml @@ -151,18 +151,18 @@ mexc_trading: # Risk management max_daily_loss_usd: 5.0 # Stop trading if daily loss exceeds $5 - max_concurrent_positions: 1 # Only 1 position at a time for testing - max_trades_per_hour: 2 # Maximum 2 trades per hour - min_trade_interval_seconds: 300 # Minimum 5 minutes between trades + max_concurrent_positions: 3 # Only 1 position at a time for testing + max_trades_per_hour: 60 # Maximum 60 trades per hour + min_trade_interval_seconds: 30 Minimum between trades # Order configuration order_type: "market" # Use market orders for immediate execution timeout_seconds: 30 # Order timeout - retry_attempts: 3 # Number of retry attempts for failed orders + retry_attempts: 0 # Number of retry attempts for failed orders # Safety features - dry_run_mode: true # Log trades but don't execute (for testing) - require_confirmation: true # Require manual confirmation for trades + dry_run_mode: false # Execute real trades (was true for testing) + require_confirmation: false # No manual confirmation for live trading emergency_stop: false # Emergency stop all trading # Supported symbols for live trading @@ -178,8 +178,8 @@ mexc_trading: # Memory Management memory: - total_limit_gb: 8.0 # Total system memory limit - model_limit_gb: 2.0 # Per-model memory limit + total_limit_gb: 28.0 # Total system memory limit + model_limit_gb: 4.0 # Per-model memory limit cleanup_interval: 1800 # Memory cleanup every 30 minutes # Web Dashboard diff --git a/test_mexc_public_api.py b/test_mexc_public_api.py new file mode 100644 index 0000000..09abe86 --- /dev/null +++ b/test_mexc_public_api.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +""" +Test script for MEXC public API endpoints +""" + +import sys +import os +import logging + +# Add project root to path +sys.path.insert(0, os.path.abspath('.')) + +from NN.exchanges.mexc_interface import MEXCInterface + +# Setup logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def test_mexc_public_api(): + """Test MEXC public API endpoints""" + print("="*60) + print("TESTING MEXC PUBLIC API") + print("="*60) + + try: + # Initialize MEXC interface without API keys (public access only) + mexc = MEXCInterface() + + print("\n1. Testing server connectivity...") + try: + # Test ping + ping_result = mexc.ping() + print(f"✅ Ping successful: {ping_result}") + except Exception as e: + print(f"❌ Ping failed: {e}") + + print("\n2. Testing server time...") + try: + # Test server time + time_result = mexc.get_server_time() + print(f"✅ Server time: {time_result}") + except Exception as e: + print(f"❌ Server time failed: {e}") + + print("\n3. Testing ticker data...") + symbols_to_test = ['BTC/USDT', 'ETH/USDT'] + + for symbol in symbols_to_test: + try: + ticker = mexc.get_ticker(symbol) + if ticker: + print(f"✅ {symbol}: ${ticker['last']:.2f} (bid: ${ticker['bid']:.2f}, ask: ${ticker['ask']:.2f})") + else: + print(f"❌ {symbol}: No data returned") + except Exception as e: + print(f"❌ {symbol}: Error - {e}") + + print("\n" + "="*60) + print("PUBLIC API TEST COMPLETED") + print("="*60) + + except Exception as e: + print(f"❌ Error initializing MEXC interface: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + test_mexc_public_api() \ No newline at end of file diff --git a/web/dashboard.py b/web/dashboard.py index 6cf4985..32d2022 100644 --- a/web/dashboard.py +++ b/web/dashboard.py @@ -149,26 +149,34 @@ class TradingDashboard: if self.trading_executor and hasattr(self.trading_executor, 'get_account_balance'): logger.info("Fetching initial balance from MEXC...") - # Get USDT balance from MEXC - balance_info = self.trading_executor.get_account_balance() - if balance_info and 'USDT' in balance_info: - usdt_balance = float(balance_info['USDT'].get('free', 0)) - if usdt_balance > 0: - logger.info(f"MEXC: Retrieved USDT balance: ${usdt_balance:.2f}") - return usdt_balance - else: - logger.warning("MEXC: No USDT balance found") + # Check if trading is enabled and not in dry run mode + if not self.trading_executor.trading_enabled: + logger.warning("MEXC: Trading not enabled - using default balance") + elif self.trading_executor.dry_run: + logger.warning("MEXC: Dry run mode enabled - using default balance") else: - logger.warning("MEXC: Failed to retrieve balance info") + # Get USDT balance from MEXC + balance_info = self.trading_executor.get_account_balance() + if balance_info and 'USDT' in balance_info: + usdt_balance = float(balance_info['USDT'].get('free', 0)) + if usdt_balance > 0: + logger.info(f"MEXC: Retrieved USDT balance: ${usdt_balance:.2f}") + return usdt_balance + else: + logger.warning("MEXC: No USDT balance found in account") + else: + logger.error("MEXC: Failed to retrieve balance info from API") else: logger.info("MEXC: Trading executor not available for balance retrieval") except Exception as e: logger.error(f"Error getting MEXC balance: {e}") + import traceback + logger.error(traceback.format_exc()) # Fallback to default default_balance = 100.0 - logger.info(f"Using default starting balance: ${default_balance:.2f}") + logger.warning(f"Using default starting balance: ${default_balance:.2f}") return default_balance def _setup_layout(self): @@ -180,7 +188,7 @@ class TradingDashboard: html.I(className="fas fa-chart-line me-2"), "Live Trading Dashboard" ], className="text-white mb-1"), - html.P(f"Ultra-Fast Updates • Starting Balance: ${self.starting_balance:,.0f}", + html.P(f"Ultra-Fast Updates • Portfolio: ${self.starting_balance:,.0f} • {'MEXC Live' if (self.trading_executor and self.trading_executor.trading_enabled and not self.trading_executor.dry_run) else 'Demo Mode'}", className="text-light mb-0 opacity-75 small") ], className="bg-dark p-2 mb-2"), @@ -530,11 +538,12 @@ class TradingDashboard: trade_count_text = f"{len(self.session_trades)}" portfolio_text = f"${portfolio_value:,.2f}" - # MEXC status + # MEXC status with detailed information if self.trading_executor and self.trading_executor.trading_enabled: - mexc_status = "LIVE" - elif self.trading_executor and self.trading_executor.dry_run: - mexc_status = "DRY RUN" + if self.trading_executor.dry_run: + mexc_status = "DRY RUN" + else: + mexc_status = "LIVE" else: mexc_status = "OFFLINE" @@ -1404,16 +1413,24 @@ class TradingDashboard: # Add MEXC execution status to decision record decision['mexc_executed'] = mexc_success - # Calculate USD-based position size for testing ($1 orders) + # Calculate position size based on confidence and configuration if current_price and current_price > 0: - usd_size = 1.0 # $1 per trade for testing + # Get position sizing from trading executor configuration + if self.trading_executor: + usd_size = self.trading_executor._calculate_position_size(decision['confidence'], current_price) + else: + # Fallback calculation based on confidence + max_usd = 1.0 # Default max position + min_usd = 0.1 # Default min position + usd_size = max(min_usd, min(max_usd * decision['confidence'], max_usd)) + position_size = usd_size / current_price # Convert USD to crypto amount decision['size'] = round(position_size, 6) # Update decision with calculated size decision['usd_size'] = usd_size # Track USD amount for logging else: # Fallback if no price available decision['size'] = 0.001 - decision['usd_size'] = 1.0 + decision['usd_size'] = 0.1 if decision['action'] == 'BUY': # First, close any existing SHORT position @@ -1485,7 +1502,7 @@ class TradingDashboard: trade_record['fees'] = fee self.session_trades.append(trade_record) - logger.info(f"[TRADE] OPENED LONG: {decision['size']:.6f} (~${decision.get('usd_size', 1.0):.2f}) @ ${decision['price']:.2f} (confidence: {decision['confidence']:.1%})") + logger.info(f"[TRADE] OPENED LONG: {decision['size']:.6f} (${decision.get('usd_size', 0.1):.2f}) @ ${decision['price']:.2f} (confidence: {decision['confidence']:.1%})") elif self.current_position['side'] == 'LONG': # Already have a long position - could add to it or replace it @@ -1610,7 +1627,7 @@ class TradingDashboard: trade_record['fees'] = fee self.session_trades.append(trade_record) - logger.info(f"[TRADE] OPENED SHORT: {decision['size']:.6f} (~${decision.get('usd_size', 1.0):.2f}) @ ${decision['price']:.2f} (confidence: {decision['confidence']:.1%})") + logger.info(f"[TRADE] OPENED SHORT: {decision['size']:.6f} (${decision.get('usd_size', 0.1):.2f}) @ ${decision['price']:.2f} (confidence: {decision['confidence']:.1%})") elif self.current_position['side'] == 'SHORT': # Already have a short position - could add to it or replace it