""" 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. """ 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 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 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', '')) self.exchange = MEXCInterface( api_key=api_key, api_secret=api_secret, test_mode=self.mexc_config.get('test_mode', True) ) # 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.dry_run = self.mexc_config.get('dry_run_mode', True) # Thread safety self.lock = Lock() # Connect to exchange if self.trading_enabled: self._connect_exchange() def _connect_exchange(self) -> bool: """Connect to the MEXC exchange""" try: connected = self.exchange.connect() if connected: logger.info("Successfully connected to MEXC exchange") return True else: logger.error("Failed to connect to MEXC exchange") self.trading_enabled = False return False except Exception as e: logger.error(f"Error connecting to MEXC exchange: {e}") self.trading_enabled = False return False def execute_signal(self, symbol: str, action: str, confidence: float, current_price: 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 """ if not self.trading_enabled: logger.info(f"Trading disabled - Signal: {action} {symbol} (confidence: {confidence:.2f})") 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: logger.error(f"Failed to get current price for {symbol}") return False current_price = ticker['last'] 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) 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_trades_per_hour', 2) * 24 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', 300) 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 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 BUY: {quantity:.6f} {symbol} at ${current_price:.2f} " f"(value: ${position_value:.2f}, confidence: {confidence:.2f})") if self.dry_run: logger.info("DRY RUN MODE - Trade logged but not executed") # 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"dry_run_{int(time.time())}" ) self.last_trade_time[symbol] = datetime.now() self.daily_trades += 1 return True try: # Place market buy order order = self.exchange.place_order( symbol=symbol, side='buy', order_type='market', quantity=quantity ) if order: # 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}") return False position = self.positions[symbol] logger.info(f"Executing SELL: {position.quantity:.6f} {symbol} at ${current_price:.2f} " f"(confidence: {confidence:.2f})") if self.dry_run: logger.info("DRY RUN MODE - Trade logged but not executed") # Calculate P&L pnl = position.calculate_pnl(current_price) # 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=datetime.now(), pnl=pnl, fees=0.0, confidence=confidence ) 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"Position closed - P&L: ${pnl:.2f}") return True try: # Place market sell order order = self.exchange.place_order( symbol=symbol, side='sell', order_type='market', quantity=position.quantity ) if order: # Calculate P&L pnl = position.calculate_pnl(current_price) fees = current_price * position.quantity * self.mexc_config.get('trading_fee', 0.0002) # 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=datetime.now(), pnl=pnl - fees, fees=fees, confidence=confidence ) self.trade_history.append(trade_record) self.daily_loss += max(0, -(pnl - fees)) # 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"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 _calculate_position_size(self, confidence: float, current_price: float) -> float: """Calculate position size based on configuration and confidence""" max_value = self.mexc_config.get('max_position_value_usd', 1.0) min_value = self.mexc_config.get('min_position_value_usd', 0.1) # Scale position size by confidence base_value = max_value * confidence position_value = max(min_value, min(base_value, max_value)) return position_value 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""" total_pnl = sum(trade.pnl for trade in self.trade_history) winning_trades = len([t for t in self.trade_history if t.pnl > 0]) losing_trades = len([t for t in self.trade_history if t.pnl < 0]) return { 'daily_trades': self.daily_trades, 'daily_loss': self.daily_loss, 'total_pnl': total_pnl, 'winning_trades': winning_trades, 'losing_trades': losing_trades, 'win_rate': winning_trades / max(1, len(self.trade_history)), 'positions_count': len(self.positions) } 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 Returns: Dict with asset balances in format: { 'USDT': {'free': 100.0, 'locked': 0.0}, 'ETH': {'free': 0.5, 'locked': 0.0}, ... } """ try: if not self.exchange: logger.error("Exchange interface not available") return {} # Get account info from MEXC account_info = self.exchange.get_account_info() if not account_info: logger.error("Failed to get account info from MEXC") return {} balances = {} for balance in account_info.get('balances', []): asset = balance.get('asset', '') free = float(balance.get('free', 0)) locked = float(balance.get('locked', 0)) # Only include assets with non-zero balance if free > 0 or locked > 0: balances[asset] = { 'free': free, 'locked': locked, 'total': free + locked } logger.info(f"Retrieved balances for {len(balances)} assets") return balances except Exception as e: logger.error(f"Error getting account balance: {e}") return {}