diff --git a/NN/exchanges/mexc_interface.py b/NN/exchanges/mexc_interface.py index 42ebb98..94a165d 100644 --- a/NN/exchanges/mexc_interface.py +++ b/NN/exchanges/mexc_interface.py @@ -149,7 +149,7 @@ class MEXCInterface(ExchangeInterface): return {} 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""" + """Send a private request to the exchange with proper signature and MEXC error handling""" if params is None: params = {} @@ -191,8 +191,51 @@ class MEXCInterface(ExchangeInterface): if response.status_code == 200: return response.json() else: - logger.error(f"API error: Status Code: {response.status_code}, Response: {response.text}") - return None + # Parse error response for specific error codes + try: + error_data = response.json() + error_code = error_data.get('code') + error_msg = error_data.get('msg', 'Unknown error') + + # Handle specific MEXC error codes + if error_code == 30005: # Oversold + logger.warning(f"MEXC Oversold detected (Code 30005) for {endpoint}. This indicates risk control measures are active.") + logger.warning(f"Possible causes: Market manipulation detection, abnormal trading patterns, or position limits.") + logger.warning(f"Action: Waiting before retry and reducing position size if needed.") + + # For oversold errors, we should not retry immediately + # Return a special error structure that the trading executor can handle + return { + 'error': 'oversold', + 'code': 30005, + 'message': error_msg, + 'retry_after': 60 # Suggest waiting 60 seconds + } + elif error_code == 30001: # Transaction direction not allowed + logger.error(f"MEXC: Transaction direction not allowed for {endpoint}") + return { + 'error': 'direction_not_allowed', + 'code': 30001, + 'message': error_msg + } + elif error_code == 30004: # Insufficient position + logger.error(f"MEXC: Insufficient position for {endpoint}") + return { + 'error': 'insufficient_position', + 'code': 30004, + 'message': error_msg + } + else: + logger.error(f"MEXC API error: Code: {error_code}, Message: {error_msg}") + return { + 'error': 'api_error', + 'code': error_code, + 'message': error_msg + } + except: + # Fallback if response is not JSON + 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}") @@ -297,75 +340,92 @@ class MEXCInterface(ExchangeInterface): def place_order(self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None) -> Dict[str, Any]: """Place a new order on MEXC.""" - formatted_symbol = self._format_spot_symbol(symbol) - - # Check if symbol is supported for API trading - if not self.is_symbol_supported(symbol): - supported_symbols = self.get_api_symbols() - logger.error(f"Symbol {formatted_symbol} is not supported for API trading") - logger.info(f"Supported symbols include: {supported_symbols[:10]}...") # Show first 10 - return {} - - # 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 - - # 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 {} + try: + logger.info(f"MEXC: place_order called with symbol={symbol}, side={side}, order_type={order_type}, quantity={quantity}, price={price}") - # 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") + formatted_symbol = self._format_spot_symbol(symbol) + logger.info(f"MEXC: Formatted symbol: {symbol} -> {formatted_symbol}") + + # Check if symbol is supported for API trading + if not self.is_symbol_supported(symbol): + supported_symbols = self.get_api_symbols() + logger.error(f"Symbol {formatted_symbol} is not supported for API trading") + logger.info(f"Supported symbols include: {supported_symbols[:10]}...") # Show first 10 + return {} + + # Round quantity to MEXC precision requirements and ensure minimum order value + # MEXC ETHUSDC requires precision based on baseAssetPrecision (5 decimals for ETH) + original_quantity = quantity + 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 + + if quantity != original_quantity: + logger.info(f"MEXC: Adjusted quantity: {original_quantity} -> {quantity}") + + # 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: - # Format price to remove unnecessary decimal places (e.g., 2900.0 -> 2900) - params['price'] = str(int(price)) if price == int(price) else str(price) + # 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: + # 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}") - - # 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") + logger.info(f"MEXC: Placing {side.upper()} {order_type.upper()} order for {quantity} {formatted_symbol} at price {price}") + logger.info(f"MEXC: Order parameters: {params}") + + # 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 - _send_private_request returned None/empty result") + logger.error(f"MEXC: Failed order details - symbol: {formatted_symbol}, side: {side}, type: {order_type}, quantity: {quantity}, price: {price}") + return {} + + except Exception as e: + logger.error(f"MEXC: Exception in place_order: {e}") + logger.error(f"MEXC: Exception details - symbol: {symbol}, side: {side}, type: {order_type}, quantity: {quantity}, price: {price}") + import traceback + logger.error(f"MEXC: Full traceback: {traceback.format_exc()}") return {} def cancel_order(self, symbol: str, order_id: str) -> Dict[str, Any]: diff --git a/NN/models/dqn_agent.py b/NN/models/dqn_agent.py index e2e069f..6a26a28 100644 --- a/NN/models/dqn_agent.py +++ b/NN/models/dqn_agent.py @@ -757,20 +757,98 @@ class DQNAgent: # Sanitize and stack states and next_states sanitized_states = [] sanitized_next_states = [] + sanitized_experiences = [] + for i, e in enumerate(experiences): try: - state = np.asarray(e[0], dtype=np.float32) - next_state = np.asarray(e[3], dtype=np.float32) + # Extract experience components + state, action, reward, next_state, done = e + + # Sanitize state - convert any dict/object to float arrays + state = self._sanitize_state_data(state) + next_state = self._sanitize_state_data(next_state) + + # Sanitize action - ensure it's an integer + if isinstance(action, dict): + # If action is a dict, try to extract action value + action = action.get('action', action.get('value', 0)) + action = int(action) if not isinstance(action, (int, np.integer)) else action + + # Sanitize reward - ensure it's a float + if isinstance(reward, dict): + # If reward is a dict, try to extract reward value + reward = reward.get('reward', reward.get('value', 0.0)) + reward = float(reward) if not isinstance(reward, (float, np.floating)) else reward + + # Sanitize done - ensure it's a boolean/float + if isinstance(done, dict): + done = done.get('done', done.get('value', False)) + done = bool(done) if not isinstance(done, (bool, np.bool_)) else done + + # Convert state to proper numpy array + state = np.asarray(state, dtype=np.float32) + next_state = np.asarray(next_state, dtype=np.float32) + + # Add to sanitized lists sanitized_states.append(state) sanitized_next_states.append(next_state) + sanitized_experiences.append((state, action, reward, next_state, done)) + except Exception as ex: print(f"[DQNAgent] Bad experience at index {i}: {ex}") continue + if not sanitized_states or not sanitized_next_states: print("[DQNAgent] No valid states in replay batch.") return 0.0 # Return float instead of None for consistency - states = torch.FloatTensor(np.stack(sanitized_states)).to(self.device) - next_states = torch.FloatTensor(np.stack(sanitized_next_states)).to(self.device) + + # Validate all states have the same dimensions before stacking + expected_dim = getattr(self, 'state_size', getattr(self, 'state_dim', 403)) + if isinstance(expected_dim, tuple): + expected_dim = np.prod(expected_dim) + + # Filter out states with wrong dimensions and fix them + valid_states = [] + valid_next_states = [] + valid_experiences = [] + + for i, (state, next_state, exp) in enumerate(zip(sanitized_states, sanitized_next_states, sanitized_experiences)): + # Ensure states have correct dimensions + if len(state) != expected_dim: + logger.debug(f"Fixing state dimension: {len(state)} -> {expected_dim}") + if len(state) < expected_dim: + # Pad with zeros + padded_state = np.zeros(expected_dim, dtype=np.float32) + padded_state[:len(state)] = state + state = padded_state + else: + # Truncate + state = state[:expected_dim] + + if len(next_state) != expected_dim: + logger.debug(f"Fixing next_state dimension: {len(next_state)} -> {expected_dim}") + if len(next_state) < expected_dim: + # Pad with zeros + padded_next_state = np.zeros(expected_dim, dtype=np.float32) + padded_next_state[:len(next_state)] = next_state + next_state = padded_next_state + else: + # Truncate + next_state = next_state[:expected_dim] + + valid_states.append(state) + valid_next_states.append(next_state) + valid_experiences.append(exp) + + if not valid_states: + print("[DQNAgent] No valid states after dimension fixing.") + return 0.0 + + # Use validated experiences for training + experiences = valid_experiences + + states = torch.FloatTensor(np.stack(valid_states)).to(self.device) + next_states = torch.FloatTensor(np.stack(valid_next_states)).to(self.device) # Choose appropriate replay method if self.use_mixed_precision: @@ -797,28 +875,42 @@ class DQNAgent: extrema_indices = np.random.choice(len(self.extrema_memory), size=min(self.batch_size, len(self.extrema_memory)), replace=False) extrema_batch = [self.extrema_memory[i] for i in extrema_indices] - # Extract tensors from extrema batch - extrema_states = torch.FloatTensor(np.array([e[0] for e in extrema_batch])).to(self.device) - extrema_actions = torch.LongTensor(np.array([e[1] for e in extrema_batch])).to(self.device) - extrema_rewards = torch.FloatTensor(np.array([e[2] for e in extrema_batch])).to(self.device) - extrema_next_states = torch.FloatTensor(np.array([e[3] for e in extrema_batch])).to(self.device) - extrema_dones = torch.FloatTensor(np.array([e[4] for e in extrema_batch])).to(self.device) + # Sanitize extrema batch + sanitized_extrema = [] + for e in extrema_batch: + try: + state, action, reward, next_state, done = e + state = self._sanitize_state_data(state) + next_state = self._sanitize_state_data(next_state) + state = np.asarray(state, dtype=np.float32) + next_state = np.asarray(next_state, dtype=np.float32) + sanitized_extrema.append((state, action, reward, next_state, done)) + except: + continue - # Use a slightly reduced learning rate for extrema training - old_lr = self.optimizer.param_groups[0]['lr'] - self.optimizer.param_groups[0]['lr'] = old_lr * 0.8 - - # Train on extrema memory - if self.use_mixed_precision: - extrema_loss = self._replay_mixed_precision(extrema_states, extrema_actions, extrema_rewards, extrema_next_states, extrema_dones) - else: - extrema_loss = self._replay_standard(extrema_batch) - - # Reset learning rate - self.optimizer.param_groups[0]['lr'] = old_lr - - # Log extrema loss - logger.info(f"Extra training on extrema points, loss: {extrema_loss:.4f}") + if sanitized_extrema: + # Extract tensors from extrema batch + extrema_states = torch.FloatTensor(np.array([e[0] for e in sanitized_extrema])).to(self.device) + extrema_actions = torch.LongTensor(np.array([e[1] for e in sanitized_extrema])).to(self.device) + extrema_rewards = torch.FloatTensor(np.array([e[2] for e in sanitized_extrema])).to(self.device) + extrema_next_states = torch.FloatTensor(np.array([e[3] for e in sanitized_extrema])).to(self.device) + extrema_dones = torch.FloatTensor(np.array([e[4] for e in sanitized_extrema])).to(self.device) + + # Use a slightly reduced learning rate for extrema training + old_lr = self.optimizer.param_groups[0]['lr'] + self.optimizer.param_groups[0]['lr'] = old_lr * 0.8 + + # Train on extrema memory + if self.use_mixed_precision: + extrema_loss = self._replay_mixed_precision(extrema_states, extrema_actions, extrema_rewards, extrema_next_states, extrema_dones) + else: + extrema_loss = self._replay_standard(sanitized_extrema) + + # Reset learning rate + self.optimizer.param_groups[0]['lr'] = old_lr + + # Log extrema loss + logger.info(f"Extra training on extrema points, loss: {extrema_loss:.4f}") # Randomly train on price movement examples (similar to extrema) if random.random() < 0.3 and len(self.price_movement_memory) >= self.batch_size: @@ -826,28 +918,42 @@ class DQNAgent: price_indices = np.random.choice(len(self.price_movement_memory), size=min(self.batch_size, len(self.price_movement_memory)), replace=False) price_batch = [self.price_movement_memory[i] for i in price_indices] - # Extract tensors from price movement batch - price_states = torch.FloatTensor(np.array([e[0] for e in price_batch])).to(self.device) - price_actions = torch.LongTensor(np.array([e[1] for e in price_batch])).to(self.device) - price_rewards = torch.FloatTensor(np.array([e[2] for e in price_batch])).to(self.device) - price_next_states = torch.FloatTensor(np.array([e[3] for e in price_batch])).to(self.device) - price_dones = torch.FloatTensor(np.array([e[4] for e in price_batch])).to(self.device) + # Sanitize price movement batch + sanitized_price = [] + for e in price_batch: + try: + state, action, reward, next_state, done = e + state = self._sanitize_state_data(state) + next_state = self._sanitize_state_data(next_state) + state = np.asarray(state, dtype=np.float32) + next_state = np.asarray(next_state, dtype=np.float32) + sanitized_price.append((state, action, reward, next_state, done)) + except: + continue - # Use a slightly reduced learning rate for price movement training - old_lr = self.optimizer.param_groups[0]['lr'] - self.optimizer.param_groups[0]['lr'] = old_lr * 0.75 - - # Train on price movement memory - if self.use_mixed_precision: - price_loss = self._replay_mixed_precision(price_states, price_actions, price_rewards, price_next_states, price_dones) - else: - price_loss = self._replay_standard(price_batch) - - # Reset learning rate - self.optimizer.param_groups[0]['lr'] = old_lr - - # Log price movement loss - logger.info(f"Extra training on price movement examples, loss: {price_loss:.4f}") + if sanitized_price: + # Extract tensors from price movement batch + price_states = torch.FloatTensor(np.array([e[0] for e in sanitized_price])).to(self.device) + price_actions = torch.LongTensor(np.array([e[1] for e in sanitized_price])).to(self.device) + price_rewards = torch.FloatTensor(np.array([e[2] for e in sanitized_price])).to(self.device) + price_next_states = torch.FloatTensor(np.array([e[3] for e in sanitized_price])).to(self.device) + price_dones = torch.FloatTensor(np.array([e[4] for e in sanitized_price])).to(self.device) + + # Use a slightly reduced learning rate for price movement training + old_lr = self.optimizer.param_groups[0]['lr'] + self.optimizer.param_groups[0]['lr'] = old_lr * 0.75 + + # Train on price movement memory + if self.use_mixed_precision: + price_loss = self._replay_mixed_precision(price_states, price_actions, price_rewards, price_next_states, price_dones) + else: + price_loss = self._replay_standard(sanitized_price) + + # Reset learning rate + self.optimizer.param_groups[0]['lr'] = old_lr + + # Log price movement loss + logger.info(f"Extra training on price movement examples, loss: {price_loss:.4f}") return loss @@ -1452,4 +1558,106 @@ class DQNAgent: total_params = 0 for param in self.policy_net.parameters(): total_params += param.numel() - return total_params \ No newline at end of file + return total_params + + def _sanitize_state_data(self, state): + """Sanitize state data to ensure it's a proper numeric array""" + try: + # If state is already a numpy array, return it + if isinstance(state, np.ndarray): + # Check for non-numeric data and handle it + if state.dtype == object: + # Convert object array to float array + sanitized = np.zeros_like(state, dtype=np.float32) + for i in range(state.shape[0]): + if len(state.shape) > 1: + for j in range(state.shape[1]): + sanitized[i, j] = self._extract_numeric_value(state[i, j]) + else: + sanitized[i] = self._extract_numeric_value(state[i]) + return sanitized + else: + return state.astype(np.float32) + + # If state is a list or tuple, convert to array + elif isinstance(state, (list, tuple)): + # Recursively sanitize each element + sanitized = [] + for item in state: + if isinstance(item, (list, tuple)): + sanitized_row = [] + for sub_item in item: + sanitized_row.append(self._extract_numeric_value(sub_item)) + sanitized.append(sanitized_row) + else: + sanitized.append(self._extract_numeric_value(item)) + return np.array(sanitized, dtype=np.float32) + + # If state is a dict, try to extract values + elif isinstance(state, dict): + # Try to extract meaningful values from dict + values = [] + for key in sorted(state.keys()): # Sort for consistency + values.append(self._extract_numeric_value(state[key])) + return np.array(values, dtype=np.float32) + + # If state is a single value, make it an array + else: + return np.array([self._extract_numeric_value(state)], dtype=np.float32) + + except Exception as e: + logger.warning(f"Error sanitizing state data: {e}. Using zero array with expected dimensions.") + # Return a zero array as fallback with the expected state dimension + # Use the state_dim from initialization, fallback to 403 if not available + expected_size = getattr(self, 'state_size', getattr(self, 'state_dim', 403)) + if isinstance(expected_size, tuple): + expected_size = np.prod(expected_size) + return np.zeros(int(expected_size), dtype=np.float32) + + def _extract_numeric_value(self, value): + """Extract a numeric value from various data types""" + try: + # Handle None values + if value is None: + return 0.0 + + # Handle numeric types + if isinstance(value, (int, float, np.number)): + return float(value) + + # Handle dict values + elif isinstance(value, dict): + # Try common keys for numeric data + for key in ['value', 'price', 'close', 'last', 'amount', 'quantity']: + if key in value: + return self._extract_numeric_value(value[key]) + # If no common keys, try to get first numeric value + for v in value.values(): + if isinstance(v, (int, float, np.number)): + return float(v) + return 0.0 + + # Handle string values that might be numeric + elif isinstance(value, str): + try: + return float(value) + except: + return 0.0 + + # Handle datetime objects + elif hasattr(value, 'timestamp'): + return float(value.timestamp()) + + # Handle boolean values + elif isinstance(value, bool): + return float(value) + + # Handle list/tuple - take first numeric value + elif isinstance(value, (list, tuple)) and len(value) > 0: + return self._extract_numeric_value(value[0]) + + else: + return 0.0 + + except: + return 0.0 \ No newline at end of file diff --git a/core/trading_executor.py b/core/trading_executor.py index 0edf0f0..d0256f5 100644 --- a/core/trading_executor.py +++ b/core/trading_executor.py @@ -365,66 +365,27 @@ class TradingExecutor: self._cancel_open_orders(symbol) # Calculate position size - position_value = self._calculate_position_size(confidence, current_price) - quantity = position_value / current_price + position_size = self._calculate_position_size(confidence, current_price) + quantity = position_size / current_price - logger.info(f"Executing BUY: {quantity:.6f} {symbol} at ${current_price:.2f} " - f"(value: ${position_value:.2f}, confidence: {confidence:.2f}) " - f"[{'SIMULATION' if self.simulation_mode else 'LIVE'}]") + logger.info(f"Executing BUY: {quantity:.6f} {symbol} at ${current_price:.2f} (value: ${position_size:.2f}, confidence: {confidence:.2f}) [{'SIM' if self.simulation_mode else 'LIVE'}]") if self.simulation_mode: - logger.info(f"SIMULATION MODE ({self.trading_mode.upper()}) - Trade logged but not executed") - # Calculate simulated fees in simulation mode - taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006) - simulated_fees = quantity * current_price * taker_fee_rate - - # Create mock position for tracking + # Create simulated position self.positions[symbol] = Position( symbol=symbol, side='LONG', quantity=quantity, entry_price=current_price, entry_time=datetime.now(), - order_id=f"sim_{int(time.time())}" + order_id=f"sim_{int(datetime.now().timestamp())}" ) - self.last_trade_time[symbol] = datetime.now() - self.daily_trades += 1 + logger.info(f"Simulated BUY order: {quantity:.6f} {symbol} at ${current_price:.2f}") return True - - try: - # Get order type from config - order_type = self.mexc_config.get('order_type', 'market').lower() - - # For limit orders, set price slightly above market for immediate execution - limit_price = None - if order_type == 'limit': - # Set buy price slightly above market to ensure immediate execution - limit_price = current_price * 1.001 # 0.1% above market - - # Place buy order - if order_type == 'market': - order = self.exchange.place_order( - symbol=symbol, - side='buy', - order_type=order_type, - quantity=quantity - ) - else: - # For limit orders, price is required - assert limit_price is not None, "limit_price required for limit orders" - order = self.exchange.place_order( - symbol=symbol, - side='buy', - order_type=order_type, - quantity=quantity, - price=limit_price - ) - - if order: - # Calculate simulated fees in simulation mode - taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006) - simulated_fees = quantity * current_price * taker_fee_rate - + else: + # Place real order with enhanced error handling + result = self._place_order_with_retry(symbol, 'BUY', 'MARKET', quantity, current_price) + if result and 'orderId' in result: # Create position record self.positions[symbol] = Position( symbol=symbol, @@ -432,233 +393,58 @@ class TradingExecutor: quantity=quantity, entry_price=current_price, entry_time=datetime.now(), - order_id=order.get('orderId', 'unknown') + order_id=result['orderId'] ) - + logger.info(f"BUY order executed: {result}") self.last_trade_time[symbol] = datetime.now() - self.daily_trades += 1 - - logger.info(f"BUY order executed: {order}") return True else: logger.error("Failed to place BUY order") return False - - except Exception as e: - logger.error(f"Error executing BUY order: {e}") - return False - + def _execute_sell(self, symbol: str, confidence: float, current_price: float) -> bool: - """Execute a sell order""" - # Check if we have a position to sell - if symbol not in self.positions: + """Execute a sell order (close long position or open short position)""" + if symbol in self.positions: + position = self.positions[symbol] + if position.side == 'LONG': + logger.info(f"Closing LONG position in {symbol}") + return self._close_long_position(symbol, confidence, current_price) + else: + logger.info(f"Already have SHORT position in {symbol}") + return False + else: + # No position to sell, open short position logger.info(f"No position to sell in {symbol}. Opening short position") return self._execute_short(symbol, confidence, current_price) - - position = self.positions[symbol] - + + def _execute_short(self, symbol: str, confidence: float, current_price: float) -> bool: + """Execute a short order (sell without holding the asset)""" # Cancel any existing open orders before placing new order if not self.simulation_mode: self._cancel_open_orders(symbol) - logger.info(f"Executing SELL: {position.quantity:.6f} {symbol} at ${current_price:.2f} " - f"(confidence: {confidence:.2f}) [{'SIMULATION' if self.simulation_mode else 'LIVE'}]") - - if self.simulation_mode: - logger.info(f"SIMULATION MODE ({self.trading_mode.upper()}) - Trade logged but not executed") - # Calculate P&L and hold time - pnl = position.calculate_pnl(current_price) - exit_time = datetime.now() - hold_time_seconds = (exit_time - position.entry_time).total_seconds() - - # Calculate simulated fees in simulation mode - taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006) - simulated_fees = position.quantity * current_price * taker_fee_rate - - # Create trade record - trade_record = TradeRecord( - symbol=symbol, - side='LONG', - quantity=position.quantity, - entry_price=position.entry_price, - exit_price=current_price, - entry_time=position.entry_time, - exit_time=exit_time, - pnl=pnl, - fees=simulated_fees, - confidence=confidence, - hold_time_seconds=hold_time_seconds - ) - - self.trade_history.append(trade_record) - self.daily_loss += max(0, -pnl) # Add to daily loss if negative - - # Update consecutive losses - if pnl < -0.001: # A losing trade - self.consecutive_losses += 1 - elif pnl > 0.001: # A winning trade - self.consecutive_losses = 0 - else: # Breakeven trade - self.consecutive_losses = 0 - - # Remove position - del self.positions[symbol] - self.last_trade_time[symbol] = datetime.now() - self.daily_trades += 1 - - logger.info(f"Position closed - P&L: ${pnl:.2f}") - return True - - try: - # Get order type from config - order_type = self.mexc_config.get('order_type', 'market').lower() - - # For limit orders, set price slightly below market for immediate execution - limit_price = None - if order_type == 'limit': - # Set sell price slightly below market to ensure immediate execution - limit_price = current_price * 0.999 # 0.1% below market - - # Place sell order - if order_type == 'market': - order = self.exchange.place_order( - symbol=symbol, - side='sell', - order_type=order_type, - quantity=position.quantity - ) - else: - # For limit orders, price is required - assert limit_price is not None, "limit_price required for limit orders" - order = self.exchange.place_order( - symbol=symbol, - side='sell', - order_type=order_type, - quantity=position.quantity, - price=limit_price - ) - - if order: - # Calculate simulated fees in simulation mode - taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006) - simulated_fees = position.quantity * current_price * taker_fee_rate - - # Calculate P&L, fees, and hold time - pnl = position.calculate_pnl(current_price) - fees = simulated_fees - exit_time = datetime.now() - hold_time_seconds = (exit_time - position.entry_time).total_seconds() - - # Create trade record - trade_record = TradeRecord( - symbol=symbol, - side='LONG', - quantity=position.quantity, - entry_price=position.entry_price, - exit_price=current_price, - entry_time=position.entry_time, - exit_time=exit_time, - pnl=pnl - fees, - fees=fees, - confidence=confidence, - hold_time_seconds=hold_time_seconds - ) - - self.trade_history.append(trade_record) - self.daily_loss += max(0, -(pnl - fees)) # Add to daily loss if negative - - # Update consecutive losses - if pnl < -0.001: # A losing trade - self.consecutive_losses += 1 - elif pnl > 0.001: # A winning trade - self.consecutive_losses = 0 - else: # Breakeven trade - self.consecutive_losses = 0 - - # Remove position - del self.positions[symbol] - self.last_trade_time[symbol] = datetime.now() - self.daily_trades += 1 - - logger.info(f"SELL order executed: {order}") - logger.info(f"Position closed - P&L: ${pnl - fees:.2f}") - return True - else: - logger.error("Failed to place SELL order") - return False - - except Exception as e: - logger.error(f"Error executing SELL order: {e}") - return False - - def _execute_short(self, symbol: str, confidence: float, current_price: float) -> bool: - """Execute a short position opening""" - # Check if we already have a position - if symbol in self.positions: - logger.info(f"Already have position in {symbol}") - return False - # Calculate position size - position_value = self._calculate_position_size(confidence, current_price) - quantity = position_value / current_price + position_size = self._calculate_position_size(confidence, current_price) + quantity = position_size / current_price - logger.info(f"Executing SHORT: {quantity:.6f} {symbol} at ${current_price:.2f} " - f"(value: ${position_value:.2f}, confidence: {confidence:.2f}) " - f"[{'SIMULATION' if self.simulation_mode else 'LIVE'}]") + logger.info(f"Executing SHORT: {quantity:.6f} {symbol} at ${current_price:.2f} (value: ${position_size:.2f}, confidence: {confidence:.2f}) [{'SIM' if self.simulation_mode else 'LIVE'}]") if self.simulation_mode: - logger.info(f"SIMULATION MODE ({self.trading_mode.upper()}) - Short position logged but not executed") - # Calculate simulated fees in simulation mode - taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006) - simulated_fees = quantity * current_price * taker_fee_rate - - # Create mock short position for tracking + # Create simulated short position self.positions[symbol] = Position( symbol=symbol, side='SHORT', quantity=quantity, entry_price=current_price, entry_time=datetime.now(), - order_id=f"sim_short_{int(time.time())}" + order_id=f"sim_{int(datetime.now().timestamp())}" ) - self.last_trade_time[symbol] = datetime.now() - self.daily_trades += 1 + logger.info(f"Simulated SHORT order: {quantity:.6f} {symbol} at ${current_price:.2f}") return True - - try: - # Get order type from config - order_type = self.mexc_config.get('order_type', 'market').lower() - - # For limit orders, set price slightly below market for immediate execution - limit_price = None - if order_type == 'limit': - # Set short price slightly below market to ensure immediate execution - limit_price = current_price * 0.999 # 0.1% below market - - # Place short sell order - if order_type == 'market': - order = self.exchange.place_order( - symbol=symbol, - side='sell', # Short selling starts with a sell order - order_type=order_type, - quantity=quantity - ) - else: - # For limit orders, price is required - assert limit_price is not None, "limit_price required for limit orders" - order = self.exchange.place_order( - symbol=symbol, - side='sell', # Short selling starts with a sell order - order_type=order_type, - quantity=quantity, - price=limit_price - ) - - if order: - # Calculate simulated fees in simulation mode - taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006) - simulated_fees = quantity * current_price * taker_fee_rate - + else: + # Place real short order with enhanced error handling + result = self._place_order_with_retry(symbol, 'SELL', 'MARKET', quantity, current_price) + if result and 'orderId' in result: # Create short position record self.positions[symbol] = Position( symbol=symbol, @@ -666,24 +452,88 @@ class TradingExecutor: quantity=quantity, entry_price=current_price, entry_time=datetime.now(), - order_id=order.get('orderId', 'unknown') + order_id=result['orderId'] ) - + logger.info(f"SHORT order executed: {result}") self.last_trade_time[symbol] = datetime.now() - self.daily_trades += 1 - - logger.info(f"SHORT order executed: {order}") return True else: logger.error("Failed to place SHORT order") return False + + def _place_order_with_retry(self, symbol: str, side: str, order_type: str, quantity: float, current_price: float, max_retries: int = 3) -> Dict[str, Any]: + """Place order with retry logic for MEXC error handling""" + for attempt in range(max_retries): + try: + result = self.exchange.place_order(symbol, side, order_type, quantity, current_price) - except Exception as e: - logger.error(f"Error executing SHORT order: {e}") - return False + # Check if result contains error information + if isinstance(result, dict) and 'error' in result: + error_type = result.get('error') + error_code = result.get('code') + error_msg = result.get('message', 'Unknown error') + + if error_type == 'oversold' and error_code == 30005: + logger.warning(f"MEXC Oversold error on attempt {attempt + 1}/{max_retries}") + logger.warning(f"Error: {error_msg}") + + if attempt < max_retries - 1: + # Wait with exponential backoff + wait_time = result.get('retry_after', 60) * (2 ** attempt) + logger.info(f"Waiting {wait_time} seconds before retry due to oversold condition...") + time.sleep(wait_time) + + # Reduce quantity for next attempt to avoid oversold + quantity = quantity * 0.8 # Reduce by 20% + logger.info(f"Reducing quantity to {quantity:.6f} for retry") + continue + else: + logger.error(f"Max retries reached for oversold condition") + return {} + + elif error_type == 'direction_not_allowed': + logger.error(f"Trading direction not allowed for {symbol} {side}") + return {} + + elif error_type == 'insufficient_position': + logger.error(f"Insufficient position for {symbol} {side}") + return {} + + else: + logger.error(f"MEXC API error: {error_code} - {error_msg}") + if attempt < max_retries - 1: + time.sleep(5 * (attempt + 1)) # Wait 5, 10, 15 seconds + continue + else: + return {} + + # Success case + elif isinstance(result, dict) and ('orderId' in result or 'symbol' in result): + logger.info(f"Order placed successfully on attempt {attempt + 1}") + return result + + # Empty result - treat as failure + else: + logger.warning(f"Empty result on attempt {attempt + 1}/{max_retries}") + if attempt < max_retries - 1: + time.sleep(2 * (attempt + 1)) # Wait 2, 4, 6 seconds + continue + else: + return {} + + except Exception as e: + logger.error(f"Exception on order attempt {attempt + 1}/{max_retries}: {e}") + if attempt < max_retries - 1: + time.sleep(3 * (attempt + 1)) # Wait 3, 6, 9 seconds + continue + else: + return {} + + logger.error(f"Failed to place order after {max_retries} attempts") + return {} def _close_short_position(self, symbol: str, confidence: float, current_price: float) -> bool: - """Close a short position by buying back""" + """Close a short position by buying""" if symbol not in self.positions: logger.warning(f"No position to close in {symbol}") return False @@ -724,13 +574,21 @@ class TradingExecutor: self.trade_history.append(trade_record) self.daily_loss += max(0, -pnl) # Add to daily loss if negative + + # Update consecutive losses + if pnl < -0.001: # A losing trade + self.consecutive_losses += 1 + elif pnl > 0.001: # A winning trade + self.consecutive_losses = 0 + else: # Breakeven trade + self.consecutive_losses = 0 # Remove position del self.positions[symbol] self.last_trade_time[symbol] = datetime.now() self.daily_trades += 1 - logger.info(f"SHORT position closed - P&L: ${pnl:.2f}") + logger.info(f"Position closed - P&L: ${pnl:.2f}") return True try: @@ -814,6 +672,147 @@ class TradingExecutor: except Exception as e: logger.error(f"Error closing SHORT position: {e}") return False + + def _close_long_position(self, symbol: str, confidence: float, current_price: float) -> bool: + """Close a long position by selling""" + if symbol not in self.positions: + logger.warning(f"No position to close in {symbol}") + return False + + position = self.positions[symbol] + if position.side != 'LONG': + logger.warning(f"Position in {symbol} is not LONG, cannot close with SELL") + return False + + logger.info(f"Closing LONG position: {position.quantity:.6f} {symbol} at ${current_price:.2f} " + f"(confidence: {confidence:.2f})") + + if self.simulation_mode: + logger.info(f"SIMULATION MODE ({self.trading_mode.upper()}) - Long close logged but not executed") + # Calculate simulated fees in simulation mode + taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006) + simulated_fees = position.quantity * current_price * taker_fee_rate + + # Calculate P&L for long position and hold time + pnl = position.calculate_pnl(current_price) + exit_time = datetime.now() + hold_time_seconds = (exit_time - position.entry_time).total_seconds() + + # Create trade record + trade_record = TradeRecord( + symbol=symbol, + side='LONG', + quantity=position.quantity, + entry_price=position.entry_price, + exit_price=current_price, + entry_time=position.entry_time, + exit_time=exit_time, + pnl=pnl, + fees=simulated_fees, + confidence=confidence, + hold_time_seconds=hold_time_seconds + ) + + self.trade_history.append(trade_record) + self.daily_loss += max(0, -pnl) # Add to daily loss if negative + + # Update consecutive losses + if pnl < -0.001: # A losing trade + self.consecutive_losses += 1 + elif pnl > 0.001: # A winning trade + self.consecutive_losses = 0 + else: # Breakeven trade + self.consecutive_losses = 0 + + # Remove position + del self.positions[symbol] + self.last_trade_time[symbol] = datetime.now() + self.daily_trades += 1 + + logger.info(f"Position closed - P&L: ${pnl:.2f}") + return True + + try: + # Get order type from config + order_type = self.mexc_config.get('order_type', 'market').lower() + + # For limit orders, set price slightly below market for immediate execution + limit_price = None + if order_type == 'limit': + # Set sell price slightly below market to ensure immediate execution + limit_price = current_price * 0.999 # 0.1% below market + + # Place sell order to close long + if order_type == 'market': + order = self.exchange.place_order( + symbol=symbol, + side='sell', # Sell to close long position + order_type=order_type, + quantity=position.quantity + ) + else: + # For limit orders, price is required + assert limit_price is not None, "limit_price required for limit orders" + order = self.exchange.place_order( + symbol=symbol, + side='sell', # Sell to close long position + order_type=order_type, + quantity=position.quantity, + price=limit_price + ) + + if order: + # Calculate simulated fees in simulation mode + taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006) + simulated_fees = position.quantity * current_price * taker_fee_rate + + # Calculate P&L, fees, and hold time + pnl = position.calculate_pnl(current_price) + fees = simulated_fees + exit_time = datetime.now() + hold_time_seconds = (exit_time - position.entry_time).total_seconds() + + # Create trade record + trade_record = TradeRecord( + symbol=symbol, + side='LONG', + quantity=position.quantity, + entry_price=position.entry_price, + exit_price=current_price, + entry_time=position.entry_time, + exit_time=exit_time, + pnl=pnl - fees, + fees=fees, + confidence=confidence, + hold_time_seconds=hold_time_seconds + ) + + self.trade_history.append(trade_record) + self.daily_loss += max(0, -(pnl - fees)) # Add to daily loss if negative + + # Update consecutive losses + if pnl < -0.001: # A losing trade + self.consecutive_losses += 1 + elif pnl > 0.001: # A winning trade + self.consecutive_losses = 0 + else: # Breakeven trade + self.consecutive_losses = 0 + + # Remove position + del self.positions[symbol] + self.last_trade_time[symbol] = datetime.now() + self.daily_trades += 1 + + logger.info(f"LONG close order executed: {order}") + logger.info(f"LONG position closed - P&L: ${pnl - fees:.2f}") + return True + else: + logger.error("Failed to place LONG close order") + return False + + except Exception as e: + logger.error(f"Error closing LONG position: {e}") + return False def _calculate_position_size(self, confidence: float, current_price: float) -> float: """Calculate position size - use 100% of account balance for short-term scalping""" diff --git a/web/clean_dashboard.py b/web/clean_dashboard.py index cc57d11..0caec6f 100644 --- a/web/clean_dashboard.py +++ b/web/clean_dashboard.py @@ -134,6 +134,9 @@ class CleanTradingDashboard: self.total_fees = 0.0 self.current_position: Optional[dict] = None + # Live balance caching for real-time portfolio updates + self._cached_live_balance: float = 0.0 + # ENHANCED: Model control toggles - separate inference and training self.dqn_inference_enabled = True # Default: enabled self.dqn_training_enabled = True # Default: enabled @@ -319,6 +322,42 @@ class CleanTradingDashboard: logger.warning(f"Error getting balance: {e}") return 100.0 # Default balance + def _get_live_account_balance(self) -> float: + """Get live account balance from MEXC API in real-time""" + try: + if not self.trading_executor: + return self._get_initial_balance() + + # If in simulation mode, use simulation balance + if hasattr(self.trading_executor, 'simulation_mode') and self.trading_executor.simulation_mode: + return self._get_initial_balance() + + # For live trading, get actual MEXC balance + if hasattr(self.trading_executor, 'get_account_balance'): + balances = self.trading_executor.get_account_balance() + if balances: + # Get USDC balance (MEXC primary) and USDT as fallback + usdc_balance = balances.get('USDC', {}).get('total', 0.0) + usdt_balance = balances.get('USDT', {}).get('total', 0.0) + + # Use the higher balance (primary currency) + live_balance = max(usdc_balance, usdt_balance) + + if live_balance > 0: + logger.debug(f"Live MEXC balance: USDC=${usdc_balance:.2f}, USDT=${usdt_balance:.2f}, Using=${live_balance:.2f}") + return live_balance + else: + logger.warning("Live account balance is $0.00 - check MEXC account funding") + return 0.0 + + # Fallback to initial balance if API calls fail + logger.warning("Failed to get live balance, using initial balance") + return self._get_initial_balance() + + except Exception as e: + logger.warning(f"Error getting live account balance: {e}, using initial balance") + return self._get_initial_balance() + def _setup_layout(self): """Setup the dashboard layout using layout manager""" self.app.layout = self.layout_manager.create_main_layout() @@ -411,9 +450,15 @@ class CleanTradingDashboard: trade_count = len(self.closed_trades) trade_str = f"{trade_count} Trades" - # Portfolio value - initial_balance = self._get_initial_balance() - portfolio_value = initial_balance + total_session_pnl # Use total P&L including unrealized + # Portfolio value - use live balance every 10 seconds to avoid API spam + if n % 10 == 0 or not hasattr(self, '_cached_live_balance'): + self._cached_live_balance = self._get_live_account_balance() + logger.debug(f"Updated live balance cache: ${self._cached_live_balance:.2f}") + + # For live trading, show actual account balance + session P&L + # For simulation, show starting balance + session P&L + current_balance = self._cached_live_balance if hasattr(self, '_cached_live_balance') else self._get_initial_balance() + portfolio_value = current_balance + total_session_pnl # Live balance + unrealized P&L portfolio_str = f"${portfolio_value:.2f}" # MEXC status diff --git a/web/layout_manager.py b/web/layout_manager.py index dcd0bf0..ad0a97c 100644 --- a/web/layout_manager.py +++ b/web/layout_manager.py @@ -33,7 +33,7 @@ class DashboardLayoutManager: "Clean Trading Dashboard" ], className="text-light mb-0"), html.P( - f"Ultra-Fast Updates • Portfolio: ${self.starting_balance:,.0f} • {trading_mode}", + f"Ultra-Fast Updates • Live Account Balance Sync • {trading_mode}", className="text-light mb-0 opacity-75 small" ) ], className="bg-dark p-2 mb-2")