diff --git a/NN/exchanges/mexc_interface.py b/NN/exchanges/mexc_interface.py index 7c4e6ea..6a01eca 100644 --- a/NN/exchanges/mexc_interface.py +++ b/NN/exchanges/mexc_interface.py @@ -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.""" diff --git a/config.yaml b/config.yaml index 15ef108..c974c97 100644 --- a/config.yaml +++ b/config.yaml @@ -159,14 +159,14 @@ trading: # MEXC Trading API Configuration mexc_trading: enabled: true - trading_mode: live # simulation, testnet, live + trading_mode: simulation # simulation, testnet, live # Position sizing as percentage of account balance base_position_percent: 1 # 0.5% base position of account (MUCH SAFER) max_position_percent: 5.0 # 2% max position of account (REDUCED) min_position_percent: 0.5 # 0.2% min position of account (REDUCED) leverage: 1.0 # 1x leverage (NO LEVERAGE FOR TESTING) - simulation_account_usd: 100.0 # $100 simulation account balance + simulation_account_usd: 99.9 # $100 simulation account balance # Risk management max_daily_loss_usd: 200.0 diff --git a/core/trading_executor.py b/core/trading_executor.py index 13c55c3..fae776f 100644 --- a/core/trading_executor.py +++ b/core/trading_executor.py @@ -114,12 +114,17 @@ class TradingExecutor: # Thread safety self.lock = Lock() - # Connect to exchange + # Connect to exchange - skip connection check in simulation mode if self.trading_enabled: - logger.info("TRADING EXECUTOR: Attempting to connect to exchange...") - if not self._connect_exchange(): - logger.error("TRADING EXECUTOR: Failed initial exchange connection. Trading will be disabled.") - self.trading_enabled = False + if self.simulation_mode: + logger.info("TRADING EXECUTOR: Simulation mode - skipping exchange connection check") + # In simulation mode, we don't need a real exchange connection + # Trading should remain enabled for simulation trades + else: + logger.info("TRADING EXECUTOR: Attempting to connect to exchange...") + if not self._connect_exchange(): + logger.error("TRADING EXECUTOR: Failed initial exchange connection. Trading will be disabled.") + self.trading_enabled = False else: logger.info("TRADING EXECUTOR: Trading is explicitly disabled in config.") diff --git a/enhanced_realtime_training.py b/enhanced_realtime_training.py index 9ad86ec..1fd383c 100644 --- a/enhanced_realtime_training.py +++ b/enhanced_realtime_training.py @@ -1884,7 +1884,10 @@ class EnhancedRealtimeTrainingSystem: if (self.orchestrator and hasattr(self.orchestrator, 'rl_agent') and self.orchestrator.rl_agent): - # Get Q-values from model + # Use RL agent to make prediction + current_state = self._get_dqn_state(symbol) + if current_state is None: + return action = self.orchestrator.rl_agent.act(current_state, explore=False) # Get Q-values separately if available if hasattr(self.orchestrator.rl_agent, 'policy_net'): @@ -1893,13 +1896,11 @@ class EnhancedRealtimeTrainingSystem: q_values_tensor = self.orchestrator.rl_agent.policy_net(state_tensor) if isinstance(q_values_tensor, tuple): q_values = q_values_tensor[0].cpu().numpy()[0].tolist() - else: - q_values = q_values_tensor.cpu().numpy()[0].tolist() else: q_values = [0.33, 0.33, 0.34] # Default uniform distribution confidence = max(q_values) / sum(q_values) if sum(q_values) > 0 else 0.33 - + else: # Fallback to technical analysis-based prediction action, q_values, confidence = self._technical_analysis_prediction(symbol) diff --git a/web/clean_dashboard.py b/web/clean_dashboard.py index f5a042b..3ca7ef3 100644 --- a/web/clean_dashboard.py +++ b/web/clean_dashboard.py @@ -205,6 +205,9 @@ class CleanTradingDashboard: # Start signal generation loop to ensure continuous trading signals self._start_signal_generation_loop() + # Start live balance sync for trading + self._start_live_balance_sync() + # Start training sessions if models are showing FRESH status threading.Thread(target=self._delayed_training_check, daemon=True).start() @@ -329,14 +332,14 @@ class CleanTradingDashboard: hasattr(self.trading_executor, 'simulation_mode') and not self.trading_executor.simulation_mode) - if is_live and hasattr(self.trading_executor, 'exchange_interface'): + if is_live and hasattr(self.trading_executor, 'exchange'): # Get real balance from exchange (throttled to avoid API spam) import time current_time = time.time() - # Cache balance for 10 seconds to avoid excessive API calls - if not hasattr(self, '_last_balance_check') or current_time - self._last_balance_check > 10: - exchange = self.trading_executor.exchange_interface + # Cache balance for 5 seconds for more frequent updates in live trading + if not hasattr(self, '_last_balance_check') or current_time - self._last_balance_check > 5: + exchange = self.trading_executor.exchange if hasattr(exchange, 'get_balance'): live_balance = exchange.get_balance('USDC') if live_balance is not None and live_balance > 0: @@ -354,13 +357,20 @@ class CleanTradingDashboard: logger.info(f"LIVE BALANCE: Using USDT balance ${usdt_balance:.2f}") return usdt_balance else: - logger.warning("LIVE BALANCE: Exchange interface does not have get_balance method") + logger.warning("LIVE BALANCE: Exchange does not have get_balance method") else: # Return cached balance if within 10 second window if hasattr(self, '_cached_live_balance'): return self._cached_live_balance + elif hasattr(self.trading_executor, 'simulation_mode') and self.trading_executor.simulation_mode: + # In simulation mode, show dynamic balance based on P&L + initial_balance = self._get_initial_balance() + realized_pnl = sum(trade.get('pnl', 0) for trade in self.closed_trades) + simulation_balance = initial_balance + realized_pnl + logger.debug(f"SIMULATION BALANCE: ${simulation_balance:.2f} (Initial: ${initial_balance:.2f} + P&L: ${realized_pnl:.2f})") + return simulation_balance else: - logger.debug("LIVE BALANCE: Not in live trading mode, using simulation balance") + logger.debug("LIVE BALANCE: Not in live trading mode, using initial balance") # Fallback to initial balance for simulation mode return self._get_initial_balance() @@ -484,10 +494,13 @@ class CleanTradingDashboard: mexc_status = "SIM" if self.trading_executor: if hasattr(self.trading_executor, 'trading_enabled') and self.trading_executor.trading_enabled: - if hasattr(self.trading_executor, 'simulation_mode') and not self.trading_executor.simulation_mode: + if hasattr(self.trading_executor, 'simulation_mode') and self.trading_executor.simulation_mode: + # Show simulation mode status with simulated balance + mexc_status = f"SIM - ${current_balance:.2f}" + elif hasattr(self.trading_executor, 'simulation_mode') and not self.trading_executor.simulation_mode: # Show live balance in MEXC status - detect currency try: - exchange = self.trading_executor.exchange_interface + exchange = self.trading_executor.exchange usdc_balance = exchange.get_balance('USDC') if hasattr(exchange, 'get_balance') else 0 usdt_balance = exchange.get_balance('USDT') if hasattr(exchange, 'get_balance') else 0 @@ -2957,6 +2970,39 @@ class CleanTradingDashboard: except Exception as e: logger.error(f"Error starting signal generation loop: {e}") + + def _start_live_balance_sync(self): + """Start continuous live balance synchronization for trading""" + def balance_sync_worker(): + while True: + try: + if self.trading_executor: + is_live = (hasattr(self.trading_executor, 'trading_enabled') and + self.trading_executor.trading_enabled and + hasattr(self.trading_executor, 'simulation_mode') and + not self.trading_executor.simulation_mode) + + if is_live and hasattr(self.trading_executor, 'exchange'): + # Force balance refresh every 15 seconds in live mode + if hasattr(self, '_last_balance_check'): + del self._last_balance_check # Force refresh + + balance = self._get_live_balance() + if balance > 0: + logger.debug(f"BALANCE SYNC: Live balance: ${balance:.2f}") + else: + logger.warning("BALANCE SYNC: Could not retrieve live balance") + + # Sync balance every 15 seconds for live trading + time.sleep(15) + except Exception as e: + logger.debug(f"Error in balance sync loop: {e}") + time.sleep(30) # Wait longer on error + + # Start balance sync thread only if we have trading enabled + if self.trading_executor: + threading.Thread(target=balance_sync_worker, daemon=True).start() + logger.info("BALANCE SYNC: Background balance synchronization started") def _generate_dqn_signal(self, symbol: str, current_price: float) -> Optional[Dict]: """Generate trading signal using DQN agent - NOT AVAILABLE IN BASIC ORCHESTRATOR""" @@ -4600,28 +4646,35 @@ class CleanTradingDashboard: imbalance = cob_snapshot['stats']['imbalance'] abs_imbalance = abs(imbalance) - # Dynamic threshold based on imbalance strength + # Dynamic threshold based on imbalance strength with realistic confidence if abs_imbalance > 0.8: # Very strong imbalance (>80%) threshold = 0.05 # 5% threshold for very strong signals - confidence_multiplier = 3.0 + base_confidence = 0.85 # High but not perfect confidence + confidence_boost = (abs_imbalance - 0.8) * 0.75 # Scale remaining 15% elif abs_imbalance > 0.5: # Strong imbalance (>50%) threshold = 0.1 # 10% threshold for strong signals - confidence_multiplier = 2.5 + base_confidence = 0.70 # Good confidence + confidence_boost = (abs_imbalance - 0.5) * 0.50 # Scale up to 85% elif abs_imbalance > 0.3: # Moderate imbalance (>30%) threshold = 0.15 # 15% threshold for moderate signals - confidence_multiplier = 2.0 + base_confidence = 0.55 # Moderate confidence + confidence_boost = (abs_imbalance - 0.3) * 0.75 # Scale up to 70% else: # Weak imbalance threshold = 0.2 # 20% threshold for weak signals - confidence_multiplier = 1.5 + base_confidence = 0.35 # Low confidence + confidence_boost = abs_imbalance * 0.67 # Scale up to 55% # Generate signal if imbalance exceeds threshold if abs_imbalance > threshold: + # Calculate more realistic confidence (never exactly 1.0) + final_confidence = min(0.95, base_confidence + confidence_boost) + signal = { 'timestamp': datetime.now(), 'type': 'cob_liquidity_imbalance', 'action': 'BUY' if imbalance > 0 else 'SELL', 'symbol': symbol, - 'confidence': min(1.0, abs_imbalance * confidence_multiplier), + 'confidence': final_confidence, 'strength': abs_imbalance, 'threshold_used': threshold, 'signal_strength': 'very_strong' if abs_imbalance > 0.8 else 'strong' if abs_imbalance > 0.5 else 'moderate' if abs_imbalance > 0.3 else 'weak',