UI and stability
This commit is contained in:
@ -36,6 +36,12 @@ import math
|
||||
# Suppress ta library deprecation warnings
|
||||
warnings.filterwarnings("ignore", category=FutureWarning, module="ta")
|
||||
|
||||
# Import timezone utilities
|
||||
from utils.timezone_utils import (
|
||||
normalize_timestamp, normalize_dataframe_timestamps, normalize_dataframe_index,
|
||||
now_system, now_utc, to_sofia, UTC, SOFIA_TZ, log_timezone_info
|
||||
)
|
||||
|
||||
from .config import get_config
|
||||
from .tick_aggregator import RealTimeTickAggregator, RawTick, OHLCVBar
|
||||
from .cnn_monitor import log_cnn_prediction
|
||||
@ -472,7 +478,7 @@ class DataProvider:
|
||||
# Create raw tick entry
|
||||
raw_tick = {
|
||||
'symbol': symbol,
|
||||
'timestamp': datetime.utcnow(),
|
||||
'timestamp': now_system(), # Use system timezone consistently
|
||||
'bids': actual_data.get('bids', [])[:50], # Top 50 levels
|
||||
'asks': actual_data.get('asks', [])[:50], # Top 50 levels
|
||||
'stats': actual_data.get('stats', {}),
|
||||
@ -1097,8 +1103,7 @@ class DataProvider:
|
||||
|
||||
# Process columns with proper timezone handling (MEXC returns UTC timestamps)
|
||||
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)
|
||||
# Convert from UTC to Europe/Sofia timezone
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert('Europe/Sofia')
|
||||
# Keep in UTC to match COB WebSocket data (no timezone conversion)
|
||||
for col in ['open', 'high', 'low', 'close', 'volume']:
|
||||
df[col] = df[col].astype(float)
|
||||
|
||||
@ -1144,18 +1149,16 @@ class DataProvider:
|
||||
if isinstance(timestamp, (int, float)):
|
||||
import pytz
|
||||
utc = pytz.UTC
|
||||
sofia_tz = pytz.timezone('Europe/Sofia')
|
||||
tick_time = datetime.fromtimestamp(timestamp, tz=utc)
|
||||
tick_time = tick_time.astimezone(sofia_tz)
|
||||
# Keep in UTC to match COB WebSocket data
|
||||
elif isinstance(timestamp, datetime):
|
||||
import pytz
|
||||
sofia_tz = pytz.timezone('Europe/Sofia')
|
||||
utc = pytz.UTC
|
||||
tick_time = timestamp
|
||||
# If no timezone info, assume UTC and convert to Europe/Sofia
|
||||
# If no timezone info, assume UTC and keep in UTC
|
||||
if tick_time.tzinfo is None:
|
||||
utc = pytz.UTC
|
||||
tick_time = utc.localize(tick_time)
|
||||
tick_time = tick_time.astimezone(sofia_tz)
|
||||
# Keep in UTC (no timezone conversion)
|
||||
else:
|
||||
continue
|
||||
|
||||
@ -1195,15 +1198,15 @@ class DataProvider:
|
||||
|
||||
# Convert to DataFrame
|
||||
df = pd.DataFrame(candles)
|
||||
# Ensure timestamps are timezone-aware (Europe/Sofia)
|
||||
# Ensure timestamps are timezone-aware (UTC to match COB WebSocket data)
|
||||
if not df.empty and 'timestamp' in df.columns:
|
||||
import pytz
|
||||
sofia_tz = pytz.timezone('Europe/Sofia')
|
||||
# If timestamps are not timezone-aware, make them Europe/Sofia
|
||||
utc = pytz.UTC
|
||||
# If timestamps are not timezone-aware, make them UTC
|
||||
if df['timestamp'].dt.tz is None:
|
||||
df['timestamp'] = df['timestamp'].dt.tz_localize(sofia_tz)
|
||||
df['timestamp'] = df['timestamp'].dt.tz_localize(utc)
|
||||
else:
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert(sofia_tz)
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert(utc)
|
||||
|
||||
df = df.sort_values('timestamp').reset_index(drop=True)
|
||||
|
||||
@ -1283,8 +1286,8 @@ class DataProvider:
|
||||
|
||||
# Process columns with proper timezone handling (Binance returns UTC timestamps)
|
||||
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)
|
||||
# Convert from UTC to Europe/Sofia timezone
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert('Europe/Sofia')
|
||||
# Keep in UTC to match COB WebSocket data (no timezone conversion)
|
||||
# This prevents the 3-hour gap when appending live COB data
|
||||
for col in ['open', 'high', 'low', 'close', 'volume']:
|
||||
df[col] = df[col].astype(float)
|
||||
|
||||
@ -1491,9 +1494,8 @@ class DataProvider:
|
||||
|
||||
import pytz
|
||||
utc = pytz.UTC
|
||||
sofia_tz = pytz.timezone('Europe/Sofia')
|
||||
|
||||
end_time = datetime.utcnow().replace(tzinfo=utc).astimezone(sofia_tz)
|
||||
end_time = datetime.utcnow().replace(tzinfo=utc)
|
||||
start_time = end_time - timedelta(days=30)
|
||||
|
||||
if cached_data is not None and not cached_data.empty:
|
||||
@ -1596,8 +1598,7 @@ class DataProvider:
|
||||
|
||||
# Process columns with proper timezone handling
|
||||
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)
|
||||
# Convert from UTC to Europe/Sofia timezone to match cached data
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert('Europe/Sofia')
|
||||
# Keep in UTC to match COB WebSocket data (no timezone conversion)
|
||||
for col in ['open', 'high', 'low', 'close', 'volume']:
|
||||
df[col] = df[col].astype(float)
|
||||
|
||||
@ -1669,8 +1670,7 @@ class DataProvider:
|
||||
|
||||
# Process columns with proper timezone handling
|
||||
batch_df['timestamp'] = pd.to_datetime(batch_df['timestamp'], unit='ms', utc=True)
|
||||
# Convert from UTC to Europe/Sofia timezone to match cached data
|
||||
batch_df['timestamp'] = batch_df['timestamp'].dt.tz_convert('Europe/Sofia')
|
||||
# Keep in UTC to match COB WebSocket data (no timezone conversion)
|
||||
for col in ['open', 'high', 'low', 'close', 'volume']:
|
||||
batch_df[col] = batch_df[col].astype(float)
|
||||
|
||||
@ -2033,15 +2033,14 @@ class DataProvider:
|
||||
if cache_file.exists():
|
||||
try:
|
||||
df = pd.read_parquet(cache_file)
|
||||
# Ensure cached monthly data has proper timezone (Europe/Sofia)
|
||||
# Ensure cached monthly data has proper timezone (UTC to match COB WebSocket data)
|
||||
if not df.empty and 'timestamp' in df.columns:
|
||||
if df['timestamp'].dt.tz is None:
|
||||
# If no timezone info, assume UTC and convert to Europe/Sofia
|
||||
# If no timezone info, assume UTC and keep in UTC
|
||||
df['timestamp'] = pd.to_datetime(df['timestamp'], utc=True)
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert('Europe/Sofia')
|
||||
elif str(df['timestamp'].dt.tz) != 'Europe/Sofia':
|
||||
# Convert to Europe/Sofia if different timezone
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert('Europe/Sofia')
|
||||
elif str(df['timestamp'].dt.tz) != 'UTC':
|
||||
# Convert to UTC if different timezone
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert('UTC')
|
||||
logger.info(f"Loaded {len(df)} 1m candles from cache for {symbol}")
|
||||
return df
|
||||
except Exception as parquet_e:
|
||||
@ -2317,15 +2316,14 @@ class DataProvider:
|
||||
if cache_age < max_age:
|
||||
try:
|
||||
df = pd.read_parquet(cache_file)
|
||||
# Ensure cached data has proper timezone (Europe/Sofia)
|
||||
# Ensure cached data has proper timezone (UTC to match COB WebSocket data)
|
||||
if not df.empty and 'timestamp' in df.columns:
|
||||
if df['timestamp'].dt.tz is None:
|
||||
# If no timezone info, assume UTC and convert to Europe/Sofia
|
||||
# If no timezone info, assume UTC and keep in UTC
|
||||
df['timestamp'] = pd.to_datetime(df['timestamp'], utc=True)
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert('Europe/Sofia')
|
||||
elif str(df['timestamp'].dt.tz) != 'Europe/Sofia':
|
||||
# Convert to Europe/Sofia if different timezone
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert('Europe/Sofia')
|
||||
elif str(df['timestamp'].dt.tz) != 'UTC':
|
||||
# Convert to UTC if different timezone
|
||||
df['timestamp'] = df['timestamp'].dt.tz_convert('UTC')
|
||||
logger.debug(f"Loaded {len(df)} rows from cache for {symbol} {timeframe} (age: {cache_age/60:.1f}min)")
|
||||
return df
|
||||
except Exception as parquet_e:
|
||||
|
@ -2589,24 +2589,11 @@ class TradingOrchestrator:
|
||||
|
||||
# Method 3: Dictionary with feature data
|
||||
if isinstance(model_input, dict):
|
||||
# Check if dictionary is empty
|
||||
# Check if dictionary is empty - this is the main issue!
|
||||
if not model_input:
|
||||
logger.warning(f"Empty dictionary passed as model_input for {model_name}, using fallback")
|
||||
# Try to use data provider to build state as fallback
|
||||
if hasattr(self, 'data_provider'):
|
||||
try:
|
||||
base_data = self.data_provider.build_base_data_input('ETH/USDT')
|
||||
if base_data and hasattr(base_data, 'get_feature_vector'):
|
||||
state = base_data.get_feature_vector()
|
||||
if isinstance(state, np.ndarray):
|
||||
logger.debug(f"Used data provider fallback for empty dict in {model_name}")
|
||||
return state
|
||||
except Exception as e:
|
||||
logger.debug(f"Data provider fallback failed for empty dict in {model_name}: {e}")
|
||||
|
||||
# Final fallback: return default state
|
||||
logger.warning(f"Using default state for empty dict in {model_name}")
|
||||
return np.zeros(403, dtype=np.float32) # Default state size
|
||||
logger.warning(f"Empty dictionary passed as model_input for {model_name}, using data provider fallback")
|
||||
# Use data provider to build proper state as fallback
|
||||
return self._generate_fresh_state_fallback(model_name)
|
||||
|
||||
# Try to extract features from dictionary
|
||||
if 'features' in model_input:
|
||||
@ -2629,7 +2616,8 @@ class TradingOrchestrator:
|
||||
if feature_list:
|
||||
return np.array(feature_list, dtype=np.float32)
|
||||
else:
|
||||
logger.warning(f"No numerical features found in dictionary for {model_name}, using fallback")
|
||||
logger.warning(f"No numerical features found in dictionary for {model_name}, using data provider fallback")
|
||||
return self._generate_fresh_state_fallback(model_name)
|
||||
|
||||
# Method 4: List or tuple
|
||||
if isinstance(model_input, (list, tuple)):
|
||||
@ -2642,24 +2630,57 @@ class TradingOrchestrator:
|
||||
if isinstance(model_input, (int, float)):
|
||||
return np.array([model_input], dtype=np.float32)
|
||||
|
||||
# Method 6: Try to use data provider to build state
|
||||
if hasattr(self, 'data_provider'):
|
||||
try:
|
||||
base_data = self.data_provider.build_base_data_input('ETH/USDT')
|
||||
if base_data and hasattr(base_data, 'get_feature_vector'):
|
||||
state = base_data.get_feature_vector()
|
||||
if isinstance(state, np.ndarray):
|
||||
logger.debug(f"Used data provider fallback for {model_name}")
|
||||
return state
|
||||
except Exception as e:
|
||||
logger.debug(f"Data provider fallback failed for {model_name}: {e}")
|
||||
|
||||
logger.warning(f"Cannot convert model_input to RL state for {model_name}: {type(model_input)}")
|
||||
return None
|
||||
# Method 6: Final fallback - generate fresh state
|
||||
logger.warning(f"Cannot convert model_input to RL state for {model_name}: {type(model_input)}, using fresh state fallback")
|
||||
return self._generate_fresh_state_fallback(model_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting model_input to RL state for {model_name}: {e}")
|
||||
return None
|
||||
return self._generate_fresh_state_fallback(model_name)
|
||||
|
||||
def _generate_fresh_state_fallback(self, model_name: str) -> np.ndarray:
|
||||
"""Generate a fresh state from current market data when model_input is empty/invalid"""
|
||||
try:
|
||||
# Try to use data provider to build fresh state
|
||||
if hasattr(self, 'data_provider') and self.data_provider:
|
||||
try:
|
||||
# Build fresh BaseDataInput with current market data
|
||||
base_data = self.data_provider.build_base_data_input('ETH/USDT')
|
||||
if base_data and hasattr(base_data, 'get_feature_vector'):
|
||||
state = base_data.get_feature_vector()
|
||||
if isinstance(state, np.ndarray) and state.size > 0:
|
||||
logger.info(f"Generated fresh state for {model_name} from data provider: shape={state.shape}")
|
||||
return state
|
||||
except Exception as e:
|
||||
logger.debug(f"Data provider fresh state generation failed for {model_name}: {e}")
|
||||
|
||||
# Try to get state from model registry
|
||||
if hasattr(self, 'model_registry') and self.model_registry:
|
||||
try:
|
||||
model_interface = self.model_registry.models.get(model_name)
|
||||
if model_interface and hasattr(model_interface, 'get_current_state'):
|
||||
state = model_interface.get_current_state()
|
||||
if isinstance(state, np.ndarray) and state.size > 0:
|
||||
logger.info(f"Generated fresh state for {model_name} from model interface: shape={state.shape}")
|
||||
return state
|
||||
except Exception as e:
|
||||
logger.debug(f"Model interface fresh state generation failed for {model_name}: {e}")
|
||||
|
||||
# Final fallback: create a reasonable default state with proper dimensions
|
||||
# Use the expected state size for DQN models (403 features)
|
||||
default_state_size = 403
|
||||
if 'cnn' in model_name.lower():
|
||||
default_state_size = 500 # Larger for CNN models
|
||||
elif 'cob' in model_name.lower():
|
||||
default_state_size = 2000 # Much larger for COB models
|
||||
|
||||
logger.warning(f"Using default zero state for {model_name} with size {default_state_size}")
|
||||
return np.zeros(default_state_size, dtype=np.float32)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating fresh state fallback for {model_name}: {e}")
|
||||
# Ultimate fallback
|
||||
return np.zeros(403, dtype=np.float32)
|
||||
|
||||
async def _train_cnn_model(self, model, model_name: str, record: Dict, prediction: Dict, reward: float) -> bool:
|
||||
"""Train CNN model directly (no adapter)"""
|
||||
@ -3785,6 +3806,35 @@ class TradingOrchestrator:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting training dashboard: {e}")
|
||||
|
||||
def set_cold_start_training_enabled(self, enabled: bool) -> bool:
|
||||
"""Enable or disable cold start training (excessive training during cold start)
|
||||
|
||||
Args:
|
||||
enabled: Whether to enable cold start training
|
||||
|
||||
Returns:
|
||||
bool: True if setting was applied successfully
|
||||
"""
|
||||
try:
|
||||
# Store the setting
|
||||
self.cold_start_enabled = enabled
|
||||
|
||||
# Adjust training frequency based on cold start mode
|
||||
if enabled:
|
||||
# High frequency training during cold start
|
||||
self.training_frequency = 'high'
|
||||
logger.info("ORCHESTRATOR: Cold start training ENABLED - Excessive training on every signal")
|
||||
else:
|
||||
# Normal training frequency
|
||||
self.training_frequency = 'normal'
|
||||
logger.info("ORCHESTRATOR: Cold start training DISABLED - Normal training frequency")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting cold start training: {e}")
|
||||
return False
|
||||
|
||||
def get_universal_data_stream(self, current_time: Optional[datetime] = None):
|
||||
"""Get universal data stream for external consumers like dashboard - DELEGATED to data provider"""
|
||||
|
@ -1247,27 +1247,23 @@ class TradingExecutor:
|
||||
taker_fee_rate = trading_fees.get('taker_fee', trading_fees.get('default_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()
|
||||
|
||||
# Get current leverage setting from dashboard or config
|
||||
# Get current leverage setting
|
||||
leverage = self.get_leverage()
|
||||
|
||||
# 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
|
||||
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
|
||||
# Calculate hold time
|
||||
exit_time = datetime.now()
|
||||
hold_time_seconds = (exit_time - position.entry_time).total_seconds()
|
||||
|
||||
# Create trade record with corrected PnL calculations
|
||||
trade_record = TradeRecord(
|
||||
symbol=symbol,
|
||||
side='SHORT',
|
||||
@ -1287,16 +1283,16 @@ class TradingExecutor:
|
||||
)
|
||||
|
||||
self.trade_history.append(trade_record)
|
||||
self.trade_records.append(trade_record) # Add to trade records for success rate tracking
|
||||
self.daily_loss += max(0, -pnl) # Add to daily loss if negative
|
||||
self.trade_records.append(trade_record)
|
||||
self.daily_loss += max(0, -net_pnl) # Use net_pnl instead of pnl
|
||||
|
||||
# Adjust profitability reward multiplier based on recent performance
|
||||
self._adjust_profitability_reward_multiplier()
|
||||
|
||||
# Update consecutive losses
|
||||
if pnl < -0.001: # A losing trade
|
||||
# Update consecutive losses using net_pnl
|
||||
if net_pnl < -0.001: # A losing trade
|
||||
self.consecutive_losses += 1
|
||||
elif pnl > 0.001: # A winning trade
|
||||
elif net_pnl > 0.001: # A winning trade
|
||||
self.consecutive_losses = 0
|
||||
else: # Breakeven trade
|
||||
self.consecutive_losses = 0
|
||||
@ -1306,7 +1302,7 @@ class TradingExecutor:
|
||||
self.last_trade_time[symbol] = datetime.now()
|
||||
self.daily_trades += 1
|
||||
|
||||
logger.info(f"Position closed - P&L: ${pnl:.2f}")
|
||||
logger.info(f"SHORT position closed - Gross P&L: ${gross_pnl:.2f}, Net P&L: ${net_pnl:.2f}, Fees: ${simulated_fees:.3f}")
|
||||
return True
|
||||
|
||||
try:
|
||||
@ -1342,27 +1338,23 @@ class TradingExecutor:
|
||||
# Calculate fees using real API data when available
|
||||
fees = self._calculate_real_trading_fees(order, symbol, position.quantity, current_price)
|
||||
|
||||
# Calculate P&L, fees, and hold time
|
||||
pnl = position.calculate_pnl(current_price)
|
||||
exit_time = datetime.now()
|
||||
hold_time_seconds = (exit_time - position.entry_time).total_seconds()
|
||||
|
||||
# Get current leverage setting from dashboard or config
|
||||
# Get current leverage setting
|
||||
leverage = self.get_leverage()
|
||||
|
||||
# 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
|
||||
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
|
||||
# Calculate hold time
|
||||
exit_time = datetime.now()
|
||||
hold_time_seconds = (exit_time - position.entry_time).total_seconds()
|
||||
|
||||
# Create trade record with corrected PnL calculations
|
||||
trade_record = TradeRecord(
|
||||
symbol=symbol,
|
||||
side='SHORT',
|
||||
@ -1382,16 +1374,16 @@ class TradingExecutor:
|
||||
)
|
||||
|
||||
self.trade_history.append(trade_record)
|
||||
self.trade_records.append(trade_record) # Add to trade records for success rate tracking
|
||||
self.daily_loss += max(0, -(pnl - fees)) # Add to daily loss if negative
|
||||
self.trade_records.append(trade_record)
|
||||
self.daily_loss += max(0, -net_pnl) # Use net_pnl instead of pnl
|
||||
|
||||
# Adjust profitability reward multiplier based on recent performance
|
||||
self._adjust_profitability_reward_multiplier()
|
||||
|
||||
# Update consecutive losses
|
||||
if pnl < -0.001: # A losing trade
|
||||
# Update consecutive losses using net_pnl
|
||||
if net_pnl < -0.001: # A losing trade
|
||||
self.consecutive_losses += 1
|
||||
elif pnl > 0.001: # A winning trade
|
||||
elif net_pnl > 0.001: # A winning trade
|
||||
self.consecutive_losses = 0
|
||||
else: # Breakeven trade
|
||||
self.consecutive_losses = 0
|
||||
@ -1402,7 +1394,7 @@ class TradingExecutor:
|
||||
self.daily_trades += 1
|
||||
|
||||
logger.info(f"SHORT close order executed: {order}")
|
||||
logger.info(f"SHORT position closed - P&L: ${pnl - fees:.2f}")
|
||||
logger.info(f"SHORT position closed - Gross P&L: ${gross_pnl:.2f}, Net P&L: ${net_pnl:.2f}, Fees: ${fees:.3f}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Failed to place SHORT close order")
|
||||
@ -1417,7 +1409,7 @@ class TradingExecutor:
|
||||
if symbol not in self.positions:
|
||||
logger.warning(f"No position to close in {symbol}")
|
||||
return False
|
||||
|
||||
|
||||
position = self.positions[symbol]
|
||||
if position.side != 'LONG':
|
||||
logger.warning(f"Position in {symbol} is not LONG, cannot close with SELL")
|
||||
@ -1429,15 +1421,27 @@ class TradingExecutor:
|
||||
if self.simulation_mode:
|
||||
logger.info(f"SIMULATION MODE ({self.trading_mode.upper()}) - Long close logged but not executed")
|
||||
# Calculate simulated fees in simulation mode
|
||||
taker_fee_rate = self.mexc_config.get('trading_fees', {}).get('taker_fee', 0.0006)
|
||||
trading_fees = self.exchange_config.get('trading_fees', {})
|
||||
taker_fee_rate = trading_fees.get('taker_fee', trading_fees.get('default_fee', 0.0006))
|
||||
simulated_fees = position.quantity * current_price * taker_fee_rate
|
||||
|
||||
# Calculate P&L for long position and hold time
|
||||
pnl = position.calculate_pnl(current_price)
|
||||
# Get current leverage setting
|
||||
leverage = self.get_leverage()
|
||||
|
||||
# Calculate position size in USD
|
||||
position_size_usd = position.quantity * position.entry_price
|
||||
|
||||
# Calculate gross PnL (before fees) with leverage
|
||||
gross_pnl = (current_price - position.entry_price) * position.quantity * leverage
|
||||
|
||||
# Calculate net PnL (after fees)
|
||||
net_pnl = gross_pnl - simulated_fees
|
||||
|
||||
# Calculate hold time
|
||||
exit_time = datetime.now()
|
||||
hold_time_seconds = (exit_time - position.entry_time).total_seconds()
|
||||
|
||||
# Create trade record
|
||||
# Create trade record with corrected PnL calculations
|
||||
trade_record = TradeRecord(
|
||||
symbol=symbol,
|
||||
side='LONG',
|
||||
@ -1446,23 +1450,27 @@ 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)
|
||||
self.trade_records.append(trade_record) # Add to trade records for success rate tracking
|
||||
self.daily_loss += max(0, -pnl) # Add to daily loss if negative
|
||||
self.trade_records.append(trade_record)
|
||||
self.daily_loss += max(0, -net_pnl) # Use net_pnl instead of pnl
|
||||
|
||||
# Adjust profitability reward multiplier based on recent performance
|
||||
self._adjust_profitability_reward_multiplier()
|
||||
|
||||
# Update consecutive losses
|
||||
if pnl < -0.001: # A losing trade
|
||||
|
||||
# Update consecutive losses using net_pnl
|
||||
if net_pnl < -0.001: # A losing trade
|
||||
self.consecutive_losses += 1
|
||||
elif pnl > 0.001: # A winning trade
|
||||
elif net_pnl > 0.001: # A winning trade
|
||||
self.consecutive_losses = 0
|
||||
else: # Breakeven trade
|
||||
self.consecutive_losses = 0
|
||||
@ -1472,7 +1480,7 @@ class TradingExecutor:
|
||||
self.last_trade_time[symbol] = datetime.now()
|
||||
self.daily_trades += 1
|
||||
|
||||
logger.info(f"Position closed - P&L: ${pnl:.2f}")
|
||||
logger.info(f"LONG position closed - Gross P&L: ${gross_pnl:.2f}, Net P&L: ${net_pnl:.2f}, Fees: ${simulated_fees:.3f}")
|
||||
return True
|
||||
|
||||
try:
|
||||
@ -1508,12 +1516,23 @@ class TradingExecutor:
|
||||
# Calculate fees using real API data when available
|
||||
fees = self._calculate_real_trading_fees(order, symbol, position.quantity, current_price)
|
||||
|
||||
# Calculate P&L, fees, and hold time
|
||||
pnl = position.calculate_pnl(current_price)
|
||||
# Get current leverage setting
|
||||
leverage = self.get_leverage()
|
||||
|
||||
# Calculate position size in USD
|
||||
position_size_usd = position.quantity * position.entry_price
|
||||
|
||||
# Calculate gross PnL (before fees) with leverage
|
||||
gross_pnl = (current_price - position.entry_price) * position.quantity * leverage
|
||||
|
||||
# Calculate net PnL (after fees)
|
||||
net_pnl = gross_pnl - fees
|
||||
|
||||
# Calculate hold time
|
||||
exit_time = datetime.now()
|
||||
hold_time_seconds = (exit_time - position.entry_time).total_seconds()
|
||||
|
||||
# Create trade record
|
||||
# Create trade record with corrected PnL calculations
|
||||
trade_record = TradeRecord(
|
||||
symbol=symbol,
|
||||
side='LONG',
|
||||
@ -1522,23 +1541,27 @@ 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)
|
||||
self.trade_records.append(trade_record) # Add to trade records for success rate tracking
|
||||
self.daily_loss += max(0, -(pnl - fees)) # Add to daily loss if negative
|
||||
self.trade_records.append(trade_record)
|
||||
self.daily_loss += max(0, -net_pnl) # Use net_pnl instead of pnl
|
||||
|
||||
# Adjust profitability reward multiplier based on recent performance
|
||||
self._adjust_profitability_reward_multiplier()
|
||||
|
||||
# Update consecutive losses
|
||||
if pnl < -0.001: # A losing trade
|
||||
# Update consecutive losses using net_pnl
|
||||
if net_pnl < -0.001: # A losing trade
|
||||
self.consecutive_losses += 1
|
||||
elif pnl > 0.001: # A winning trade
|
||||
elif net_pnl > 0.001: # A winning trade
|
||||
self.consecutive_losses = 0
|
||||
else: # Breakeven trade
|
||||
self.consecutive_losses = 0
|
||||
@ -1549,7 +1572,7 @@ class TradingExecutor:
|
||||
self.daily_trades += 1
|
||||
|
||||
logger.info(f"LONG close order executed: {order}")
|
||||
logger.info(f"LONG position closed - P&L: ${pnl - fees:.2f}")
|
||||
logger.info(f"LONG position closed - Gross P&L: ${gross_pnl:.2f}, Net P&L: ${net_pnl:.2f}, Fees: ${fees:.3f}")
|
||||
return True
|
||||
else:
|
||||
logger.error("Failed to place LONG close order")
|
||||
@ -2406,6 +2429,44 @@ class TradingExecutor:
|
||||
else:
|
||||
logger.info("TRADING EXECUTOR: Test mode disabled - normal safety checks active")
|
||||
|
||||
def set_trading_mode(self, mode: str) -> bool:
|
||||
"""Set trading mode (simulation/live) and update all related settings
|
||||
|
||||
Args:
|
||||
mode: Trading mode ('simulation' or 'live')
|
||||
|
||||
Returns:
|
||||
bool: True if mode was set successfully
|
||||
"""
|
||||
try:
|
||||
if mode not in ['simulation', 'live']:
|
||||
logger.error(f"Invalid trading mode: {mode}. Must be 'simulation' or 'live'")
|
||||
return False
|
||||
|
||||
# Store original mode if not already stored
|
||||
if not hasattr(self, 'original_trading_mode'):
|
||||
self.original_trading_mode = self.trading_mode
|
||||
|
||||
# Update trading mode
|
||||
self.trading_mode = mode
|
||||
self.simulation_mode = (mode == 'simulation')
|
||||
|
||||
# Update primary config if available
|
||||
if hasattr(self, 'primary_config') and self.primary_config:
|
||||
self.primary_config['trading_mode'] = mode
|
||||
|
||||
# Log the change
|
||||
if mode == 'live':
|
||||
logger.warning("TRADING EXECUTOR: MODE CHANGED TO LIVE - Real orders will be executed!")
|
||||
else:
|
||||
logger.info("TRADING EXECUTOR: MODE CHANGED TO SIMULATION - Orders are simulated")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting trading mode to {mode}: {e}")
|
||||
return False
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get trading executor status with safety feature information"""
|
||||
try:
|
||||
@ -2731,3 +2792,85 @@ class TradingExecutor:
|
||||
import traceback
|
||||
logger.error(f"CORRECTIVE: Full traceback: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
def recalculate_all_trade_records(self):
|
||||
"""Recalculate all existing trade records with correct leverage and PnL"""
|
||||
logger.info("Recalculating all trade records with correct leverage and PnL...")
|
||||
|
||||
updated_count = 0
|
||||
for i, trade in enumerate(self.trade_history):
|
||||
try:
|
||||
# Get current leverage setting
|
||||
leverage = self.get_leverage()
|
||||
|
||||
# Calculate position size in USD
|
||||
position_size_usd = trade.entry_price * trade.quantity
|
||||
|
||||
# Calculate gross PnL (before fees) with leverage
|
||||
if trade.side == 'LONG':
|
||||
gross_pnl = (trade.exit_price - trade.entry_price) * trade.quantity * leverage
|
||||
else: # SHORT
|
||||
gross_pnl = (trade.entry_price - trade.exit_price) * trade.quantity * leverage
|
||||
|
||||
# Calculate fees (0.1% open + 0.1% close = 0.2% total)
|
||||
entry_value = trade.entry_price * trade.quantity
|
||||
exit_value = trade.exit_price * trade.quantity
|
||||
fees = (entry_value + exit_value) * 0.001
|
||||
|
||||
# Calculate net PnL (after fees)
|
||||
net_pnl = gross_pnl - fees
|
||||
|
||||
# Update trade record with corrected values
|
||||
trade.leverage = leverage
|
||||
trade.position_size_usd = position_size_usd
|
||||
trade.gross_pnl = gross_pnl
|
||||
trade.net_pnl = net_pnl
|
||||
trade.pnl = net_pnl # Main PnL field
|
||||
trade.fees = fees
|
||||
|
||||
updated_count += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error recalculating trade record {i}: {e}")
|
||||
continue
|
||||
|
||||
logger.info(f"Updated {updated_count} trade records with correct leverage and PnL calculations")
|
||||
|
||||
# Also update trade_records list if it exists
|
||||
if hasattr(self, 'trade_records') and self.trade_records:
|
||||
logger.info("Updating trade_records list...")
|
||||
for i, trade in enumerate(self.trade_records):
|
||||
try:
|
||||
# Get current leverage setting
|
||||
leverage = self.get_leverage()
|
||||
|
||||
# Calculate position size in USD
|
||||
position_size_usd = trade.entry_price * trade.quantity
|
||||
|
||||
# Calculate gross PnL (before fees) with leverage
|
||||
if trade.side == 'LONG':
|
||||
gross_pnl = (trade.exit_price - trade.entry_price) * trade.quantity * leverage
|
||||
else: # SHORT
|
||||
gross_pnl = (trade.entry_price - trade.exit_price) * trade.quantity * leverage
|
||||
|
||||
# Calculate fees (0.1% open + 0.1% close = 0.2% total)
|
||||
entry_value = trade.entry_price * trade.quantity
|
||||
exit_value = trade.exit_price * trade.quantity
|
||||
fees = (entry_value + exit_value) * 0.001
|
||||
|
||||
# Calculate net PnL (after fees)
|
||||
net_pnl = gross_pnl - fees
|
||||
|
||||
# Update trade record with corrected values
|
||||
trade.leverage = leverage
|
||||
trade.position_size_usd = position_size_usd
|
||||
trade.gross_pnl = gross_pnl
|
||||
trade.net_pnl = net_pnl
|
||||
trade.pnl = net_pnl # Main PnL field
|
||||
trade.fees = fees
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error recalculating trade_records entry {i}: {e}")
|
||||
continue
|
||||
|
||||
logger.info("Trade record recalculation completed")
|
||||
|
Reference in New Issue
Block a user