470 lines
17 KiB
Python
470 lines
17 KiB
Python
"""
|
|
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', ''))
|
|
|
|
# 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
|
|
|
|
# Legacy compatibility (deprecated)
|
|
self.dry_run = self.simulation_mode
|
|
|
|
# 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")
|
|
if not self.dry_run:
|
|
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.simulation_mode:
|
|
logger.info(f"SIMULATION MODE ({self.trading_mode.upper()}) - 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"sim_{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.simulation_mode:
|
|
logger.info(f"SIMULATION MODE ({self.trading_mode.upper()}) - 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 {}
|