diff --git a/crypto/gogo2/.vscode/launch.json b/crypto/gogo2/.vscode/launch.json index 5245d38..2a83bf9 100644 --- a/crypto/gogo2/.vscode/launch.json +++ b/crypto/gogo2/.vscode/launch.json @@ -5,8 +5,8 @@ "name": "Train Bot", "type": "python", "request": "launch", - "program": "main.py", - "args": ["--mode", "train", "--episodes", "100"], + "program": "main_multiu_broken.py", + "args": ["--mode", "train", "--episodes", "10000"], "console": "integratedTerminal", "justMyCode": true }, @@ -14,7 +14,7 @@ "name": "Evaluate Bot", "type": "python", "request": "launch", - "program": "main.py", + "program": "main_multiu_broken.py", "args": ["--mode", "eval", "--episodes", "10"], "console": "integratedTerminal", "justMyCode": true @@ -23,7 +23,7 @@ "name": "Live Trading (Demo)", "type": "python", "request": "launch", - "program": "main.py", + "program": "main_multiu_broken.py", "args": ["--mode", "live", "--demo"], "console": "integratedTerminal", "justMyCode": true @@ -32,7 +32,7 @@ "name": "Live Trading (Real)", "type": "python", "request": "launch", - "program": "main.py", + "program": "main_multiu_broken.py", "args": ["--mode", "live"], "console": "integratedTerminal", "justMyCode": true @@ -41,7 +41,7 @@ "name": "Continuous Training", "type": "python", "request": "launch", - "program": "main.py", + "program": "main_multiu_broken.py", "args": ["--mode", "continuous", "--refresh-data"], "console": "integratedTerminal", "justMyCode": true diff --git a/crypto/gogo2/enhanced_models.py b/crypto/gogo2/enhanced_models.py index 5dc3b72..e505897 100644 --- a/crypto/gogo2/enhanced_models.py +++ b/crypto/gogo2/enhanced_models.py @@ -201,6 +201,28 @@ class EnhancedReplayBuffer: self.gamma = gamma # Discount factor self.n_step_buffer = [] self.max_priority = 1.0 + + def add(self, state, action, reward, next_state, done): + """ + Add a new experience to the buffer (simplified version of push for compatibility) + + Args: + state: Current state + action: Action taken + reward: Reward received + next_state: Next state + done: Whether the episode is done + """ + # Store in replay buffer with max priority + if len(self.buffer) < self.capacity: + self.buffer.append(None) + self.buffer[self.position] = (state, action, reward, next_state, done) + + # Set priority to max priority to ensure it gets sampled + self.priorities[self.position] = self.max_priority + + # Move position pointer + self.position = (self.position + 1) % self.capacity def push(self, state, action, reward, next_state, done): # Store experience in n-step buffer @@ -278,15 +300,8 @@ class EnhancedReplayBuffer: next_states.append(next_state) dones.append(done) - return ( - torch.stack(states), - torch.tensor(actions), - torch.tensor(rewards, dtype=torch.float32), - torch.stack(next_states), - torch.tensor(dones, dtype=torch.float32), - indices, - weights - ) + # Return only the states, actions, rewards, next_states, dones for compatibility with learn function + return states, actions, rewards, next_states, dones def update_priorities(self, indices, td_errors): for idx, td_error in zip(indices, td_errors): diff --git a/crypto/gogo2/enhanced_training.py b/crypto/gogo2/enhanced_training.py index f4e8dde..6dc3620 100644 --- a/crypto/gogo2/enhanced_training.py +++ b/crypto/gogo2/enhanced_training.py @@ -2,6 +2,7 @@ import os import torch import torch.nn as nn import torch.optim as optim +import torch.nn.functional as F from torch.amp import GradScaler, autocast import numpy as np import matplotlib.pyplot as plt @@ -9,7 +10,7 @@ from datetime import datetime from tensorboardX import SummaryWriter # Import our enhanced models -from enhanced_models import EnhancedPricePredictionModel, EnhancedDQN, EnhancedReplayBuffer, train_price_predictor, prepare_multi_timeframe_data +from enhanced_models import EnhancedPricePredictionModel, EnhancedDQN, EnhancedReplayBuffer # Constants TIMEFRAMES = ['1m', '15m', '1h'] @@ -218,285 +219,453 @@ def load_checkpoint(price_model, dqn_model, optimizer, episode=None): print("No checkpoint found, starting training from scratch") return 0, [], [], [] -def enhanced_train_agent(exchange, num_episodes=NUM_EPISODES, continuous=CONTINUOUS_MODE, start_episode=CONTINUOUS_START_EPISODE): +def enhanced_train_agent(exchange, num_episodes=NUM_EPISODES, continuous=CONTINUOUS_MODE, start_episode=CONTINUOUS_START_EPISODE, verbose=False): """ - Train the enhanced trading agent using multi-timeframe data + Train an enhanced trading agent with multi-timeframe models Args: - exchange: Exchange object to fetch data from + exchange: Exchange simulator or real exchange num_episodes: Number of episodes to train for continuous: Whether to continue training from a checkpoint - start_episode: Episode to start from if continuous training + start_episode: Episode to start from for continuous training + verbose: Whether to enable verbose logging """ - print(f"Training on device: {DEVICE}") - # Set up TensorBoard writer = setup_tensorboard() # Initialize models - state_dim = 100 # Increased state dimension for multi-timeframe features - action_dim = 3 # Buy, Sell, Hold + state_dim = 100 # Increased state dimension for enhanced features + action_dim = 3 # 0: HOLD, 1: BUY, 2: SELL + # Initialize price prediction model with multi-timeframe support price_model = EnhancedPricePredictionModel( input_dim=2, # Price and volume hidden_dim=256, num_layers=3, - output_dim=5, # Predict next 5 candles + output_dim=5, # OHLCV prediction num_timeframes=len(TIMEFRAMES) ).to(DEVICE) + # Initialize DQN model with enhanced architecture dqn_model = EnhancedDQN( state_dim=state_dim, action_dim=action_dim, hidden_dim=512 ).to(DEVICE) - target_dqn = EnhancedDQN( + # Initialize target network + target_model = EnhancedDQN( state_dim=state_dim, action_dim=action_dim, hidden_dim=512 ).to(DEVICE) - - # Copy initial weights to target network - target_dqn.load_state_dict(dqn_model.state_dict()) + target_model.load_state_dict(dqn_model.state_dict()) # Initialize optimizer - optimizer = optim.Adam(list(price_model.parameters()) + list(dqn_model.parameters()), lr=LEARNING_RATE) + optimizer = optim.Adam(dqn_model.parameters(), lr=LEARNING_RATE) - # Initialize replay buffer - replay_buffer = EnhancedReplayBuffer( - capacity=REPLAY_BUFFER_SIZE, - alpha=0.6, - beta=0.4, - beta_increment=0.001, - n_step=3, - gamma=GAMMA - ) + # Initialize replay buffer with prioritized experience replay + replay_buffer = EnhancedReplayBuffer(REPLAY_BUFFER_SIZE) - # Initialize gradient scaler for mixed precision training - scaler = GradScaler(enabled=(DEVICE.type == 'cuda')) - - # Initialize tracking variables + # Initialize training metrics rewards = [] profits = [] win_rates = [] best_reward = float('-inf') best_pnl = float('-inf') - best_winrate = float('-inf') + best_winrate = 0 - # Load checkpoint if continuous training + # Initialize epsilon for exploration + epsilon = EPSILON_START + + # Load checkpoint if continuing training if continuous: - start_episode, rewards, profits, win_rates = load_checkpoint( - price_model, dqn_model, optimizer, start_episode - ) + try: + checkpoint_path = 'models/enhanced_trading_agent_latest.pt' + if os.path.exists(checkpoint_path): + checkpoint = torch.load(checkpoint_path, map_location=DEVICE) + price_model.load_state_dict(checkpoint['price_model_state_dict']) + dqn_model.load_state_dict(checkpoint['dqn_model_state_dict']) + target_model.load_state_dict(checkpoint['dqn_model_state_dict']) + optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + + if 'rewards' in checkpoint: + rewards = checkpoint['rewards'] + if 'profits' in checkpoint: + profits = checkpoint['profits'] + if 'win_rates' in checkpoint: + win_rates = checkpoint['win_rates'] + if 'best_reward' in checkpoint: + best_reward = checkpoint['best_reward'] + if 'best_pnl' in checkpoint: + best_pnl = checkpoint['best_pnl'] + if 'best_winrate' in checkpoint: + best_winrate = checkpoint['best_winrate'] + + print(f"Loaded checkpoint from {checkpoint_path}") + print(f"Continuing from episode {start_episode}") + + # Decay epsilon based on start episode + for _ in range(start_episode): + epsilon = max(EPSILON_END, epsilon * EPSILON_DECAY) + else: + print(f"No checkpoint found at {checkpoint_path}, starting from scratch") + except Exception as e: + print(f"Error loading checkpoint: {e}") + print("Starting from scratch") - # Prepare multi-timeframe data for price prediction model training - data_loaders = prepare_multi_timeframe_data(exchange, TIMEFRAMES) + # Set models to training mode + price_model.train() + dqn_model.train() + + # Initialize gradient scaler for mixed precision training + scaler = GradScaler() + + print(f"Training on device: {DEVICE}") # Pre-train price prediction model print("Pre-training price prediction model...") - train_price_predictor(price_model, data_loaders, optimizer, DEVICE, epochs=5) + train_price_predictor(price_model, exchange, TIMEFRAMES, DEVICE, num_epochs=5, batch_size=32) # Main training loop - epsilon = EPSILON_START - - for episode in range(start_episode, num_episodes): - print(f"Episode {episode+1}/{num_episodes}") - - # Reset environment + for episode in range(start_episode, start_episode + num_episodes): + # Initialize state state = initialize_state(exchange, TIMEFRAMES) - total_reward = 0 + + # Reset environment for new episode + exchange.reset() + + # Track episode metrics + episode_reward = 0 + episode_steps = 0 + done = False trades = [] wins = 0 losses = 0 + # Enable verbose logging for prediction validation if requested + if verbose: + # Set logging level to DEBUG for more detailed logs + import logging + logging.getLogger("trading_bot").setLevel(logging.DEBUG) + + # Add a hook to log prediction validations + def log_prediction_validation(pred, actual, was_correct): + if was_correct: + print(f"CORRECT prediction: predicted={pred:.2f}, actual={actual:.2f}") + else: + print(f"INCORRECT prediction: predicted={pred:.2f}, actual={actual:.2f}") + + # Monkey patch the validate_predictions method if possible + if hasattr(exchange, 'validate_predictions'): + original_validate = exchange.validate_predictions + + def verbose_validate(self, new_candle): + result = original_validate(new_candle) + print(f"Validated predictions for candle at {new_candle['timestamp']}") + if hasattr(self, 'prediction_history'): + validated = [p for p in self.prediction_history if p['validated']] + correct = [p for p in validated if p['was_correct']] + if validated: + accuracy = len(correct) / len(validated) + print(f"Prediction accuracy: {accuracy:.2f} ({len(correct)}/{len(validated)})") + return result + + exchange.validate_predictions = verbose_validate.__get__(exchange, exchange.__class__) + # Episode loop - for step in range(MAX_STEPS_PER_EPISODE): + while not done and episode_steps < MAX_STEPS_PER_EPISODE: # Epsilon-greedy action selection if np.random.random() < epsilon: action = np.random.randint(0, action_dim) else: with torch.no_grad(): - state_tensor = torch.FloatTensor(state).unsqueeze(0).to(DEVICE) - q_values, _, _ = dqn_model(state_tensor) + q_values = dqn_model(torch.FloatTensor(state).unsqueeze(0).to(DEVICE)) action = q_values.argmax().item() - # Execute action and get next state and reward - next_state, reward, done, trade_info = step_environment( - exchange, state, action, price_model, TIMEFRAMES, DEVICE - ) + # Take action in environment + next_state, reward, done, info = exchange.step(action) # Store transition in replay buffer - replay_buffer.push( - torch.FloatTensor(state), - action, - reward, - torch.FloatTensor(next_state), - done - ) + replay_buffer.add(state, action, reward, next_state, done) # Update state and accumulate reward state = next_state - total_reward += reward + episode_reward += reward # Track trade outcomes - if trade_info is not None: - trades.append(trade_info) - if trade_info['pnl'] > 0: - wins += 1 - elif trade_info['pnl'] < 0: - losses += 1 + if 'trade' in info and info['trade']: + trades.append(info['trade']) + if 'pnl_dollar' in info['trade']: + if info['trade']['pnl_dollar'] > 0: + wins += 1 + elif info['trade']['pnl_dollar'] < 0: + losses += 1 + + # Log trade to TensorBoard + if writer: + writer.add_scalar('Trade/PnL', info['trade']['pnl_dollar'], episode) + if 'duration' in info['trade']: + writer.add_scalar('Trade/Duration', info['trade']['duration'], episode) + + # Log prediction validation metrics if available + if verbose and 'prediction_accuracy' in info: + writer.add_scalar('Prediction/Accuracy', info['prediction_accuracy'], episode) + if 'low_prediction_accuracy' in info: + writer.add_scalar('Prediction/LowAccuracy', info['low_prediction_accuracy'], episode) + if 'high_prediction_accuracy' in info: + writer.add_scalar('Prediction/HighAccuracy', info['high_prediction_accuracy'], episode) # Learn from experiences if enough samples if len(replay_buffer) > BATCH_SIZE: - learn(dqn_model, target_dqn, replay_buffer, optimizer, scaler, DEVICE) + learn(dqn_model, target_model, replay_buffer, optimizer, scaler, DEVICE) - if done: - break + episode_steps += 1 + + # Log verbose information about the current state if requested + if verbose and episode_steps % 10 == 0: + print(f"Episode {episode+1}, Step {episode_steps}, Action: {['HOLD', 'BUY', 'SELL'][action]}, Reward: {reward:.4f}") + if hasattr(exchange, 'position') and exchange.position != 'FLAT': + print(f" Position: {exchange.position}, Entry Price: {exchange.entry_price:.2f}, Current PnL: {exchange.calculate_pnl():.2f}") + + # Log prediction validation status + if hasattr(exchange, 'prediction_history'): + recent_predictions = [p for p in exchange.prediction_history if p['validated']][-5:] + if recent_predictions: + print(" Recent prediction validations:") + for pred in recent_predictions: + status = "✓" if pred['was_correct'] else "✗" + print(f" {status} {pred['type']} prediction: {pred['predicted_value']:.2f} vs {pred['actual_value']:.2f}") # Update target network if episode % TARGET_UPDATE == 0: - target_dqn.load_state_dict(dqn_model.state_dict()) + target_model.load_state_dict(dqn_model.state_dict()) # Calculate episode metrics - avg_reward = total_reward / (step + 1) - total_pnl = sum(trade['pnl'] for trade in trades) if trades else 0 + avg_reward = episode_reward / max(1, episode_steps) + total_pnl = sum(trade.get('pnl_dollar', 0) for trade in trades) if trades else 0 win_rate = (wins / (wins + losses) * 100) if (wins + losses) > 0 else 0 - # Decay epsilon - epsilon = max(EPSILON_END, epsilon * EPSILON_DECAY) - - # Track metrics + # Store metrics rewards.append(avg_reward) profits.append(total_pnl) win_rates.append(win_rate) + # Decay epsilon + epsilon = max(EPSILON_END, epsilon * EPSILON_DECAY) + + # Log episode metrics + print(f"Episode {episode+1}: Reward={avg_reward:.4f}, PnL=${total_pnl:.2f}, Win Rate={win_rate:.1f}%, Epsilon={epsilon:.4f}") + # Log to TensorBoard - writer.add_scalar('Training/Reward', avg_reward, episode) - writer.add_scalar('Training/Profit', total_pnl, episode) - writer.add_scalar('Training/WinRate', win_rate, episode) - writer.add_scalar('Training/Epsilon', epsilon, episode) + if writer: + writer.add_scalar('Metrics/Reward', avg_reward, episode) + writer.add_scalar('Metrics/PnL', total_pnl, episode) + writer.add_scalar('Metrics/WinRate', win_rate, episode) + writer.add_scalar('Metrics/Epsilon', epsilon, episode) + + # Log prediction validation metrics if available + if hasattr(exchange, 'prediction_history'): + validated_predictions = [p for p in exchange.prediction_history if p['validated']] + if validated_predictions: + correct_predictions = [p for p in validated_predictions if p['was_correct']] + accuracy = len(correct_predictions) / len(validated_predictions) + writer.add_scalar('Prediction/OverallAccuracy', accuracy, episode) + + # Log separate accuracies for low and high predictions + low_predictions = [p for p in validated_predictions if p['type'] == 'low'] + high_predictions = [p for p in validated_predictions if p['type'] == 'high'] + + if low_predictions: + correct_lows = [p for p in low_predictions if p['was_correct']] + low_accuracy = len(correct_lows) / len(low_predictions) + writer.add_scalar('Prediction/LowAccuracy', low_accuracy, episode) + + if high_predictions: + correct_highs = [p for p in high_predictions if p['was_correct']] + high_accuracy = len(correct_highs) / len(high_predictions) + writer.add_scalar('Prediction/HighAccuracy', high_accuracy, episode) + + if verbose: + print(f"Prediction accuracy: {accuracy:.2f} ({len(correct_predictions)}/{len(validated_predictions)})") + if low_predictions: + print(f" Low prediction accuracy: {low_accuracy:.2f} ({len(correct_lows)}/{len(low_predictions)})") + if high_predictions: + print(f" High prediction accuracy: {high_accuracy:.2f} ({len(correct_highs)}/{len(high_predictions)})") - # Print episode summary - print(f"Episode {episode+1} - Avg Reward: {avg_reward:.2f}, PnL: ${total_pnl:.2f}, Win Rate: {win_rate:.1f}%") + # Save best models + if avg_reward > best_reward: + best_reward = avg_reward + torch.save({ + 'price_model_state_dict': price_model.state_dict(), + 'dqn_model_state_dict': dqn_model.state_dict(), + 'optimizer_state_dict': optimizer.state_dict(), + 'episode': episode, + 'reward': best_reward, + 'pnl': total_pnl, + 'win_rate': win_rate + }, 'models/enhanced_trading_agent_best_reward.pt') + print(f"Saved best reward model: {best_reward:.4f}") - # Save models and plot results - if episode % SAVE_INTERVAL == 0 or episode == num_episodes - 1: - best_reward, best_pnl, best_winrate = save_models( - price_model, dqn_model, optimizer, episode, - rewards, profits, win_rates, - best_reward, best_pnl, best_winrate - ) - plot_training_results(rewards, profits, win_rates, episode) + if total_pnl > best_pnl: + best_pnl = total_pnl + torch.save({ + 'price_model_state_dict': price_model.state_dict(), + 'dqn_model_state_dict': dqn_model.state_dict(), + 'optimizer_state_dict': optimizer.state_dict(), + 'episode': episode, + 'reward': avg_reward, + 'pnl': best_pnl, + 'win_rate': win_rate + }, 'models/enhanced_trading_agent_best_pnl.pt') + print(f"Saved best PnL model: ${best_pnl:.2f}") + + if win_rate > best_winrate: + best_winrate = win_rate + torch.save({ + 'price_model_state_dict': price_model.state_dict(), + 'dqn_model_state_dict': dqn_model.state_dict(), + 'optimizer_state_dict': optimizer.state_dict(), + 'episode': episode, + 'reward': avg_reward, + 'pnl': total_pnl, + 'win_rate': best_winrate + }, 'models/enhanced_trading_agent_best_winrate.pt') + print(f"Saved best win rate model: {best_winrate:.1f}%") + + # Save latest model for continuous training + torch.save({ + 'price_model_state_dict': price_model.state_dict(), + 'dqn_model_state_dict': dqn_model.state_dict(), + 'optimizer_state_dict': optimizer.state_dict(), + 'episode': episode, + 'rewards': rewards, + 'profits': profits, + 'win_rates': win_rates, + 'best_reward': best_reward, + 'best_pnl': best_pnl, + 'best_winrate': best_winrate + }, 'models/enhanced_trading_agent_latest.pt') + + # Add additional verbose logging at the end of each episode + if verbose: + print("\nEpisode Summary:") + print(f" Total Steps: {episode_steps}") + print(f" Total Trades: {len(trades)}") + print(f" Wins/Losses: {wins}/{losses}") + print(f" Average Trade PnL: ${total_pnl/max(1, len(trades)):.2f}") + + # Log prediction validation summary + if hasattr(exchange, 'prediction_history'): + validated = [p for p in exchange.prediction_history if p['validated']] + if validated: + correct = [p for p in validated if p['was_correct']] + print("\nPrediction Validation Summary:") + print(f" Total Predictions: {len(exchange.prediction_history)}") + print(f" Validated Predictions: {len(validated)}") + print(f" Correct Predictions: {len(correct)}") + print(f" Accuracy: {len(correct)/len(validated):.2f}") + + # Reset prediction history for next episode if it's getting too large + if len(exchange.prediction_history) > 1000: + print(" Resetting prediction history (too large)") + exchange.prediction_history = exchange.prediction_history[-100:] - # Close TensorBoard writer - writer.close() - - # Final save and plot - best_reward, best_pnl, best_winrate = save_models( - price_model, dqn_model, optimizer, num_episodes - 1, - rewards, profits, win_rates, - best_reward, best_pnl, best_winrate - ) - plot_training_results(rewards, profits, win_rates, num_episodes - 1) - - print("Training complete!") - return price_model, dqn_model + # Return final metrics + return rewards, profits, win_rates def learn(dqn, target_dqn, replay_buffer, optimizer, scaler, device): - """Update the DQN model using experiences from the replay buffer""" - # Sample from replay buffer - states, actions, rewards, next_states, dones, indices, weights = replay_buffer.sample(BATCH_SIZE) + """ + Update the DQN model using experiences from the replay buffer - # Move to device - states = states.to(device) - actions = actions.to(device) - rewards = rewards.to(device) - next_states = next_states.to(device) - dones = dones.to(device) - weights = weights.to(device) + Args: + dqn: The DQN model to update + target_dqn: The target DQN model for stable Q-value estimates + replay_buffer: Replay buffer containing experiences + optimizer: Optimizer for updating the DQN model + scaler: Gradient scaler for mixed precision training + device: Device to train on (CPU or GPU) + """ + if len(replay_buffer) < BATCH_SIZE: + return + + # Sample batch from replay buffer + states, actions, rewards, next_states, dones = replay_buffer.sample(BATCH_SIZE) + + # Convert to tensors + states = torch.FloatTensor(states).to(device) + actions = torch.LongTensor(actions).to(device) + rewards = torch.FloatTensor(rewards).to(device) + next_states = torch.FloatTensor(next_states).to(device) + dones = torch.FloatTensor(dones).to(device) # Get current Q values - if device.type == 'cuda': - with autocast(device_type='cuda', enabled=True): - current_q_values, _, _ = dqn(states) - current_q_values = current_q_values.gather(1, actions.unsqueeze(1)).squeeze(1) - - # Compute target Q values - with torch.no_grad(): - next_q_values, _, _ = target_dqn(next_states) - max_next_q_values = next_q_values.max(1)[0] - target_q_values = rewards + (1 - dones) * GAMMA * max_next_q_values - - # Compute loss with importance sampling weights - td_errors = target_q_values - current_q_values - loss = (weights * td_errors.pow(2)).mean() - else: - # CPU version without autocast - current_q_values, _, _ = dqn(states) - current_q_values = current_q_values.gather(1, actions.unsqueeze(1)).squeeze(1) + with autocast(device_type='cuda' if device.type == 'cuda' else 'cpu'): + q_values = dqn(states) + if isinstance(q_values, tuple): + q_values = q_values[0] # Extract the tensor from the tuple + q_values = q_values.gather(1, actions.unsqueeze(1)).squeeze(1) - # Compute target Q values + # Get next Q values from target network with torch.no_grad(): - next_q_values, _, _ = target_dqn(next_states) - max_next_q_values = next_q_values.max(1)[0] - target_q_values = rewards + (1 - dones) * GAMMA * max_next_q_values + next_q_values = target_dqn(next_states) + if isinstance(next_q_values, tuple): + next_q_values = next_q_values[0] # Extract the tensor from the tuple + next_q_values = next_q_values.max(1)[0] + target_q_values = rewards + GAMMA * next_q_values * (1 - dones) - # Compute loss with importance sampling weights - td_errors = target_q_values - current_q_values - loss = (weights * td_errors.pow(2)).mean() + # Calculate loss + loss = F.smooth_l1_loss(q_values, target_q_values) - # Update priorities in replay buffer - replay_buffer.update_priorities(indices, td_errors.abs().detach().cpu().numpy()) - - # Optimize the model with mixed precision + # Optimize the model optimizer.zero_grad() - - if device.type == 'cuda': - scaler.scale(loss).backward() - scaler.unscale_(optimizer) - torch.nn.utils.clip_grad_norm_(dqn.parameters(), max_norm=1.0) - scaler.step(optimizer) - scaler.update() - else: - # CPU version without scaler - loss.backward() - torch.nn.utils.clip_grad_norm_(dqn.parameters(), max_norm=1.0) - optimizer.step() + scaler.scale(loss).backward() + scaler.step(optimizer) + scaler.update() def initialize_state(exchange, timeframes): - """Initialize the state with data from multiple timeframes""" + """ + Initialize the state for the trading agent using multi-timeframe data + + Args: + exchange: Exchange object to fetch data from + timeframes: List of timeframes to use + + Returns: + state: Initial state vector + """ + # Initialize empty state + state = [] + # Fetch data for each timeframe timeframe_data = {} for tf in timeframes: candles = exchange.fetch_ohlcv(timeframe=tf, limit=30) - timeframe_data[tf] = candles - - # Extract features from each timeframe - state = [] - - for tf in timeframes: - candles = timeframe_data[tf] + if not candles or len(candles) < 10: + print(f"Not enough data for timeframe {tf}, using zeros") + # Use zeros if not enough data + state.extend([0] * 20) # 20 features per timeframe + continue - # Price features + timeframe_data[tf] = candles + + # Extract features for this timeframe prices = [candle[4] for candle in candles[-10:]] # Last 10 close prices price_changes = [prices[i]/prices[i-1] - 1 for i in range(1, len(prices))] - # Volume features volumes = [candle[5] for candle in candles[-10:]] # Last 10 volumes volume_changes = [volumes[i]/volumes[i-1] - 1 for i in range(1, len(volumes))] # Technical indicators - # Simple Moving Averages sma_5 = sum(prices[-5:]) / 5 sma_10 = sum(prices) / 10 - # Relative Strength Index (simplified) - gains = [max(0, price_changes[i]) for i in range(len(price_changes))] - losses = [max(0, -price_changes[i]) for i in range(len(price_changes))] - avg_gain = sum(gains) / len(gains) - avg_loss = sum(losses) / len(losses) - rs = avg_gain / (avg_loss + 1e-10) # Avoid division by zero + # RSI (simplified) + gains = [max(0, pc) for pc in price_changes] + losses = [max(0, -pc) for pc in price_changes] + avg_gain = sum(gains) / len(gains) if gains else 0 + avg_loss = sum(losses) / len(losses) if losses else 1e-10 + rs = avg_gain / avg_loss rsi = 100 - (100 / (1 + rs)) # Add features to state @@ -506,85 +675,15 @@ def initialize_state(exchange, timeframes): state.append(sma_10 / prices[-1] - 1) # 1 feature state.append(rsi / 100) # 1 feature - # Add market regime features - # This is a placeholder - in a real implementation, you would use the market_regime_classifier - # from the DQN model to predict the current market regime - state.extend([0, 0, 0]) # 3 features for market regime (one-hot encoded) + # Add market regime features (placeholder) + state.extend([0, 0, 0]) # 3 features for market regime - # Add additional features to reach the expected dimension of 100 - # Calculate more technical indicators - for tf in timeframes: - candles = timeframe_data[tf] - prices = [candle[4] for candle in candles[-20:]] # Last 20 close prices - - # Bollinger Bands - window = 20 - if len(prices) >= window: - sma_20 = sum(prices[-window:]) / window - std_dev = (sum((price - sma_20) ** 2 for price in prices[-window:]) / window) ** 0.5 - upper_band = sma_20 + 2 * std_dev - lower_band = sma_20 - 2 * std_dev - - # Add normalized Bollinger Band features - state.append((prices[-1] - sma_20) / (upper_band - sma_20 + 1e-10)) # Position within upper band - state.append((prices[-1] - lower_band) / (sma_20 - lower_band + 1e-10)) # Position within lower band - else: - # Fallback if not enough data - state.extend([0, 0]) - - # MACD (Moving Average Convergence Divergence) - if len(prices) >= 26: - ema_12 = sum(prices[-12:]) / 12 # Simplified EMA - ema_26 = sum(prices[-26:]) / 26 # Simplified EMA - macd = ema_12 - ema_26 - - # Add normalized MACD - state.append(macd / prices[-1]) - else: - # Fallback if not enough data - state.append(0) - - # Add price momentum features - for tf in timeframes: - candles = timeframe_data[tf] - prices = [candle[4] for candle in candles[-30:]] - - # Calculate momentum over different periods - if len(prices) >= 30: - momentum_5 = prices[-1] / prices[-5] - 1 - momentum_10 = prices[-1] / prices[-10] - 1 - momentum_20 = prices[-1] / prices[-20] - 1 - momentum_30 = prices[-1] / prices[-30] - 1 - - state.extend([momentum_5, momentum_10, momentum_20, momentum_30]) - else: - # Fallback if not enough data - state.extend([0, 0, 0, 0]) - - # Add volume profile features - for tf in timeframes: - candles = timeframe_data[tf] - volumes = [candle[5] for candle in candles[-10:]] - - # Volume profile - avg_volume = sum(volumes) / len(volumes) - volume_ratio = volumes[-1] / avg_volume - - # Volume trend - volume_trend = sum(1 for i in range(1, len(volumes)) if volumes[i] > volumes[i-1]) / (len(volumes) - 1) - - state.extend([volume_ratio, volume_trend]) - - # Pad with zeros if needed to reach exactly 100 dimensions - while len(state) < 100: - state.append(0) - - # Ensure state has exactly 100 dimensions - if len(state) > 100: + # Pad state to reach expected dimension of 100 + if len(state) < 100: + state.extend([0] * (100 - len(state))) + elif len(state) > 100: state = state[:100] - assert len(state) == 100, f"State dimension mismatch: {len(state)} != 100" - return state def step_environment(exchange, state, action, price_model, timeframes, device): @@ -744,6 +843,145 @@ def step_environment(exchange, state, action, price_model, timeframes, device): return next_state, reward, done, trade_info +def train_price_predictor(model, exchange, timeframes, device, num_epochs=5, batch_size=32): + """ + Train the price prediction model using data from the exchange + + Args: + model: The EnhancedPricePredictionModel + exchange: Exchange object to fetch data from + timeframes: List of timeframes to use + device: Device to train on (CPU or GPU) + num_epochs: Number of training epochs + batch_size: Batch size for training + """ + print(f"Training price prediction model for {num_epochs} epochs with batch size {batch_size}") + + # Create optimizer + optimizer = optim.Adam(model.parameters(), lr=0.001) + + # Set model to training mode + model.train() + + # Fetch data for each timeframe + timeframe_data = {} + for tf in timeframes: + # Fetch more data for training + candles = exchange.fetch_ohlcv(timeframe=tf, limit=500) + if not candles or len(candles) < 30: + print(f"Not enough data for timeframe {tf}, skipping training") + return model + timeframe_data[tf] = candles + + # Prepare training data + for epoch in range(num_epochs): + total_loss = 0 + num_batches = 0 + + # Create batches + for i in range(0, len(timeframe_data[timeframes[0]]) - 30, batch_size): + if i + 30 + 5 >= len(timeframe_data[timeframes[0]]): + break + + # Prepare inputs for each timeframe + inputs_list = [] + for tf in timeframes: + if i + 30 >= len(timeframe_data[tf]): + continue + + # Extract price and volume data for input + input_data = [] + for j in range(i, i + 30): + if j < len(timeframe_data[tf]): + candle = timeframe_data[tf][j] + # Use close price and volume + input_data.append([candle[4], candle[5]]) + + # Convert to tensor and add batch dimension + input_tensor = torch.tensor(input_data, dtype=torch.float32).unsqueeze(0).to(device) + inputs_list.append(input_tensor) + + # Skip if we don't have data for all timeframes + if len(inputs_list) != len(timeframes): + continue + + # Prepare targets (next 5 candles) + target_data = [] + for j in range(i + 30, i + 35): + if j < len(timeframe_data[timeframes[0]]): + candle = timeframe_data[timeframes[0]][j] + # OHLCV values + target_data.append([candle[1], candle[2], candle[3], candle[4], candle[5]]) + + # Convert targets to tensor + price_targets = torch.tensor(target_data, dtype=torch.float32).to(device) + + # Create extrema targets (binary classification for high/low points) + # Make sure it has the same shape as the model output (batch_size, 10) + extrema_targets = torch.zeros(1, 10, dtype=torch.float32).to(device) # 5 time steps, 2 classes each + + # Create volume targets + volume_targets = torch.tensor([candle[5] for candle in timeframe_data[timeframes[0]][i+30:i+35]], + dtype=torch.float32).to(device) + + # Zero gradients + optimizer.zero_grad() + + try: + # Forward pass + price_pred, extrema_logits, volume_pred = model(inputs_list) + + # Ensure targets have the same shape as predictions + if price_pred.shape != price_targets.shape: + # Reshape price_targets to match price_pred + price_targets = price_targets.view(price_pred.shape) + + if volume_pred.shape != volume_targets.shape: + # Reshape volume_targets to match volume_pred + volume_targets = volume_targets.view(volume_pred.shape) + + # Calculate losses + price_loss = F.mse_loss(price_pred, price_targets) + extrema_loss = F.binary_cross_entropy_with_logits(extrema_logits, extrema_targets) + volume_loss = F.mse_loss(volume_pred, volume_targets) + + # Combined loss with weighting + loss = price_loss + 0.5 * extrema_loss + 0.3 * volume_loss + + # Backward pass + loss.backward() + + # Gradient clipping to prevent exploding gradients + torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) + + optimizer.step() + + total_loss += loss.item() + num_batches += 1 + + if num_batches % 10 == 0: + print(f"Epoch {epoch+1}/{num_epochs}, Batch {num_batches}, Loss: {loss.item():.6f}") + except Exception as e: + print(f"Error in batch: {e}") + print(f"Shapes - price_pred: {price_pred.shape}, price_targets: {price_targets.shape}") + print(f"Shapes - extrema_logits: {extrema_logits.shape}, extrema_targets: {extrema_targets.shape}") + print(f"Shapes - volume_pred: {volume_pred.shape}, volume_targets: {volume_targets.shape}") + continue + + if num_batches > 0: + avg_loss = total_loss / num_batches + print(f"Epoch {epoch+1}/{num_epochs}, Avg Loss: {avg_loss:.6f}") + else: + print(f"Epoch {epoch+1}/{num_epochs}, No batches processed") + + # Learning rate scheduling + if epoch > 0 and epoch % 2 == 0: + for param_group in optimizer.param_groups: + param_group['lr'] *= 0.9 + + print("Price prediction model training completed") + return model + # Main function to run training def main(): from exchange_simulator import ExchangeSimulator @@ -752,7 +990,7 @@ def main(): exchange = ExchangeSimulator() # Train agent - price_model, dqn_model = enhanced_train_agent( + rewards, profits, win_rates = enhanced_train_agent( exchange=exchange, num_episodes=NUM_EPISODES, continuous=CONTINUOUS_MODE, diff --git a/crypto/gogo2/exchange_simulator.py b/crypto/gogo2/exchange_simulator.py index 04aae1b..a74aec4 100644 --- a/crypto/gogo2/exchange_simulator.py +++ b/crypto/gogo2/exchange_simulator.py @@ -344,6 +344,439 @@ class ExchangeSimulator: 'BTC': 0.5 } } + + def reset(self): + """ + Reset the exchange simulator to its initial state + + Returns: + Self for method chaining + """ + # Reset timestamp + self.current_timestamp = datetime.now() + + # Regenerate data for each timeframe + for tf in self.timeframes: + self._generate_initial_data(tf) + + # Reset any internal state + self.position = 'flat' + self.position_size = 0 + self.entry_price = 0 + self.stop_loss = 0 + self.take_profit = 0 + + # Reset prediction history if it exists + if hasattr(self, 'prediction_history'): + self.prediction_history = [] + + return self + + def step(self, action): + """ + Take a step in the environment by executing an action + + Args: + action: Action to take (0: HOLD, 1: BUY/LONG, 2: SELL/SHORT) + + Returns: + next_state: Next state after taking action + reward: Reward received + done: Whether episode is done + info: Additional information + """ + # Get current price + current_price = self.data['1m'][-1][4] + + # Initialize info dictionary + info = { + 'price': current_price, + 'timestamp': self.data['1m'][-1][0], + 'trade': None + } + + # Process action + if action == 0: # HOLD + pass # No action needed + + elif action == 1: # BUY/LONG + if self.position == 'flat': + # Open a new long position + self.position = 'long' + self.entry_price = current_price + self.position_size = 100 # Simplified position sizing + + # Set stop loss and take profit levels + self.stop_loss = current_price * 0.99 # 1% stop loss + self.take_profit = current_price * 1.02 # 2% take profit + + # Record entry time + self.entry_time = self.data['1m'][-1][0] + + # Add to info + info['trade'] = { + 'type': 'long', + 'entry': current_price, + 'entry_time': self.data['1m'][-1][0], + 'size': self.position_size, + 'stop_loss': self.stop_loss, + 'take_profit': self.take_profit + } + + elif self.position == 'short': + # Close short position and open long + pnl = self.entry_price - current_price + pnl_percent = pnl / self.entry_price * 100 + pnl_dollar = pnl_percent / 100 * self.position_size + + # Add to info + info['trade'] = { + 'type': 'short', + 'entry': self.entry_price, + 'exit': current_price, + 'entry_time': self.entry_time, + 'exit_time': self.data['1m'][-1][0], + 'pnl_percent': pnl_percent, + 'pnl_dollar': pnl_dollar, + 'duration': (self.data['1m'][-1][0] - self.entry_time) / (1000 * 60) # Duration in minutes + } + + # Open new long position + self.position = 'long' + self.entry_price = current_price + self.position_size = 100 # Simplified position sizing + + # Set stop loss and take profit levels + self.stop_loss = current_price * 0.99 # 1% stop loss + self.take_profit = current_price * 1.02 # 2% take profit + + # Record entry time + self.entry_time = self.data['1m'][-1][0] + + elif action == 2: # SELL/SHORT + if self.position == 'flat': + # Open a new short position + self.position = 'short' + self.entry_price = current_price + self.position_size = 100 # Simplified position sizing + + # Set stop loss and take profit levels + self.stop_loss = current_price * 1.01 # 1% stop loss + self.take_profit = current_price * 0.98 # 2% take profit + + # Record entry time + self.entry_time = self.data['1m'][-1][0] + + # Add to info + info['trade'] = { + 'type': 'short', + 'entry': current_price, + 'entry_time': self.data['1m'][-1][0], + 'size': self.position_size, + 'stop_loss': self.stop_loss, + 'take_profit': self.take_profit + } + + elif self.position == 'long': + # Close long position and open short + pnl = current_price - self.entry_price + pnl_percent = pnl / self.entry_price * 100 + pnl_dollar = pnl_percent / 100 * self.position_size + + # Add to info + info['trade'] = { + 'type': 'long', + 'entry': self.entry_price, + 'exit': current_price, + 'entry_time': self.entry_time, + 'exit_time': self.data['1m'][-1][0], + 'pnl_percent': pnl_percent, + 'pnl_dollar': pnl_dollar, + 'duration': (self.data['1m'][-1][0] - self.entry_time) / (1000 * 60) # Duration in minutes + } + + # Open new short position + self.position = 'short' + self.entry_price = current_price + self.position_size = 100 # Simplified position sizing + + # Set stop loss and take profit levels + self.stop_loss = current_price * 1.01 # 1% stop loss + self.take_profit = current_price * 0.98 # 2% take profit + + # Record entry time + self.entry_time = self.data['1m'][-1][0] + + # Generate next candle + self._add_new_candle('1m') + + # Check if stop loss or take profit has been hit + self._check_sl_tp(info) + + # Validate predictions if available + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + self.validate_predictions(self.data['1m'][-1]) + + # Prepare next state (simplified) + next_state = self._get_state() + + # Calculate reward (simplified) + reward = 0 + if info['trade'] is not None and 'pnl_dollar' in info['trade']: + reward = info['trade']['pnl_dollar'] + + # Check if done (simplified) + done = False + + return next_state, reward, done, info + + def _get_state(self): + """ + Get the current state of the environment + + Returns: + List representing the current state + """ + # Simplified state representation + state = [] + + # Add price features + for tf in ['1m', '5m', '15m']: + if tf in self.data: + # Get last 10 candles + candles = self.data[tf][-10:] + + # Extract close prices + prices = [candle[4] for candle in candles] + + # Calculate price changes + price_changes = [prices[i]/prices[i-1] - 1 for i in range(1, len(prices))] + + # Add to state + state.extend(price_changes) + + # Add current price relative to SMA + sma_5 = sum(prices[-5:]) / 5 + sma_10 = sum(prices) / 10 + state.append(prices[-1] / sma_5 - 1) + state.append(prices[-1] / sma_10 - 1) + + # Pad state to 100 dimensions + while len(state) < 100: + state.append(0) + + # Ensure state has exactly 100 dimensions + if len(state) > 100: + state = state[:100] + + return state + + def _check_sl_tp(self, info): + """ + Check if stop loss or take profit has been hit + + Args: + info: Info dictionary to update + """ + if self.position == 'flat': + return + + # Get current price + current_price = self.data['1m'][-1][4] + + if self.position == 'long': + # Check stop loss + if current_price <= self.stop_loss: + # Stop loss hit + pnl_percent = (self.stop_loss - self.entry_price) / self.entry_price * 100 + pnl_dollar = pnl_percent / 100 * self.position_size + + # Add to info + info['trade'] = { + 'type': 'long', + 'entry': self.entry_price, + 'exit': self.stop_loss, + 'entry_time': self.entry_time, + 'exit_time': self.data['1m'][-1][0], + 'pnl_percent': pnl_percent, + 'pnl_dollar': pnl_dollar, + 'reason': 'stop_loss', + 'duration': (self.data['1m'][-1][0] - self.entry_time) / (1000 * 60) # Duration in minutes + } + + # Reset position + self.position = 'flat' + self.entry_price = 0 + self.position_size = 0 + self.stop_loss = 0 + self.take_profit = 0 + + # Check take profit + elif current_price >= self.take_profit: + # Take profit hit + pnl_percent = (self.take_profit - self.entry_price) / self.entry_price * 100 + pnl_dollar = pnl_percent / 100 * self.position_size + + # Add to info + info['trade'] = { + 'type': 'long', + 'entry': self.entry_price, + 'exit': self.take_profit, + 'entry_time': self.entry_time, + 'exit_time': self.data['1m'][-1][0], + 'pnl_percent': pnl_percent, + 'pnl_dollar': pnl_dollar, + 'reason': 'take_profit', + 'duration': (self.data['1m'][-1][0] - self.entry_time) / (1000 * 60) # Duration in minutes + } + + # Reset position + self.position = 'flat' + self.entry_price = 0 + self.position_size = 0 + self.stop_loss = 0 + self.take_profit = 0 + + elif self.position == 'short': + # Check stop loss + if current_price >= self.stop_loss: + # Stop loss hit + pnl_percent = (self.entry_price - self.stop_loss) / self.entry_price * 100 + pnl_dollar = pnl_percent / 100 * self.position_size + + # Add to info + info['trade'] = { + 'type': 'short', + 'entry': self.entry_price, + 'exit': self.stop_loss, + 'entry_time': self.entry_time, + 'exit_time': self.data['1m'][-1][0], + 'pnl_percent': pnl_percent, + 'pnl_dollar': pnl_dollar, + 'reason': 'stop_loss', + 'duration': (self.data['1m'][-1][0] - self.entry_time) / (1000 * 60) # Duration in minutes + } + + # Reset position + self.position = 'flat' + self.entry_price = 0 + self.position_size = 0 + self.stop_loss = 0 + self.take_profit = 0 + + # Check take profit + elif current_price <= self.take_profit: + # Take profit hit + pnl_percent = (self.entry_price - self.take_profit) / self.entry_price * 100 + pnl_dollar = pnl_percent / 100 * self.position_size + + # Add to info + info['trade'] = { + 'type': 'short', + 'entry': self.entry_price, + 'exit': self.take_profit, + 'entry_time': self.entry_time, + 'exit_time': self.data['1m'][-1][0], + 'pnl_percent': pnl_percent, + 'pnl_dollar': pnl_dollar, + 'reason': 'take_profit', + 'duration': (self.data['1m'][-1][0] - self.entry_time) / (1000 * 60) # Duration in minutes + } + + # Reset position + self.position = 'flat' + self.entry_price = 0 + self.position_size = 0 + self.stop_loss = 0 + self.take_profit = 0 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data + + Args: + new_candle: New candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + # Extract candle data + timestamp = new_candle[0] + high_price = new_candle[2] + low_price = new_candle[3] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred.get('validated', False): + continue + + # Check if this prediction's time has come (or passed) + if 'predicted_timestamp' in pred and timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + + if was_correct: + correct_count += 1 + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + + if was_correct: + correct_count += 1 + + # Return validation metrics + if validated_count > 0: + return { + 'validated_count': validated_count, + 'correct_count': correct_count, + 'accuracy': correct_count / validated_count + } + + return None + + def calculate_pnl(self): + """ + Calculate the current profit/loss of the open position + + Returns: + float: Current PnL in dollars, 0 if no position is open + """ + if self.position == 'flat': + return 0.0 + + current_price = self.data['1m'][-1][4] + + if self.position == 'long': + pnl_percent = (current_price - self.entry_price) / self.entry_price * 100 + elif self.position == 'short': + pnl_percent = (self.entry_price - current_price) / self.entry_price * 100 + else: + return 0.0 + + pnl_dollar = pnl_percent / 100 * self.position_size + return pnl_dollar # Example usage if __name__ == "__main__": diff --git a/crypto/gogo2/main.py b/crypto/gogo2/main.py deleted file mode 100644 index 4ca74cc..0000000 --- a/crypto/gogo2/main.py +++ /dev/null @@ -1,3750 +0,0 @@ -import os -import sys -import time -import json -import logging -import asyncio -import argparse -import traceback -import datetime -import pandas as pd -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.dates as mdates -from matplotlib.ticker import FuncFormatter -import mplfinance as mpf -from collections import deque, namedtuple -import random -from typing import List, Dict, Tuple, Optional, Union, Any -from dotenv import load_dotenv -import torch.nn.functional as F -import math -from mexc_trading import MexcTradingClient - -import torch -import torch.nn as nn -import torch.optim as optim -from torch.utils.tensorboard import SummaryWriter -import torch.amp as amp # Update import to use torch.amp instead of torch.cuda.amp -from sklearn.preprocessing import MinMaxScaler - -import ccxt.async_support as ccxt -import websockets -from data_cache import ohlcv_cache - - - -if sys.platform == 'win32': - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - -# Constants -INITIAL_BALANCE = 1000.0 -MAX_LEVERAGE = 1.0 # Max leverage to use -STOP_LOSS_PERCENT = 2.0 # Default stop loss percentage -TAKE_PROFIT_PERCENT = 5.0 # Default take profit percentage -STATE_SIZE = 128 # Size of state representation -LEARNING_RATE = 0.0001 -MODEL_DIR = "models_improved" # New models directory - -# Load environment variables -load_dotenv() -MEXC_API_KEY = os.getenv('MEXC_API_KEY') -MEXC_SECRET_KEY = os.getenv('MEXC_SECRET_KEY') - -# Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') -logger = logging.getLogger('trading_bot') - -# Constants -INITIAL_BALANCE = 1000.0 # Starting balance in USDT -MAX_LEVERAGE = 1 # Maximum leverage to use -STOP_LOSS_PERCENT = 2.0 # Default stop loss percentage -TAKE_PROFIT_PERCENT = 4.0 # Default take profit percentage -STATE_SIZE = 40 # Size of the state vector for the agent (matches saved model) -LEARNING_RATE = 1e-4 # Learning rate for the optimizer -MEMORY_SIZE = 100000 # Size of the replay memory -BATCH_SIZE = 64 # Batch size for training -GAMMA = 0.99 # Discount factor for future rewards -EPSILON_START = 1.0 # Starting value of epsilon for exploration -EPSILON_END = 0.05 # Minimum value of epsilon -EPSILON_DECAY = 10000 # Decay rate for epsilon -TARGET_UPDATE = 10 # Update target network every N episodes - -# Experience replay tuple -Experience = namedtuple('Experience', ['state', 'action', 'reward', 'next_state', 'done']) - -# Add this function near the top of the file, after the imports but before any classes -def find_local_extrema(prices, window=5, volumes=None, volume_threshold=0.7): - """ - Find local extrema (peaks and troughs) in price data with improved accuracy. - - Args: - prices: List of price values - window: Window size for extrema detection - volumes: Optional list of volume values - volume_threshold: Volume threshold for confirming extrema - - Returns: - peaks: Indices of local maxima - troughs: Indices of local minima - """ - if len(prices) < 2 * window + 1: - return [], [] - - # Convert to numpy array if not already - prices = np.array(prices) - - # Find potential extrema using rolling window - peaks = [] - troughs = [] - - for i in range(window, len(prices) - window): - # Get window around current point - window_left = prices[i - window:i] - window_right = prices[i + 1:i + window + 1] - current = prices[i] - - # Check for peak - if current > np.max(window_left) and current > np.max(window_right): - # Volume confirmation if available - if volumes is None or volumes[i] > np.mean(volumes) * volume_threshold: - peaks.append(i) - - # Check for trough - if current < np.min(window_left) and current < np.min(window_right): - # Volume confirmation if available - if volumes is None or volumes[i] > np.mean(volumes) * volume_threshold: - troughs.append(i) - - # Apply additional filtering to remove false extrema - if len(peaks) > 1: - # Filter out peaks that are too close to each other - filtered_peaks = [peaks[0]] - for i in range(1, len(peaks)): - if peaks[i] - filtered_peaks[-1] >= window: - filtered_peaks.append(peaks[i]) - peaks = filtered_peaks - - if len(troughs) > 1: - # Filter out troughs that are too close to each other - filtered_troughs = [troughs[0]] - for i in range(1, len(troughs)): - if troughs[i] - filtered_troughs[-1] >= window: - filtered_troughs.append(troughs[i]) - troughs = filtered_troughs - - return peaks, troughs - -class ReplayMemory: - def __init__(self, capacity, alpha=0.6, beta=0.4, beta_increment=0.001, n_step=3, gamma=0.99): - self.capacity = capacity - self.memory = [] - self.position = 0 - self.Transition = namedtuple('Transition', ('state', 'action', 'reward', 'next_state', 'done')) - - # Prioritized Experience Replay parameters - self.alpha = alpha # How much prioritization to use (0 = uniform sampling) - self.beta = beta # Importance sampling correction (0 = no correction) - self.beta_increment = beta_increment # Increment beta over time to 1 - self.max_priority = 1.0 # Initial max priority - - # N-step learning parameters - self.n_step = n_step - self.gamma = gamma - self.n_step_buffer = deque(maxlen=n_step) - - def push(self, state, action, reward, next_state, done): - """Store transition with maximum priority""" - # Store experience in n-step buffer - self.n_step_buffer.append((state, action, reward, next_state, done)) - - # If we don't have enough transitions for n-step yet, return - if len(self.n_step_buffer) < self.n_step and not done: - return - - # Calculate n-step reward and get the appropriate next state - n_step_reward = 0 - n_step_next_state = None - n_step_done = False - - # If the episode ended before we could collect n steps - if done and len(self.n_step_buffer) < self.n_step: - # Use what we have - n_step_next_state = self.n_step_buffer[-1][3] - n_step_done = True - - # Calculate n-step reward with discount - for i, (_, _, r, _, _) in enumerate(self.n_step_buffer): - n_step_reward += r * (self.gamma ** i) - else: - # Get the state after n steps - n_step_next_state = self.n_step_buffer[-1][3] - n_step_done = self.n_step_buffer[-1][4] - - # Calculate n-step reward with discount - for i, (_, _, r, _, _) in enumerate(self.n_step_buffer): - n_step_reward += r * (self.gamma ** i) - - # Get the initial state and action - initial_state = self.n_step_buffer[0][0] - initial_action = self.n_step_buffer[0][1] - - # Create transition with n-step values - transition = self.Transition(initial_state, initial_action, n_step_reward, n_step_next_state, n_step_done) - - # Add to memory with maximum priority - if len(self.memory) < self.capacity: - self.memory.append((transition, self.max_priority)) - else: - self.memory[self.position] = (transition, self.max_priority) - - self.position = (self.position + 1) % self.capacity - - # If this was the end of an episode, clear the n-step buffer - if done: - self.n_step_buffer.clear() - - def sample(self, batch_size): - """Sample a batch of transitions with prioritized sampling""" - if len(self.memory) < batch_size: - return None - - # Calculate sampling probabilities - priorities = np.array([p for _, p in self.memory]) - probs = priorities ** self.alpha - probs /= probs.sum() - - # Sample indices based on priorities - indices = np.random.choice(len(self.memory), batch_size, p=probs) - - # Get the sampled transitions - transitions = [self.memory[idx][0] for idx in indices] - - # Calculate importance sampling weights - weights = (len(self.memory) * probs[indices]) ** (-self.beta) - weights /= weights.max() # Normalize weights - - # Increment beta for next time - self.beta = min(1.0, self.beta + self.beta_increment) - - # Convert to batch - batch = self.Transition(*zip(*transitions)) - - return batch, indices, weights - - def update_priorities(self, indices, td_errors): - """Update priorities based on TD errors""" - for idx, td_error in zip(indices, td_errors): - # Add a small constant to avoid zero priority - priority = (abs(td_error) + 1e-5) ** self.alpha - self.memory[idx] = (self.memory[idx][0], priority) - self.max_priority = max(self.max_priority, priority) - - def __len__(self): - return len(self.memory) - -class DQN(nn.Module): - def __init__(self, state_size, action_size, hidden_size=384, lstm_layers=2, attention_heads=4): - super(DQN, self).__init__() - - # Feature extraction layers with increased regularization - self.feature_extraction = nn.Sequential( - nn.Linear(state_size, hidden_size), - nn.LeakyReLU(), - nn.Dropout(0.2), # Increased dropout - nn.LayerNorm(hidden_size), # Layer normalization for stability - nn.Linear(hidden_size, hidden_size), - nn.LeakyReLU(), - nn.Dropout(0.2), # Increased dropout - nn.LayerNorm(hidden_size) # Layer normalization for stability - ) - - # LSTM for sequential processing - self.lstm = nn.LSTM( - input_size=hidden_size, - hidden_size=hidden_size, - num_layers=lstm_layers, - batch_first=True, - dropout=0.2 if lstm_layers > 1 else 0 # Increased dropout - ) - - # Dueling network architecture - # Advantage stream - self.advantage_stream = nn.Sequential( - nn.Linear(hidden_size, hidden_size // 2), - nn.LeakyReLU(), - nn.Dropout(0.2), # Added dropout - nn.Linear(hidden_size // 2, action_size) - ) - - # Value stream - self.value_stream = nn.Sequential( - nn.Linear(hidden_size, hidden_size // 2), - nn.LeakyReLU(), - nn.Dropout(0.2), # Added dropout - nn.Linear(hidden_size // 2, 1) - ) - - # Market regime classification - self.market_regime_classifier = nn.Sequential( - nn.Linear(hidden_size, hidden_size // 2), - nn.LeakyReLU(), - nn.Dropout(0.2), # Added dropout - nn.Linear(hidden_size // 2, 3) # 3 regimes: trending, ranging, volatile - ) - - # Initialize weights - self._initialize_weights() - - def _initialize_weights(self): - for m in self.modules(): - if isinstance(m, nn.Linear): - nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') - if m.bias is not None: - nn.init.constant_(m.bias, 0) - - def forward(self, x, hidden=None): - # Extract features - features = self.feature_extraction(x) - - # Add sequence dimension for LSTM if not present - if len(features.shape) == 2: - features = features.unsqueeze(1) - - # LSTM processing - lstm_out, lstm_hidden = self.lstm(features, hidden) - - # Use the last LSTM output - lstm_out = lstm_out[:, -1, :] - - # Dueling architecture - advantage = self.advantage_stream(lstm_out) - value = self.value_stream(lstm_out) - - # Combine value and advantage for Q-values - # Q(s,a) = V(s) + (A(s,a) - mean(A(s,a'))) - q_values = value + advantage - advantage.mean(dim=1, keepdim=True) - - # Market regime classification - market_regime = self.market_regime_classifier(lstm_out) - - return q_values, lstm_hidden, market_regime - -class PricePredictionModel(nn.Module): - def __init__(self, input_size=2, hidden_size=256, output_size=5, num_layers=3, num_timeframes=3): - super(PricePredictionModel, self).__init__() - self.hidden_size = hidden_size - self.num_layers = num_layers - self.num_timeframes = num_timeframes - self.output_size = output_size - - # Separate LSTM for each timeframe - self.timeframe_lstms = nn.ModuleList([ - nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=0.2) - for _ in range(num_timeframes) - ]) - - # Self-attention for each timeframe - self.self_attentions = nn.ModuleList([ - nn.MultiheadAttention(hidden_size, num_heads=4, batch_first=True, dropout=0.1) - for _ in range(num_timeframes) - ]) - - # Timeframe fusion layer - self.fusion_layer = nn.Sequential( - nn.Linear(hidden_size * num_timeframes, hidden_size * 2), - nn.LeakyReLU(), - nn.Dropout(0.2), - nn.Linear(hidden_size * 2, hidden_size) - ) - - # Price prediction layers - self.price_fc = nn.Sequential( - nn.Linear(hidden_size, hidden_size), - nn.LeakyReLU(), - nn.Dropout(0.1), - nn.Linear(hidden_size, output_size) - ) - - # Extrema prediction layers (high and low points) - self.extrema_fc = nn.Sequential( - nn.Linear(hidden_size, hidden_size), - nn.LeakyReLU(), - nn.Dropout(0.1), - nn.Linear(hidden_size, output_size * 2) # For each time step, predict high/low probability - ) - - # Initialize scalers - self.price_scaler = MinMaxScaler(feature_range=(0, 1)) - self.volume_scaler = MinMaxScaler(feature_range=(0, 1)) - - # Initialize weights - self._initialize_weights() - - def _initialize_weights(self): - for m in self.modules(): - if isinstance(m, nn.Linear): - nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') - if m.bias is not None: - nn.init.constant_(m.bias, 0) - - def forward(self, x_list): - """ - Forward pass for multi-timeframe price prediction - - Args: - x_list: List of tensors, one for each timeframe - Each tensor shape: [batch_size, seq_len, input_features] - - Returns: - price_pred: Predicted prices for next candles - extrema_pred: Predicted extrema points (high/low probabilities) - """ - # If only one timeframe is provided, duplicate it - if not isinstance(x_list, list): - x_list = [x_list] * self.num_timeframes - - # Ensure we have the right number of timeframes - if len(x_list) < self.num_timeframes: - x_list = x_list + [x_list[0]] * (self.num_timeframes - len(x_list)) - elif len(x_list) > self.num_timeframes: - x_list = x_list[:self.num_timeframes] - - # Process each timeframe with its own LSTM - lstm_outputs = [] - for i, x in enumerate(x_list): - lstm_out, _ = self.timeframe_lstms[i](x) # lstm_out: [batch_size, seq_len, hidden_size] - lstm_outputs.append(lstm_out) - - # Apply self-attention to each timeframe - attn_outputs = [] - for i, lstm_out in enumerate(lstm_outputs): - attn_output, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) - attn_outputs.append(attn_output[:, -1, :]) # Use the last time step - - # Concatenate all timeframe representations - combined = torch.cat(attn_outputs, dim=1) # [batch_size, hidden_size * num_timeframes] - - # Fuse timeframe information - fused = self.fusion_layer(combined) # [batch_size, hidden_size] - - # Price prediction - price_pred = self.price_fc(fused) - - # Extrema prediction - extrema_logits = self.extrema_fc(fused) - extrema_pred = torch.sigmoid(extrema_logits) # Convert to probabilities - - return price_pred, extrema_pred - - def preprocess(self, price_history, volume_history=None, timeframes=None): - """ - Preprocess price and volume data for model input - - Args: - price_history: List of price histories for different timeframes - volume_history: List of volume histories for different timeframes - timeframes: List of timeframe names (for logging) - - Returns: - Preprocessed data ready for model input - """ - # If single timeframe data is provided, convert to list format - if not isinstance(price_history, list): - price_history = [price_history] - if volume_history is not None: - volume_history = [volume_history] - - # Ensure volume history exists - if volume_history is None: - volume_history = [np.ones_like(prices) for prices in price_history] - - # Process each timeframe - processed_data = [] - - for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): - # Convert to numpy arrays if they aren't already - prices = np.array(prices).reshape(-1, 1) - volumes = np.array(volumes).reshape(-1, 1) - - # Ensure volumes has the same length as prices - if len(volumes) != len(prices): - logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") - if len(volumes) > len(prices): - volumes = volumes[:len(prices)] - else: - # Pad volumes with the mean value - mean_volume = np.mean(volumes) - padding = np.full((len(prices) - len(volumes), 1), mean_volume) - volumes = np.vstack((volumes, padding)) - - # Fit and transform the data - if not hasattr(self, f'price_scaler_{i}'): - setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) - setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) - - price_scaler = getattr(self, f'price_scaler_{i}') - volume_scaler = getattr(self, f'volume_scaler_{i}') - - # Fit scalers if not already fit - if not hasattr(price_scaler, 'data_min_'): - price_scaler.fit(prices) - if not hasattr(volume_scaler, 'data_min_'): - volume_scaler.fit(volumes) - - # Transform the data - scaled_prices = price_scaler.transform(prices) - scaled_volumes = volume_scaler.transform(volumes) - - # Combine price and volume data - combined_data = np.hstack((scaled_prices, scaled_volumes)) - - # Convert to tensor and move to the same device as the model - tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) # Add batch dimension - tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model - - processed_data.append(tensor_data) - - if timeframes: - timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" - logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") - - return processed_data - - def postprocess_price(self, scaled_predictions, timeframe_idx=0): - """ - Convert scaled predictions back to actual price values - - Args: - scaled_predictions: Model output predictions (scaled) - timeframe_idx: Index of the timeframe to use for inverse scaling - - Returns: - Actual price predictions - """ - # Get the appropriate scaler - price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', self.price_scaler) - - # Convert to numpy and reshape - scaled_predictions = scaled_predictions.detach().cpu().numpy() - scaled_predictions = scaled_predictions.reshape(-1, 1) - - # Inverse transform - actual_predictions = price_scaler.inverse_transform(scaled_predictions) - - return actual_predictions.flatten() - - def predict_next_candles(self, price_history, volume_history=None, timeframes=None, num_candles=5): - """ - Predict the next several candles and potential extrema points - - Args: - price_history: List of price histories for different timeframes - volume_history: List of volume histories for different timeframes - timeframes: List of timeframe names (for logging) - num_candles: Number of future candles to predict - - Returns: - price_predictions: Predicted prices for next candles - extrema_predictions: Predicted extrema points (high/low probabilities) - """ - # Preprocess the data - processed_data = self.preprocess(price_history, volume_history, timeframes) - - # Make prediction - with torch.no_grad(): - price_pred, extrema_pred = self.forward(processed_data) - - # Convert predictions back to actual values - price_predictions = self.postprocess_price(price_pred[0], timeframe_idx=0) - - # Process extrema predictions - extrema_predictions = extrema_pred[0].cpu().numpy() - - # Reshape extrema predictions to [num_candles, 2] (high/low probabilities for each candle) - extrema_predictions = extrema_predictions.reshape(num_candles, 2) - - return price_predictions, extrema_predictions - - def update_price_predictions(self): - """Update price and extrema predictions using the multi-timeframe model""" - if not hasattr(self, 'price_predictor') or self.price_predictor is None: - return - - # Check if we have price and volume data - if len(self.features['price']) == 0 or len(self.features['volume']) == 0: - logger.warning("No price or volume data available for price predictions") - return - - # Update timeframe data - self.timeframe_data['1m']['prices'] = self.features['price'].copy() - self.timeframe_data['1m']['volumes'] = self.features['volume'].copy() - - # Update higher timeframes if we have enough data - if len(self.features['price']) >= 15: - # Update 5-minute data - self.timeframe_data['5m'] = { - 'prices': [self.features['price'][i] for i in range(0, len(self.features['price']), 5)], - 'volumes': [sum(self.features['volume'][i:i+5]) for i in range(0, len(self.features['volume']), 5) if i+5 <= len(self.features['volume'])] - } - - # Update 15-minute data - self.timeframe_data['15m'] = { - 'prices': [self.features['price'][i] for i in range(0, len(self.features['price']), 15)], - 'volumes': [sum(self.features['volume'][i:i+15]) for i in range(0, len(self.features['volume']), 15) if i+15 <= len(self.features['volume'])] - } - - # Prepare multi-timeframe data - timeframe_prices = [] - timeframe_volumes = [] - timeframe_names = [] - - # Add 1-minute data - timeframe_prices.append(self.timeframe_data['1m']['prices']) - timeframe_volumes.append(self.timeframe_data['1m']['volumes']) - timeframe_names.append('1m') - - # Add 5-minute data if available - if len(self.timeframe_data['5m']) > 0 and len(self.timeframe_data['5m']['prices']) > 0: - timeframe_prices.append(self.timeframe_data['5m']['prices']) - timeframe_volumes.append(self.timeframe_data['5m']['volumes']) - timeframe_names.append('5m') - - # Add 15-minute data if available - if len(self.timeframe_data['15m']) > 0 and len(self.timeframe_data['15m']['prices']) > 0: - timeframe_prices.append(self.timeframe_data['15m']['prices']) - timeframe_volumes.append(self.timeframe_data['15m']['volumes']) - timeframe_names.append('15m') - - # Get predictions - try: - price_predictions, extrema_predictions = self.price_predictor.predict_next_candles( - timeframe_prices, - timeframe_volumes, - timeframe_names - ) - - # Store predictions - self.price_predictions = price_predictions - self.extrema_predictions = extrema_predictions - - # Check for predicted extrema in the next few candles - self.has_predicted_low = False - self.has_predicted_high = False - - # Check first 3 candles for extrema - for i in range(min(3, len(extrema_predictions))): - # Check if probability of low is high - if extrema_predictions[i, 0] > 0.7: - self.has_predicted_low = True - - # Check if probability of high is high - if extrema_predictions[i, 1] > 0.7: - self.has_predicted_high = True - - # Log predictions - logger.info(f"Price predictions: {price_predictions}") - logger.info(f"Extrema predictions: {extrema_predictions}") - logger.info(f"Predicted low: {self.has_predicted_low}, Predicted high: {self.has_predicted_high}") - - return price_predictions, extrema_predictions - except Exception as e: - logger.error(f"Error in price prediction: {e}") - logger.error(traceback.format_exc()) - return None, None - -class TradingEnvironment: - def __init__(self, initial_balance=INITIAL_BALANCE, window_size=30, demo=True, trading_client=None): - """Initialize the trading environment""" - self.initial_balance = initial_balance - self.balance = initial_balance - self.window_size = window_size - self.demo = demo - self.trading_client = trading_client - self.data = [] - self.features = {} - self.position = 'flat' # 'flat', 'long', or 'short' - self.position_size = 0 - self.entry_price = 0 - self.entry_index = 0 - self.stop_loss = 0 - self.take_profit = 0 - self.current_step = 0 - self.current_price = 0 - self.trades = [] - self.win_count = 0 - self.loss_count = 0 - self.episode_pnl = 0.0 - self.total_pnl = 0.0 - self.peak_balance = initial_balance - self.max_drawdown = 0.0 - self.action_space = 4 # 0: HOLD, 1: BUY/LONG, 2: SELL/SHORT, 3: CLOSE - self.last_action = 0 # Initialize with HOLD - - # Initialize features - self._initialize_features() - - # Initialize price predictor - self.price_predictor = None - self.price_predictions = [] - self.predicted_extrema = [] - - def _initialize_features(self): - """Initialize technical indicators and features""" - self.features = { - 'price': [], - 'volume': [], - 'rsi': [], - 'macd': [], - 'macd_signal': [], - 'macd_hist': [], - 'bollinger_upper': [], - 'bollinger_mid': [], - 'bollinger_lower': [], - 'stoch_k': [], - 'stoch_d': [], - 'ema_9': [], - 'ema_21': [], - 'atr': [] - } - - # Initialize timeframe data structure - self.timeframe_data = { - '1m': {'prices': [], 'volumes': []}, - '5m': [], - '15m': [] - } - - # Initialize optimal trade tracking - self.optimal_bottoms = [] - self.optimal_tops = [] - self.optimal_signals = np.array([]) - - # Add risk factor for curriculum learning - self.risk_factor = 1.0 # Default risk factor - - def _update_features(self): - """Update technical indicators with new data""" - self._initialize_features() # Recalculate all features - - async def fetch_new_data(self, exchange, symbol="ETH/USDT", timeframe="1m", limit=100): - """Fetch new data from the exchange and update the environment""" - from data_cache import ohlcv_cache - - try: - logger.info(f"Fetching new data for {symbol} with timeframe {timeframe}") - - # Use the refactored fetch method - data = await fetch_ohlcv_data(exchange, symbol, timeframe, limit) - - # Update environment with fetched data - if data and len(data) > 0: - self.data = data - self._initialize_features() - logger.info(f"Updated environment with {len(data)} candles") - return True - else: - logger.warning("No new data received from exchange or cache") - - # Try to use existing data if available - if self.data and len(self.data) > 0: - logger.info(f"Using existing data ({len(self.data)} candles)") - return True - - return False - except Exception as e: - logger.error(f"Error fetching new data: {e}") - - # Try to use existing data if available - if self.data and len(self.data) > 0: - logger.info(f"Using existing data ({len(self.data)} candles) after fetch error") - return True - - return False - - async def fetch_initial_data(self, exchange, symbol="ETH/USDT", timeframe="1m", limit=1000): - """Fetch initial historical data for the environment""" - from data_cache import ohlcv_cache - - try: - logger.info(f"Fetching initial data for {symbol}") - - # Use the refactored fetch method - data = await fetch_ohlcv_data(exchange, symbol, timeframe, limit) - - # Update environment with fetched data - if data and len(data) > 0: - self.data = data - self._initialize_features() - logger.info(f"Initialized environment with {len(data)} candles") - return True - else: - logger.warning("No initial data received from exchange or cache") - return False - except Exception as e: - logger.error(f"Error fetching initial data: {e}") - return False - - def step(self, action): - """Take an action in the environment and return the next state, reward, and done flag""" - # Check if we have data and if current_step is within bounds - if not self.data or self.current_step >= len(self.data): - logger.error(f"No data available or current_step ({self.current_step}) out of bounds (data length: {len(self.data) if self.data else 0})") - # Return a default state, negative reward, and done=True to end the episode - return self.get_state(), -10, True, {"error": "No data available"} - - # Store current price before taking action - self.current_price = self.data[self.current_step]['close'] - - # Store last action for reward calculation - self.last_action = action - - # Process action (0: HOLD, 1: BUY/LONG, 2: SELL/SHORT, 3: CLOSE) - if not self.demo and self.trading_client: - # Execute real trades in live mode - asyncio.create_task(self._execute_live_action(action)) - else: - # Simulate trades in demo mode - self._simulate_action(action) - - # Calculate reward (simulation still runs in parallel with live trading) - reward, info = self.calculate_reward(action) # Unpack the tuple here - - # Check for stop loss / take profit hits - self.check_sl_tp() - - # Move to next step - self.current_step += 1 - done = self.current_step >= len(self.data) - 1 - - # Get new state - next_state = self.get_state() - - return next_state, reward, done, info - - def _simulate_action(self, action): - """Simulate trading action in demo mode""" - try: - if action == 0: # HOLD - # No action needed - pass - - elif action == 1: # BUY/LONG - if self.position == 'flat': - # Open long position - self.position_size = self.calculate_position_size() - self.entry_price = self.current_price - self.entry_index = self.current_step - self.stop_loss = self.current_price * (1 - STOP_LOSS_PERCENT / 100) - self.take_profit = self.current_price * (1 + TAKE_PROFIT_PERCENT / 100) - self.position = 'long' - - logger.info(f"DEMO: Opened LONG position at {self.current_price}") - - elif self.position == 'short': - # Close short position - pnl_percent = (self.entry_price - self.current_price) / self.entry_price * 100 - pnl_dollar = pnl_percent / 100 * self.position_size - - # Apply fees - pnl_dollar -= self.calculate_fees(self.position_size) - - # Update balance - self.balance += pnl_dollar - self.total_pnl += pnl_dollar - self.episode_pnl += pnl_dollar - - # Record trade - self.trades.append({ - 'type': 'short', - 'entry': self.entry_price, - 'exit': self.current_price, - 'entry_time': self.data[self.entry_index]['timestamp'], - 'exit_time': self.data[self.current_step]['timestamp'], - 'pnl_percent': pnl_percent, - 'pnl_dollar': pnl_dollar, - 'duration': self.current_step - self.entry_index, - 'market_direction': self.get_market_direction(), - 'reason': 'switch_to_long' - }) - - # Update win/loss count - if pnl_dollar > 0: - self.win_count += 1 - else: - self.loss_count += 1 - - logger.info(f"DEMO: Closed SHORT position at {self.current_price} | PnL: {pnl_percent:.2f}% | ${pnl_dollar:.2f}") - - # Open new long position - self.position_size = self.calculate_position_size() - self.entry_price = self.current_price - self.entry_index = self.current_step - self.stop_loss = self.current_price * (1 - STOP_LOSS_PERCENT / 100) - self.take_profit = self.current_price * (1 + TAKE_PROFIT_PERCENT / 100) - self.position = 'long' - - logger.info(f"DEMO: Opened LONG position at {self.current_price}") - - elif action == 2: # SELL/SHORT - if self.position == 'flat': - # Open short position - self.position_size = self.calculate_position_size() - self.entry_price = self.current_price - self.entry_index = self.current_step - self.stop_loss = self.current_price * (1 + STOP_LOSS_PERCENT / 100) - self.take_profit = self.current_price * (1 - TAKE_PROFIT_PERCENT / 100) - self.position = 'short' - - logger.info(f"DEMO: Opened SHORT position at {self.current_price}") - - elif self.position == 'long': - # Close long position - pnl_percent = (self.current_price - self.entry_price) / self.entry_price * 100 - pnl_dollar = pnl_percent / 100 * self.position_size - - # Apply fees - pnl_dollar -= self.calculate_fees(self.position_size) - - # Update balance - self.balance += pnl_dollar - self.total_pnl += pnl_dollar - self.episode_pnl += pnl_dollar - - # Record trade - self.trades.append({ - 'type': 'long', - 'entry': self.entry_price, - 'exit': self.current_price, - 'entry_time': self.data[self.entry_index]['timestamp'], - 'exit_time': self.data[self.current_step]['timestamp'], - 'pnl_percent': pnl_percent, - 'pnl_dollar': pnl_dollar, - 'duration': self.current_step - self.entry_index, - 'market_direction': self.get_market_direction(), - 'reason': 'switch_to_short' - }) - - # Update win/loss count - if pnl_dollar > 0: - self.win_count += 1 - else: - self.loss_count += 1 - - logger.info(f"DEMO: Closed LONG position at {self.current_price} | PnL: {pnl_percent:.2f}% | ${pnl_dollar:.2f}") - - # Open new short position - self.position_size = self.calculate_position_size() - self.entry_price = self.current_price - self.entry_index = self.current_step - self.stop_loss = self.current_price * (1 + STOP_LOSS_PERCENT / 100) - self.take_profit = self.current_price * (1 - TAKE_PROFIT_PERCENT / 100) - self.position = 'short' - - logger.info(f"DEMO: Opened SHORT position at {self.current_price}") - - elif action == 3: # CLOSE - if self.position == 'long': - # Close long position - pnl_percent = (self.current_price - self.entry_price) / self.entry_price * 100 - pnl_dollar = pnl_percent / 100 * self.position_size - - # Apply fees - pnl_dollar -= self.calculate_fees(self.position_size) - - # Update balance - self.balance += pnl_dollar - self.total_pnl += pnl_dollar - self.episode_pnl += pnl_dollar - - # Record trade - self.trades.append({ - 'type': 'long', - 'entry': self.entry_price, - 'exit': self.current_price, - 'entry_time': self.data[self.entry_index]['timestamp'], - 'exit_time': self.data[self.current_step]['timestamp'], - 'pnl_percent': pnl_percent, - 'pnl_dollar': pnl_dollar, - 'duration': self.current_step - self.entry_index, - 'market_direction': self.get_market_direction(), - 'reason': 'manual_close' - }) - - # Update win/loss count - if pnl_dollar > 0: - self.win_count += 1 - else: - self.loss_count += 1 - - logger.info(f"DEMO: Closed LONG position at {self.current_price} | PnL: {pnl_percent:.2f}% | ${pnl_dollar:.2f}") - - # Reset position - self.position = 'flat' - self.entry_price = 0 - self.entry_index = 0 - self.position_size = 0 - self.stop_loss = 0 - self.take_profit = 0 - - elif self.position == 'short': - # Close short position - pnl_percent = (self.entry_price - self.current_price) / self.entry_price * 100 - pnl_dollar = pnl_percent / 100 * self.position_size - - # Apply fees - pnl_dollar -= self.calculate_fees(self.position_size) - - # Update balance - self.balance += pnl_dollar - self.total_pnl += pnl_dollar - self.episode_pnl += pnl_dollar - - # Record trade - self.trades.append({ - 'type': 'short', - 'entry': self.entry_price, - 'exit': self.current_price, - 'entry_time': self.data[self.entry_index]['timestamp'], - 'exit_time': self.data[self.current_step]['timestamp'], - 'pnl_percent': pnl_percent, - 'pnl_dollar': pnl_dollar, - 'duration': self.current_step - self.entry_index, - 'market_direction': self.get_market_direction(), - 'reason': 'manual_close' - }) - - # Update win/loss count - if pnl_dollar > 0: - self.win_count += 1 - else: - self.loss_count += 1 - - logger.info(f"DEMO: Closed SHORT position at {self.current_price} | PnL: {pnl_percent:.2f}% | ${pnl_dollar:.2f}") - - # Reset position - self.position = 'flat' - self.entry_price = 0 - self.entry_index = 0 - self.position_size = 0 - self.stop_loss = 0 - self.take_profit = 0 - - except Exception as e: - logger.error(f"Error simulating action: {e}") - logger.error(traceback.format_exc()) - - async def _execute_live_action(self, action): - """Execute live trading action using the trading client""" - if not self.trading_client: - logger.warning("No trading client available for live trading") - return - - try: - if action == 0: # HOLD - # No action needed - pass - - elif action == 1: # BUY/LONG - if self.position == 'flat': - # Open long position - position_size = self.calculate_position_size() - stop_loss = self.current_price * (1 - STOP_LOSS_PERCENT / 100) - take_profit = self.current_price * (1 + TAKE_PROFIT_PERCENT / 100) - - success = await self.trading_client.open_position( - position_type='long', - size=position_size, - entry_price=self.current_price, - stop_loss=stop_loss, - take_profit=take_profit - ) - - if success: - logger.info(f"LIVE: Successfully opened LONG position at {self.current_price}") - else: - logger.error("LIVE: Failed to open LONG position") - - elif self.position == 'short': - # Close short position and open long - await self.trading_client.close_position(reason="switch_to_long") - - # Open new long position - position_size = self.calculate_position_size() - stop_loss = self.current_price * (1 - STOP_LOSS_PERCENT / 100) - take_profit = self.current_price * (1 + TAKE_PROFIT_PERCENT / 100) - - await self.trading_client.open_position( - position_type='long', - size=position_size, - entry_price=self.current_price, - stop_loss=stop_loss, - take_profit=take_profit - ) - - elif action == 2: # SELL/SHORT - if self.position == 'flat': - # Open short position - position_size = self.calculate_position_size() - stop_loss = self.current_price * (1 + STOP_LOSS_PERCENT / 100) - take_profit = self.current_price * (1 - TAKE_PROFIT_PERCENT / 100) - - success = await self.trading_client.open_position( - position_type='short', - size=position_size, - entry_price=self.current_price, - stop_loss=stop_loss, - take_profit=take_profit - ) - - if success: - logger.info(f"LIVE: Successfully opened SHORT position at {self.current_price}") - else: - logger.error("LIVE: Failed to open SHORT position") - - elif self.position == 'long': - # Close long position and open short - await self.trading_client.close_position(reason="switch_to_short") - - # Open new short position - position_size = self.calculate_position_size() - stop_loss = self.current_price * (1 + STOP_LOSS_PERCENT / 100) - take_profit = self.current_price * (1 - TAKE_PROFIT_PERCENT / 100) - - await self.trading_client.open_position( - position_type='short', - size=position_size, - entry_price=self.current_price, - stop_loss=stop_loss, - take_profit=take_profit - ) - - elif action == 3: # CLOSE - if self.position != 'flat': - # Close any open position - success = await self.trading_client.close_position(reason="manual_close") - - if success: - logger.info(f"LIVE: Successfully closed {self.position} position") - else: - logger.error(f"LIVE: Failed to close {self.position} position") - - except Exception as e: - logger.error(f"Error executing live action: {e}") - logger.error(traceback.format_exc()) - - def check_sl_tp(self): - """Check if stop loss or take profit has been hit""" - if self.position == 'flat': - return - - if self.position == 'long': - # Check stop loss - if self.current_price <= self.stop_loss: - # Stop loss hit - pnl_percent = (self.stop_loss - self.entry_price) / self.entry_price * 100 - pnl_dollar = pnl_percent / 100 * self.position_size - - # Apply fees - pnl_dollar -= self.calculate_fees(self.position_size) - - # Update balance - self.balance += pnl_dollar - self.total_pnl += pnl_dollar - self.episode_pnl += pnl_dollar - - # Update max drawdown - if self.balance > self.peak_balance: - self.peak_balance = self.balance - drawdown = (self.peak_balance - self.balance) / self.peak_balance - self.max_drawdown = max(self.max_drawdown, drawdown) - - # Record trade - self.trades.append({ - 'type': 'long', - 'entry': self.entry_price, - 'exit': self.stop_loss, - 'entry_time': self.data[self.entry_index]['timestamp'], - 'exit_time': self.data[self.current_step]['timestamp'], - 'pnl_percent': pnl_percent, - 'pnl_dollar': pnl_dollar, - 'duration': self.current_step - self.entry_index, - 'market_direction': self.get_market_direction(), - 'reason': 'stop_loss' - }) - - # Update win/loss count - self.loss_count += 1 - - logger.info(f"STOP LOSS hit for long at {self.stop_loss} | PnL: {pnl_percent:.2f}% | ${pnl_dollar:.2f}") - - # Reset position - self.position = 'flat' - self.entry_price = 0 - self.entry_index = 0 - self.position_size = 0 - self.stop_loss = 0 - self.take_profit = 0 - - # Check take profit - elif self.current_price >= self.take_profit: - # Take profit hit - pnl_percent = (self.take_profit - self.entry_price) / self.entry_price * 100 - pnl_dollar = pnl_percent / 100 * self.position_size - - # Apply fees - pnl_dollar -= self.calculate_fees(self.position_size) - - # Update balance - self.balance += pnl_dollar - self.total_pnl += pnl_dollar - self.episode_pnl += pnl_dollar - - # Update max drawdown - if self.balance > self.peak_balance: - self.peak_balance = self.balance - - # Record trade - self.trades.append({ - 'type': 'long', - 'entry': self.entry_price, - 'exit': self.take_profit, - 'entry_time': self.data[self.entry_index]['timestamp'], - 'exit_time': self.data[self.current_step]['timestamp'], - 'pnl_percent': pnl_percent, - 'pnl_dollar': pnl_dollar, - 'duration': self.current_step - self.entry_index, - 'market_direction': self.get_market_direction(), - 'reason': 'take_profit' - }) - - # Update win/loss count - self.win_count += 1 - - logger.info(f"TAKE PROFIT hit for long at {self.take_profit} | PnL: {pnl_percent:.2f}% | ${pnl_dollar:.2f}") - - # Reset position - self.position = 'flat' - self.entry_price = 0 - self.entry_index = 0 - self.position_size = 0 - self.stop_loss = 0 - self.take_profit = 0 - - elif self.position == 'short': - # Check stop loss - if self.current_price >= self.stop_loss: - # Stop loss hit - pnl_percent = (self.entry_price - self.stop_loss) / self.entry_price * 100 - pnl_dollar = pnl_percent / 100 * self.position_size - - # Apply fees - pnl_dollar -= self.calculate_fees(self.position_size) - - # Update balance - self.balance += pnl_dollar - self.total_pnl += pnl_dollar - self.episode_pnl += pnl_dollar - - # Update max drawdown - if self.balance > self.peak_balance: - self.peak_balance = self.balance - drawdown = (self.peak_balance - self.balance) / self.peak_balance - self.max_drawdown = max(self.max_drawdown, drawdown) - - # Record trade - self.trades.append({ - 'type': 'short', - 'entry': self.entry_price, - 'exit': self.stop_loss, - 'entry_time': self.data[self.entry_index]['timestamp'], - 'exit_time': self.data[self.current_step]['timestamp'], - 'pnl_percent': pnl_percent, - 'pnl_dollar': pnl_dollar, - 'duration': self.current_step - self.entry_index, - 'market_direction': self.get_market_direction(), - 'reason': 'stop_loss' - }) - - # Update win/loss count - self.loss_count += 1 - - logger.info(f"STOP LOSS hit for short at {self.stop_loss} | PnL: {pnl_percent:.2f}% | ${pnl_dollar:.2f}") - - # Reset position - self.position = 'flat' - self.entry_price = 0 - self.entry_index = 0 - self.position_size = 0 - self.stop_loss = 0 - self.take_profit = 0 - - # Check take profit - elif self.current_price <= self.take_profit: - # Take profit hit - pnl_percent = (self.entry_price - self.take_profit) / self.entry_price * 100 - pnl_dollar = pnl_percent / 100 * self.position_size - - # Apply fees - pnl_dollar -= self.calculate_fees(self.position_size) - - # Update balance - self.balance += pnl_dollar - self.total_pnl += pnl_dollar - self.episode_pnl += pnl_dollar - - # Update max drawdown - if self.balance > self.peak_balance: - self.peak_balance = self.balance - - # Record trade - self.trades.append({ - 'type': 'short', - 'entry': self.entry_price, - 'exit': self.take_profit, - 'entry_time': self.data[self.entry_index]['timestamp'], - 'exit_time': self.data[self.current_step]['timestamp'], - 'pnl_percent': pnl_percent, - 'pnl_dollar': pnl_dollar, - 'duration': self.current_step - self.entry_index, - 'market_direction': self.get_market_direction(), - 'reason': 'take_profit' - }) - - # Update win/loss count - self.win_count += 1 - - logger.info(f"TAKE PROFIT hit for short at {self.take_profit} | PnL: {pnl_percent:.2f}% | ${pnl_dollar:.2f}") - - # Reset position - self.position = 'flat' - self.entry_price = 0 - self.entry_index = 0 - self.position_size = 0 - self.stop_loss = 0 - self.take_profit = 0 - - def get_state(self): - """Create state representation for the agent""" - if len(self.data) < 30 or len(self.features['price']) == 0: - # Return zeros if not enough data - return np.zeros(STATE_SIZE, dtype=np.float32) # Ensure float32 - - # Create a normalized state vector with recent price action and indicators - state_components = [] - - # Price features (normalize recent prices by the latest price) - latest_price = self.features['price'][-1] - - # Price change percentages over different timeframes - price_changes = [] - for period in [1, 3, 5, 10, 20]: - if len(self.features['price']) > period: - change = (self.features['price'][-1] / self.features['price'][-(period+1)] - 1.0) * 100 - price_changes.append(change) - else: - price_changes.append(0.0) - state_components.append(np.array(price_changes, dtype=np.float32) / 5.0) # Normalize by typical 5% move - - # Recent price pattern (last 10 prices normalized) - price_features = np.array(self.features['price'][-10:], dtype=np.float32) / latest_price - 1.0 - state_components.append(price_features) - - # Volume features (normalize by max volume) - max_vol = max(self.features['volume'][-20:]) if len(self.features['volume']) >= 20 else 1 - vol_features = np.array(self.features['volume'][-5:]) / max_vol - state_components.append(vol_features) - - # Technical indicators - rsi = np.array(self.features['rsi'][-3:]) / 100.0 # Scale to 0-1 - state_components.append(rsi) - - # MACD (normalize) - macd_vals = np.array(self.features['macd'][-3:]) - macd_signal = np.array(self.features['macd_signal'][-3:]) - macd_hist = np.array(self.features['macd_hist'][-3:]) - macd_scale = max(abs(np.max(macd_vals)), abs(np.min(macd_vals)), 1e-5) - macd_norm = macd_vals / macd_scale - macd_signal_norm = macd_signal / macd_scale - macd_hist_norm = macd_hist / macd_scale - - state_components.extend([macd_norm, macd_signal_norm, macd_hist_norm]) - - # Bollinger position (where is price relative to bands) - bb_upper = np.array(self.features['bollinger_upper'][-3:]) - bb_lower = np.array(self.features['bollinger_lower'][-3:]) - bb_mid = np.array(self.features['bollinger_mid'][-3:]) - price = np.array(self.features['price'][-3:]) - - # Calculate position of price within Bollinger Bands (0 to 1) - bb_pos = [(p - l) / (u - l) if u != l else 0.5 for p, u, l in zip(price, bb_upper, bb_lower)] - state_components.append(np.array(bb_pos)) - - # Bollinger band width (volatility indicator) - bb_width = [(u - l) / m for u, l, m in zip(bb_upper, bb_lower, bb_mid)] - state_components.append(np.array(bb_width) / 0.05) # Normalize by typical width - - # Stochastic oscillator - state_components.append(np.array(self.features['stoch_k'][-3:]) / 100.0) - state_components.append(np.array(self.features['stoch_d'][-3:]) / 100.0) - - # EMA trend indicators - if len(self.features['ema_9']) > 3 and len(self.features['ema_21']) > 3: - ema_9 = np.array(self.features['ema_9'][-3:]) - ema_21 = np.array(self.features['ema_21'][-3:]) - # EMA crossover indicator (-1 to 1) - ema_cross = (ema_9 - ema_21) / latest_price - state_components.append(ema_cross * 100) - else: - state_components.append(np.zeros(3)) - - # ATR volatility indicator (normalized) - if len(self.features['atr']) > 0: - atr = np.array(self.features['atr'][-3:]) - atr_norm = atr / latest_price * 100 # ATR as percentage of price - state_components.append(atr_norm / 2.0) # Normalize by typical 2% ATR - else: - state_components.append(np.zeros(3)) - - # Add recent volatility - recent_volatility = self.get_recent_volatility() * 100 # Convert to percentage - state_components.append(np.array([recent_volatility / 2.0])) # Normalize by typical 2% volatility - - # Add market direction indicator - market_direction = self.get_market_direction() - state_components.append(np.array([market_direction])) - - # Add time-based features (hour of day, day of week) - if len(self.data) > 0 and 'timestamp' in self.data[self.current_step]: - timestamp = self.data[self.current_step]['timestamp'] - # Hour of day (0-23) normalized to 0-1 - hour = timestamp.hour / 24.0 - # Day of week (0-6) normalized to 0-1 - day = timestamp.weekday() / 7.0 - # Is market typically high volatility time? (e.g., market open/close) - high_vol_time = 1.0 if (8 <= timestamp.hour <= 10 or 14 <= timestamp.hour <= 16) else 0.0 - state_components.append(np.array([hour, day, high_vol_time])) - else: - state_components.append(np.zeros(3)) - - # Position info - position_info = np.zeros(5) - if self.position == 'long': - position_info[0] = 1.0 # Position is long - position_info[1] = (latest_price - self.entry_price) / self.entry_price # Unrealized PnL % - position_info[2] = (self.stop_loss - self.entry_price) / self.entry_price # Stop loss % - position_info[3] = (self.take_profit - self.entry_price) / self.entry_price # Take profit % - position_info[4] = self.position_size / self.balance # Position size relative to balance - elif self.position == 'short': - position_info[0] = -1.0 # Position is short - position_info[1] = (self.entry_price - latest_price) / self.entry_price # Unrealized PnL % - position_info[2] = (self.entry_price - self.stop_loss) / self.entry_price # Stop loss % - position_info[3] = (self.entry_price - self.take_profit) / self.entry_price # Take profit % - position_info[4] = self.position_size / self.balance # Position size relative to balance - - state_components.append(position_info) - - # Combine all features - state = np.concatenate([comp.flatten() for comp in state_components]) - - # Replace any NaN values - state = np.nan_to_num(state, nan=0.0) - - # Ensure state has exactly STATE_SIZE elements - if len(state) > STATE_SIZE: - # Truncate if too long - state = state[:STATE_SIZE] - elif len(state) < STATE_SIZE: - # Pad with zeros if too short - padding = np.zeros(STATE_SIZE - len(state), dtype=np.float32) # Ensure float32 - state = np.concatenate([state, padding]) - - # Ensure float32 type - return state.astype(np.float32) - - def calculate_reward(self, action): - """Calculate the reward for the current action.""" - reward = 0 - info = {} - - # Get current market state - current_price = self.current_price - - # Only give significant rewards when a position is closed or when hitting TP/SL - if self.position != 'flat' and (action == 3 or self.last_action != action): - # Calculate direct PnL reward - if self.position == 'long': - pnl = current_price - self.entry_price - pnl_percent = pnl / self.entry_price * 100 - pnl_dollar = pnl_percent / 100 * self.position_size - else: # short - pnl = self.entry_price - current_price - pnl_percent = pnl / self.entry_price * 100 - pnl_dollar = pnl_percent / 100 * self.position_size - - # Subtract trading fees - pnl_dollar -= self.calculate_fees(self.position_size) - - # Asymmetric rewards (penalize losses more) - if pnl_dollar < 0: - reward = pnl_dollar * 150 # Higher penalty for losses - else: - reward = pnl_dollar * 100 # Reward for gains - - # Add a small bonus for closing positions in the right market direction - market_direction = self.get_market_direction() - if (self.position == 'long' and market_direction < -0.3) or (self.position == 'short' and market_direction > 0.3): - reward += 5 # Bonus for closing a position that's against the market direction - - # Track the reward components for analysis - info['pnl_reward'] = reward - else: - # Small negative reward for each action to discourage excessive trading - reward -= 0.5 - info['action_penalty'] = -0.5 - - # Small reward for proper directional trading - market_direction = self.get_market_direction() - if action == 1 and market_direction > 0.3: # BUY in uptrend - reward += 2 - info['direction_reward'] = 2 - elif action == 2 and market_direction < -0.3: # SELL in downtrend - reward += 2 - info['direction_reward'] = 2 - elif action == 0 and abs(market_direction) < 0.2: # HOLD in sideways - reward += 1 - info['direction_reward'] = 1 - - # Add volatility-based penalty for opening new positions in high volatility - if (action == 1 or action == 2) and self.position == 'flat': - volatility = self.get_recent_volatility() - if volatility > 0.015: # High volatility threshold - vol_penalty = -3 * volatility * 100 - reward += vol_penalty - info['volatility_penalty'] = vol_penalty - - # Keep track of total reward - info['total_reward'] = reward - - return reward, info - - def evaluate_entry_quality(self, position_type): - """Evaluate how good the entry timing was based on local extrema.""" - if len(self.features['price']) < 10: - return 0 - - # Get recent price window - recent_prices = self.features['price'][-10:] - - if position_type == 'long': - # For long positions, check if we bought near a local minimum - local_min = min(recent_prices) - entry_price = self.entry_price - - # Calculate how close we are to the local minimum (0 to 1 scale) - price_range = max(recent_prices) - local_min - if price_range > 0: - entry_quality = 1 - (entry_price - local_min) / price_range - else: - entry_quality = 0.5 # Neutral if no range - - return entry_quality - - elif position_type == 'short': - # For short positions, check if we sold near a local maximum - local_max = max(recent_prices) - entry_price = self.entry_price - - # Calculate how close we are to the local maximum (0 to 1 scale) - price_range = local_max - min(recent_prices) - if price_range > 0: - entry_quality = 1 - (local_max - entry_price) / price_range - else: - entry_quality = 0.5 # Neutral if no range - - return entry_quality - - return 0 # No position - - def get_recent_volatility(self): - """Calculate recent price volatility""" - if len(self.features['price']) < 10: - return 0 - - # Use ATR if available - if len(self.features['atr']) > 0: - return self.features['atr'][-1] / self.current_price - - # Otherwise calculate simple volatility - recent_prices = self.features['price'][-10:] - returns = [recent_prices[i] / recent_prices[i-1] - 1 for i in range(1, len(recent_prices))] - return np.std(returns) * 100 # Volatility as percentage - - def is_downtrend(self): - """Check if the market is in a downtrend""" - if len(self.features['price']) < 20: - return False - - # Use EMA to determine trend - short_ema = self.features['ema_9'][-1] - long_ema = self.features['ema_21'][-1] - - # Downtrend if short EMA is below long EMA - return short_ema < long_ema - - def is_uptrend(self): - """Check if the market is in an uptrend""" - if len(self.features['price']) < 20: - return False - - # Use EMA to determine trend - short_ema = self.features['ema_9'][-1] - long_ema = self.features['ema_21'][-1] - - # Uptrend if short EMA is above long EMA - return short_ema > long_ema - - def get_market_direction(self): - """Get the current market direction as a numeric value between -1 and 1""" - if len(self.features['price']) < 20: - return 0.0 # Neutral if not enough data - - # Use EMA to determine trend - if len(self.features['ema_9']) > 0 and len(self.features['ema_21']) > 0: - short_ema = self.features['ema_9'][-1] - long_ema = self.features['ema_21'][-1] - - # Calculate trend strength - if short_ema > long_ema: - # Uptrend - strength based on percentage difference - strength = min((short_ema / long_ema - 1) * 10, 1.0) - return strength # Value between 0 and 1 - else: - # Downtrend - strength based on percentage difference - strength = min((long_ema / short_ema - 1) * 10, 1.0) - return -strength # Value between -1 and 0 - - # Fallback to price-based trend detection - if len(self.features['price']) >= 20: - recent_prices = self.features['price'][-20:] - price_change = (recent_prices[-1] / recent_prices[0]) - 1 - - # Normalize to a value between -1 and 1 - return max(min(price_change * 10, 1.0), -1.0) - - return 0.0 # Neutral if no data - - def analyze_trades(self): - """Analyze completed trades to identify patterns""" - if not self.trades: - return {} - - analysis = { - 'total_trades': len(self.trades), - 'winning_trades': sum(1 for t in self.trades if t.get('pnl_dollar', 0) > 0), - 'losing_trades': sum(1 for t in self.trades if t.get('pnl_dollar', 0) <= 0), - 'avg_win': 0, - 'avg_loss': 0, - 'avg_duration': 0, - 'uptrend_win_rate': 0, - 'downtrend_win_rate': 0, - 'sideways_win_rate': 0 - } - - # Calculate averages - wins = [t.get('pnl_dollar', 0) for t in self.trades if t.get('pnl_dollar', 0) > 0] - losses = [t.get('pnl_dollar', 0) for t in self.trades if t.get('pnl_dollar', 0) <= 0] - durations = [t.get('duration', 0) for t in self.trades] - - analysis['avg_win'] = sum(wins) / len(wins) if wins else 0 - analysis['avg_loss'] = sum(losses) / len(losses) if losses else 0 - analysis['avg_duration'] = sum(durations) / len(durations) if durations else 0 - - # Calculate win rates by market direction - for direction in ['uptrend', 'downtrend', 'sideways']: - direction_trades = [t for t in self.trades if t.get('market_direction') == direction] - if direction_trades: - wins_in_direction = sum(1 for t in direction_trades if t.get('pnl_dollar', 0) > 0) - analysis[f'{direction}_win_rate'] = wins_in_direction / len(direction_trades) * 100 - - return analysis - - def initialize_price_predictor(self, device="cpu"): - """Initialize the price prediction model""" - # Check if we have enough data - if not self.data or len(self.data) < 30: - logger.warning("Not enough data to initialize price predictor (need at least 30 candles)") - return False - - # Extract price and volume history - price_history = np.array([candle['close'] for candle in self.data[-100:]]) - volume_history = np.array([candle['volume'] for candle in self.data[-100:]]) - - if len(price_history) == 0 or len(volume_history) == 0: - logger.warning("No price or volume data available for price predictor initialization") - return False - - # Initialize price predictor model - self.price_predictor = PricePredictionModel(input_size=2, hidden_size=256, output_size=5, num_layers=3) - self.price_predictor.to(device) - - # Initialize optimizer - self.price_optimizer = optim.Adam(self.price_predictor.parameters(), lr=0.001) - - # Initialize arrays for predicted prices and extrema - self.predicted_prices = np.array([]) - self.predicted_extrema = np.array([]) - - # Threshold for extrema prediction confidence - self.extrema_threshold = 0.7 # Threshold for extrema prediction confidence - - return True - - def train_price_predictor(self): - """Train the price prediction model on historical data with multi-timeframe support""" - if not hasattr(self, 'price_predictor') or self.price_predictor is None: - self.initialize_price_predictor() - - # Need enough data for all timeframes - if len(self.features['price']) < 30: - logger.warning("Not enough data to train price predictor (need at least 30 candles)") - return False - - try: - # Create optimizer if not already created - if not hasattr(self, 'price_predictor_optimizer'): - self.price_predictor_optimizer = optim.Adam(self.price_predictor.parameters(), lr=0.001) - - # Prepare multi-timeframe data - timeframe_prices = [] - timeframe_volumes = [] - timeframe_names = [] - - # Add 1-minute data - timeframe_prices.append(self.timeframe_data['1m']['prices']) - timeframe_volumes.append(self.timeframe_data['1m']['volumes']) - timeframe_names.append('1m') - - # Add 5-minute data if available - if '5m' in self.timeframe_data and len(self.timeframe_data['5m']) > 0 and len(self.timeframe_data['5m']['prices']) > 0: - timeframe_prices.append(self.timeframe_data['5m']['prices']) - timeframe_volumes.append(self.timeframe_data['5m']['volumes']) - timeframe_names.append('5m') - - # Add 15-minute data if available - if '15m' in self.timeframe_data and len(self.timeframe_data['15m']) > 0 and len(self.timeframe_data['15m']['prices']) > 0: - timeframe_prices.append(self.timeframe_data['15m']['prices']) - timeframe_volumes.append(self.timeframe_data['15m']['volumes']) - timeframe_names.append('15m') - - # Ensure we have at least one timeframe - if len(timeframe_prices) == 0: - logger.warning("No timeframe data available for training") - return False - - # Preprocess data for training - processed_data = self.price_predictor.preprocess(timeframe_prices, timeframe_volumes, timeframe_names) - - # Get the device that the model is on - device = next(self.price_predictor.parameters()).device - - # Create targets for each timeframe (next 5 candles) - targets = [] - for i, prices in enumerate(timeframe_prices): - if len(prices) > 5: - # Get the next 5 candles as targets - target_prices = np.array(prices[1:6]) - - # Scale targets using the same scaler - price_scaler = getattr(self.price_predictor, f'price_scaler_{i}', self.price_predictor.price_scaler) - target_prices = price_scaler.transform(target_prices.reshape(-1, 1)).flatten() - - # Create tensor and move to the same device as the model - target_tensor = torch.FloatTensor(target_prices).unsqueeze(0).to(device) - targets.append(target_tensor) - - # If we don't have targets, return - if len(targets) == 0: - logger.warning("No target data available for training") - return False - - # Find local extrema for extrema prediction training - extrema_targets = [] - for i, prices in enumerate(timeframe_prices): - if len(prices) > 5: - # Find peaks and troughs in the next 5 candles - peaks, troughs = find_local_extrema(prices[:10], window=2) - - # Create binary targets for each of the next 5 candles - extrema_target = np.zeros((1, 5, 2)) # [batch, time_steps, (low, high)] - - for j in range(5): - if j+1 in troughs: # +1 because we're looking at future candles - extrema_target[0, j, 0] = 1.0 # Low point - if j+1 in peaks: # +1 because we're looking at future candles - extrema_target[0, j, 1] = 1.0 # High point - - # Move to the same device as the model - extrema_tensor = torch.FloatTensor(extrema_target.reshape(1, -1)).to(device) - extrema_targets.append(extrema_tensor) - - # If we don't have extrema targets, create empty ones - if len(extrema_targets) == 0: - for _ in range(len(targets)): - extrema_targets.append(torch.zeros(1, 10).to(device)) # 5 candles * 2 (low/high) - - # Train for a few epochs - epochs = 5 - total_loss = 0 - - for epoch in range(epochs): - # Zero gradients - self.price_predictor_optimizer.zero_grad() - - # Forward pass - price_preds, extrema_preds = self.price_predictor(processed_data) - - # Calculate loss for each timeframe - price_loss = 0 - extrema_loss = 0 - - for i in range(len(targets)): - if i < len(price_preds): - # Price prediction loss - ensure shapes match - price_pred = price_preds[i] - price_target = targets[i] - - # Make sure both have the same shape - if price_target.shape != price_pred.shape: - if len(price_target.shape) > len(price_pred.shape): - price_target = price_target.squeeze(0) - elif len(price_pred.shape) > len(price_target.shape): - price_pred = price_pred.squeeze(0) - - price_loss += F.mse_loss(price_pred, price_target) - - # Extrema prediction loss - ensure shapes match - extrema_target = extrema_targets[i] - extrema_pred = extrema_preds[i] - - # Make sure both have the same shape - if extrema_target.shape != extrema_pred.shape: - if len(extrema_target.shape) > len(extrema_pred.shape): - extrema_target = extrema_target.squeeze(0) - elif len(extrema_pred.shape) > len(extrema_target.shape): - extrema_pred = extrema_pred.squeeze(0) - - extrema_loss += F.binary_cross_entropy(extrema_pred, extrema_target) - - # Combined loss - loss = price_loss + 0.5 * extrema_loss - - # Backward pass and optimize - loss.backward() - self.price_predictor_optimizer.step() - - total_loss += loss.item() - - # Update predictions - self.update_price_predictions() - - logger.info(f"Price predictor trained for {epochs} epochs, avg loss: {total_loss/epochs:.6f}") - return total_loss / epochs - - except Exception as e: - logger.error(f"Error training price predictor: {e}") - logger.error(traceback.format_exc()) - return False - - def update_price_predictions(self): - """Update price and extrema predictions using the multi-timeframe model""" - if not hasattr(self, 'price_predictor') or self.price_predictor is None: - return - - # Check if we have price and volume data - if len(self.features['price']) == 0 or len(self.features['volume']) == 0: - logger.warning("No price or volume data available for price predictions") - return - - # Update timeframe data - self.timeframe_data['1m']['prices'] = self.features['price'].copy() - self.timeframe_data['1m']['volumes'] = self.features['volume'].copy() - - # Update higher timeframes if we have enough data - if len(self.features['price']) >= 15: - # Update 5-minute data - self.timeframe_data['5m'] = { - 'prices': [self.features['price'][i] for i in range(0, len(self.features['price']), 5)], - 'volumes': [sum(self.features['volume'][i:i+5]) for i in range(0, len(self.features['volume']), 5) if i+5 <= len(self.features['volume'])] - } - - # Update 15-minute data - self.timeframe_data['15m'] = { - 'prices': [self.features['price'][i] for i in range(0, len(self.features['price']), 15)], - 'volumes': [sum(self.features['volume'][i:i+15]) for i in range(0, len(self.features['volume']), 15) if i+15 <= len(self.features['volume'])] - } - - # Prepare multi-timeframe data - timeframe_prices = [] - timeframe_volumes = [] - timeframe_names = [] - - # Add 1-minute data - timeframe_prices.append(self.timeframe_data['1m']['prices']) - timeframe_volumes.append(self.timeframe_data['1m']['volumes']) - timeframe_names.append('1m') - - # Add 5-minute data if available - if len(self.timeframe_data['5m']) > 0 and len(self.timeframe_data['5m']['prices']) > 0: - timeframe_prices.append(self.timeframe_data['5m']['prices']) - timeframe_volumes.append(self.timeframe_data['5m']['volumes']) - timeframe_names.append('5m') - - # Add 15-minute data if available - if len(self.timeframe_data['15m']) > 0 and len(self.timeframe_data['15m']['prices']) > 0: - timeframe_prices.append(self.timeframe_data['15m']['prices']) - timeframe_volumes.append(self.timeframe_data['15m']['volumes']) - timeframe_names.append('15m') - - # Get predictions - try: - price_predictions, extrema_predictions = self.price_predictor.predict_next_candles( - timeframe_prices, - timeframe_volumes, - timeframe_names - ) - - # Store predictions - self.price_predictions = price_predictions - self.extrema_predictions = extrema_predictions - - # Check for predicted extrema in the next few candles - self.has_predicted_low = False - self.has_predicted_high = False - - # Check first 3 candles for extrema - for i in range(min(3, len(extrema_predictions))): - # Check if probability of low is high - if extrema_predictions[i, 0] > 0.7: - self.has_predicted_low = True - - # Check if probability of high is high - if extrema_predictions[i, 1] > 0.7: - self.has_predicted_high = True - - # Log predictions - logger.info(f"Price predictions: {price_predictions}") - logger.info(f"Extrema predictions: {extrema_predictions}") - logger.info(f"Predicted low: {self.has_predicted_low}, Predicted high: {self.has_predicted_high}") - - return price_predictions, extrema_predictions - except Exception as e: - logger.error(f"Error in price prediction: {e}") - logger.error(traceback.format_exc()) - return None, None - - def identify_optimal_trades(self): - """Identify optimal entry and exit points based on local extrema and volume""" - if len(self.features['price']) < 20: - return - - # Find local bottoms and tops with volume confirmation - bottoms, tops = find_local_extrema( - self.features['price'], - window=5, - volumes=self.features['volume'], - volume_threshold=0.7 - ) - - # Store optimal trade points - self.optimal_bottoms = bottoms # Buy points - self.optimal_tops = tops # Sell points - - # Create optimal trade signals - self.optimal_signals = np.zeros(len(self.features['price'])) - for i in bottoms: - if 0 <= i < len(self.optimal_signals): # Ensure index is valid - self.optimal_signals[i] = 1 # Buy signal - for i in tops: - if 0 <= i < len(self.optimal_signals): # Ensure index is valid - self.optimal_signals[i] = -1 # Sell signal - - logger.info(f"Identified {len(bottoms)} optimal buy points and {len(tops)} optimal sell points") - - # Integrate predicted extrema into decision making - if hasattr(self, 'predicted_extrema') and len(self.predicted_extrema) > 0: - # Check if we predict any extrema in the near future - has_predicted_low = any(p_low > self.extrema_threshold for p_low, _ in self.predicted_extrema) - has_predicted_high = any(p_high > self.extrema_threshold for _, p_high in self.predicted_extrema) - - if has_predicted_low: - logger.info("Predicting a significant low point in the next few candles") - if has_predicted_high: - logger.info("Predicting a significant high point in the next few candles") - - # Store these predictions for use in action selection - self.has_predicted_low = has_predicted_low - self.has_predicted_high = has_predicted_high - - def calculate_position_size(self): - """Calculate position size based on current balance, volatility, win rate, and risk parameters using Kelly criterion""" - # Get recent volatility - volatility = self.get_recent_volatility() - - # Calculate win rate based on recent trades - recent_trades = self.trades[-20:] if len(self.trades) > 0 else [] - win_count = sum(1 for trade in recent_trades if trade.get('pnl_dollar', 0) > 0) - total_trades = len(recent_trades) - - # Calculate win rate (default to 0.5 if not enough trades) - win_rate = win_count / total_trades if total_trades >= 5 else 0.5 - - # Calculate average win and loss sizes - wins = [trade.get('pnl_dollar', 0) for trade in recent_trades if trade.get('pnl_dollar', 0) > 0] - losses = [abs(trade.get('pnl_dollar', 0)) for trade in recent_trades if trade.get('pnl_dollar', 0) < 0] - - avg_win = sum(wins) / len(wins) if len(wins) > 0 else 1.0 - avg_loss = sum(losses) / len(losses) if len(losses) > 0 else 1.0 - - # Calculate Kelly fraction - if avg_loss > 0: - w = win_rate - r = avg_win / avg_loss # Profit/loss ratio - kelly_fraction = (w * (r + 1) - 1) / r if r > 0 else 0 - else: - kelly_fraction = 0.02 # Default to 2% if no loss data available - - # Apply safety factor and constraints - kelly_fraction = max(0.01, min(kelly_fraction, 0.2)) # Cap between 1% and 20% - - # Reduce position size during high volatility - volatility_factor = 1.0 / (1.0 + volatility * 10) - kelly_fraction *= volatility_factor - - # Apply drawdown protection - reduce position size after losses - if len(self.trades) >= 3: - recent_results = [trade.get('pnl_dollar', 0) for trade in self.trades[-3:]] - consecutive_losses = sum(1 for pnl in recent_results if pnl < 0) - if consecutive_losses >= 2: - # Reduce position size by 50% after 2 consecutive losses - kelly_fraction *= 0.5 - - # Calculate position size with Kelly and leverage - kelly_position = self.balance * kelly_fraction * MAX_LEVERAGE - - # Ensure minimum position size - min_position = 10.0 # Minimum position size in USD - position_size = max(kelly_position, min(min_position, self.balance * 0.5)) - - # Ensure position size doesn't exceed balance * leverage - max_position = self.balance * MAX_LEVERAGE - position_size = min(position_size, max_position) - - # Adjust stop loss and take profit based on volatility and win rate - global STOP_LOSS_PERCENT, TAKE_PROFIT_PERCENT - - # Wider stops in high volatility, tighter stops in low volatility - STOP_LOSS_PERCENT = 0.5 * (1 + volatility * 10) - - # Adjust take profit based on win rate and volatility - # Higher win rate = we can afford tighter take profits - # Lower win rate = need higher take profits to compensate - risk_reward_ratio = 1.5 * (1 / (win_rate + 0.2)) # Higher ratio for lower win rates - TAKE_PROFIT_PERCENT = STOP_LOSS_PERCENT * risk_reward_ratio - - # Apply risk factor from curriculum learning - position_size *= self.risk_factor - - return position_size - - def calculate_fees(self, position_size): - """Calculate trading fees for a given position size""" - # Typical fee rate for crypto exchanges (0.1%) - fee_rate = 0.001 - - # Calculate fee - fee = position_size * fee_rate - - return fee - - def reset(self): - """Reset the environment to initial state""" - self.balance = self.initial_balance - self.position = 'flat' - self.position_size = 0 - self.entry_price = 0 - self.entry_index = 0 - self.stop_loss = 0 - self.take_profit = 0 - self.trades = [] - self.win_count = 0 - self.loss_count = 0 - self.episode_pnl = 0.0 - self.peak_balance = self.initial_balance - self.max_drawdown = 0.0 - self.current_step = 0 - self.last_action = 0 # Reset to HOLD - - # Keep data but reset current position - if len(self.data) > self.window_size: - self.current_step = self.window_size - self.current_price = self.data[self.current_step]['close'] - - return self.get_state() - - def add_data(self, candle): - """Add a new candle to the data""" - from data_cache import ohlcv_cache - - # Add candle to data - self.data.append(candle) - - # Update features - self._update_features() - self.current_price = candle['close'] - - # Cache the new candle - try: - # Use ETH/USDT as default symbol and 1m as default timeframe - # In a real implementation, you would track the actual symbol and timeframe - ohlcv_cache.append(candle, "ETH/USDT", "1m") - except Exception as e: - logger.error(f"Error caching new candle: {e}") - - return True - -# Ensure GPU usage if available -def get_device(device_preference='auto'): - """Get the device to use (GPU or CPU) based on preference and availability""" - if device_preference.lower() in ['gpu', 'auto'] and torch.cuda.is_available(): - device = torch.device("cuda") - # Set default tensor type to float32 for CUDA - torch.set_default_tensor_type(torch.FloatTensor) - logger.info(f"Using GPU: {torch.cuda.get_device_name(0)}") - else: - device = torch.device("cpu") - if device_preference.lower() in ['gpu', 'auto']: - logger.info("GPU requested but not available, using CPU instead") - else: - logger.info("Using CPU as requested") - return device - -# Update Agent class to use GPU properly -class Agent: - def __init__(self, state_size, action_size, hidden_size=256, lstm_layers=2, attention_heads=4, device=None): - # Set device - self.device = device if device is not None else get_device() - - # Model parameters - self.state_size = state_size - self.action_size = action_size - self.hidden_size = hidden_size - self.lstm_layers = lstm_layers - - # Initialize networks - self.policy_net = DQN(state_size, action_size, hidden_size, lstm_layers, attention_heads).to(self.device) - self.target_net = DQN(state_size, action_size, hidden_size, lstm_layers, attention_heads).to(self.device) - self.target_net.load_state_dict(self.policy_net.state_dict()) - self.target_net.eval() # Set target network to evaluation mode - - # Optimizer with lower learning rate for stability - self.optimizer = optim.Adam(self.policy_net.parameters(), lr=0.0001) - self.scheduler = optim.lr_scheduler.StepLR(self.optimizer, step_size=5000, gamma=0.5) - - # Replay memory with prioritized experience replay - increased capacity - self.memory = ReplayMemory(capacity=200000, alpha=0.6, beta=0.4, n_step=3, gamma=0.99) - - # Exploration parameters - slower decay for more exploration - self.eps_start = 1.0 - self.eps_end = 0.05 - self.eps_decay = 0.9999 # Slower decay - self.epsilon = self.eps_start - - # Learning parameters - self.gamma = 0.99 - self.batch_size = 128 # Increased batch size - self.target_update = 500 # Update target network more frequently - - # Training tracking - self.steps_done = 0 - self.episodes_done = 0 - self.market_regime = None # Current detected market regime - - # LSTM hidden state - self.hidden = None - - # Tracking performance for adaptive learning - self.recent_losses = deque(maxlen=100) - self.training_iterations = 0 - self.recent_rewards = deque(maxlen=100) - - def expand_model(self, new_state_size, new_hidden_size=512, new_lstm_layers=3, new_attention_heads=8): - """Expand the model to handle more features or increase capacity""" - logger.info(f"Expanding model: {self.state_size} → {new_state_size}, " - f"hidden: {self.policy_net.hidden_size} → {new_hidden_size}") - - # Save old weights - old_state_dict = self.policy_net.state_dict() - - # Create new larger networks - new_policy_net = DQN(new_state_size, self.action_size, - new_hidden_size, new_lstm_layers, new_attention_heads).to(self.device) - new_target_net = DQN(new_state_size, self.action_size, - new_hidden_size, new_lstm_layers, new_attention_heads).to(self.device) - - # Transfer weights for common layers - new_state_dict = new_policy_net.state_dict() - for name, param in old_state_dict.items(): - if name in new_state_dict: - # If shapes match, copy directly - if new_state_dict[name].shape == param.shape: - new_state_dict[name] = param - # For first layer, copy weights for the original input dimensions - elif name == "fc1.weight": - new_state_dict[name][:, :self.state_size] = param - # For other layers, initialize with a strategy that preserves scale - else: - logger.info(f"Layer {name} shapes don't match: {param.shape} vs {new_state_dict[name].shape}") - - # Load transferred weights - new_policy_net.load_state_dict(new_state_dict) - new_target_net.load_state_dict(new_state_dict) - - # Replace networks - self.policy_net = new_policy_net - self.target_net = new_target_net - self.target_net.eval() - - # Update optimizer - self.optimizer = optim.Adam(self.policy_net.parameters(), lr=LEARNING_RATE) - - # Update state size - self.state_size = new_state_size - - # Print new model size - total_params = sum(p.numel() for p in self.policy_net.parameters()) - logger.info(f"New model size: {total_params:,} parameters") - - return True - - def select_action(self, state, training=True): - """Select an action using epsilon-greedy policy with market regime awareness""" - # Update epsilon - self.epsilon = max(self.eps_end, self.epsilon * self.eps_decay) - - # Convert state to tensor - state = torch.FloatTensor(state).to(self.device) - - # Add batch dimension if needed - if state.dim() == 1: - state = state.unsqueeze(0) - - # Epsilon-greedy action selection - if training and random.random() < self.epsilon: - # Random action - action = random.randrange(self.action_size) - # Reset hidden state when taking random actions - self.hidden = None - return action - - # Get action from policy network - with torch.no_grad(): - q_values, self.hidden, market_regime = self.policy_net(state, self.hidden) - - # Update market regime information - self.market_regime = torch.softmax(market_regime, dim=1).cpu().numpy()[0] - - # Get best action - action = q_values.max(1)[1].item() - - return action - - def learn(self): - """Update the network weights using prioritized experience replay and n-step returns""" - if len(self.memory) < self.batch_size: - return 0.0 # Not enough samples yet - - # Sample batch with priorities - batch, indices, weights = self.memory.sample(self.batch_size) - - # Convert weights to tensor - weights = torch.FloatTensor(weights).to(self.device) - - # Extract batch components - state_batch = torch.FloatTensor(np.array(batch.state)).to(self.device) - action_batch = torch.LongTensor(np.array(batch.action)).to(self.device) - reward_batch = torch.FloatTensor(np.array(batch.reward)).to(self.device) - next_state_batch = torch.FloatTensor(np.array(batch.next_state)).to(self.device) - done_batch = torch.FloatTensor(np.array(batch.done)).to(self.device) - - # Compute Q values for current states - q_values, _, _ = self.policy_net(state_batch) - q_values = q_values.gather(1, action_batch.unsqueeze(1)).squeeze(1) - - # Compute Q values for next states with Double Q-learning - with torch.no_grad(): - # Get actions from policy network - next_q_values, _, _ = self.policy_net(next_state_batch) - next_actions = next_q_values.max(1)[1].unsqueeze(1) - - # Get Q values from target network for those actions - next_target_q_values, _, _ = self.target_net(next_state_batch) - next_target_values = next_target_q_values.gather(1, next_actions).squeeze(1) - - # Zero out values for terminal states - next_target_values = next_target_values * (1 - done_batch) - - # Compute target Q values - target_q_values = reward_batch + (self.gamma ** self.memory.n_step) * next_target_values - - # Compute loss with importance sampling weights - td_errors = target_q_values - q_values - loss = (weights * (td_errors ** 2)).mean() - - # Update priorities in replay buffer - self.memory.update_priorities(indices, td_errors.detach().cpu().numpy()) - - # Optimize the model - self.optimizer.zero_grad() - loss.backward() - - # Clip gradients to prevent exploding gradients - torch.nn.utils.clip_grad_norm_(self.policy_net.parameters(), 1.0) - - self.optimizer.step() - self.scheduler.step() - - # Update target network periodically - if self.steps_done % self.target_update == 0: - self.update_target_network() - - # Track loss for adaptive learning - loss_item = loss.item() - self.recent_losses.append(loss_item) - self.training_iterations += 1 - - # Every 1000 training iterations, analyze performance and adjust hyperparameters - if self.training_iterations % 1000 == 0 and len(self.recent_losses) > 50: - avg_loss = sum(self.recent_losses) / len(self.recent_losses) - # If loss is stable and low, we can accelerate learning - if avg_loss < 0.1 and np.std(list(self.recent_losses)) < 0.05: - # Increase learning rate slightly - for param_group in self.optimizer.param_groups: - param_group['lr'] = min(param_group['lr'] * 1.2, 0.001) - # If loss is unstable or high, slow down learning - elif avg_loss > 1.0 or np.std(list(self.recent_losses)) > 0.5: - # Decrease learning rate - for param_group in self.optimizer.param_groups: - param_group['lr'] = max(param_group['lr'] * 0.5, 0.00001) - - self.steps_done += 1 - - return loss_item - - def update_target_network(self): - self.target_net.load_state_dict(self.policy_net.state_dict()) - - def save(self, path): - """Save model to path""" - try: - # Create directory if it doesn't exist - directory = os.path.dirname(path) - if MODEL_DIR in directory: - # Already using our directory - os.makedirs(directory, exist_ok=True) - else: - # Replace default models dir with our new one - new_directory = os.path.join(MODEL_DIR, os.path.basename(directory)) if directory else MODEL_DIR - os.makedirs(new_directory, exist_ok=True) - path = os.path.join(new_directory, os.path.basename(path)) - - # Save model state - torch.save({ - 'policy_net': self.policy_net.state_dict(), - 'target_net': self.target_net.state_dict(), - 'optimizer': self.optimizer.state_dict(), - 'steps_done': self.steps_done - }, path) - - logger.info(f"Model saved to {path}") - except Exception as e: - logger.error(f"Failed to save model: {e}") - logger.error(f"Traceback: {traceback.format_exc()}") - - def load(self, path): - """Load model from path""" - try: - # Check if path exists, if not try with MODEL_DIR - if not os.path.exists(path): - alt_path = os.path.join(MODEL_DIR, os.path.basename(path)) - if os.path.exists(alt_path): - path = alt_path - else: - logger.warning(f"Could not find model at {path} or {alt_path}") - return False - - logger.info(f"Loading model from {path}") - - # Load checkpoint - checkpoint = torch.load(path, map_location=self.device) - - try: - # Try to load with strict matching - self.policy_net.load_state_dict(checkpoint['policy_net']) - self.target_net.load_state_dict(checkpoint['target_net']) - self.optimizer.load_state_dict(checkpoint['optimizer']) - self.steps_done = checkpoint['steps_done'] - return True - except Exception as e: - logger.warning(f"Could not load with weights_only=True: {e}") - logger.warning("Attempting to load with weights_only=False (less secure)") - print() - - try: - # Load policy network only, ignoring optimizer - self.policy_net.load_state_dict(checkpoint['policy_net']) - self.target_net.load_state_dict(checkpoint['target_net']) - return True - except Exception as e: - logger.error(f"Failed to load model: {e}") - logger.error(f"Traceback: {traceback.format_exc()}") - return False - - except FileNotFoundError: - logger.warning(f"Model file not found: {path}") - return False - except Exception as e: - logger.error(f"Error loading model: {e}") - logger.error(f"Traceback: {traceback.format_exc()}") - return False - -async def get_live_prices(symbol="ETH/USDT", timeframe="1m"): - """Get live price data using websockets""" - # Connect to MEXC websocket - uri = "wss://stream.mexc.com/ws" - - async with websockets.connect(uri) as websocket: - # Subscribe to kline data - subscribe_msg = { - "method": "SUBSCRIPTION", - "params": [f"spot@public.kline.v3.api@{symbol.replace('/', '').lower()}@{timeframe}"] - } - await websocket.send(json.dumps(subscribe_msg)) - - logger.info(f"Connected to MEXC websocket, subscribed to {symbol} {timeframe} klines") - - while True: - try: - response = await websocket.recv() - data = json.loads(response) - - if 'data' in data: - kline = data['data'] - candle = { - 'timestamp': kline['t'], - 'open': float(kline['o']), - 'high': float(kline['h']), - 'low': float(kline['l']), - 'close': float(kline['c']), - 'volume': float(kline['v']) - } - yield candle - - except Exception as e: - logger.error(f"Websocket error: {e}") - # Try to reconnect - await asyncio.sleep(5) - break - -async def train_agent(agent, env, num_episodes=1000, max_steps_per_episode=1000, exchange=None, args=None, continuous=False): - """Train the agent on the environment""" - start_time = time.time() - episode_rewards = [] - episode_lengths = [] - balances = [] - win_rates = [] - cumulative_pnl = [] - episode_pnls = [] - drawdowns = [] - prediction_accuracies = [] - - # Set up TensorBoard logging - writer = SummaryWriter(log_dir=f"{MODEL_DIR}/tensorboard_logs") - - # Track best models - best_reward = float('-inf') - best_profit = float('-inf') - best_win_rate = 0 - - # For curriculum learning - risk_factor = 0.5 # Start with reduced risk - - # Initialize for periodic evaluation and model saving - eval_interval = 5 # Evaluate every 5 episodes - save_interval = 10 # Save model every 10 episodes - - for episode in range(num_episodes): - # Reset the environment - state = env.reset() - episode_reward = 0 - step = 0 - prediction_accuracy = 0 - - # For curriculum learning - gradually increase risk factor - env.risk_factor = min(1.0, risk_factor + episode * 0.01) - - # Optionally refresh data at beginning of episode - if continuous and args and args.refresh_data: - logger.info(f"Refreshing market data with timeframe {args.timeframe}...") - await env.fetch_initial_data(exchange, symbol=args.symbol if args else "ETH/USDT", - timeframe=args.timeframe if args else "1m", - limit=1000) - - # Train price predictor if enough data is available - if hasattr(env, 'train_price_predictor'): - if len(env.features['price']) > 30: # Need at least 30 candles - env.train_price_predictor() - else: - logger.warning("Not enough data to train price predictor (need at least 30 candles)") - - # Log episode info - if continuous: - logger.info(f"Continuous training - Episode {episode+1}") - else: - logger.info(f"Episode {episode+1}/{num_episodes}") - - # Reset hidden state - agent.hidden = None - - done = False - - # Episode loop - while not done and step < max_steps_per_episode: - # Select action - action = agent.select_action(state) - - # Take action - next_state, reward, done, info = env.step(action) - - # Store transition in replay memory - agent.memory.push(state, action, reward, next_state, done) - - # Learn from experience - loss = agent.learn() - - # Update state and statistics - state = next_state - episode_reward += reward - step += 1 - - # Fetch new data in continuous mode - if continuous and step % 10 == 0 and exchange is not None: - await env.fetch_new_data(exchange, symbol=args.symbol if args else "ETH/USDT") - - # End of episode - episode_rewards.append(episode_reward) - episode_lengths.append(step) - balances.append(env.balance) - win_rate = env.win_count / (env.win_count + env.loss_count) * 100 if (env.win_count + env.loss_count) > 0 else 0 - win_rates.append(win_rate) - episode_pnls.append(env.episode_pnl) - cumulative_pnl.append(env.total_pnl) - drawdowns.append(env.max_drawdown) - prediction_accuracies.append(prediction_accuracy) - - # Log episode statistics - logger.info(f"Episode {episode+1}: Reward={episode_reward:.2f}, Balance=${env.balance:.2f}, Win Rate={win_rate:.1f}%, " - f"Trades={env.win_count + env.loss_count}, Episode PnL=${env.episode_pnl:.2f}, " - f"Total PnL=${env.total_pnl:.2f}") - - # Log to TensorBoard - writer.add_scalar('Reward/episode', episode_reward, episode) - writer.add_scalar('Balance/episode', env.balance, episode) - writer.add_scalar('WinRate/episode', win_rate, episode) - writer.add_scalar('Trades/episode', env.win_count + env.loss_count, episode) - writer.add_scalar('PnL/episode', env.episode_pnl, episode) - writer.add_scalar('PnL/cumulative', env.total_pnl, episode) - writer.add_scalar('MaxDrawdown/episode', env.max_drawdown, episode) - writer.add_scalar('Loss/episode', loss if loss else 0, episode) - - # Save models periodically - if (episode + 1) % save_interval == 0 or episode == num_episodes - 1: - model_path = f"{MODEL_DIR}/trading_agent_{episode+1}.pt" - agent.save(model_path) - - # Visualize trading and save images - visualize_training_results(env, agent, episode) - - # Save best models based on different metrics - if episode_reward > best_reward: - best_reward = episode_reward - agent.save(f"{MODEL_DIR}/trading_agent_best_reward.pt") - - if env.balance > best_profit: - best_profit = env.balance - agent.save(f"{MODEL_DIR}/trading_agent_best_pnl.pt") - - if win_rate > best_win_rate and env.win_count + env.loss_count >= 10: - best_win_rate = win_rate - agent.save(f"{MODEL_DIR}/trading_agent_best_winrate.pt") - - # For continuous training, save a continuous model - if continuous: - agent.save(f"{MODEL_DIR}/trading_agent_continuous_{episode+1}.pt") - logger.info(f"Saved continuous model: {MODEL_DIR}/trading_agent_continuous_{episode+1}.pt") - - # Update risk factor for curriculum learning - if env.balance > INITIAL_BALANCE: - # Doing well, increase risk - risk_factor = min(1.0, risk_factor * 1.05) - else: - # Doing poorly, reduce risk - risk_factor = max(0.2, risk_factor * 0.95) - - # End of training - elapsed_time = time.time() - start_time - logger.info(f"Training completed in {elapsed_time:.2f}s") - - # Plot the results - plot_training_results({ - 'episode_rewards': episode_rewards, - 'balances': balances, - 'win_rates': win_rates, - 'episode_pnls': episode_pnls, - 'cumulative_pnl': cumulative_pnl, - 'drawdowns': drawdowns - }) - - # Close TensorBoard writer - writer.close() - - return episode_rewards, balances, win_rates - -def plot_training_results(stats): - """Plot training results""" - # Create figure with subplots - fig, axs = plt.subplots(3, 2, figsize=(15, 12)) - fig.suptitle('Trading Agent Training Results', fontsize=16) - - # Plot rewards - axs[0, 0].plot(stats['episode_rewards'], 'b-') - axs[0, 0].set_title('Episode Rewards') - axs[0, 0].set_xlabel('Episode') - axs[0, 0].set_ylabel('Reward') - axs[0, 0].grid(True) - - # Plot episode profits instead of balances - axs[0, 1].plot(stats['episode_profits'], 'g-') - axs[0, 1].set_title('Episode Profits') - axs[0, 1].set_xlabel('Episode') - axs[0, 1].set_ylabel('Profit ($)') - axs[0, 1].grid(True) - - # Plot win rates - axs[1, 0].plot(stats['win_rates'], 'r-') - axs[1, 0].set_title('Win Rate') - axs[1, 0].set_xlabel('Episode') - axs[1, 0].set_ylabel('Win Rate (%)') - axs[1, 0].grid(True) - - # Plot trade counts - axs[1, 1].plot(stats['trade_counts'], 'm-') - axs[1, 1].set_title('Trade Counts') - axs[1, 1].set_xlabel('Episode') - axs[1, 1].set_ylabel('Number of Trades') - axs[1, 1].grid(True) - - # Plot cumulative profits (calculated from episode_profits) - if len(stats['episode_profits']) > 0: - cumulative_profits = np.cumsum(stats['episode_profits']) - axs[2, 0].plot(cumulative_profits, 'c-') - axs[2, 0].set_title('Cumulative Profits') - axs[2, 0].set_xlabel('Episode') - axs[2, 0].set_ylabel('Cumulative Profit ($)') - axs[2, 0].grid(True) - - # Plot prediction accuracy - axs[2, 1].plot(stats['prediction_accuracies'], 'y-') - axs[2, 1].set_title('Prediction Accuracy') - axs[2, 1].set_xlabel('Episode') - axs[2, 1].set_ylabel('Accuracy (%)') - axs[2, 1].grid(True) - - plt.tight_layout(rect=[0, 0, 1, 0.96]) - plt.savefig('training_results.png') - plt.close() - -def evaluate_agent(agent, env, num_episodes=10): - """Evaluate the agent on test data""" - total_reward = 0 - total_profit = 0 - total_trades = 0 - winning_trades = 0 - - for episode in range(num_episodes): - state = env.reset() - episode_reward = 0 - initial_balance = env.balance - - done = False - while not done: - # Select action (no exploration) - action = agent.select_action(state, training=False) - next_state, reward, done, info = env.step(action) - - state = next_state - episode_reward += reward - - total_reward += episode_reward - total_profit += env.balance - initial_balance - - # Count trades and wins - for trade in env.trades: - if 'pnl_percent' in trade: - total_trades += 1 - if trade['pnl_percent'] > 0: - winning_trades += 1 - - # Calculate averages - avg_reward = total_reward / num_episodes - avg_profit = total_profit / num_episodes - win_rate = winning_trades / total_trades * 100 if total_trades > 0 else 0 - - logger.info(f"Evaluation results: Avg Reward={avg_reward:.2f}, Avg Profit=${avg_profit:.2f}, " - f"Win Rate={win_rate:.1f}%") - - return avg_reward, avg_profit, win_rate - -async def test_training(): - """Test the training process with a small number of episodes""" - logger.info("Starting training tests...") - - # Initialize exchange - exchange = ccxt.mexc({ - 'apiKey': MEXC_API_KEY, - 'secret': MEXC_SECRET_KEY, - 'enableRateLimit': True, - }) - - try: - # Create environment with small initial balance for testing - env = TradingEnvironment( - exchange=exchange, - symbol="ETH/USDT", - timeframe="1m", - leverage=MAX_LEVERAGE, - initial_balance=100, # Small balance for testing - demo=True # Always use demo mode for testing - ) - - # Fetch initial data - await env.fetch_initial_data(exchange, "ETH/USDT", "1m", 1000) - - # Create agent - agent = Agent(state_size=STATE_SIZE, action_size=env.action_space) - - # Run a few test episodes - test_episodes = 3 - logger.info(f"Running {test_episodes} test episodes...") - - for episode in range(test_episodes): - state = env.reset() - episode_reward = 0 - done = False - step = 0 - - while not done and step < 100: # Limit steps for testing - # Select action - action = agent.select_action(state) - - # Take action - next_state, reward, done, info = env.step(action) - - # Store experience - agent.memory.push(state, action, reward, next_state, done) - - # Learn - loss = agent.learn() - - state = next_state - episode_reward += reward - step += 1 - - # Print progress - if step % 10 == 0: - logger.info(f"Episode {episode + 1}, Step {step}, Reward: {episode_reward:.2f}") - - logger.info(f"Test episode {episode + 1} completed with reward: {episode_reward:.2f}") - - # Test model saving - try: - agent.save("models/test_model.pt") - logger.info("Successfully saved model") - except Exception as e: - logger.error(f"Error saving model: {e}") - - logger.info("Training tests completed successfully") - return True - - except Exception as e: - logger.error(f"Training test failed: {e}") - return False - - finally: - await exchange.close() - -async def initialize_exchange(): - """Initialize the exchange connection""" - try: - # Try to initialize with async support first - try: - exchange = ccxt.pro.mexc({ - 'apiKey': MEXC_API_KEY, - 'secret': MEXC_SECRET_KEY, - 'enableRateLimit': True - }) - logger.info(f"Exchange initialized with async support: {exchange.id}") - except (AttributeError, ImportError): - # Fall back to standard CCXT - exchange = ccxt.mexc({ - 'apiKey': MEXC_API_KEY, - 'secret': MEXC_SECRET_KEY, - 'enableRateLimit': True - }) - logger.info(f"Exchange initialized with standard CCXT: {exchange.id}") - - return exchange - except Exception as e: - logger.error(f"Failed to initialize exchange: {e}") - raise - -async def get_historical_data(exchange, symbol="ETH/USDT", timeframe="1m", limit=1000): - """Fetch historical OHLCV data from the exchange""" - try: - logger.info(f"Fetching historical data for {symbol}, timeframe {timeframe}, limit {limit}") - - # Use the refactored fetch method - data = await fetch_ohlcv_data(exchange, symbol, timeframe, limit) - - if not data: - logger.warning("No historical data received") - - return data - except Exception as e: - logger.error(f"Failed to fetch historical data: {e}") - return [] - -async def live_trading(agent, env, exchange, demo=True): - """Run live trading with the trained agent""" - logger.info(f"Starting live trading (demo mode: {demo})") - - try: - # Initialize trading client for live trading if not in demo mode - trading_client = None - if not demo: - trading_client = MexcTradingClient(symbol="ETH/USDT") - if not trading_client.client: - logger.error("Failed to initialize MEXC trading client. Check API keys.") - return - - # Update environment with trading client - env.trading_client = trading_client - - # Fetch initial account balance - balance = await trading_client.fetch_account_balance() - if balance > 0: - logger.info(f"Initial account balance: ${balance:.2f}") - env.balance = balance - env.initial_balance = balance - else: - logger.warning("Could not fetch account balance, using default") - - # Subscribe to websocket for real-time data - symbol = "ETH/USDT" - timeframe = "1m" - - # Initialize with historical data - success = await env.fetch_initial_data(exchange, symbol, timeframe, 100) - if not success: - logger.error("Failed to initialize with historical data") - return - - # Initialize price predictor - env.initialize_price_predictor(device=agent.device) - logger.info("Price predictor initialized") - - # Initialize TensorBoard writer if not already present - if not hasattr(agent, 'writer'): - from torch.utils.tensorboard import SummaryWriter - log_dir = os.path.join("logs", "live_trading", datetime.datetime.now().strftime("%Y%m%d-%H%M%S")) - agent.writer = SummaryWriter(log_dir) - logger.info(f"TensorBoard logs will be saved to {log_dir}") - - # Initialize step counter for TensorBoard - tb_step = 0 - - # Main trading loop - while True: - # Wait for the next candle (1 minute) - await asyncio.sleep(5) # Check every 5 seconds - - # Fetch latest candle - latest_candle = await get_latest_candle(exchange, symbol) - - if not latest_candle: - logger.warning("No latest candle received, skipping update") - continue - - # Update environment with new data - env.add_data(latest_candle) - - # Train price predictor and update predictions - env.train_price_predictor() - env.update_price_predictions() - - # Get current state - state = env.get_state() - - # Select action (no exploration in live trading) - action = agent.select_action(state, training=False) - - # Take action - _, reward, _, _ = env.step(action) - - # Log trading activity - action_names = ["HOLD", "BUY", "SELL", "CLOSE"] - logger.info(f"Price: ${latest_candle['close']:.2f} | Action: {action_names[action]}") - - # Log performance metrics - if env.trades: - wins = sum(1 for t in env.trades if t.get('pnl_percent', 0) > 0) - win_rate = wins / len(env.trades) * 100 - total_pnl = sum(t.get('pnl_dollar', 0) for t in env.trades) - - logger.info(f"Balance: ${env.balance:.2f} | Trades: {len(env.trades)} | " - f"Win Rate: {win_rate:.1f}% | Total PnL: ${total_pnl:.2f}") - - # Analyze recent trades - trade_analysis = env.analyze_trades() - if trade_analysis: - logger.info(f"Recent Performance: Win Rate={trade_analysis.get('uptrend_win_rate', 0):.1f}% in uptrends, " - f"{trade_analysis.get('downtrend_win_rate', 0):.1f}% in downtrends") - - # If not in demo mode, update actual balance from exchange - if not demo and trading_client: - try: - actual_balance = await trading_client.fetch_account_balance() - if actual_balance > 0: - # Update environment balance with actual balance - env.balance = actual_balance - logger.info(f"Updated actual account balance: ${actual_balance:.2f}") - except Exception as e: - logger.error(f"Error updating account balance: {e}") - - # Log to TensorBoard every 5 steps - if tb_step % 5 == 0: - # Create a DataFrame from the environment's data - df_ohlcv = pd.DataFrame([{ - 'timestamp': candle['timestamp'], - 'open': candle['open'], - 'high': candle['high'], - 'low': candle['low'], - 'close': candle['close'], - 'volume': candle['volume'] - } for candle in env.data[-100:]]) # Use last 100 candles - - # Convert timestamp to datetime - df_ohlcv['timestamp'] = pd.to_datetime(df_ohlcv['timestamp'], unit='ms') - df_ohlcv.set_index('timestamp', inplace=True) - - # Extract buy/sell signals from trades - buy_signals = [] - sell_signals = [] - - if hasattr(env, 'trades') and env.trades: - for trade in env.trades: - if 'entry_time' in trade and 'entry' in trade: - if trade['type'] == 'long': - # Buy signal - entry_time = pd.to_datetime(trade['entry_time'], unit='ms') - buy_signals.append((entry_time, trade['entry'])) - - # Sell signal if closed - if 'exit_time' in trade and 'exit' in trade and trade['exit'] > 0: - exit_time = pd.to_datetime(trade['exit_time'], unit='ms') - sell_signals.append((exit_time, trade['exit'])) - - elif trade['type'] == 'short': - # Sell short signal - entry_time = pd.to_datetime(trade['entry_time'], unit='ms') - sell_signals.append((entry_time, trade['entry'])) - - # Buy to cover signal if closed - if 'exit_time' in trade and 'exit' in trade and trade['exit'] > 0: - exit_time = pd.to_datetime(trade['exit_time'], unit='ms') - buy_signals.append((exit_time, trade['exit'])) - - # Log to TensorBoard with a fixed tag to overwrite previous charts - log_ohlcv_to_tensorboard( - agent.writer, - df_ohlcv, - buy_signals, - sell_signals, - tb_step, - tag_prefix="live_trading" - ) - - # Log additional metrics - agent.writer.add_scalar("live/balance", env.balance, tb_step) - agent.writer.add_scalar("live/total_pnl", env.total_pnl, tb_step) - if env.trades: - agent.writer.add_scalar("live/win_rate", win_rate, tb_step) - agent.writer.add_scalar("live/trade_count", len(env.trades), tb_step) - - # Increment TensorBoard step counter - tb_step += 1 - - except KeyboardInterrupt: - logger.info("Live trading stopped by user") - except Exception as e: - logger.error(f"Error in live trading: {e}") - logger.error(traceback.format_exc()) - raise - -async def get_latest_candle(exchange, symbol): - """Get the latest candle data""" - try: - # Use the refactored fetch method with limit=1 - data = await fetch_ohlcv_data(exchange, symbol, "1m", 1) - - if data and len(data) > 0: - return data[0] - else: - logger.warning("No candle data received") - return None - except Exception as e: - logger.error(f"Failed to fetch latest candle: {e}") - return None - - -async def fetch_ohlcv_data(exchange, symbol, timeframe, limit): - """Fetch OHLCV data with proper handling for both async and standard CCXT""" - from data_cache import ohlcv_cache - - try: - # Check if exchange has fetchOHLCV method - if not hasattr(exchange, 'fetchOHLCV') and not hasattr(exchange, 'fetch_ohlcv'): - logger.error("Exchange does not support OHLCV data fetching") - # Try to get data from cache as fallback - cached_data = ohlcv_cache.load(symbol, timeframe) - if cached_data: - logger.info(f"Using cached data for {symbol} ({timeframe}) as exchange doesn't support OHLCV") - return cached_data - return [] - - # Handle different exchange implementations - try: - if hasattr(exchange, 'has') and isinstance(exchange.has, dict) and exchange.has.get('fetchOHLCVAsync', False): - # Use async method if available (CCXT Pro) - ohlcv = await exchange.fetchOHLCV(symbol, timeframe, limit=limit) - elif hasattr(exchange, 'fetch_ohlcv'): - # ExchangeSimulator or custom implementation - if asyncio.iscoroutinefunction(exchange.fetch_ohlcv): - ohlcv = await exchange.fetch_ohlcv(symbol=symbol, timeframe=timeframe, limit=limit) - else: - ohlcv = exchange.fetch_ohlcv(symbol=symbol, timeframe=timeframe, limit=limit) - else: - # Standard CCXT - run synchronously in executor to avoid blocking - loop = asyncio.get_event_loop() - - # Check if fetch_ohlcv is a coroutine function - if asyncio.iscoroutinefunction(exchange.fetch_ohlcv): - ohlcv = await exchange.fetch_ohlcv(symbol, timeframe, limit=limit) - else: - ohlcv = await loop.run_in_executor( - None, - lambda: exchange.fetch_ohlcv(symbol, timeframe, limit=limit) - ) - - # Check if ohlcv is a coroutine (sometimes happens with certain exchange implementations) - if asyncio.iscoroutine(ohlcv): - ohlcv = await ohlcv - - # Convert to list of dictionaries - data = [] - for candle in ohlcv: - timestamp, open_price, high, low, close, volume = candle - data.append({ - 'timestamp': timestamp, - 'open': open_price, - 'high': high, - 'low': low, - 'close': close, - 'volume': volume - }) - - # Cache the data for future use - if data: - ohlcv_cache.save(data, symbol, timeframe) - - logger.info(f"Fetched {len(data)} candles for {symbol} ({timeframe})") - return data - - except Exception as e: - logger.error(f"Error fetching from exchange: {e}") - logger.error(f"Traceback: {traceback.format_exc()}") - - # Try to get data from cache as fallback - logger.info(f"Attempting to use cached data for {symbol} ({timeframe})") - cached_data = ohlcv_cache.load(symbol, timeframe) - if cached_data: - logger.info(f"Using cached data ({len(cached_data)} candles) as fallback") - return cached_data - - # If no cached data, re-raise the exception - logger.error(f"No cached data available for {symbol} ({timeframe})") - return [] - - except Exception as e: - logger.error(f"Error fetching OHLCV data: {e}") - logger.error(f"Traceback: {traceback.format_exc()}") - - # Try to get data from cache as last resort - cached_data = ohlcv_cache.load(symbol, timeframe) - if cached_data: - logger.info(f"Using cached data ({len(cached_data)} candles) as last resort") - return cached_data - - return [] - -async def main(): - """Main function to run the trading bot""" - # Parse command line arguments - import argparse - import traceback - - parser = argparse.ArgumentParser(description='Run the trading bot') - parser.add_argument('--mode', type=str, default='train', choices=['train', 'evaluate', 'live', 'continuous'], help='Mode to run the bot in') - parser.add_argument('--episodes', type=int, default=100, help='Number of episodes to train for') - parser.add_argument('--demo', action='store_true', help='Run in demo mode (no real trades)') - parser.add_argument('--device', type=str, default='auto', choices=['cpu', 'gpu', 'auto'], help='Device to use for training') - parser.add_argument('--refresh-data', '--refresh_data', dest='refresh_data', action='store_true', help='Refresh data at the start of each episode') - parser.add_argument('--timeframe', type=str, default='1m', help='Timeframe for data (e.g., 1s, 1m, 5m, 15m, 1h)') - args = parser.parse_args() - - # Set device - device = get_device(args.device) - logger.info(f"Using device: {device}") - - try: - # Initialize exchange - exchange = await initialize_exchange() - - # Determine if we're in demo mode - demo_mode = args.demo - if args.mode != 'live': - # Always use demo mode for training and evaluation - demo_mode = True - - logger.info(f"Running in {'demo' if demo_mode else 'live trading'} mode") - - # Create environment with the correct parameters - env = TradingEnvironment( - initial_balance=INITIAL_BALANCE, - window_size=30, - demo=demo_mode - ) - - # Fetch initial data - logger.info("Fetching initial data for ETH/USDT") - await env.fetch_initial_data(exchange, "ETH/USDT", "1m", 1500) - - # Initialize agent - agent = Agent(STATE_SIZE, 4, hidden_size=384, lstm_layers=2, attention_heads=4, device=device) - - if args.mode == 'train': - # Train the agent - logger.info(f"Starting training for {args.episodes} episodes...") - stats = await train_agent(agent, env, num_episodes=args.episodes, exchange=exchange, args=args) - - # Plot training results - plot_training_results(stats) - - # Save the trained agent - agent.save(f"{MODEL_DIR}/trading_agent_latest.pt") - - # Evaluate the agent - logger.info("Evaluating agent...") - results = evaluate_agent(agent, env, num_episodes=10) - logger.info(f"Evaluation results: {results}") - - elif args.mode == 'continuous': - # Continuous training mode - train indefinitely with data refreshing - logger.info("Starting continuous training mode...") - - # Set refresh_data to True for continuous mode - args.refresh_data = True - - # Create directories for continuous models - os.makedirs(MODEL_DIR, exist_ok=True) - - # Track best PnL for model selection - best_pnl = float('-inf') - best_pnl_model_path = f"{MODEL_DIR}/trading_agent_best_pnl.pt" - - # Load the best PnL model if it exists - if os.path.exists(best_pnl_model_path): - logger.info(f"Loading best PnL model: {best_pnl_model_path}") - agent.load(best_pnl_model_path) - - # Try to load best PnL value from checkpoint file - checkpoint_info_path = "checkpoints/best_metrics.json" - if os.path.exists(checkpoint_info_path): - with open(checkpoint_info_path, 'r') as f: - best_metrics = json.load(f) - best_pnl = best_metrics.get('best_pnl', best_pnl) - logger.info(f"Loaded best PnL from checkpoint: ${best_pnl:.2f}") - - # Initialize episode counter - episode = 0 - - # Get timeframe from args - timeframe = args.timeframe - logger.info(f"Using timeframe: {timeframe}") - - # Initialize TensorBoard writer - from torch.utils.tensorboard import SummaryWriter - tensorboard_dir = f"runs/continuous_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}" - writer = SummaryWriter(tensorboard_dir) - logger.info(f"TensorBoard logs will be saved to {tensorboard_dir}") - - # Attach writer to agent - agent.writer = writer - - # Initialize stats dictionary for plotting - stats = { - 'episode_rewards': [], - 'episode_profits': [], - 'win_rates': [], - 'trade_counts': [], - 'prediction_accuracies': [] - } - - # Train continuously - try: - while True: - try: - logger.info(f"Continuous training - Episode {episode}") - - # Refresh data from exchange with the specified timeframe - logger.info(f"Refreshing market data with timeframe {timeframe}...") - success = await env.fetch_new_data(exchange, "ETH/USDT", timeframe, 100) - - if not success: - logger.error("Failed to fetch market data from exchange and no cache available.") - logger.info("Waiting 60 seconds before retrying...") - await asyncio.sleep(60) - continue - - # Reset environment - state = env.reset() - - # Initialize price predictor if not already initialized - if not hasattr(env, 'price_predictor') or env.price_predictor is None: - logger.info("Initializing price predictor...") - env.initialize_price_predictor(device=agent.device) - - # Initialize episode variables - episode_reward = 0 - done = False - - # Train price predictor - if hasattr(env, 'price_predictor') and env.price_predictor is not None: - train_result = env.train_price_predictor() - if isinstance(train_result, (float, int)): - logger.info(f"Price predictor training loss: {train_result:.6f}") - writer.add_scalar('Loss/price_predictor', train_result, episode) - else: - logger.warning("Price predictor not initialized, skipping training") - train_result = 0 - - # Update price predictions - if hasattr(env, 'price_predictor') and env.price_predictor is not None: - env.update_price_predictions() - else: - logger.warning("Price predictor not initialized, skipping predictions") - - # Training loop for this episode - while not done: - # Select action - action = agent.select_action(state) - - # Take action - next_state, reward, done, info = env.step(action) - - # Check if there was an error during the step - if isinstance(info, dict) and "error" in info: - logger.error(f"Error during step: {info['error']}") - break - - # Store experience - agent.memory.push(state, action, reward, next_state, done) - - # Learn from experience - loss = agent.learn() - - # Update state and reward - state = next_state - episode_reward += reward - - # Calculate win rate - total_trades = env.win_count + env.loss_count - win_rate = (env.win_count / total_trades * 100) if total_trades > 0 else 0 - - # Calculate prediction accuracy - if hasattr(env, 'predicted_prices') and len(env.predicted_prices) > 0: - # Compare predictions with actual prices - actual_prices = env.features['price'][-len(env.predicted_prices):] - prediction_errors = np.abs(env.predicted_prices - actual_prices) / actual_prices - prediction_accuracy = 100 * (1 - np.mean(prediction_errors)) - else: - prediction_accuracy = 0 - - # Update stats - stats['episode_rewards'].append(episode_reward) - stats['episode_profits'].append(env.episode_pnl) - stats['win_rates'].append(win_rate) - stats['trade_counts'].append(total_trades) - stats['prediction_accuracies'].append(prediction_accuracy) - - # Log to TensorBoard - writer.add_scalar('Reward/continuous', episode_reward, episode) - writer.add_scalar('Balance/continuous', env.balance, episode) - writer.add_scalar('WinRate/continuous', win_rate, episode) - writer.add_scalar('PnL/episode', env.episode_pnl, episode) - writer.add_scalar('PnL/cumulative', env.total_pnl, episode) - writer.add_scalar('Drawdown/percent', env.max_drawdown * 100, episode) - writer.add_scalar('PredictionLoss', train_result, episode) - writer.add_scalar('PredictionAccuracy', prediction_accuracy, episode) - - # Rest of the code... - - # Increment episode counter - episode += 1 - - except Exception as e: - logger.error(f"Error in continuous training episode {episode}: {e}") - logger.error(f"Traceback: {traceback.format_exc()}") - - # Save emergency checkpoint - emergency_model_path = f"{MODEL_DIR}/trading_agent_continuous_emergency_{episode}.pt" - agent.save(emergency_model_path) - logger.info(f"Model saved to {emergency_model_path}") - - # Wait before retrying - logger.info("Waiting 60 seconds before starting next episode...") - await asyncio.sleep(60) - - # Increment episode counter - episode += 1 - - except Exception as e: - logger.error(f"Error in continuous training: {e}") - logger.error(f"Traceback: {traceback.format_exc()}") - - # Save emergency checkpoint - emergency_model_path = f"{MODEL_DIR}/trading_agent_continuous_emergency_{episode}.pt" - agent.save(emergency_model_path) - logger.info(f"Model saved to {emergency_model_path}") - - # Close TensorBoard writer - writer.close() - - elif args.mode == 'evaluate': - # Load the best model - agent.load("models/trading_agent_best_pnl.pt") - - # Evaluate the agent - logger.info("Evaluating agent...") - results = evaluate_agent(agent, env, num_episodes=10) - logger.info(f"Evaluation results: {results}") - - elif args.mode == 'live': - # Load the best model - agent.load("models/trading_agent_best_pnl.pt") - - # Run live trading - logger.info("Starting live trading...") - await live_trading(agent, env, exchange, demo=demo_mode) - - except Exception as e: - logger.error(f"Error: {e}") - logger.error(f"Traceback: {traceback.format_exc()}") - finally: - # Close exchange connection - if exchange: - try: - # Some CCXT exchanges have close method, others don't - if hasattr(exchange, 'close'): - await exchange.close() - elif hasattr(exchange, 'client') and hasattr(exchange.client, 'close'): - await exchange.client.close() - logger.info("Exchange connection closed") - except Exception as e: - logger.warning(f"Could not properly close exchange connection: {e}") - -def ensure_model_float32(model): - """Ensure all model parameters are float32""" - for param in model.parameters(): - param.data = param.data.float() # Convert to float32 - return model - -def ensure_float32(tensor_or_array): - """Ensure the input is a float32 tensor or numpy array""" - if isinstance(tensor_or_array, torch.Tensor): - return tensor_or_array.float() - elif isinstance(tensor_or_array, np.ndarray): - return tensor_or_array.astype(np.float32) - else: - return np.array(tensor_or_array, dtype=np.float32) - -def visualize_training_results(env, agent, episode_num): - """Visualize the training results with OHLCV data, actions, and predictions""" - try: - # Create directory for visualizations if it doesn't exist - os.makedirs("visualizations", exist_ok=True) - - # Get the data for visualization - if len(env.data) < 100: - logger.warning("Not enough data for visualization") - return - - # Use the last 100 candles for visualization - data = env.data[-100:] - - # Create a pandas DataFrame for easier plotting - df = pd.DataFrame([{ - 'timestamp': candle['timestamp'], - 'open': candle['open'], - 'high': candle['high'], - 'low': candle['low'], - 'close': candle['close'], - 'volume': candle['volume'] - } for candle in data]) - - # Convert timestamp to datetime - df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') - df.set_index('timestamp', inplace=True) - - # Create the plot - plt.figure(figsize=(16, 12)) - - # Plot OHLC data - ax1 = plt.subplot(3, 1, 1) - ax1.set_title(f'Training Visualization - Episode {episode_num}') - - # Plot candlestick chart - from mplfinance.original_flavor import candlestick_ohlc - import matplotlib.dates as mdates - - # Convert date to numerical format for candlestick - df_ohlc = df.reset_index() - df_ohlc['timestamp'] = df_ohlc['timestamp'].map(mdates.date2num) - - candlestick_ohlc(ax1, df_ohlc[['timestamp', 'open', 'high', 'low', 'close']].values, - width=0.6, colorup='green', colordown='red') - - ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d %H:%M')) - ax1.set_ylabel('Price (USD)') - - # Plot buy/sell actions if available - if hasattr(env, 'trades') and env.trades: - # Filter trades that occurred in the visualization window - recent_trades = [t for t in env.trades if t.get('timestamp', 0) >= df.index[0].timestamp() * 1000] - - for trade in recent_trades: - if trade['type'] == 'long': - # Buy point - entry_time = pd.to_datetime(trade['entry_time'], unit='ms') - ax1.scatter(mdates.date2num(entry_time), trade['entry'], - marker='^', color='green', s=100, label='Buy') - - # Sell point if closed - if 'exit' in trade and trade['exit'] > 0: - exit_time = pd.to_datetime(trade['exit_time'], unit='ms') - ax1.scatter(mdates.date2num(exit_time), trade['exit'], - marker='v', color='blue', s=100, label='Sell Long') - - # Draw line connecting entry and exit - ax1.plot([mdates.date2num(entry_time), mdates.date2num(exit_time)], - [trade['entry'], trade['exit']], 'g--', alpha=0.5) - - elif trade['type'] == 'short': - # Sell short point - entry_time = pd.to_datetime(trade['entry_time'], unit='ms') - ax1.scatter(mdates.date2num(entry_time), trade['entry'], - marker='v', color='red', s=100, label='Sell Short') - - # Buy to cover point if closed - if 'exit' in trade and trade['exit'] > 0: - exit_time = pd.to_datetime(trade['exit_time'], unit='ms') - ax1.scatter(mdates.date2num(exit_time), trade['exit'], - marker='^', color='orange', s=100, label='Buy to Cover') - - # Draw line connecting entry and exit - ax1.plot([mdates.date2num(entry_time), mdates.date2num(exit_time)], - [trade['entry'], trade['exit']], 'r--', alpha=0.5) - - # Plot predicted prices if available - if hasattr(env, 'predicted_prices') and len(env.predicted_prices) > 0: - ax2 = plt.subplot(3, 1, 2, sharex=ax1) - ax2.set_title('Price Predictions vs Actual') - - # Plot actual prices - ax2.plot(df.index[-len(env.predicted_prices):], df['close'][-len(env.predicted_prices):], - label='Actual Price', color='blue') - - # Plot predicted prices - # Align predictions with their corresponding timestamps - prediction_dates = df.index[-len(env.predicted_prices)-1:-1] - if len(prediction_dates) == len(env.predicted_prices): - ax2.plot(prediction_dates, env.predicted_prices, - label='Predicted Price', color='orange', linestyle='--') - - # Calculate prediction error - actual = df['close'][-len(env.predicted_prices)-1:-1].values - predicted = env.predicted_prices - mape = np.mean(np.abs((actual - predicted) / actual)) * 100 - ax2.set_ylabel('Price (USD)') - ax2.set_title(f'Price Predictions vs Actual (MAPE: {mape:.2f}%)') - ax2.legend() - - # Plot technical indicators - ax3 = plt.subplot(3, 1, 3, sharex=ax1) - ax3.set_title('Technical Indicators') - - # Plot RSI if available - if 'rsi' in env.features and len(env.features['rsi']) > 0: - rsi_values = env.features['rsi'][-len(df):] - if len(rsi_values) == len(df): - ax3.plot(df.index, rsi_values, label='RSI', color='purple') - - # Add RSI overbought/oversold lines - ax3.axhline(y=70, color='r', linestyle='-', alpha=0.3) - ax3.axhline(y=30, color='g', linestyle='-', alpha=0.3) - - # Plot MACD if available - if 'macd' in env.features and 'macd_signal' in env.features: - macd_values = env.features['macd'][-len(df):] - signal_values = env.features['macd_signal'][-len(df):] - - if len(macd_values) == len(df) and len(signal_values) == len(df): - ax3.plot(df.index, macd_values, label='MACD', color='blue') - ax3.plot(df.index, signal_values, label='Signal', color='red') - - ax3.set_ylabel('Indicator Value') - ax3.legend() - - # Format x-axis - plt.xticks(rotation=45) - plt.tight_layout() - - # Save the figure - plt.savefig(f"visualizations/training_episode_{episode_num}.png") - logger.info(f"Visualization saved for episode {episode_num}") - - # Close the figure to free memory - plt.close() - - except Exception as e: - logger.error(f"Error creating visualization: {e}") - logger.error(f"Traceback: {traceback.format_exc()}") - -def log_ohlcv_to_tensorboard(writer, df_ohlcv, buy_signals, sell_signals, step, tag_prefix="trading"): - """ - Log OHLCV chart with buy/sell signals to TensorBoard - - Parameters: - ----------- - writer : torch.utils.tensorboard.SummaryWriter - TensorBoard writer instance - df_ohlcv : pandas.DataFrame - DataFrame with OHLCV data - buy_signals : list of tuples - List of (datetime, price) tuples for buy signals - sell_signals : list of tuples - List of (datetime, price) tuples for sell signals - step : int - Global step value to record - tag_prefix : str - Prefix for the tag in TensorBoard - """ - try: - import matplotlib.pyplot as plt - import matplotlib.dates as mdates - from matplotlib.figure import Figure - from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas - import numpy as np - from mplfinance.original_flavor import candlestick_ohlc - - # Check if DataFrame is empty - if df_ohlcv.empty: - logger.warning("Empty OHLCV DataFrame, skipping visualization") - return - - # Create figure - fig = Figure(figsize=(12, 8)) - canvas = FigureCanvas(fig) - - # Create subplots for price, predictions, and volume - ax1 = fig.add_subplot(3, 1, 1) # Price chart - ax2 = fig.add_subplot(3, 1, 2, sharex=ax1) # Predictions chart - ax3 = fig.add_subplot(3, 1, 3, sharex=ax1) # Volume chart - - # Convert DataFrame to OHLC format for candlestick - df_ohlc = df_ohlcv.copy() - df_ohlc.reset_index(inplace=True) - df_ohlc['date_num'] = mdates.date2num(df_ohlc['timestamp'].dt.to_pydatetime()) - ohlc_data = df_ohlc[['date_num', 'open', 'high', 'low', 'close']].values - - # Plot candlestick chart using mplfinance - candlestick_ohlc(ax1, ohlc_data, width=0.6/(24*3), colorup='green', colordown='red') - - # Add a dummy line for the legend - ax1.plot([], [], color='green', label='Bullish Candle') - ax1.plot([], [], color='red', label='Bearish Candle') - - # Plot buy signals with proper alignment - if buy_signals: - # Convert datetime to numeric format for plotting - buy_dates = [signal[0] for signal in buy_signals] - buy_dates_num = mdates.date2num(buy_dates) - buy_prices = [signal[1] for signal in buy_signals] - - # Filter signals to only include those within the chart timeframe - valid_indices = [] - for i, date_num in enumerate(buy_dates_num): - if min(df_ohlc['date_num']) <= date_num <= max(df_ohlc['date_num']): - valid_indices.append(i) - - if valid_indices: - filtered_dates = [buy_dates_num[i] for i in valid_indices] - filtered_prices = [buy_prices[i] for i in valid_indices] - ax1.scatter(filtered_dates, filtered_prices, marker='^', color='green', s=100, label='Buy') - - # Plot sell signals with proper alignment - if sell_signals: - # Convert datetime to numeric format for plotting - sell_dates = [signal[0] for signal in sell_signals] - sell_dates_num = mdates.date2num(sell_dates) - sell_prices = [signal[1] for signal in sell_signals] - - # Filter signals to only include those within the chart timeframe - valid_indices = [] - for i, date_num in enumerate(sell_dates_num): - if min(df_ohlc['date_num']) <= date_num <= max(df_ohlc['date_num']): - valid_indices.append(i) - - if valid_indices: - filtered_dates = [sell_dates_num[i] for i in valid_indices] - filtered_prices = [sell_prices[i] for i in valid_indices] - ax1.scatter(filtered_dates, filtered_prices, marker='v', color='red', s=100, label='Sell') - - # Plot predicted prices if available in the environment - from inspect import currentframe, getframeinfo - frame = currentframe() - has_predictions = False - while frame: - if 'env' in frame.f_locals: - env = frame.f_locals['env'] - if hasattr(env, 'predicted_prices') and len(env.predicted_prices) > 0: - # Get the last timestamp from the data - last_timestamp = df_ohlc['timestamp'].iloc[-1] - - # Create future timestamps for predictions (assuming 1-minute intervals) - future_timestamps = [last_timestamp + pd.Timedelta(minutes=i+1) for i in range(len(env.predicted_prices))] - future_dates_num = mdates.date2num(future_timestamps) - - # Plot actual close prices - ax2.plot(df_ohlc['date_num'], df_ohlc['close'], color='blue', label='Actual Price') - - # Plot predicted prices - ax2.plot(future_dates_num, env.predicted_prices, color='orange', linestyle='--', marker='o', label='Predicted Price') - - # Add shading to indicate prediction area - ax2.axvspan(df_ohlc['date_num'].iloc[-1], future_dates_num[-1], alpha=0.2, color='yellow') - - # Set title and legend - ax2.set_title('Price Predictions') - ax2.set_ylabel('Price') - ax2.legend() - ax2.grid(True) - has_predictions = True - break - frame = frame.f_back - - # If no predictions were found, use the subplot for something else - if not has_predictions: - # Plot moving averages instead - if len(df_ohlc) >= 20: - df_ohlc['MA20'] = df_ohlc['close'].rolling(window=20).mean() - ax2.plot(df_ohlc['date_num'], df_ohlc['close'], color='blue', label='Close Price') - ax2.plot(df_ohlc['date_num'], df_ohlc['MA20'], color='orange', label='20-period MA') - ax2.set_title('Price and Moving Average') - ax2.set_ylabel('Price') - ax2.legend() - ax2.grid(True) - - # Plot volume - ax3.bar(df_ohlc['date_num'], df_ohlc['volume'], width=0.6/(24*3), color='blue', alpha=0.5) - - # Format axes - ax1.set_title(f'OHLC with Buy/Sell Signals - {tag_prefix}') - ax1.set_ylabel('Price') - ax1.legend() - ax1.grid(True) - - ax3.set_xlabel('Date') - ax3.set_ylabel('Volume') - ax3.grid(True) - - # Format date for all axes - date_format = mdates.DateFormatter('%Y-%m-%d %H:%M') - ax1.xaxis.set_major_formatter(date_format) - ax2.xaxis.set_major_formatter(date_format) - ax3.xaxis.set_major_formatter(date_format) - - # Set x-axis limits to ensure all subplots show the same time range - # Include future predictions if available - if has_predictions and 'future_dates_num' in locals(): - x_min = min(df_ohlc['date_num']) - x_max = max(future_dates_num) - ax1.set_xlim(x_min, x_max) - ax2.set_xlim(x_min, x_max) - ax3.set_xlim(x_min, x_max) - - fig.autofmt_xdate() - - # Adjust layout - fig.tight_layout() - - # Log to TensorBoard - if tag_prefix == "latest_trading_data": - # For the latest data, use a fixed tag without step to overwrite previous charts - writer.add_figure(f"{tag_prefix}/ohlcv_chart", fig) - else: - # For other charts, include the step - writer.add_figure(f"{tag_prefix}/ohlcv_chart", fig, global_step=step) - - # Clean up - plt.close(fig) - - except Exception as e: - logger.error(f"Error in log_ohlcv_to_tensorboard: {e}") - logger.error(f"Traceback: {traceback.format_exc()}") - -if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - logger.info("Program terminated by user") \ No newline at end of file diff --git a/crypto/gogo2/main_function.py b/crypto/gogo2/main_function.py new file mode 100644 index 0000000..9891f8a --- /dev/null +++ b/crypto/gogo2/main_function.py @@ -0,0 +1,24 @@ +import asyncio +from exchange_simulator import ExchangeSimulator +import logging + +# Set up logging +logger = logging.getLogger(__name__) + +async def main(): + """ + Main function to run the training process. + """ + # Initialize exchange simulator + exchange = ExchangeSimulator() + + # Train agent + print("Starting training process...") + # Add your training code here + print("Training complete!") + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("Program terminated by user") \ No newline at end of file diff --git a/crypto/gogo2/main_multiu_broken.py b/crypto/gogo2/main_multiu_broken.py new file mode 100644 index 0000000..2fe0840 --- /dev/null +++ b/crypto/gogo2/main_multiu_broken.py @@ -0,0 +1,8195 @@ +import os +import sys +import time +import json +import logging +import asyncio +import argparse +import traceback +import datetime +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +from matplotlib.ticker import FuncFormatter +import mplfinance as mpf +from collections import deque, namedtuple +import random +from typing import List, Dict, Tuple, Optional, Union, Any +from dotenv import load_dotenv +import torch.nn.functional as F +import math +from mexc_trading import MexcTradingClient + +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.tensorboard import SummaryWriter +import torch.amp as amp # Update import to use torch.amp instead of torch.cuda.amp +from sklearn.preprocessing import MinMaxScaler + +import ccxt.async_support as ccxt +import websockets +from data_cache import ohlcv_cache + +# Fix for Windows asyncio +if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler("trading_bot.log"), + logging.StreamHandler() + ] +) +logger = logging.getLogger("trading_bot") + +# Constants +INITIAL_BALANCE = 1000.0 +MAX_LEVERAGE = 1.0 # Max leverage to use +STOP_LOSS_PERCENT = 2.0 # Default stop loss percentage +TAKE_PROFIT_PERCENT = 4.0 # Default take profit percentage +LEARNING_RATE = 0.0001 +MODEL_DIR = "models_improved" # New models directory + +# Load environment variables +load_dotenv() +MEXC_API_KEY = os.getenv('MEXC_API_KEY') +MEXC_SECRET_KEY = os.getenv('MEXC_SECRET_KEY') + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger('trading_bot') + +# Constants +INITIAL_BALANCE = 1000.0 # Starting balance in USDT +MAX_LEVERAGE = 1 # Maximum leverage to use +STOP_LOSS_PERCENT = 2.0 # Default stop loss percentage +TAKE_PROFIT_PERCENT = 4.0 # Default take profit percentage +# Calculate STATE_SIZE based on features and window size +# 14 features * 30 window size + 3 position info = 423 +STATE_SIZE = 423 # Size of the state vector for the agent +MEMORY_SIZE = 100000 # Size of the replay memory +BATCH_SIZE = 64 # Batch size for training +GAMMA = 0.99 # Discount factor for future rewards +EPSILON_START = 1.0 # Starting value of epsilon for exploration +EPSILON_END = 0.05 # Minimum value of epsilon +EPSILON_DECAY = 10000 # Decay rate for epsilon +TARGET_UPDATE = 10 # Update target network every N episodes + +# Experience replay tuple +Experience = namedtuple('Experience', ['state', 'action', 'reward', 'next_state', 'done']) + +# Add this function near the top of the file, after the imports but before any classes +def find_local_extrema(prices, window=5, volumes=None, volume_threshold=0.7): + """ + Find local extrema (peaks and troughs) in price data with improved accuracy. + + Args: + prices: List of price values + window: Window size for extrema detection + volumes: Optional list of volume values + volume_threshold: Volume threshold for confirming extrema + + Returns: + peaks: Indices of local maxima + troughs: Indices of local minima + """ + if len(prices) < 2 * window + 1: + return [], [] + + # Convert to numpy array if not already + prices = np.array(prices) + + # Find potential extrema using rolling window + peaks = [] + troughs = [] + + for i in range(window, len(prices) - window): + # Get window around current point + window_left = prices[i - window:i] + window_right = prices[i + 1:i + window + 1] + current = prices[i] + + # Check for peak + if current > np.max(window_left) and current > np.max(window_right): + # Volume confirmation if available + if volumes is None or volumes[i] > np.mean(volumes) * volume_threshold: + peaks.append(i) + + # Check for trough + if current < np.min(window_left) and current < np.min(window_right): + # Volume confirmation if available + if volumes is None or volumes[i] > np.mean(volumes) * volume_threshold: + troughs.append(i) + + # Apply additional filtering to remove false extrema + if len(peaks) > 1: + # Filter out peaks that are too close to each other + filtered_peaks = [peaks[0]] + for i in range(1, len(peaks)): + if peaks[i] - filtered_peaks[-1] >= window: + filtered_peaks.append(peaks[i]) + peaks = filtered_peaks + + if len(troughs) > 1: + # Filter out troughs that are too close to each other + filtered_troughs = [troughs[0]] + for i in range(1, len(troughs)): + if troughs[i] - filtered_troughs[-1] >= window: + filtered_troughs.append(troughs[i]) + troughs = filtered_troughs + + return peaks, troughs + +class ReplayMemory: + def __init__(self, capacity, alpha=0.6, beta=0.4, beta_increment=0.001, n_step=3, gamma=0.99): + self.capacity = capacity + self.memory = [] + self.position = 0 + self.Transition = namedtuple('Transition', ('state', 'action', 'reward', 'next_state', 'done')) + + # Prioritized Experience Replay parameters + self.alpha = alpha # How much prioritization to use (0 = uniform sampling) + self.beta = beta # Importance sampling correction (0 = no correction) + self.beta_increment = beta_increment # Increment beta over time to 1 + self.max_priority = 1.0 # Initial max priority + + # N-step learning parameters + self.n_step = n_step + self.gamma = gamma + self.n_step_buffer = deque(maxlen=n_step) + + def push(self, state, action, reward, next_state, done): + """Store transition with maximum priority""" + # Store experience in n-step buffer + self.n_step_buffer.append((state, action, reward, next_state, done)) + + # If we don't have enough transitions for n-step yet, return + if len(self.n_step_buffer) < self.n_step and not done: + return + + # Calculate n-step reward and get the appropriate next state + n_step_reward = 0 + n_step_next_state = None + n_step_done = False + + # If the episode ended before we could collect n steps + if done and len(self.n_step_buffer) < self.n_step: + # Use what we have + n_step_next_state = self.n_step_buffer[-1][3] + n_step_done = True + + # Calculate n-step reward with discount + for i, (_, _, r, _, _) in enumerate(self.n_step_buffer): + n_step_reward += r * (self.gamma ** i) + else: + # Get the state after n steps + n_step_next_state = self.n_step_buffer[-1][3] + n_step_done = self.n_step_buffer[-1][4] + + # Calculate n-step reward with discount + for i, (_, _, r, _, _) in enumerate(self.n_step_buffer): + n_step_reward += r * (self.gamma ** i) + + # Get the initial state and action + initial_state = self.n_step_buffer[0][0] + initial_action = self.n_step_buffer[0][1] + + # Create transition with n-step values + transition = self.Transition(initial_state, initial_action, n_step_reward, n_step_next_state, n_step_done) + + # Add to memory with maximum priority + if len(self.memory) < self.capacity: + self.memory.append((transition, self.max_priority)) + else: + self.memory[self.position] = (transition, self.max_priority) + + self.position = (self.position + 1) % self.capacity + + # If this was the end of an episode, clear the n-step buffer + if done: + self.n_step_buffer.clear() + + def sample(self, batch_size): + """Sample a batch of transitions with prioritized sampling""" + if len(self.memory) < batch_size: + return None + + # Calculate sampling probabilities + priorities = np.array([p for _, p in self.memory]) + probs = priorities ** self.alpha + probs /= probs.sum() + + # Sample indices based on priorities + indices = np.random.choice(len(self.memory), batch_size, p=probs) + + # Get the sampled transitions + transitions = [self.memory[idx][0] for idx in indices] + + # Calculate importance sampling weights + weights = (len(self.memory) * probs[indices]) ** (-self.beta) + weights /= weights.max() # Normalize weights + + # Increment beta for next time + self.beta = min(1.0, self.beta + self.beta_increment) + + # Convert to batch + batch = self.Transition(*zip(*transitions)) + + return batch, indices, weights + + def update_priorities(self, indices, td_errors): + """Update priorities based on TD errors""" + for idx, td_error in zip(indices, td_errors): + # Add a small constant to avoid zero priority + priority = (abs(td_error) + 1e-5) ** self.alpha + self.memory[idx] = (self.memory[idx][0], priority) + self.max_priority = max(self.max_priority, priority) + + def __len__(self): + return len(self.memory) + +class DQN(nn.Module): + def __init__(self, state_size, action_size, hidden_size=384, lstm_layers=2, attention_heads=4): + super(DQN, self).__init__() + + # Feature extraction layers with increased regularization + self.feature_extraction = nn.Sequential( + nn.Linear(state_size, hidden_size), + nn.LeakyReLU(), + nn.Dropout(0.2), # Increased dropout + nn.LayerNorm(hidden_size), # Layer normalization for stability + nn.Linear(hidden_size, hidden_size), + nn.LeakyReLU(), + nn.Dropout(0.2), # Increased dropout + nn.LayerNorm(hidden_size) # Layer normalization for stability + ) + + # LSTM for sequential processing + self.lstm = nn.LSTM( + input_size=hidden_size, + hidden_size=hidden_size, + num_layers=lstm_layers, + batch_first=True, + dropout=0.2 if lstm_layers > 1 else 0 # Increased dropout + ) + + # Dueling network architecture + # Advantage stream + self.advantage_stream = nn.Sequential( + nn.Linear(hidden_size, hidden_size // 2), + nn.LeakyReLU(), + nn.Dropout(0.2), # Added dropout + nn.Linear(hidden_size // 2, action_size) + ) + + # Value stream + self.value_stream = nn.Sequential( + nn.Linear(hidden_size, hidden_size // 2), + nn.LeakyReLU(), + nn.Dropout(0.2), # Added dropout + nn.Linear(hidden_size // 2, 1) + ) + + # Market regime classification + self.market_regime_classifier = nn.Sequential( + nn.Linear(hidden_size, hidden_size // 2), + nn.LeakyReLU(), + nn.Dropout(0.2), # Added dropout + nn.Linear(hidden_size // 2, 3) # 3 regimes: trending, ranging, volatile + ) + + # Initialize weights + self._initialize_weights() + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, x, hidden=None): + # Extract features + features = self.feature_extraction(x) + + # Add sequence dimension for LSTM if not present + if len(features.shape) == 2: + features = features.unsqueeze(1) + + # LSTM processing + lstm_out, lstm_hidden = self.lstm(features, hidden) + + # Use the last LSTM output + lstm_out = lstm_out[:, -1, :] + + # Dueling architecture + advantage = self.advantage_stream(lstm_out) + value = self.value_stream(lstm_out) + + # Combine value and advantage for Q-values + # Q(s,a) = V(s) + (A(s,a) - mean(A(s,a'))) + q_values = value + advantage - advantage.mean(dim=1, keepdim=True) + + # Market regime classification + market_regime = self.market_regime_classifier(lstm_out) + + return q_values, lstm_hidden, market_regime + +class PricePredictionModel(nn.Module): + def __init__(self, input_size=2, hidden_size=256, output_size=5, num_layers=3, num_timeframes=3): + super(PricePredictionModel, self).__init__() + self.hidden_size = hidden_size + self.num_layers = num_layers + self.num_timeframes = num_timeframes + self.output_size = output_size + + # Separate LSTM for each timeframe + self.timeframe_lstms = nn.ModuleList([ + nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=0.2) + for _ in range(num_timeframes) + ]) + + # Self-attention for each timeframe + self.self_attentions = nn.ModuleList([ + nn.MultiheadAttention(hidden_size, num_heads=4, batch_first=True, dropout=0.1) + for _ in range(num_timeframes) + ]) + + # Timeframe fusion layer + self.fusion_layer = nn.Sequential( + nn.Linear(hidden_size * num_timeframes, hidden_size * 2), + nn.LeakyReLU(), + nn.Dropout(0.2), + nn.Linear(hidden_size * 2, hidden_size) + ) + + # Price prediction layers + self.price_fc = nn.Sequential( + nn.Linear(hidden_size, hidden_size), + nn.LeakyReLU(), + nn.Dropout(0.1), + nn.Linear(hidden_size, output_size) + ) + + # Extrema prediction layers (high and low points) + self.extrema_fc = nn.Sequential( + nn.Linear(hidden_size, hidden_size), + nn.LeakyReLU(), + nn.Dropout(0.1), + nn.Linear(hidden_size, output_size * 2) # For each time step, predict high/low probability + ) + + # Initialize scalers + self.price_scaler = MinMaxScaler(feature_range=(0, 1)) + self.volume_scaler = MinMaxScaler(feature_range=(0, 1)) + + # Initialize weights + self._initialize_weights() + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 +import os +import sys +import time +import json +import logging +import asyncio +import argparse +import traceback +import datetime +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +from matplotlib.ticker import FuncFormatter +import mplfinance as mpf +from collections import deque, namedtuple +import random +from typing import List, Dict, Tuple, Optional, Union, Any +from dotenv import load_dotenv +import torch.nn.functional as F +import math +from mexc_trading import MexcTradingClient + +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.tensorboard import SummaryWriter +import torch.amp as amp # Update import to use torch.amp instead of torch.cuda.amp +from sklearn.preprocessing import MinMaxScaler + +import ccxt.async_support as ccxt +import websockets +from data_cache import ohlcv_cache + +# Fix for Windows asyncio +if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler("trading_bot.log"), + logging.StreamHandler() + ] +) +logger = logging.getLogger("trading_bot") + +# Constants +INITIAL_BALANCE = 1000.0 +MAX_LEVERAGE = 1.0 # Max leverage to use +STOP_LOSS_PERCENT = 2.0 # Default stop loss percentage +TAKE_PROFIT_PERCENT = 4.0 # Default take profit percentage +LEARNING_RATE = 0.0001 +MODEL_DIR = "models_improved" # New models directory + +# Load environment variables +load_dotenv() +MEXC_API_KEY = os.getenv('MEXC_API_KEY') +MEXC_SECRET_KEY = os.getenv('MEXC_SECRET_KEY') + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger('trading_bot') + +# Constants +INITIAL_BALANCE = 1000.0 # Starting balance in USDT +MAX_LEVERAGE = 1 # Maximum leverage to use +STOP_LOSS_PERCENT = 2.0 # Default stop loss percentage +TAKE_PROFIT_PERCENT = 4.0 # Default take profit percentage +# Calculate STATE_SIZE based on features and window size +# 14 features * 30 window size + 3 position info = 423 +STATE_SIZE = 423 # Size of the state vector for the agent +MEMORY_SIZE = 100000 # Size of the replay memory +BATCH_SIZE = 64 # Batch size for training +GAMMA = 0.99 # Discount factor for future rewards +EPSILON_START = 1.0 # Starting value of epsilon for exploration +EPSILON_END = 0.05 # Minimum value of epsilon +EPSILON_DECAY = 10000 # Decay rate for epsilon +TARGET_UPDATE = 10 # Update target network every N episodes + +# Experience replay tuple +Experience = namedtuple('Experience', ['state', 'action', 'reward', 'next_state', 'done']) + +# Add this function near the top of the file, after the imports but before any classes +def find_local_extrema(prices, window=5, volumes=None, volume_threshold=0.7): + """ + Find local extrema (peaks and troughs) in price data with improved accuracy. + + Args: + prices: List of price values + window: Window size for extrema detection + volumes: Optional list of volume values + volume_threshold: Volume threshold for confirming extrema + + Returns: + peaks: Indices of local maxima + troughs: Indices of local minima + """ + if len(prices) < 2 * window + 1: + return [], [] + + # Convert to numpy array if not already + prices = np.array(prices) + + # Find potential extrema using rolling window + peaks = [] + troughs = [] + + for i in range(window, len(prices) - window): + # Get window around current point + window_left = prices[i - window:i] + window_right = prices[i + 1:i + window + 1] + current = prices[i] + + # Check for peak + if current > np.max(window_left) and current > np.max(window_right): + # Volume confirmation if available + if volumes is None or volumes[i] > np.mean(volumes) * volume_threshold: + peaks.append(i) + + # Check for trough + if current < np.min(window_left) and current < np.min(window_right): + # Volume confirmation if available + if volumes is None or volumes[i] > np.mean(volumes) * volume_threshold: + troughs.append(i) + + # Apply additional filtering to remove false extrema + if len(peaks) > 1: + # Filter out peaks that are too close to each other + filtered_peaks = [peaks[0]] + for i in range(1, len(peaks)): + if peaks[i] - filtered_peaks[-1] >= window: + filtered_peaks.append(peaks[i]) + peaks = filtered_peaks + + if len(troughs) > 1: + # Filter out troughs that are too close to each other + filtered_troughs = [troughs[0]] + for i in range(1, len(troughs)): + if troughs[i] - filtered_troughs[-1] >= window: + filtered_troughs.append(troughs[i]) + troughs = filtered_troughs + + return peaks, troughs + +class ReplayMemory: + def __init__(self, capacity, alpha=0.6, beta=0.4, beta_increment=0.001, n_step=3, gamma=0.99): + self.capacity = capacity + self.memory = [] + self.position = 0 + self.Transition = namedtuple('Transition', ('state', 'action', 'reward', 'next_state', 'done')) + + # Prioritized Experience Replay parameters + self.alpha = alpha # How much prioritization to use (0 = uniform sampling) + self.beta = beta # Importance sampling correction (0 = no correction) + self.beta_increment = beta_increment # Increment beta over time to 1 + self.max_priority = 1.0 # Initial max priority + + # N-step learning parameters + self.n_step = n_step + self.gamma = gamma + self.n_step_buffer = deque(maxlen=n_step) + + def push(self, state, action, reward, next_state, done): + """Store transition with maximum priority""" + # Store experience in n-step buffer + self.n_step_buffer.append((state, action, reward, next_state, done)) + + # If we don't have enough transitions for n-step yet, return + if len(self.n_step_buffer) < self.n_step and not done: + return + + # Calculate n-step reward and get the appropriate next state + n_step_reward = 0 + n_step_next_state = None + n_step_done = False + + # If the episode ended before we could collect n steps + if done and len(self.n_step_buffer) < self.n_step: + # Use what we have + n_step_next_state = self.n_step_buffer[-1][3] + n_step_done = True + + # Calculate n-step reward with discount + for i, (_, _, r, _, _) in enumerate(self.n_step_buffer): + n_step_reward += r * (self.gamma ** i) + else: + # Get the state after n steps + n_step_next_state = self.n_step_buffer[-1][3] + n_step_done = self.n_step_buffer[-1][4] + + # Calculate n-step reward with discount + for i, (_, _, r, _, _) in enumerate(self.n_step_buffer): + n_step_reward += r * (self.gamma ** i) + + # Get the initial state and action + initial_state = self.n_step_buffer[0][0] + initial_action = self.n_step_buffer[0][1] + + # Create transition with n-step values + transition = self.Transition(initial_state, initial_action, n_step_reward, n_step_next_state, n_step_done) + + # Add to memory with maximum priority + if len(self.memory) < self.capacity: + self.memory.append((transition, self.max_priority)) + else: + self.memory[self.position] = (transition, self.max_priority) + + self.position = (self.position + 1) % self.capacity + + # If this was the end of an episode, clear the n-step buffer + if done: + self.n_step_buffer.clear() + + def sample(self, batch_size): + """Sample a batch of transitions with prioritized sampling""" + if len(self.memory) < batch_size: + return None + + # Calculate sampling probabilities + priorities = np.array([p for _, p in self.memory]) + probs = priorities ** self.alpha + probs /= probs.sum() + + # Sample indices based on priorities + indices = np.random.choice(len(self.memory), batch_size, p=probs) + + # Get the sampled transitions + transitions = [self.memory[idx][0] for idx in indices] + + # Calculate importance sampling weights + weights = (len(self.memory) * probs[indices]) ** (-self.beta) + weights /= weights.max() # Normalize weights + + # Increment beta for next time + self.beta = min(1.0, self.beta + self.beta_increment) + + # Convert to batch + batch = self.Transition(*zip(*transitions)) + + return batch, indices, weights + + def update_priorities(self, indices, td_errors): + """Update priorities based on TD errors""" + for idx, td_error in zip(indices, td_errors): + # Add a small constant to avoid zero priority + priority = (abs(td_error) + 1e-5) ** self.alpha + self.memory[idx] = (self.memory[idx][0], priority) + self.max_priority = max(self.max_priority, priority) + + def __len__(self): + return len(self.memory) + +class DQN(nn.Module): + def __init__(self, state_size, action_size, hidden_size=384, lstm_layers=2, attention_heads=4): + super(DQN, self).__init__() + + # Feature extraction layers with increased regularization + self.feature_extraction = nn.Sequential( + nn.Linear(state_size, hidden_size), + nn.LeakyReLU(), + nn.Dropout(0.2), # Increased dropout + nn.LayerNorm(hidden_size), # Layer normalization for stability + nn.Linear(hidden_size, hidden_size), + nn.LeakyReLU(), + nn.Dropout(0.2), # Increased dropout + nn.LayerNorm(hidden_size) # Layer normalization for stability + ) + + # LSTM for sequential processing + self.lstm = nn.LSTM( + input_size=hidden_size, + hidden_size=hidden_size, + num_layers=lstm_layers, + batch_first=True, + dropout=0.2 if lstm_layers > 1 else 0 # Increased dropout + ) + + # Dueling network architecture + # Advantage stream + self.advantage_stream = nn.Sequential( + nn.Linear(hidden_size, hidden_size // 2), + nn.LeakyReLU(), + nn.Dropout(0.2), # Added dropout + nn.Linear(hidden_size // 2, action_size) + ) + + # Value stream + self.value_stream = nn.Sequential( + nn.Linear(hidden_size, hidden_size // 2), + nn.LeakyReLU(), + nn.Dropout(0.2), # Added dropout + nn.Linear(hidden_size // 2, 1) + ) + + # Market regime classification + self.market_regime_classifier = nn.Sequential( + nn.Linear(hidden_size, hidden_size // 2), + nn.LeakyReLU(), + nn.Dropout(0.2), # Added dropout + nn.Linear(hidden_size // 2, 3) # 3 regimes: trending, ranging, volatile + ) + + # Initialize weights + self._initialize_weights() + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, x, hidden=None): + # Extract features + features = self.feature_extraction(x) + + # Add sequence dimension for LSTM if not present + if len(features.shape) == 2: + features = features.unsqueeze(1) + + # LSTM processing + lstm_out, lstm_hidden = self.lstm(features, hidden) + + # Use the last LSTM output + lstm_out = lstm_out[:, -1, :] + + # Dueling architecture + advantage = self.advantage_stream(lstm_out) + value = self.value_stream(lstm_out) + + # Combine value and advantage for Q-values + # Q(s,a) = V(s) + (A(s,a) - mean(A(s,a'))) + q_values = value + advantage - advantage.mean(dim=1, keepdim=True) + + # Market regime classification + market_regime = self.market_regime_classifier(lstm_out) + + return q_values, lstm_hidden, market_regime + +class PricePredictionModel(nn.Module): + def __init__(self, input_size=2, hidden_size=256, output_size=5, num_layers=3, num_timeframes=3): + super(PricePredictionModel, self).__init__() + self.hidden_size = hidden_size + self.num_layers = num_layers + self.num_timeframes = num_timeframes + self.output_size = output_size + + # Separate LSTM for each timeframe + self.timeframe_lstms = nn.ModuleList([ + nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=0.2) + for _ in range(num_timeframes) + ]) + + # Self-attention for each timeframe + self.self_attentions = nn.ModuleList([ + nn.MultiheadAttention(hidden_size, num_heads=4, batch_first=True, dropout=0.1) + for _ in range(num_timeframes) + ]) + + # Timeframe fusion layer + self.fusion_layer = nn.Sequential( + nn.Linear(hidden_size * num_timeframes, hidden_size * 2), + nn.LeakyReLU(), + nn.Dropout(0.2), + nn.Linear(hidden_size * 2, hidden_size) + ) + + # Price prediction layers + self.price_fc = nn.Sequential( + nn.Linear(hidden_size, hidden_size), + nn.LeakyReLU(), + nn.Dropout(0.1), + nn.Linear(hidden_size, output_size) + ) + + # Extrema prediction layers (high and low points) + self.extrema_fc = nn.Sequential( + nn.Linear(hidden_size, hidden_size), + nn.LeakyReLU(), + nn.Dropout(0.1), + nn.Linear(hidden_size, output_size * 2) # For each time step, predict high/low probability + ) + + # Initialize scalers + self.price_scaler = MinMaxScaler(feature_range=(0, 1)) + self.volume_scaler = MinMaxScaler(feature_range=(0, 1)) + + # Initialize weights + self._initialize_weights() + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + if hasattr(self, 'predicted_low_confidence') and hasattr(self, 'predicted_high_confidence'): + avg_confidence = (self.predicted_low_confidence + self.predicted_high_confidence) / 2 + + if avg_confidence > 0.8: + # High confidence - can lower threshold + confidence_adjustment = -0.05 + elif avg_confidence < 0.3: + # Low confidence - raise threshold + confidence_adjustment = 0.05 + + # Calculate final threshold with constraints + final_threshold = base_threshold + accuracy_adjustment + volatility_adjustment + confidence_adjustment + final_threshold = max(0.5, min(0.85, final_threshold)) # Constrain between 0.5 and 0.85 + + return final_threshold + + def update_prediction_history(self, was_correct, prediction_type): + """ + Update the history of prediction accuracy to improve future thresholds. + + Args: + was_correct: Boolean indicating if the prediction was correct + prediction_type: 'low' or 'high' indicating the type of extrema predicted + """ + if not hasattr(self, 'prediction_history'): + self.prediction_history = [] + + # Add this prediction to history + self.prediction_history.append({ + 'timestamp': time.time(), + 'type': prediction_type, + 'correct': was_correct, + 'threshold': getattr(self, 'extrema_threshold', 0.7) + }) + + # Keep only the last 100 predictions + if len(self.prediction_history) > 100: + self.prediction_history = self.prediction_history[-100:] + + def _initialize_weights(self): + for m in self.modules(): + if isinstance(m, nn.Linear): + nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='leaky_relu') + if m.bias is not None: + nn.init.constant_(m.bias, 0) + + def forward(self, timeframe_data_list): + """ + Forward pass through the model + + Args: + timeframe_data_list: List of tensors for different timeframes + Each tensor has shape [batch_size, sequence_length, input_size] + + Returns: + price_predictions: Predicted prices for the next 5 time steps + extrema_predictions: Predicted extrema (highs/lows) for the next 5 time steps + """ + batch_size = timeframe_data_list[0].size(0) + device = timeframe_data_list[0].device + + # Process each timeframe with its own LSTM + timeframe_features = [] + + for i, data in enumerate(timeframe_data_list): + if i >= self.num_timeframes: + break # Only process up to num_timeframes + + # Pass through LSTM + lstm_out, _ = self.timeframe_lstms[i](data) + + # Get the last output for each sequence in the batch + last_out = lstm_out[:, -1, :] + + # Apply self-attention to the LSTM outputs + attn_out, _ = self.self_attentions[i](lstm_out, lstm_out, lstm_out) + + # Get the last output after attention + attn_last = attn_out[:, -1, :] + + # Combine LSTM and attention outputs + combined = (last_out + attn_last) / 2 + + timeframe_features.append(combined) + + # If we have fewer timeframes than expected, pad with zeros + while len(timeframe_features) < self.num_timeframes: + timeframe_features.append(torch.zeros(batch_size, self.hidden_size, device=device)) + + # Concatenate features from all timeframes + combined_features = torch.cat(timeframe_features, dim=1) + + # Apply fusion layer + fused_features = self.fusion_layer(combined_features) + + # Generate price predictions + price_preds = self.price_fc(fused_features) + + # Generate extrema predictions + extrema_preds = self.extrema_fc(fused_features) + + return price_preds, extrema_preds + + def fit_scalers(self, price_data_list, volume_data_list=None): + """ + Explicitly fit the scalers with data + + Args: + price_data_list: List of price data arrays for different timeframes + volume_data_list: List of volume data arrays for different timeframes (optional) + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_data_list, list): + price_data_list = [price_data_list] + + # Create default volume data if not provided + if volume_data_list is None: + volume_data_list = [np.ones_like(prices) for prices in price_data_list] + elif not isinstance(volume_data_list, list): + volume_data_list = [volume_data_list] + + # Fit scalers for each timeframe + for i, (prices, volumes) in enumerate(zip(price_data_list, volume_data_list)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new price_scaler_{i}") + + if not hasattr(self, f'volume_scaler_{i}'): + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + logger.info(f"Created new volume_scaler_{i}") + + # Get the scalers + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Fit the scalers + price_scaler.fit(prices) + volume_scaler.fit(volumes) + + logger.info(f"Fitted price_scaler_{i} with {len(prices)} data points, range: [{np.min(prices):.2f}, {np.max(prices):.2f}]") + logger.info(f"Fitted volume_scaler_{i} with {len(volumes)} data points, range: [{np.min(volumes):.2f}, {np.max(volumes):.2f}]") + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + except Exception as e: + logger.error(f"Error fitting scalers for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + + def preprocess(self, price_history, volume_history=None, timeframes=None): + """ + Preprocess price and volume data for model input + + Args: + price_history: List of price histories for different timeframes + volume_history: List of volume histories for different timeframes + timeframes: List of timeframe names (for logging) + + Returns: + Preprocessed data ready for model input + """ + # If single timeframe data is provided, convert to list format + if not isinstance(price_history, list): + price_history = [price_history] + if volume_history is not None: + volume_history = [volume_history] + + # Ensure volume history exists + if volume_history is None: + volume_history = [np.ones_like(prices) for prices in price_history] + + # Process each timeframe + processed_data = [] + + for i, (prices, volumes) in enumerate(zip(price_history, volume_history)): + try: + # Convert to numpy arrays if they aren't already + prices = np.array(prices).reshape(-1, 1) + volumes = np.array(volumes).reshape(-1, 1) + + # Ensure volumes has the same length as prices + if len(volumes) != len(prices): + logger.warning(f"Volume length ({len(volumes)}) doesn't match price length ({len(prices)}). Adjusting...") + if len(volumes) > len(prices): + volumes = volumes[:len(prices)] + else: + # Pad volumes with the mean value + mean_volume = np.mean(volumes) + padding = np.full((len(prices) - len(volumes), 1), mean_volume) + volumes = np.vstack((volumes, padding)) + + # Create scalers if they don't exist + if not hasattr(self, f'price_scaler_{i}'): + logger.info(f"Creating new price_scaler_{i}") + setattr(self, f'price_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + if not hasattr(self, f'volume_scaler_{i}'): + logger.info(f"Creating new volume_scaler_{i}") + setattr(self, f'volume_scaler_{i}', MinMaxScaler(feature_range=(0, 1))) + + price_scaler = getattr(self, f'price_scaler_{i}') + volume_scaler = getattr(self, f'volume_scaler_{i}') + + # Always fit the scalers with the current data to ensure they're properly initialized + # This is the key change to fix the "Price scaler is not fitted yet" error + logger.info(f"Fitting price_scaler_{i} with {len(prices)} data points") + price_scaler.fit(prices) + + logger.info(f"Fitting volume_scaler_{i} with {len(volumes)} data points") + volume_scaler.fit(volumes) + + # Also fit the main scalers with the first timeframe data + if i == 0: + self.price_scaler.fit(prices) + self.volume_scaler.fit(volumes) + logger.info(f"Fitted main price_scaler with {len(prices)} data points") + + # Transform the data + try: + scaled_prices = price_scaler.transform(prices) + except Exception as e: + logger.warning(f"Error transforming prices with scaler {i}: {e}. Refitting scaler.") + price_scaler.fit(prices) + scaled_prices = price_scaler.transform(prices) + + try: + scaled_volumes = volume_scaler.transform(volumes) + except Exception as e: + logger.warning(f"Error transforming volumes with scaler {i}: {e}. Refitting scaler.") + volume_scaler.fit(volumes) + scaled_volumes = volume_scaler.transform(volumes) + + # Combine price and volume data + combined_data = np.hstack((scaled_prices, scaled_volumes)) + + # Convert to tensor and move to the same device as the model + tensor_data = torch.FloatTensor(combined_data).unsqueeze(0) + tensor_data = tensor_data.to(next(self.parameters()).device) # Move to same device as model + + processed_data.append(tensor_data) + + if timeframes: + timeframe_name = timeframes[i] if i < len(timeframes) else f"timeframe_{i}" + logger.info(f"Processed {timeframe_name} data: {tensor_data.shape}") + except Exception as e: + logger.error(f"Error preprocessing data for timeframe {i}: {e}") + logger.error(traceback.format_exc()) + # Create a dummy tensor with zeros if processing fails + dummy_tensor = torch.zeros((1, len(prices), 2)).to(next(self.parameters()).device) + processed_data.append(dummy_tensor) + + return processed_data + + def postprocess_price(self, scaled_predictions, timeframe_idx=0): + """ + Postprocess the predicted prices to convert them back to actual price values. + + Args: + scaled_predictions: Predicted prices in scaled format + timeframe_idx: Index of the timeframe + + Returns: + actual_prices: Predicted prices in actual format + """ + try: + # Get the appropriate scaler + price_scaler = getattr(self, f'price_scaler_{timeframe_idx}', None) + + # If the specific timeframe scaler doesn't exist, fall back to the main scaler + if price_scaler is None: + price_scaler = self.price_scaler + logger.info(f"Using main price scaler instead of timeframe_idx {timeframe_idx} scaler") + + # Check if the scaler is fitted + if not hasattr(price_scaler, 'data_min_') or not hasattr(price_scaler, 'data_max_'): + # Try to get current price from the model's last input if available + current_price = None + try: + # This is a fallback mechanism to estimate a reasonable price range + # We'll use a default range around 2000 (typical ETH price) if we can't get better data + current_price = 2000.0 # Default fallback + + # Log the issue but don't raise an exception + logger.warning(f"Price scaler for timeframe {timeframe_idx} is not fitted yet. Using default price range.") + except: + pass + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Use a reasonable default price range + if current_price is not None: + # Use a range of ±10% around the current price + price_min = current_price * 0.9 + price_max = current_price * 1.1 + else: + # Fallback to a typical ETH price range if we don't have current price + price_min = 1800 + price_max = 2000 + + # Map the scaled values (assumed to be in [0,1]) to the price range + actual_predictions = price_min + scaled_predictions.flatten() * (price_max - price_min) + + return actual_predictions + + # Convert to numpy and reshape + scaled_predictions = scaled_predictions.detach().cpu().numpy() + scaled_predictions = scaled_predictions.reshape(-1, 1) + + # Inverse transform + actual_predictions = price_scaler.inverse_transform(scaled_predictions) + + return actual_predictions.flatten() + except Exception as e: + # If there's an error, return a reasonable estimate based on typical crypto prices + logger.warning(f"Error in postprocess_price: {e}. Using default price range.") + # Get traceback for debugging + logger.debug(traceback.format_exc()) + + # Assume scaled values are in [0,1] range and map to a reasonable price range + try: + scaled_values = scaled_predictions.detach().cpu().numpy().flatten() + except: + # If we can't get the scaled values from the tensor, create a reasonable default + scaled_values = np.linspace(0.4, 0.6, len(scaled_predictions)) + + # Map to a reasonable ETH price range (1800-2000) + return 1800 + scaled_values * 200 + + def validate_predictions(self, new_candle): + """ + Validate previous extrema predictions against new candle data. + Updates prediction accuracy metrics and improves future predictions. + + Args: + new_candle: The new candle data to validate against + """ + if not hasattr(self, 'prediction_history') or not self.prediction_history: + return + + current_timestamp = new_candle['timestamp'] + current_price = new_candle['close'] + high_price = new_candle['high'] + low_price = new_candle['low'] + + # Track validation metrics + validated_count = 0 + correct_count = 0 + + # Check each prediction that hasn't been validated yet + for pred in self.prediction_history: + if pred['validated']: + continue + + # Check if this prediction's time has come (or passed) + if current_timestamp >= pred['predicted_timestamp']: + pred['validated'] = True + validated_count += 1 + + # Check if prediction was correct + if pred['type'] == 'low': + # A low prediction is correct if price went within 0.5% of predicted low + price_diff_percent = abs(low_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = low_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went lower than predicted + was_correct = price_diff_percent < 0.5 or low_price <= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT low prediction: predicted={pred['price']:.2f}, actual={low_price:.2f}, diff={price_diff_percent:.2f}%") + + elif pred['type'] == 'high': + # A high prediction is correct if price went within 0.5% of predicted high + price_diff_percent = abs(high_price - pred['price']) / pred['price'] * 100 + pred['actual_price'] = high_price + pred['price_diff_percent'] = price_diff_percent + + # Consider correct if within 0.5% or price went higher than predicted + was_correct = price_diff_percent < 0.5 or high_price >= pred['price'] + pred['was_correct'] = was_correct + pred['correct'] = was_correct # Add this line to set the 'correct' field + + if was_correct: + correct_count += 1 + logger.info(f"CORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + else: + logger.info(f"INCORRECT high prediction: predicted={pred['price']:.2f}, actual={high_price:.2f}, diff={price_diff_percent:.2f}%") + + # Update prediction accuracy metrics + if validated_count > 0: + accuracy = correct_count / validated_count + logger.info(f"Prediction accuracy: {accuracy:.2f} ({correct_count}/{validated_count})") + + # Update prediction history for adaptive threshold calculation + for pred in self.prediction_history: + if pred['validated'] and pred['was_correct'] is not None: + self.update_prediction_history(pred['was_correct'], pred['type']) + + def add_data(self, candle): + """Add a new candle to the environment data""" + if not self.data: + self.data = [candle] + else: + self.data.append(candle) + + # Update features + self._update_features() + + # Validate previous predictions with new data + self.validate_predictions(candle) + + # Update price predictions with new data + self.update_price_predictions() + + return True + + def calculate_adaptive_threshold(self): + """ + Calculate an adaptive threshold for extrema predictions based on: + 1. Historical prediction accuracy + 2. Current market volatility + 3. Recent prediction confidence + """ + # Start with a base threshold + base_threshold = 0.65 + + # 1. Adjust based on historical prediction accuracy + accuracy_adjustment = 0 + + if hasattr(self, 'prediction_history') and len(self.prediction_history) > 0: + # Calculate accuracy of past predictions + # Check if entries have 'correct' key, otherwise use 'validated' or default to False + correct_predictions = sum(1 for p in self.prediction_history if p.get('correct', p.get('validated', False))) + total_predictions = len(self.prediction_history) + + if total_predictions > 0: + accuracy = correct_predictions / total_predictions + + # Adjust threshold based on accuracy + if accuracy > 0.7: + # High accuracy - can lower threshold + accuracy_adjustment = -0.1 + elif accuracy < 0.3: + # Low accuracy - need to raise threshold + accuracy_adjustment = 0.1 + + # 2. Adjust based on market volatility + volatility_adjustment = 0 + volatility = self.get_recent_volatility() + + if volatility > 0.02: + # High volatility - raise threshold to avoid false signals + volatility_adjustment = 0.05 + elif volatility < 0.005: + # Low volatility - can lower threshold + volatility_adjustment = -0.05 + + # 3. Adjust based on recent prediction confidence + confidence_adjustment = 0 + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("Program terminated by user") \ No newline at end of file diff --git a/crypto/gogo2/main_multiu_fixed.py b/crypto/gogo2/main_multiu_fixed.py new file mode 100644 index 0000000..877466a --- /dev/null +++ b/crypto/gogo2/main_multiu_fixed.py @@ -0,0 +1,30 @@ +import asyncio +import logging +from exchange_simulator import ExchangeSimulator + +# Set up logging +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +async def main(): + """ + Main function to run the training process. + """ + # Initialize exchange simulator + exchange = ExchangeSimulator() + + # Train agent + print("Starting training process...") + # Add your training code here + print("Training complete!") + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("Program terminated by user") + except Exception as e: + logger.error(f"Error running main: {e}") + import traceback + logger.error(traceback.format_exc()) \ No newline at end of file diff --git a/crypto/gogo2/run_enhanced_training.py b/crypto/gogo2/run_enhanced_training.py index 8ca985d..ec2f86c 100644 --- a/crypto/gogo2/run_enhanced_training.py +++ b/crypto/gogo2/run_enhanced_training.py @@ -1,9 +1,15 @@ import argparse import os +import sys +import asyncio import torch from enhanced_training import enhanced_train_agent from exchange_simulator import ExchangeSimulator +# Fix for Windows asyncio +if sys.platform == 'win32': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + def main(): # Parse command line arguments parser = argparse.ArgumentParser(description='Enhanced Trading Bot Training') @@ -26,6 +32,9 @@ def main(): parser.add_argument('--refresh-data', action='store_true', help='Refresh data before training') + parser.add_argument('--verbose', action='store_true', + help='Enable verbose logging') + args = parser.parse_args() # Set device @@ -51,7 +60,8 @@ def main(): exchange=exchange, num_episodes=args.episodes, continuous=False, - start_episode=0 + start_episode=0, + verbose=args.verbose ) elif args.mode == 'continuous': @@ -61,7 +71,8 @@ def main(): exchange=exchange, num_episodes=args.episodes, continuous=True, - start_episode=args.start_episode + start_episode=args.start_episode, + verbose=args.verbose ) elif args.mode == 'evaluate': diff --git a/crypto/gogo2/run_main.py b/crypto/gogo2/run_main.py new file mode 100644 index 0000000..6029758 --- /dev/null +++ b/crypto/gogo2/run_main.py @@ -0,0 +1,18 @@ +import asyncio +import logging +from enhanced_training import main + +# Set up logging +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + logger.info("Program terminated by user") + except Exception as e: + logger.error(f"Error running main: {e}") + import traceback + logger.error(traceback.format_exc()) \ No newline at end of file diff --git a/crypto/gogo2/visualize_logs.py b/crypto/gogo2/visualize_logs.py new file mode 100644 index 0000000..05fb157 --- /dev/null +++ b/crypto/gogo2/visualize_logs.py @@ -0,0 +1,56 @@ +import os +import argparse +import subprocess +import webbrowser +import time +from pathlib import Path + +def main(): + parser = argparse.ArgumentParser(description='Visualize TensorBoard logs') + parser.add_argument('--logdir', type=str, default='./logs', help='Directory containing TensorBoard logs') + parser.add_argument('--port', type=int, default=6006, help='Port for TensorBoard server') + args = parser.parse_args() + + log_dir = Path(args.logdir) + + if not log_dir.exists(): + print(f"Log directory {log_dir} does not exist. Creating it...") + log_dir.mkdir(parents=True, exist_ok=True) + + # Check if TensorBoard is installed + try: + subprocess.run(['tensorboard', '--version'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except (subprocess.CalledProcessError, FileNotFoundError): + print("TensorBoard not found. Installing...") + subprocess.run(['pip', 'install', 'tensorboard'], check=True) + + # Start TensorBoard server + print(f"Starting TensorBoard server on port {args.port}...") + tensorboard_process = subprocess.Popen( + ['tensorboard', '--logdir', str(log_dir), '--port', str(args.port)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + # Wait for TensorBoard to start + time.sleep(3) + + # Open browser + url = f"http://localhost:{args.port}" + print(f"Opening TensorBoard in browser: {url}") + webbrowser.open(url) + + print("TensorBoard is running. Press Ctrl+C to stop.") + + try: + # Keep the script running until interrupted + while True: + time.sleep(1) + except KeyboardInterrupt: + print("Stopping TensorBoard server...") + tensorboard_process.terminate() + tensorboard_process.wait() + print("TensorBoard server stopped.") + +if __name__ == "__main__": + main() \ No newline at end of file