|
|
|
@ -40,12 +40,40 @@ class Position:
|
|
|
|
|
order_id: str
|
|
|
|
|
unrealized_pnl: float = 0.0
|
|
|
|
|
|
|
|
|
|
def calculate_pnl(self, current_price: float) -> float:
|
|
|
|
|
"""Calculate unrealized P&L for the position"""
|
|
|
|
|
def calculate_pnl(self, current_price: float, leverage: float = 1.0, include_fees: bool = True) -> float:
|
|
|
|
|
"""Calculate unrealized P&L for the position with leverage and fees
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
current_price: Current market price
|
|
|
|
|
leverage: Leverage multiplier (default: 1.0)
|
|
|
|
|
include_fees: Whether to subtract fees from PnL (default: True)
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
float: Unrealized PnL including leverage and fees
|
|
|
|
|
"""
|
|
|
|
|
# Calculate position value
|
|
|
|
|
position_value = self.entry_price * self.quantity
|
|
|
|
|
|
|
|
|
|
# Calculate base PnL
|
|
|
|
|
if self.side == 'LONG':
|
|
|
|
|
self.unrealized_pnl = (current_price - self.entry_price) * self.quantity
|
|
|
|
|
base_pnl = (current_price - self.entry_price) * self.quantity
|
|
|
|
|
else: # SHORT
|
|
|
|
|
self.unrealized_pnl = (self.entry_price - current_price) * self.quantity
|
|
|
|
|
base_pnl = (self.entry_price - current_price) * self.quantity
|
|
|
|
|
|
|
|
|
|
# Apply leverage
|
|
|
|
|
leveraged_pnl = base_pnl * leverage
|
|
|
|
|
|
|
|
|
|
# Calculate fees (0.1% open + 0.1% close = 0.2% total)
|
|
|
|
|
fees = 0.0
|
|
|
|
|
if include_fees:
|
|
|
|
|
# Open fee already paid
|
|
|
|
|
open_fee = position_value * 0.001
|
|
|
|
|
# Close fee will be paid when position is closed
|
|
|
|
|
close_fee = (current_price * self.quantity) * 0.001
|
|
|
|
|
fees = open_fee + close_fee
|
|
|
|
|
|
|
|
|
|
# Final PnL after fees
|
|
|
|
|
self.unrealized_pnl = leveraged_pnl - fees
|
|
|
|
|
return self.unrealized_pnl
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
@ -62,6 +90,10 @@ class TradeRecord:
|
|
|
|
|
fees: float
|
|
|
|
|
confidence: float
|
|
|
|
|
hold_time_seconds: float = 0.0 # Hold time in seconds
|
|
|
|
|
leverage: float = 1.0 # Leverage used for the trade
|
|
|
|
|
position_size_usd: float = 0.0 # Position size in USD
|
|
|
|
|
gross_pnl: float = 0.0 # PnL before fees
|
|
|
|
|
net_pnl: float = 0.0 # PnL after fees
|
|
|
|
|
|
|
|
|
|
class TradingExecutor:
|
|
|
|
|
"""Handles trade execution through multiple exchange APIs with risk management"""
|
|
|
|
@ -79,19 +111,22 @@ class TradingExecutor:
|
|
|
|
|
# Set primary exchange as main interface
|
|
|
|
|
self.exchange = self.primary_exchange
|
|
|
|
|
|
|
|
|
|
# Get primary exchange name and config first
|
|
|
|
|
primary_name = self.exchanges_config.get('primary', 'deribit')
|
|
|
|
|
primary_config = self.exchanges_config.get(primary_name, {})
|
|
|
|
|
|
|
|
|
|
# Determine trading and simulation modes
|
|
|
|
|
trading_mode = primary_config.get('trading_mode', 'simulation')
|
|
|
|
|
self.trading_enabled = self.trading_config.get('enabled', True)
|
|
|
|
|
self.simulation_mode = trading_mode == 'simulation'
|
|
|
|
|
|
|
|
|
|
if not self.exchange:
|
|
|
|
|
logger.error("Failed to initialize primary exchange")
|
|
|
|
|
self.trading_enabled = False
|
|
|
|
|
self.simulation_mode = True
|
|
|
|
|
if self.simulation_mode:
|
|
|
|
|
logger.info("Failed to initialize primary exchange, but simulation mode is enabled - trading allowed")
|
|
|
|
|
else:
|
|
|
|
|
logger.error("Failed to initialize primary exchange and not in simulation mode - trading disabled")
|
|
|
|
|
self.trading_enabled = False
|
|
|
|
|
else:
|
|
|
|
|
primary_name = self.exchanges_config.get('primary', 'deribit')
|
|
|
|
|
primary_config = self.exchanges_config.get(primary_name, {})
|
|
|
|
|
|
|
|
|
|
# Determine trading and simulation modes
|
|
|
|
|
trading_mode = primary_config.get('trading_mode', 'simulation')
|
|
|
|
|
self.trading_enabled = self.trading_config.get('enabled', True)
|
|
|
|
|
self.simulation_mode = trading_mode == 'simulation'
|
|
|
|
|
|
|
|
|
|
logger.info(f"Trading Executor initialized with {primary_name} as primary exchange")
|
|
|
|
|
logger.info(f"Trading mode: {trading_mode}, Simulation: {self.simulation_mode}")
|
|
|
|
|
|
|
|
|
@ -121,6 +156,13 @@ class TradingExecutor:
|
|
|
|
|
# Store trading mode for compatibility
|
|
|
|
|
self.trading_mode = self.primary_config.get('trading_mode', 'simulation')
|
|
|
|
|
|
|
|
|
|
# Safety feature: Auto-disable live trading after consecutive losses
|
|
|
|
|
self.max_consecutive_losses = 5 # Disable live trading after 5 consecutive losses
|
|
|
|
|
self.min_success_rate_to_reenable = 0.55 # Require 55% success rate to re-enable
|
|
|
|
|
self.trades_to_evaluate = 20 # Evaluate last 20 trades for success rate
|
|
|
|
|
self.original_trading_mode = self.trading_mode # Store original mode
|
|
|
|
|
self.safety_triggered = False # Track if safety feature was triggered
|
|
|
|
|
|
|
|
|
|
# Initialize session stats
|
|
|
|
|
self.session_start_time = datetime.now()
|
|
|
|
|
self.session_trades = 0
|
|
|
|
@ -130,7 +172,19 @@ class TradingExecutor:
|
|
|
|
|
self.positions = {} # symbol -> Position object
|
|
|
|
|
self.trade_records = [] # List of TradeRecord objects
|
|
|
|
|
|
|
|
|
|
# Simulation balance tracking
|
|
|
|
|
self.simulation_balance = self.trading_config.get('simulation_account_usd', 100.0)
|
|
|
|
|
self.simulation_positions = {} # symbol -> position data with real entry prices
|
|
|
|
|
|
|
|
|
|
# Trading fees configuration (0.1% for both open and close)
|
|
|
|
|
self.trading_fees = {
|
|
|
|
|
'open_fee_percent': 0.001, # 0.1% fee when opening position
|
|
|
|
|
'close_fee_percent': 0.001, # 0.1% fee when closing position
|
|
|
|
|
'total_round_trip_fee': 0.002 # 0.2% total for round trip
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info(f"TradingExecutor initialized - Trading: {self.trading_enabled}, Mode: {self.trading_mode}")
|
|
|
|
|
logger.info(f"Simulation balance: ${self.simulation_balance:.2f}")
|
|
|
|
|
|
|
|
|
|
# Legacy compatibility (deprecated)
|
|
|
|
|
self.dry_run = self.simulation_mode
|
|
|
|
@ -152,10 +206,13 @@ class TradingExecutor:
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
if self.simulation_mode:
|
|
|
|
|
logger.info("TRADING EXECUTOR: Simulation mode - trading enabled without exchange connection")
|
|
|
|
|
else:
|
|
|
|
|
logger.info("TRADING EXECUTOR: Attempting to connect to exchange...")
|
|
|
|
|
if not self._connect_exchange():
|
|
|
|
|
logger.error("TRADING EXECUTOR: Failed initial exchange connection. Trading will be disabled.")
|
|
|
|
|
self.trading_enabled = False
|
|
|
|
|
else:
|
|
|
|
|
logger.info("TRADING EXECUTOR: Trading is explicitly disabled in config.")
|
|
|
|
|
|
|
|
|
@ -210,6 +267,67 @@ class TradingExecutor:
|
|
|
|
|
logger.error(f"Error calling {method_name}: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _get_real_current_price(self, symbol: str) -> Optional[float]:
|
|
|
|
|
"""Get real current price from data provider - NEVER use simulated data"""
|
|
|
|
|
try:
|
|
|
|
|
# Try to get from data provider first (most reliable)
|
|
|
|
|
from core.data_provider import DataProvider
|
|
|
|
|
data_provider = DataProvider()
|
|
|
|
|
|
|
|
|
|
# Try multiple timeframes to get the most recent price
|
|
|
|
|
for timeframe in ['1m', '5m', '1h']:
|
|
|
|
|
try:
|
|
|
|
|
df = data_provider.get_historical_data(symbol, timeframe, limit=1, refresh=True)
|
|
|
|
|
if df is not None and not df.empty:
|
|
|
|
|
price = float(df['close'].iloc[-1])
|
|
|
|
|
if price > 0:
|
|
|
|
|
logger.debug(f"Got real price for {symbol} from {timeframe}: ${price:.2f}")
|
|
|
|
|
return price
|
|
|
|
|
except Exception as tf_error:
|
|
|
|
|
logger.debug(f"Failed to get {timeframe} data for {symbol}: {tf_error}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# Try exchange ticker if available
|
|
|
|
|
if self.exchange:
|
|
|
|
|
try:
|
|
|
|
|
ticker = self.exchange.get_ticker(symbol)
|
|
|
|
|
if ticker and 'last' in ticker:
|
|
|
|
|
price = float(ticker['last'])
|
|
|
|
|
if price > 0:
|
|
|
|
|
logger.debug(f"Got real price for {symbol} from exchange: ${price:.2f}")
|
|
|
|
|
return price
|
|
|
|
|
except Exception as ex_error:
|
|
|
|
|
logger.debug(f"Failed to get price from exchange: {ex_error}")
|
|
|
|
|
|
|
|
|
|
# Try external API as last resort
|
|
|
|
|
try:
|
|
|
|
|
import requests
|
|
|
|
|
if symbol == 'ETH/USDT':
|
|
|
|
|
response = requests.get('https://api.binance.com/api/v3/ticker/price?symbol=ETHUSDT', timeout=2)
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
data = response.json()
|
|
|
|
|
price = float(data['price'])
|
|
|
|
|
if price > 0:
|
|
|
|
|
logger.debug(f"Got real price for {symbol} from Binance API: ${price:.2f}")
|
|
|
|
|
return price
|
|
|
|
|
elif symbol == 'BTC/USDT':
|
|
|
|
|
response = requests.get('https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT', timeout=2)
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
data = response.json()
|
|
|
|
|
price = float(data['price'])
|
|
|
|
|
if price > 0:
|
|
|
|
|
logger.debug(f"Got real price for {symbol} from Binance API: ${price:.2f}")
|
|
|
|
|
return price
|
|
|
|
|
except Exception as api_error:
|
|
|
|
|
logger.debug(f"Failed to get price from external API: {api_error}")
|
|
|
|
|
|
|
|
|
|
logger.error(f"Failed to get real current price for {symbol} from all sources")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error getting real current price for {symbol}: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _connect_exchange(self) -> bool:
|
|
|
|
|
"""Connect to the primary exchange"""
|
|
|
|
|
if not self.exchange:
|
|
|
|
@ -250,11 +368,11 @@ class TradingExecutor:
|
|
|
|
|
|
|
|
|
|
# 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.")
|
|
|
|
|
# Always get real current price - never use simulated data
|
|
|
|
|
current_price = self._get_real_current_price(symbol)
|
|
|
|
|
if current_price is None:
|
|
|
|
|
logger.error(f"Failed to get real current price for {symbol}")
|
|
|
|
|
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"
|
|
|
|
@ -504,12 +622,96 @@ class TradingExecutor:
|
|
|
|
|
logger.error(f"Error cancelling open orders for {symbol}: {e}")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
def _can_reenable_live_trading(self) -> bool:
|
|
|
|
|
"""Check if trading performance has improved enough to re-enable live trading
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
bool: True if performance meets criteria to re-enable live trading
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# Need enough trades to evaluate
|
|
|
|
|
if len(self.trade_history) < self.trades_to_evaluate:
|
|
|
|
|
logger.debug(f"Not enough trades to evaluate for re-enabling live trading: {len(self.trade_history)}/{self.trades_to_evaluate}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Get the most recent trades for evaluation
|
|
|
|
|
recent_trades = self.trade_history[-self.trades_to_evaluate:]
|
|
|
|
|
|
|
|
|
|
# Calculate success rate
|
|
|
|
|
winning_trades = sum(1 for trade in recent_trades if trade.pnl > 0.001)
|
|
|
|
|
success_rate = winning_trades / len(recent_trades)
|
|
|
|
|
|
|
|
|
|
# Calculate average PnL
|
|
|
|
|
avg_pnl = sum(trade.pnl for trade in recent_trades) / len(recent_trades)
|
|
|
|
|
|
|
|
|
|
# Calculate win/loss ratio
|
|
|
|
|
losing_trades = sum(1 for trade in recent_trades if trade.pnl < -0.001)
|
|
|
|
|
win_loss_ratio = winning_trades / max(1, losing_trades) # Avoid division by zero
|
|
|
|
|
|
|
|
|
|
logger.info(f"SAFETY FEATURE: Performance evaluation - Success rate: {success_rate:.2%}, Avg PnL: ${avg_pnl:.2f}, Win/Loss ratio: {win_loss_ratio:.2f}")
|
|
|
|
|
|
|
|
|
|
# Criteria to re-enable live trading:
|
|
|
|
|
# 1. Success rate must exceed minimum threshold
|
|
|
|
|
# 2. Average PnL must be positive
|
|
|
|
|
# 3. Win/loss ratio must be at least 1.0 (equal wins and losses)
|
|
|
|
|
if (success_rate >= self.min_success_rate_to_reenable and
|
|
|
|
|
avg_pnl > 0 and
|
|
|
|
|
win_loss_ratio >= 1.0):
|
|
|
|
|
logger.info(f"SAFETY FEATURE: Performance criteria met for re-enabling live trading")
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
logger.debug(f"SAFETY FEATURE: Performance criteria not yet met for re-enabling live trading")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error evaluating trading performance: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error evaluating trading performance: {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.exchange_config.get('emergency_stop', False):
|
|
|
|
|
logger.warning("Emergency stop is active - no trades allowed")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Safety feature: Check consecutive losses and switch to simulation mode if needed
|
|
|
|
|
if not self.simulation_mode and self.consecutive_losses >= self.max_consecutive_losses:
|
|
|
|
|
logger.warning(f"SAFETY FEATURE ACTIVATED: {self.consecutive_losses} consecutive losses detected")
|
|
|
|
|
logger.warning(f"Switching from live trading to simulation mode for safety")
|
|
|
|
|
|
|
|
|
|
# Store original mode and switch to simulation
|
|
|
|
|
self.original_trading_mode = self.trading_mode
|
|
|
|
|
self.trading_mode = 'simulation'
|
|
|
|
|
self.simulation_mode = True
|
|
|
|
|
self.safety_triggered = True
|
|
|
|
|
|
|
|
|
|
# Log the event
|
|
|
|
|
logger.info(f"Trading mode changed to SIMULATION due to safety feature")
|
|
|
|
|
logger.info(f"Will continue to monitor performance and re-enable live trading when success rate improves")
|
|
|
|
|
|
|
|
|
|
# Continue allowing trades in simulation mode
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
# Check if we should try to re-enable live trading after safety feature was triggered
|
|
|
|
|
if self.simulation_mode and self.safety_triggered and self.original_trading_mode != 'simulation':
|
|
|
|
|
# Check if performance has improved enough to re-enable live trading
|
|
|
|
|
if self._can_reenable_live_trading():
|
|
|
|
|
logger.info(f"SAFETY FEATURE: Performance has improved, re-enabling live trading")
|
|
|
|
|
|
|
|
|
|
# Switch back to original mode
|
|
|
|
|
self.trading_mode = self.original_trading_mode
|
|
|
|
|
self.simulation_mode = (self.trading_mode == 'simulation')
|
|
|
|
|
self.safety_triggered = False
|
|
|
|
|
self.consecutive_losses = 0 # Reset consecutive losses counter
|
|
|
|
|
|
|
|
|
|
logger.info(f"Trading mode restored to {self.trading_mode}")
|
|
|
|
|
|
|
|
|
|
# Continue with the trade
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
# Check symbol allowlist
|
|
|
|
|
allowed_symbols = self.exchange_config.get('allowed_symbols', [])
|
|
|
|
@ -961,7 +1163,22 @@ class TradingExecutor:
|
|
|
|
|
exit_time = datetime.now()
|
|
|
|
|
hold_time_seconds = (exit_time - position.entry_time).total_seconds()
|
|
|
|
|
|
|
|
|
|
# Create trade record
|
|
|
|
|
# Get current leverage setting
|
|
|
|
|
leverage = self.trading_config.get('leverage', 1.0)
|
|
|
|
|
|
|
|
|
|
# Calculate position size in USD
|
|
|
|
|
position_size_usd = position.quantity * position.entry_price
|
|
|
|
|
|
|
|
|
|
# Calculate gross PnL (before fees) with leverage
|
|
|
|
|
if position.side == 'SHORT':
|
|
|
|
|
gross_pnl = (position.entry_price - current_price) * position.quantity * leverage
|
|
|
|
|
else: # LONG
|
|
|
|
|
gross_pnl = (current_price - position.entry_price) * position.quantity * leverage
|
|
|
|
|
|
|
|
|
|
# Calculate net PnL (after fees)
|
|
|
|
|
net_pnl = gross_pnl - simulated_fees
|
|
|
|
|
|
|
|
|
|
# Create trade record with enhanced PnL calculations
|
|
|
|
|
trade_record = TradeRecord(
|
|
|
|
|
symbol=symbol,
|
|
|
|
|
side='SHORT',
|
|
|
|
@ -970,10 +1187,14 @@ class TradingExecutor:
|
|
|
|
|
exit_price=current_price,
|
|
|
|
|
entry_time=position.entry_time,
|
|
|
|
|
exit_time=exit_time,
|
|
|
|
|
pnl=pnl,
|
|
|
|
|
pnl=net_pnl, # Store net PnL as the main PnL value
|
|
|
|
|
fees=simulated_fees,
|
|
|
|
|
confidence=confidence,
|
|
|
|
|
hold_time_seconds=hold_time_seconds
|
|
|
|
|
hold_time_seconds=hold_time_seconds,
|
|
|
|
|
leverage=leverage,
|
|
|
|
|
position_size_usd=position_size_usd,
|
|
|
|
|
gross_pnl=gross_pnl,
|
|
|
|
|
net_pnl=net_pnl
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.trade_history.append(trade_record)
|
|
|
|
@ -1033,7 +1254,22 @@ class TradingExecutor:
|
|
|
|
|
exit_time = datetime.now()
|
|
|
|
|
hold_time_seconds = (exit_time - position.entry_time).total_seconds()
|
|
|
|
|
|
|
|
|
|
# Create trade record
|
|
|
|
|
# Get current leverage setting
|
|
|
|
|
leverage = self.trading_config.get('leverage', 1.0)
|
|
|
|
|
|
|
|
|
|
# Calculate position size in USD
|
|
|
|
|
position_size_usd = position.quantity * position.entry_price
|
|
|
|
|
|
|
|
|
|
# Calculate gross PnL (before fees) with leverage
|
|
|
|
|
if position.side == 'SHORT':
|
|
|
|
|
gross_pnl = (position.entry_price - current_price) * position.quantity * leverage
|
|
|
|
|
else: # LONG
|
|
|
|
|
gross_pnl = (current_price - position.entry_price) * position.quantity * leverage
|
|
|
|
|
|
|
|
|
|
# Calculate net PnL (after fees)
|
|
|
|
|
net_pnl = gross_pnl - fees
|
|
|
|
|
|
|
|
|
|
# Create trade record with enhanced PnL calculations
|
|
|
|
|
trade_record = TradeRecord(
|
|
|
|
|
symbol=symbol,
|
|
|
|
|
side='SHORT',
|
|
|
|
@ -1042,10 +1278,14 @@ class TradingExecutor:
|
|
|
|
|
exit_price=current_price,
|
|
|
|
|
entry_time=position.entry_time,
|
|
|
|
|
exit_time=exit_time,
|
|
|
|
|
pnl=pnl - fees,
|
|
|
|
|
pnl=net_pnl, # Store net PnL as the main PnL value
|
|
|
|
|
fees=fees,
|
|
|
|
|
confidence=confidence,
|
|
|
|
|
hold_time_seconds=hold_time_seconds
|
|
|
|
|
hold_time_seconds=hold_time_seconds,
|
|
|
|
|
leverage=leverage,
|
|
|
|
|
position_size_usd=position_size_usd,
|
|
|
|
|
gross_pnl=gross_pnl,
|
|
|
|
|
net_pnl=net_pnl
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.trade_history.append(trade_record)
|
|
|
|
@ -1243,7 +1483,7 @@ class TradingExecutor:
|
|
|
|
|
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)
|
|
|
|
|
return self.simulation_balance
|
|
|
|
|
else:
|
|
|
|
|
# For live trading, get actual USDT/USDC balance
|
|
|
|
|
try:
|
|
|
|
@ -1253,7 +1493,179 @@ class TradingExecutor:
|
|
|
|
|
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)
|
|
|
|
|
return self.simulation_balance
|
|
|
|
|
|
|
|
|
|
def _calculate_pnl_with_fees(self, entry_price: float, exit_price: float, quantity: float, side: str) -> Dict[str, float]:
|
|
|
|
|
"""Calculate PnL including trading fees (0.1% open + 0.1% close = 0.2% total)"""
|
|
|
|
|
try:
|
|
|
|
|
# Calculate position value
|
|
|
|
|
position_value = entry_price * quantity
|
|
|
|
|
|
|
|
|
|
# Calculate fees
|
|
|
|
|
open_fee = position_value * self.trading_fees['open_fee_percent']
|
|
|
|
|
close_fee = (exit_price * quantity) * self.trading_fees['close_fee_percent']
|
|
|
|
|
total_fees = open_fee + close_fee
|
|
|
|
|
|
|
|
|
|
# Calculate gross PnL (before fees)
|
|
|
|
|
if side.upper() == 'LONG':
|
|
|
|
|
gross_pnl = (exit_price - entry_price) * quantity
|
|
|
|
|
else: # SHORT
|
|
|
|
|
gross_pnl = (entry_price - exit_price) * quantity
|
|
|
|
|
|
|
|
|
|
# Calculate net PnL (after fees)
|
|
|
|
|
net_pnl = gross_pnl - total_fees
|
|
|
|
|
|
|
|
|
|
# Calculate percentage returns
|
|
|
|
|
gross_pnl_percent = (gross_pnl / position_value) * 100
|
|
|
|
|
net_pnl_percent = (net_pnl / position_value) * 100
|
|
|
|
|
fee_percent = (total_fees / position_value) * 100
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'gross_pnl': gross_pnl,
|
|
|
|
|
'net_pnl': net_pnl,
|
|
|
|
|
'total_fees': total_fees,
|
|
|
|
|
'open_fee': open_fee,
|
|
|
|
|
'close_fee': close_fee,
|
|
|
|
|
'gross_pnl_percent': gross_pnl_percent,
|
|
|
|
|
'net_pnl_percent': net_pnl_percent,
|
|
|
|
|
'fee_percent': fee_percent,
|
|
|
|
|
'position_value': position_value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error calculating PnL with fees: {e}")
|
|
|
|
|
return {
|
|
|
|
|
'gross_pnl': 0.0,
|
|
|
|
|
'net_pnl': 0.0,
|
|
|
|
|
'total_fees': 0.0,
|
|
|
|
|
'open_fee': 0.0,
|
|
|
|
|
'close_fee': 0.0,
|
|
|
|
|
'gross_pnl_percent': 0.0,
|
|
|
|
|
'net_pnl_percent': 0.0,
|
|
|
|
|
'fee_percent': 0.0,
|
|
|
|
|
'position_value': 0.0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def _calculate_pivot_points(self, symbol: str) -> Dict[str, float]:
|
|
|
|
|
"""Calculate pivot points for the symbol using real market data"""
|
|
|
|
|
try:
|
|
|
|
|
from core.data_provider import DataProvider
|
|
|
|
|
data_provider = DataProvider()
|
|
|
|
|
|
|
|
|
|
# Get daily data for pivot calculation
|
|
|
|
|
df = data_provider.get_historical_data(symbol, '1d', limit=2, refresh=True)
|
|
|
|
|
if df is None or len(df) < 2:
|
|
|
|
|
logger.warning(f"Insufficient data for pivot calculation for {symbol}")
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
# Use previous day's data for pivot calculation
|
|
|
|
|
prev_day = df.iloc[-2]
|
|
|
|
|
high = float(prev_day['high'])
|
|
|
|
|
low = float(prev_day['low'])
|
|
|
|
|
close = float(prev_day['close'])
|
|
|
|
|
|
|
|
|
|
# Calculate pivot point
|
|
|
|
|
pivot = (high + low + close) / 3
|
|
|
|
|
|
|
|
|
|
# Calculate support and resistance levels
|
|
|
|
|
r1 = (2 * pivot) - low
|
|
|
|
|
s1 = (2 * pivot) - high
|
|
|
|
|
r2 = pivot + (high - low)
|
|
|
|
|
s2 = pivot - (high - low)
|
|
|
|
|
r3 = high + 2 * (pivot - low)
|
|
|
|
|
s3 = low - 2 * (high - pivot)
|
|
|
|
|
|
|
|
|
|
pivots = {
|
|
|
|
|
'pivot': pivot,
|
|
|
|
|
'r1': r1, 'r2': r2, 'r3': r3,
|
|
|
|
|
's1': s1, 's2': s2, 's3': s3,
|
|
|
|
|
'prev_high': high,
|
|
|
|
|
'prev_low': low,
|
|
|
|
|
'prev_close': close
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.debug(f"Pivot points for {symbol}: P={pivot:.2f}, R1={r1:.2f}, S1={s1:.2f}")
|
|
|
|
|
return pivots
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error calculating pivot points for {symbol}: {e}")
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
def _get_pivot_signal_strength(self, symbol: str, current_price: float, action: str) -> float:
|
|
|
|
|
"""Get signal strength based on proximity to pivot points"""
|
|
|
|
|
try:
|
|
|
|
|
pivots = self._calculate_pivot_points(symbol)
|
|
|
|
|
if not pivots:
|
|
|
|
|
return 1.0 # Default strength if no pivots available
|
|
|
|
|
|
|
|
|
|
pivot = pivots['pivot']
|
|
|
|
|
r1, r2, r3 = pivots['r1'], pivots['r2'], pivots['r3']
|
|
|
|
|
s1, s2, s3 = pivots['s1'], pivots['s2'], pivots['s3']
|
|
|
|
|
|
|
|
|
|
# Calculate distance to nearest pivot levels
|
|
|
|
|
distances = {
|
|
|
|
|
'pivot': abs(current_price - pivot),
|
|
|
|
|
'r1': abs(current_price - r1),
|
|
|
|
|
'r2': abs(current_price - r2),
|
|
|
|
|
'r3': abs(current_price - r3),
|
|
|
|
|
's1': abs(current_price - s1),
|
|
|
|
|
's2': abs(current_price - s2),
|
|
|
|
|
's3': abs(current_price - s3)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Find nearest level
|
|
|
|
|
nearest_level = min(distances.keys(), key=lambda k: distances[k])
|
|
|
|
|
nearest_distance = distances[nearest_level]
|
|
|
|
|
nearest_price = pivots[nearest_level]
|
|
|
|
|
|
|
|
|
|
# Calculate signal strength based on action and pivot context
|
|
|
|
|
strength = 1.0
|
|
|
|
|
|
|
|
|
|
if action == 'BUY':
|
|
|
|
|
# Stronger buy signals near support levels
|
|
|
|
|
if nearest_level in ['s1', 's2', 's3'] and current_price <= nearest_price:
|
|
|
|
|
strength = 1.5 # Boost buy signals at support
|
|
|
|
|
elif nearest_level in ['r1', 'r2', 'r3'] and current_price >= nearest_price:
|
|
|
|
|
strength = 0.7 # Reduce buy signals at resistance
|
|
|
|
|
|
|
|
|
|
elif action == 'SELL':
|
|
|
|
|
# Stronger sell signals near resistance levels
|
|
|
|
|
if nearest_level in ['r1', 'r2', 'r3'] and current_price >= nearest_price:
|
|
|
|
|
strength = 1.5 # Boost sell signals at resistance
|
|
|
|
|
elif nearest_level in ['s1', 's2', 's3'] and current_price <= nearest_price:
|
|
|
|
|
strength = 0.7 # Reduce sell signals at support
|
|
|
|
|
|
|
|
|
|
logger.debug(f"Pivot signal strength for {symbol} {action}: {strength:.2f} "
|
|
|
|
|
f"(near {nearest_level} at ${nearest_price:.2f}, current ${current_price:.2f})")
|
|
|
|
|
|
|
|
|
|
return strength
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error calculating pivot signal strength: {e}")
|
|
|
|
|
return 1.0
|
|
|
|
|
|
|
|
|
|
def _get_current_price_from_data_provider(self, symbol: str) -> Optional[float]:
|
|
|
|
|
"""Get current price from data provider for most up-to-date information"""
|
|
|
|
|
try:
|
|
|
|
|
from core.data_provider import DataProvider
|
|
|
|
|
data_provider = DataProvider()
|
|
|
|
|
|
|
|
|
|
# Try to get real-time price first
|
|
|
|
|
current_price = data_provider.get_current_price(symbol)
|
|
|
|
|
if current_price and current_price > 0:
|
|
|
|
|
return float(current_price)
|
|
|
|
|
|
|
|
|
|
# Fallback to latest 1m candle
|
|
|
|
|
df = data_provider.get_historical_data(symbol, '1m', limit=1, refresh=True)
|
|
|
|
|
if df is not None and len(df) > 0:
|
|
|
|
|
return float(df.iloc[-1]['close'])
|
|
|
|
|
|
|
|
|
|
logger.warning(f"Could not get current price for {symbol} from data provider")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error getting current price from data provider for {symbol}: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _check_position_size_limit(self) -> bool:
|
|
|
|
|
"""Check if total open position value exceeds the maximum allowed percentage of balance"""
|
|
|
|
@ -1272,8 +1684,12 @@ class TradingExecutor:
|
|
|
|
|
for symbol, position in self.positions.items():
|
|
|
|
|
# Get current price for the symbol
|
|
|
|
|
try:
|
|
|
|
|
ticker = self.exchange.get_ticker(symbol) if self.exchange else None
|
|
|
|
|
current_price = ticker['last'] if ticker and 'last' in ticker else position.entry_price
|
|
|
|
|
if self.exchange:
|
|
|
|
|
ticker = self.exchange.get_ticker(symbol)
|
|
|
|
|
current_price = ticker['last'] if ticker and 'last' in ticker else position.entry_price
|
|
|
|
|
else:
|
|
|
|
|
# Simulation mode - use entry price or default
|
|
|
|
|
current_price = position.entry_price
|
|
|
|
|
except Exception:
|
|
|
|
|
# Fallback to entry price if we can't get current price
|
|
|
|
|
current_price = position.entry_price
|
|
|
|
@ -1393,9 +1809,13 @@ class TradingExecutor:
|
|
|
|
|
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'])
|
|
|
|
|
if self.exchange:
|
|
|
|
|
ticker = self.exchange.get_ticker(symbol)
|
|
|
|
|
if ticker:
|
|
|
|
|
self._execute_sell(symbol, 1.0, ticker['last'])
|
|
|
|
|
else:
|
|
|
|
|
# Simulation mode - use entry price for closing
|
|
|
|
|
self._execute_sell(symbol, 1.0, position.entry_price)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error closing position {symbol} during emergency stop: {e}")
|
|
|
|
|
|
|
|
|
@ -1746,11 +2166,10 @@ class TradingExecutor:
|
|
|
|
|
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}")
|
|
|
|
|
# Always get real current price - never use simulated data
|
|
|
|
|
current_price = self._get_real_current_price(symbol)
|
|
|
|
|
if current_price is None:
|
|
|
|
|
logger.error(f"Failed to get real current price for {symbol}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# Calculate confidence based on manual trade (high confidence)
|
|
|
|
@ -1881,6 +2300,88 @@ class TradingExecutor:
|
|
|
|
|
logger.info("TRADING EXECUTOR: Test mode enabled - bypassing safety checks")
|
|
|
|
|
else:
|
|
|
|
|
logger.info("TRADING EXECUTOR: Test mode disabled - normal safety checks active")
|
|
|
|
|
|
|
|
|
|
def get_status(self) -> Dict[str, Any]:
|
|
|
|
|
"""Get trading executor status with safety feature information"""
|
|
|
|
|
try:
|
|
|
|
|
# Get account balance
|
|
|
|
|
if self.simulation_mode:
|
|
|
|
|
balance = self.simulation_balance
|
|
|
|
|
else:
|
|
|
|
|
balance = self.exchange.get_balance('USDT') if self.exchange else 0.0
|
|
|
|
|
|
|
|
|
|
# Get open positions
|
|
|
|
|
positions = self.get_positions()
|
|
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
maker_fee_rate = self.exchange_config.get('maker_fee', 0.0002)
|
|
|
|
|
taker_fee_rate = self.exchange_config.get('taker_fee', 0.0006)
|
|
|
|
|
avg_fee_rate = (maker_fee_rate + taker_fee_rate) / 2
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
# Calculate success rate for recent trades
|
|
|
|
|
recent_trades = self.trade_history[-self.trades_to_evaluate:] if len(self.trade_history) >= self.trades_to_evaluate else self.trade_history
|
|
|
|
|
winning_trades = sum(1 for trade in recent_trades if trade.pnl > 0.001) if recent_trades else 0
|
|
|
|
|
success_rate = (winning_trades / len(recent_trades)) if recent_trades else 0
|
|
|
|
|
|
|
|
|
|
# Safety feature status
|
|
|
|
|
safety_status = {
|
|
|
|
|
'active': self.safety_triggered,
|
|
|
|
|
'consecutive_losses': self.consecutive_losses,
|
|
|
|
|
'max_consecutive_losses': self.max_consecutive_losses,
|
|
|
|
|
'original_mode': self.original_trading_mode if self.safety_triggered else self.trading_mode,
|
|
|
|
|
'success_rate': success_rate,
|
|
|
|
|
'min_success_rate_to_reenable': self.min_success_rate_to_reenable,
|
|
|
|
|
'trades_evaluated': len(recent_trades),
|
|
|
|
|
'trades_needed': self.trades_to_evaluate,
|
|
|
|
|
'can_reenable': self._can_reenable_live_trading() if self.safety_triggered else False
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'trading_enabled': self.trading_enabled,
|
|
|
|
|
'simulation_mode': self.simulation_mode,
|
|
|
|
|
'trading_mode': self.trading_mode,
|
|
|
|
|
'balance': balance,
|
|
|
|
|
'positions': len(positions),
|
|
|
|
|
'daily_trades': self.daily_trades,
|
|
|
|
|
'daily_pnl': self.daily_pnl,
|
|
|
|
|
'daily_loss': self.daily_loss,
|
|
|
|
|
'consecutive_losses': self.consecutive_losses,
|
|
|
|
|
'total_trades': len(self.trade_history),
|
|
|
|
|
'safety_feature': safety_status,
|
|
|
|
|
'pnl': {
|
|
|
|
|
'total': total_pnl,
|
|
|
|
|
'gross': gross_pnl,
|
|
|
|
|
'fees': total_fees,
|
|
|
|
|
'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': {
|
|
|
|
|
'total_volume': total_volume,
|
|
|
|
|
'total_fees': total_fees,
|
|
|
|
|
'effective_fee_rate': (total_fees / max(0.01, total_volume)) if total_volume > 0 else 0,
|
|
|
|
|
'expected_fee_rate': avg_fee_rate,
|
|
|
|
|
'fee_efficiency': (avg_fee_rate / ((total_fees / max(0.01, total_volume)) if total_volume > 0 else 1)) if avg_fee_rate > 0 else 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error getting trading executor status: {e}")
|
|
|
|
|
return {
|
|
|
|
|
'trading_enabled': self.trading_enabled,
|
|
|
|
|
'simulation_mode': self.simulation_mode,
|
|
|
|
|
'trading_mode': self.trading_mode,
|
|
|
|
|
'error': str(e)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def sync_position_with_mexc(self, symbol: str, desired_state: str) -> bool:
|
|
|
|
|
"""Synchronize dashboard position state with actual MEXC account positions
|
|
|
|
@ -2015,9 +2516,13 @@ class TradingExecutor:
|
|
|
|
|
def _get_current_price_for_sync(self, symbol: str) -> Optional[float]:
|
|
|
|
|
"""Get current price for position synchronization"""
|
|
|
|
|
try:
|
|
|
|
|
ticker = self.exchange.get_ticker(symbol)
|
|
|
|
|
if ticker and 'last' in ticker:
|
|
|
|
|
return float(ticker['last'])
|
|
|
|
|
if self.exchange:
|
|
|
|
|
ticker = self.exchange.get_ticker(symbol)
|
|
|
|
|
if ticker and 'last' in ticker:
|
|
|
|
|
return float(ticker['last'])
|
|
|
|
|
else:
|
|
|
|
|
# Get real current price - never use simulated data
|
|
|
|
|
return self._get_real_current_price(symbol)
|
|
|
|
|
return None
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error getting current price for sync: {e}")
|
|
|
|
|