""" Trading Executor for MEXC API Integration This module handles the execution of trading signals through the MEXC exchange API. It includes position management, risk controls, and safety features. https://github.com/mexcdevelop/mexc-api-postman/blob/main/MEXC%20V3.postman_collection.json MEXC V3.postman_collection.json """ import logging import time import os from datetime import datetime, timedelta from typing import Dict, List, Optional, Any from dataclasses import dataclass from threading import Lock import sys # Add NN directory to path for exchange interfaces sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'NN')) from NN.exchanges import MEXCInterface from .config import get_config from .config_sync import ConfigSynchronizer logger = logging.getLogger(__name__) @dataclass class Position: """Represents an open trading position""" symbol: str side: str # 'LONG' or 'SHORT' quantity: float entry_price: float entry_time: datetime order_id: str unrealized_pnl: float = 0.0 def calculate_pnl(self, current_price: float) -> float: """Calculate unrealized P&L for the position""" if self.side == 'LONG': self.unrealized_pnl = (current_price - self.entry_price) * self.quantity else: # SHORT self.unrealized_pnl = (self.entry_price - current_price) * self.quantity return self.unrealized_pnl @dataclass class TradeRecord: """Record of a completed trade""" symbol: str side: str quantity: float entry_price: float exit_price: float entry_time: datetime exit_time: datetime pnl: float fees: float confidence: float hold_time_seconds: float = 0.0 # Hold time in seconds class TradingExecutor: """Handles trade execution through MEXC API with risk management""" def __init__(self, config_path: str = "config.yaml"): """Initialize the trading executor""" self.config = get_config(config_path) self.mexc_config = self.config.get('mexc_trading', {}) # Initialize MEXC interface api_key = os.getenv('MEXC_API_KEY', self.mexc_config.get('api_key', '')) api_secret = os.getenv('MEXC_SECRET_KEY', self.mexc_config.get('api_secret', '')) # Determine trading mode from unified config trading_mode = self.mexc_config.get('trading_mode', 'simulation') # Map trading mode to exchange test_mode and execution mode if trading_mode == 'simulation': exchange_test_mode = True self.simulation_mode = True elif trading_mode == 'testnet': exchange_test_mode = True self.simulation_mode = False elif trading_mode == 'live': exchange_test_mode = False self.simulation_mode = False else: logger.warning(f"Unknown trading_mode '{trading_mode}', defaulting to simulation") exchange_test_mode = True self.simulation_mode = True self.exchange = MEXCInterface( api_key=api_key, api_secret=api_secret, test_mode=exchange_test_mode, ) # Trading state self.positions: Dict[str, Position] = {} self.trade_history: List[TradeRecord] = [] self.daily_trades = 0 self.daily_loss = 0.0 self.last_trade_time = {} self.trading_enabled = self.mexc_config.get('enabled', False) self.trading_mode = trading_mode self.consecutive_losses = 0 # Track consecutive losing trades logger.debug(f"TRADING EXECUTOR: Initial trading_enabled state from config: {self.trading_enabled}") # Legacy compatibility (deprecated) self.dry_run = self.simulation_mode # Thread safety self.lock = Lock() # Connect to exchange 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 else: logger.info("TRADING EXECUTOR: Trading is explicitly disabled in config.") logger.info(f"Trading Executor initialized - Mode: {self.trading_mode}, Enabled: {self.trading_enabled}") # Initialize config synchronizer for automatic fee updates self.config_synchronizer = ConfigSynchronizer( config_path=config_path, mexc_interface=self.exchange if self.trading_enabled else None ) # Perform initial fee sync on startup if trading is enabled if self.trading_enabled and self.exchange: try: logger.info("TRADING EXECUTOR: Performing initial fee synchronization with MEXC API") sync_result = self.config_synchronizer.sync_trading_fees(force=True) if sync_result.get('status') == 'success': logger.info("TRADING EXECUTOR: Fee synchronization completed successfully") if sync_result.get('changes_made'): logger.info(f"TRADING EXECUTOR: Fee changes applied: {list(sync_result['changes'].keys())}") # Reload config to get updated fees self.config = get_config(config_path) self.mexc_config = self.config.get('mexc_trading', {}) elif sync_result.get('status') == 'warning': logger.warning("TRADING EXECUTOR: Fee sync completed with warnings") else: logger.warning(f"TRADING EXECUTOR: Fee sync failed: {sync_result.get('status')}") except Exception as e: logger.warning(f"TRADING EXECUTOR: Initial fee sync failed: {e}") logger.info(f"Trading Executor initialized - Mode: {self.trading_mode}, Enabled: {self.trading_enabled}") def _connect_exchange(self) -> bool: """Connect to the MEXC exchange""" try: logger.debug("TRADING EXECUTOR: Calling self.exchange.connect()...") connected = self.exchange.connect() logger.debug(f"TRADING EXECUTOR: self.exchange.connect() returned: {connected}") if connected: logger.info("Successfully connected to MEXC exchange") return True else: logger.error("Failed to connect to MEXC exchange: Connection returned False.") if not self.dry_run: logger.info("TRADING EXECUTOR: Setting trading_enabled to False due to connection failure.") self.trading_enabled = False return False except Exception as e: logger.error(f"Error connecting to MEXC exchange: {e}. Setting trading_enabled to False.") self.trading_enabled = False return False def execute_signal(self, symbol: str, action: str, confidence: float, current_price: Optional[float] = None) -> bool: """Execute a trading signal Args: symbol: Trading symbol (e.g., 'ETH/USDT') action: Trading action ('BUY', 'SELL', 'HOLD') confidence: Confidence level (0.0 to 1.0) current_price: Current market price Returns: bool: True if trade executed successfully """ logger.debug(f"TRADING EXECUTOR: execute_signal called. trading_enabled: {self.trading_enabled}") if not self.trading_enabled: logger.info(f"Trading disabled - Signal: {action} {symbol} (confidence: {confidence:.2f}) - Reason: Trading executor is not enabled.") return False if action == 'HOLD': return True # Check safety conditions if not self._check_safety_conditions(symbol, action): return False # Get current price if not provided if current_price is None: ticker = self.exchange.get_ticker(symbol) if not ticker or 'last' not in ticker: logger.error(f"Failed to get current price for {symbol} or ticker is malformed.") return False current_price = ticker['last'] # Assert that current_price is not None for type checking assert current_price is not None, "current_price should not be None at this point" # --- Balance check before executing trade (skip in simulation mode) --- # Only perform balance check for live trading, not simulation if not self.simulation_mode and (action == 'BUY' or (action == 'SELL' and symbol not in self.positions) or (action == 'SHORT')): # Determine the quote asset (e.g., USDT, USDC) from the symbol if '/' in symbol: quote_asset = symbol.split('/')[1].upper() # Assuming symbol is like ETH/USDT # Convert USDT to USDC for MEXC spot trading if quote_asset == 'USDT': quote_asset = 'USDC' else: # Fallback for symbols like ETHUSDT (assuming last 4 chars are quote) quote_asset = symbol[-4:].upper() # Convert USDT to USDC for MEXC spot trading if quote_asset == 'USDT': quote_asset = 'USDC' # Calculate required capital for the trade # If we are selling (to open a short position), we need collateral based on the position size # For simplicity, assume required capital is the full position value in USD required_capital = self._calculate_position_size(confidence, current_price) # Get available balance for the quote asset # For MEXC, prioritize USDT over USDC since most accounts have USDT if quote_asset == 'USDC': # Check USDT first (most common balance) usdt_balance = self.exchange.get_balance('USDT') usdc_balance = self.exchange.get_balance('USDC') if usdt_balance >= required_capital: available_balance = usdt_balance quote_asset = 'USDT' # Use USDT for trading logger.info(f"BALANCE CHECK: Using USDT balance for {symbol} (preferred)") elif usdc_balance >= required_capital: available_balance = usdc_balance logger.info(f"BALANCE CHECK: Using USDC balance for {symbol}") else: # Use the larger balance for reporting available_balance = max(usdt_balance, usdc_balance) quote_asset = 'USDT' if usdt_balance > usdc_balance else 'USDC' else: available_balance = self.exchange.get_balance(quote_asset) logger.info(f"BALANCE CHECK: Symbol: {symbol}, Action: {action}, Required: ${required_capital:.2f} {quote_asset}, Available: ${available_balance:.2f} {quote_asset}") if available_balance < required_capital: logger.warning(f"Trade blocked for {symbol} {action}: Insufficient {quote_asset} balance. " f"Required: ${required_capital:.2f}, Available: ${available_balance:.2f}") return False elif self.simulation_mode: logger.debug(f"SIMULATION MODE: Skipping balance check for {symbol} {action} - allowing trade for model training") # --- End Balance check --- with self.lock: try: if action == 'BUY': return self._execute_buy(symbol, confidence, current_price) elif action == 'SELL': return self._execute_sell(symbol, confidence, current_price) elif action == 'SHORT': # Explicitly handle SHORT if it's a direct signal return self._execute_short(symbol, confidence, current_price) else: logger.warning(f"Unknown action: {action}") return False except Exception as e: logger.error(f"Error executing {action} signal for {symbol}: {e}") return False def _check_safety_conditions(self, symbol: str, action: str) -> bool: """Check if it's safe to execute a trade""" # Check if trading is stopped if self.mexc_config.get('emergency_stop', False): logger.warning("Emergency stop is active - no trades allowed") return False # Check symbol allowlist allowed_symbols = self.mexc_config.get('allowed_symbols', []) if allowed_symbols and symbol not in allowed_symbols: logger.warning(f"Symbol {symbol} not in allowed list: {allowed_symbols}") return False # Check daily loss limit max_daily_loss = self.mexc_config.get('max_daily_loss_usd', 5.0) if self.daily_loss >= max_daily_loss: logger.warning(f"Daily loss limit reached: ${self.daily_loss:.2f} >= ${max_daily_loss}") return False # Check daily trade limit # max_daily_trades = self.mexc_config.get('max_daily_trades', 100) # if self.daily_trades >= max_daily_trades: # logger.warning(f"Daily trade limit reached: {self.daily_trades}") # return False # Check trade interval min_interval = self.mexc_config.get('min_trade_interval_seconds', 5) last_trade = self.last_trade_time.get(symbol, datetime.min) if (datetime.now() - last_trade).total_seconds() < min_interval: logger.info(f"Trade interval not met for {symbol}") return False # Check concurrent positions max_positions = self.mexc_config.get('max_concurrent_positions', 1) if len(self.positions) >= max_positions and action == 'BUY': logger.warning(f"Maximum concurrent positions reached: {len(self.positions)}") return False return True def _execute_buy(self, symbol: str, confidence: float, current_price: float) -> bool: """Execute a buy order""" # Check if we have a short position to close if symbol in self.positions: position = self.positions[symbol] if position.side == 'SHORT': logger.info(f"Closing SHORT position in {symbol}") return self._close_short_position(symbol, confidence, current_price) else: logger.info(f"Already have LONG position in {symbol}") return False # Calculate position size position_value = self._calculate_position_size(confidence, current_price) quantity = position_value / 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'}]") 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 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())}" ) self.last_trade_time[symbol] = datetime.now() self.daily_trades += 1 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 # Create position record self.positions[symbol] = Position( symbol=symbol, side='LONG', quantity=quantity, entry_price=current_price, entry_time=datetime.now(), order_id=order.get('orderId', 'unknown') ) 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: logger.info(f"No position to sell in {symbol}. Opening short position") return self._execute_short(symbol, confidence, current_price) position = self.positions[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 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'}]") 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 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())}" ) self.last_trade_time[symbol] = datetime.now() self.daily_trades += 1 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 # Create short position record self.positions[symbol] = Position( symbol=symbol, side='SHORT', quantity=quantity, entry_price=current_price, entry_time=datetime.now(), order_id=order.get('orderId', 'unknown') ) 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 except Exception as e: logger.error(f"Error executing SHORT order: {e}") return False def _close_short_position(self, symbol: str, confidence: float, current_price: float) -> bool: """Close a short position by buying back""" if symbol not in self.positions: logger.warning(f"No position to close in {symbol}") return False position = self.positions[symbol] if position.side != 'SHORT': logger.warning(f"Position in {symbol} is not SHORT, cannot close with BUY") return False logger.info(f"Closing SHORT 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()}) - Short 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 short 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='SHORT', 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 # 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}") 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 to close short if order_type == 'market': order = self.exchange.place_order( symbol=symbol, side='buy', # Buy to close short 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='buy', # Buy to close short 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='SHORT', 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"SHORT close order executed: {order}") logger.info(f"SHORT position closed - P&L: ${pnl - fees:.2f}") return True else: logger.error("Failed to place SHORT close order") return False except Exception as e: logger.error(f"Error closing SHORT position: {e}") return False def _calculate_position_size(self, confidence: float, current_price: float) -> float: """Calculate position size based on percentage of account balance, confidence, and leverage""" # Get account balance (simulation or real) account_balance = self._get_account_balance_for_sizing() # Get position sizing percentages max_percent = self.mexc_config.get('max_position_percent', 20.0) / 100.0 min_percent = self.mexc_config.get('min_position_percent', 2.0) / 100.0 base_percent = self.mexc_config.get('base_position_percent', 5.0) / 100.0 leverage = self.mexc_config.get('leverage', 50.0) # Scale position size by confidence position_percent = min(max_percent, max(min_percent, base_percent * confidence)) position_value = account_balance * position_percent # Apply leverage to get effective position size leveraged_position_value = position_value * leverage # Apply reduction based on consecutive losses reduction_factor = self.mexc_config.get('consecutive_loss_reduction_factor', 0.8) adjusted_reduction_factor = reduction_factor ** self.consecutive_losses leveraged_position_value *= adjusted_reduction_factor logger.debug(f"Position calculation: account=${account_balance:.2f}, " f"percent={position_percent*100:.1f}%, base=${position_value:.2f}, " f"leverage={leverage}x, effective=${leveraged_position_value:.2f}, " f"confidence={confidence:.2f}") return leveraged_position_value def _get_account_balance_for_sizing(self) -> float: """Get account balance for position sizing calculations""" if self.simulation_mode: return self.mexc_config.get('simulation_account_usd', 100.0) else: # For live trading, get actual USDT/USDC balance try: balances = self.get_account_balance() usdt_balance = balances.get('USDT', {}).get('total', 0) usdc_balance = balances.get('USDC', {}).get('total', 0) return max(usdt_balance, usdc_balance) except Exception as e: logger.warning(f"Failed to get live account balance: {e}, using simulation default") return self.mexc_config.get('simulation_account_usd', 100.0) def update_positions(self, symbol: str, current_price: float): """Update position P&L with current market price""" if symbol in self.positions: with self.lock: self.positions[symbol].calculate_pnl(current_price) def get_positions(self) -> Dict[str, Position]: """Get current positions""" return self.positions.copy() def get_trade_history(self) -> List[TradeRecord]: """Get trade history""" return self.trade_history.copy() def get_daily_stats(self) -> Dict[str, Any]: """Get daily trading statistics with enhanced fee analysis""" total_pnl = sum(trade.pnl for trade in self.trade_history) total_fees = sum(trade.fees for trade in self.trade_history) gross_pnl = total_pnl + total_fees # P&L before fees winning_trades = len([t for t in self.trade_history if t.pnl > 0.001]) # Avoid rounding issues losing_trades = len([t for t in self.trade_history if t.pnl < -0.001]) # Avoid rounding issues total_trades = len(self.trade_history) breakeven_trades = total_trades - winning_trades - losing_trades # Calculate average trade values avg_trade_pnl = total_pnl / max(1, total_trades) avg_trade_fee = total_fees / max(1, total_trades) avg_winning_trade = sum(t.pnl for t in self.trade_history if t.pnl > 0.001) / max(1, winning_trades) avg_losing_trade = sum(t.pnl for t in self.trade_history if t.pnl < -0.001) / max(1, losing_trades) # Enhanced fee analysis from config fee_structure = self.mexc_config.get('trading_fees', {}) maker_fee_rate = fee_structure.get('maker', 0.0000) taker_fee_rate = fee_structure.get('taker', 0.0005) default_fee_rate = fee_structure.get('default', 0.0005) # Calculate fee efficiency total_volume = sum(trade.quantity * trade.exit_price for trade in self.trade_history) effective_fee_rate = (total_fees / max(0.01, total_volume)) if total_volume > 0 else 0 fee_impact_on_pnl = (total_fees / max(0.01, abs(gross_pnl))) * 100 if gross_pnl != 0 else 0 return { 'daily_trades': self.daily_trades, 'daily_loss': self.daily_loss, 'total_pnl': total_pnl, 'gross_pnl': gross_pnl, 'total_fees': total_fees, 'winning_trades': winning_trades, 'losing_trades': losing_trades, 'breakeven_trades': breakeven_trades, 'total_trades': total_trades, 'win_rate': winning_trades / max(1, total_trades), 'avg_trade_pnl': avg_trade_pnl, 'avg_trade_fee': avg_trade_fee, 'avg_winning_trade': avg_winning_trade, 'avg_losing_trade': avg_losing_trade, 'positions_count': len(self.positions), 'fee_rates': { 'maker': f"{maker_fee_rate*100:.3f}%", 'taker': f"{taker_fee_rate*100:.3f}%", 'default': f"{default_fee_rate*100:.3f}%", 'effective': f"{effective_fee_rate*100:.3f}%" # Actual rate based on trades }, 'fee_analysis': { 'total_volume': total_volume, 'fee_impact_percent': fee_impact_on_pnl, 'is_fee_efficient': fee_impact_on_pnl < 5.0, # Less than 5% impact is good 'fee_savings_vs_market': (0.001 - effective_fee_rate) * total_volume if effective_fee_rate < 0.001 else 0 } } def emergency_stop(self): """Emergency stop all trading""" logger.warning("EMERGENCY STOP ACTIVATED") self.trading_enabled = False # Close all positions if in live mode if not self.dry_run: for symbol, position in self.positions.items(): try: ticker = self.exchange.get_ticker(symbol) if ticker: self._execute_sell(symbol, 1.0, ticker['last']) except Exception as e: logger.error(f"Error closing position {symbol} during emergency stop: {e}") def reset_daily_stats(self): """Reset daily statistics (call at start of new day)""" self.daily_trades = 0 self.daily_loss = 0.0 logger.info("Daily trading statistics reset") def get_account_balance(self) -> Dict[str, Dict[str, float]]: """Get account balance information from MEXC, including spot and futures. Returns: Dict with asset balances in format: { 'USDT': {'free': 100.0, 'locked': 0.0, 'total': 100.0, 'type': 'spot'}, 'ETH': {'free': 0.5, 'locked': 0.0, 'total': 0.5, 'type': 'spot'}, 'FUTURES_USDT': {'free': 500.0, 'locked': 50.0, 'total': 550.0, 'type': 'futures'} ... } """ try: if not self.exchange: logger.error("Exchange interface not available") return {} combined_balances = {} # 1. Get Spot Account Info spot_account_info = self.exchange.get_account_info() if spot_account_info and 'balances' in spot_account_info: for balance in spot_account_info['balances']: asset = balance.get('asset', '') free = float(balance.get('free', 0)) locked = float(balance.get('locked', 0)) if free > 0 or locked > 0: combined_balances[asset] = { 'free': free, 'locked': locked, 'total': free + locked, 'type': 'spot' } else: logger.warning("Failed to get spot account info from MEXC or no balances found.") # 2. Get Futures Account Info (commented out until futures API is implemented) # futures_account_info = self.exchange.get_futures_account_info() # if futures_account_info: # for currency, asset_data in futures_account_info.items(): # # MEXC Futures API returns 'availableBalance' and 'frozenBalance' # free = float(asset_data.get('availableBalance', 0)) # locked = float(asset_data.get('frozenBalance', 0)) # total = free + locked # total is the sum of available and frozen # if free > 0 or locked > 0: # # Prefix with 'FUTURES_' to distinguish from spot, or decide on a unified key # # For now, let's keep them distinct for clarity # combined_balances[f'FUTURES_{currency}'] = { # 'free': free, # 'locked': locked, # 'total': total, # 'type': 'futures' # } # else: # logger.warning("Failed to get futures account info from MEXC or no futures assets found.") logger.info(f"Retrieved combined balances for {len(combined_balances)} assets.") return combined_balances except Exception as e: logger.error(f"Error getting account balance: {e}") return {} def _calculate_trading_fee(self, order_result: Dict[str, Any], symbol: str, quantity: float, price: float) -> float: """Calculate trading fee based on order execution details with enhanced MEXC API support Args: order_result: Order result from exchange API symbol: Trading symbol quantity: Order quantity price: Execution price Returns: float: Trading fee amount in quote currency """ try: # 1. Try to get actual fee from API response (most accurate) # MEXC API can return fees in different formats depending on the endpoint # Check for 'fills' array (most common for filled orders) if order_result and 'fills' in order_result: total_commission = 0.0 commission_asset = None for fill in order_result['fills']: commission = float(fill.get('commission', 0)) commission_asset = fill.get('commissionAsset', '') total_commission += commission if total_commission > 0: logger.info(f"Using actual API fee from fills: {total_commission} {commission_asset}") # If commission is in different asset, we might need conversion # For now, assume it's in quote currency (USDC/USDT) return total_commission # 2. Check if order result has fee information directly fee_fields = ['fee', 'commission', 'tradeFee', 'fees'] for field in fee_fields: if order_result and field in order_result: fee = float(order_result[field]) if fee > 0: logger.info(f"Using API fee field '{field}': {fee}") return fee # 3. Check for executedQty and cummulativeQuoteQty for more accurate calculation if order_result and 'executedQty' in order_result and 'cummulativeQuoteQty' in order_result: executed_qty = float(order_result['executedQty']) executed_value = float(order_result['cummulativeQuoteQty']) if executed_qty > 0 and executed_value > 0: # Use executed values instead of provided price/quantity quantity = executed_qty price = executed_value / executed_qty logger.info(f"Using executed order data: {quantity} @ {price:.6f}") # 4. Fall back to config-based fee calculation with enhanced logic trading_fees = self.mexc_config.get('trading_fees', {}) # Determine if this was a maker or taker trade order_type = order_result.get('type', 'MARKET') if order_result else 'MARKET' order_status = order_result.get('status', 'UNKNOWN') if order_result else 'UNKNOWN' time_in_force = order_result.get('timeInForce', 'GTC') if order_result else 'GTC' # Enhanced maker/taker detection logic if order_type.upper() == 'LIMIT': # For limit orders, check execution speed and market conditions if order_status == 'FILLED': # If it's an IOC (Immediate or Cancel) order, it's likely a taker if time_in_force == 'IOC' or time_in_force == 'FOK': fee_rate = trading_fees.get('taker', 0.0005) logger.info(f"Using taker fee rate for {time_in_force} limit order: {fee_rate*100:.3f}%") else: # For GTC orders, assume taker if aggressive pricing is used # This is a heuristic based on our trading strategy fee_rate = trading_fees.get('taker', 0.0005) logger.info(f"Using taker fee rate for aggressive limit order: {fee_rate*100:.3f}%") else: # If not immediately filled, likely a maker (though we don't usually reach here) fee_rate = trading_fees.get('maker', 0.0000) logger.info(f"Using maker fee rate for pending/partial limit order: {fee_rate*100:.3f}%") elif order_type.upper() == 'LIMIT_MAKER': # LIMIT_MAKER orders are guaranteed to be makers fee_rate = trading_fees.get('maker', 0.0000) logger.info(f"Using maker fee rate for LIMIT_MAKER order: {fee_rate*100:.3f}%") else: # Market orders and other types are always takers fee_rate = trading_fees.get('taker', 0.0005) logger.info(f"Using taker fee rate for {order_type} order: {fee_rate*100:.3f}%") # Calculate fee amount trade_value = quantity * price fee_amount = trade_value * fee_rate logger.info(f"Calculated fee: ${fee_amount:.6f} ({fee_rate*100:.3f}% of ${trade_value:.2f})") return fee_amount except Exception as e: logger.warning(f"Error calculating trading fee: {e}") # Ultimate fallback using default rate default_fee_rate = self.mexc_config.get('trading_fees', {}).get('default', 0.0005) fallback_rate = self.mexc_config.get('trading_fee', default_fee_rate) # Legacy support fee_amount = quantity * price * fallback_rate logger.info(f"Using fallback fee: ${fee_amount:.6f} ({fallback_rate*100:.3f}%)") return fee_amount def get_fee_analysis(self) -> Dict[str, Any]: """Get detailed fee analysis and statistics Returns: Dict with fee breakdowns, rates, and impact analysis """ try: fee_structure = self.mexc_config.get('trading_fees', {}) maker_rate = fee_structure.get('maker', 0.0000) taker_rate = fee_structure.get('taker', 0.0005) default_rate = fee_structure.get('default', 0.0005) # Calculate total fees paid total_fees = sum(trade.fees for trade in self.trade_history) total_volume = sum(trade.quantity * trade.exit_price for trade in self.trade_history) # Estimate fee breakdown (since we don't track maker vs taker separately) # Assume most of our limit orders are takers due to our pricing strategy estimated_taker_volume = total_volume * 0.9 # 90% taker assumption estimated_maker_volume = total_volume * 0.1 # 10% maker assumption estimated_taker_fees = estimated_taker_volume * taker_rate estimated_maker_fees = estimated_maker_volume * maker_rate # Fee impact analysis total_pnl = sum(trade.pnl for trade in self.trade_history) gross_pnl = total_pnl + total_fees fee_impact_percent = (total_fees / max(1, abs(gross_pnl))) * 100 if gross_pnl != 0 else 0 return { 'fee_rates': { 'maker': { 'rate': maker_rate, 'rate_percent': f"{maker_rate*100:.3f}%" }, 'taker': { 'rate': taker_rate, 'rate_percent': f"{taker_rate*100:.3f}%" }, 'default': { 'rate': default_rate, 'rate_percent': f"{default_rate*100:.3f}%" } }, 'total_fees': total_fees, 'total_volume': total_volume, 'estimated_breakdown': { 'taker_fees': estimated_taker_fees, 'maker_fees': estimated_maker_fees, 'taker_volume': estimated_taker_volume, 'maker_volume': estimated_maker_volume }, 'impact_analysis': { 'fee_impact_percent': fee_impact_percent, 'pnl_after_fees': total_pnl, 'pnl_before_fees': gross_pnl, 'avg_fee_per_trade': total_fees / max(1, len(self.trade_history)) }, 'fee_efficiency': { 'volume_to_fee_ratio': total_volume / max(0.01, total_fees), 'is_efficient': fee_impact_percent < 5.0 # Less than 5% impact is good } } except Exception as e: logger.error(f"Error calculating fee analysis: {e}") return { 'error': str(e), 'fee_rates': { 'maker': {'rate': 0.0000, 'rate_percent': '0.000%'}, 'taker': {'rate': 0.0005, 'rate_percent': '0.050%'} } } def sync_fees_with_api(self, force: bool = False) -> Dict[str, Any]: """Manually trigger fee synchronization with MEXC API Args: force: Force sync even if last sync was recent Returns: dict: Sync result with status and details """ if not self.config_synchronizer: return { 'status': 'error', 'error': 'Config synchronizer not initialized' } try: logger.info("TRADING EXECUTOR: Manual fee sync requested") sync_result = self.config_synchronizer.sync_trading_fees(force=force) # If fees were updated, reload config if sync_result.get('changes_made'): logger.info("TRADING EXECUTOR: Reloading config after fee sync") self.config = get_config(self.config_synchronizer.config_path) self.mexc_config = self.config.get('mexc_trading', {}) return sync_result except Exception as e: logger.error(f"TRADING EXECUTOR: Error in manual fee sync: {e}") return { 'status': 'error', 'error': str(e) } def auto_sync_fees_if_needed(self) -> bool: """Automatically sync fees if needed (called periodically) Returns: bool: True if sync was performed successfully """ if not self.config_synchronizer: return False try: return self.config_synchronizer.auto_sync_fees() except Exception as e: logger.error(f"TRADING EXECUTOR: Error in auto fee sync: {e}") return False def get_fee_sync_status(self) -> Dict[str, Any]: """Get current fee synchronization status Returns: dict: Fee sync status and history """ if not self.config_synchronizer: return { 'sync_available': False, 'error': 'Config synchronizer not initialized' } try: status = self.config_synchronizer.get_sync_status() status['sync_available'] = True return status except Exception as e: logger.error(f"TRADING EXECUTOR: Error getting sync status: {e}") return { 'sync_available': False, 'error': str(e) } def execute_trade(self, symbol: str, action: str, quantity: float) -> bool: """Execute a trade directly (compatibility method for dashboard) Args: symbol: Trading symbol (e.g., 'ETH/USDT') action: Trading action ('BUY', 'SELL') quantity: Quantity to trade Returns: bool: True if trade executed successfully """ try: # Get current price current_price = None ticker = self.exchange.get_ticker(symbol) if ticker: current_price = ticker['last'] else: logger.error(f"Failed to get current price for {symbol}") return False # Calculate confidence based on manual trade (high confidence) confidence = 1.0 # Execute using the existing signal execution method return self.execute_signal(symbol, action, confidence, current_price) except Exception as e: logger.error(f"Error executing trade {action} for {symbol}: {e}") return False def get_closed_trades(self) -> List[Dict[str, Any]]: """Get closed trades in dashboard format""" try: trades = [] for trade in self.trade_history: trade_dict = { 'symbol': trade.symbol, 'side': trade.side, 'quantity': trade.quantity, 'entry_price': trade.entry_price, 'exit_price': trade.exit_price, 'entry_time': trade.entry_time, 'exit_time': trade.exit_time, 'pnl': trade.pnl, 'fees': trade.fees, 'confidence': trade.confidence, 'hold_time_seconds': trade.hold_time_seconds } trades.append(trade_dict) return trades except Exception as e: logger.error(f"Error getting closed trades: {e}") return [] def get_current_position(self, symbol: Optional[str] = None) -> Optional[Dict[str, Any]]: """Get current position for a symbol or all positions Args: symbol: Optional symbol to get position for. If None, returns first position. Returns: dict: Position information or None if no position """ try: if symbol: if symbol in self.positions: pos = self.positions[symbol] return { 'symbol': pos.symbol, 'side': pos.side, 'size': pos.quantity, 'price': pos.entry_price, 'entry_time': pos.entry_time, 'unrealized_pnl': pos.unrealized_pnl } return None else: # Return first position if no symbol specified if self.positions: first_symbol = list(self.positions.keys())[0] return self.get_current_position(first_symbol) return None except Exception as e: logger.error(f"Error getting current position: {e}") return None def get_leverage(self) -> float: """Get current leverage setting""" return self.mexc_config.get('leverage', 50.0) def set_leverage(self, leverage: float) -> bool: """Set leverage (for UI control) Args: leverage: New leverage value Returns: bool: True if successful """ try: # Update in-memory config self.mexc_config['leverage'] = leverage logger.info(f"TRADING EXECUTOR: Leverage updated to {leverage}x") return True except Exception as e: logger.error(f"Error setting leverage: {e}") return False def get_account_info(self) -> Dict[str, Any]: """Get account information for UI display""" try: account_balance = self._get_account_balance_for_sizing() leverage = self.get_leverage() return { 'account_balance': account_balance, 'leverage': leverage, 'trading_mode': self.trading_mode, 'simulation_mode': self.simulation_mode, 'trading_enabled': self.trading_enabled, 'position_sizing': { 'base_percent': self.mexc_config.get('base_position_percent', 5.0), 'max_percent': self.mexc_config.get('max_position_percent', 20.0), 'min_percent': self.mexc_config.get('min_position_percent', 2.0) } } except Exception as e: logger.error(f"Error getting account info: {e}") return { 'account_balance': 100.0, 'leverage': 50.0, 'trading_mode': 'simulation', 'simulation_mode': True, 'trading_enabled': False, 'position_sizing': { 'base_percent': 5.0, 'max_percent': 20.0, 'min_percent': 2.0 } }