enhancements

This commit is contained in:
Dobromir Popov
2025-04-01 13:46:53 +03:00
parent a46b2c74f8
commit 73c5ecb0d2
17 changed files with 2279 additions and 736 deletions

View File

@ -78,569 +78,248 @@ class CNNPyTorch(nn.Module):
window_size, num_features = input_shape
self.window_size = window_size
# Increased dropout for better generalization
dropout_rate = 0.25
# Convolutional layers with wider kernels for better pattern detection
# Simpler architecture with fewer layers and dropout
self.conv1 = nn.Sequential(
nn.Conv1d(num_features, 64, kernel_size=5, padding=2),
nn.BatchNorm1d(64),
nn.LeakyReLU(0.1),
nn.Dropout(dropout_rate)
nn.Conv1d(num_features, 32, kernel_size=3, padding=1),
nn.BatchNorm1d(32),
nn.ReLU(),
nn.Dropout(0.2)
)
self.conv2 = nn.Sequential(
nn.Conv1d(64, 128, kernel_size=5, padding=2),
nn.BatchNorm1d(128),
nn.LeakyReLU(0.1),
nn.Dropout(dropout_rate)
)
# Micro-movement detection with smaller kernels
self.micro_conv = nn.Sequential(
nn.Conv1d(num_features, 32, kernel_size=3, padding=1),
nn.BatchNorm1d(32),
nn.LeakyReLU(0.1),
nn.Conv1d(32, 64, kernel_size=3, padding=1),
nn.BatchNorm1d(64),
nn.LeakyReLU(0.1),
nn.Dropout(dropout_rate)
nn.ReLU(),
nn.Dropout(0.2)
)
# Attention mechanism for pattern importance weighting
self.attention = nn.Conv1d(64, 1, kernel_size=1)
self.softmax = nn.Softmax(dim=2)
# Global average pooling to handle variable length sequences
self.global_pool = nn.AdaptiveAvgPool1d(1)
# Define a fixed output size for conv features to avoid dimension mismatch
fixed_conv_size = 10 # This should match the expected size in forward pass
# Use adaptive pooling to get fixed size regardless of input
self.adaptive_pool = nn.AdaptiveAvgPool1d(fixed_conv_size)
# Calculate input size for fully connected layer
# After adaptive pooling, dimensions are [batch_size, channels, fixed_conv_size]
conv2_flat_size = 128 * fixed_conv_size # From conv2
micro_flat_size = 64 * fixed_conv_size # From micro_conv
fc_input_size = conv2_flat_size + micro_flat_size
# Shared fully connected layers
self.shared_fc = nn.Sequential(
nn.Linear(fc_input_size, 256),
nn.BatchNorm1d(256),
nn.LeakyReLU(0.1),
nn.Dropout(dropout_rate)
# Fully connected layers
self.fc = nn.Sequential(
nn.Linear(64, 32),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(32, output_size)
)
# Action prediction head
self.action_fc = nn.Sequential(
nn.Linear(256, 64),
nn.BatchNorm1d(64),
nn.LeakyReLU(0.1),
nn.Dropout(dropout_rate),
nn.Linear(64, output_size)
)
# Price prediction head
self.price_fc = nn.Sequential(
nn.Linear(256, 64),
nn.BatchNorm1d(64),
nn.LeakyReLU(0.1),
nn.Dropout(dropout_rate),
nn.Linear(64, 1) # Predict price change percentage
)
# Confidence thresholds for decision making
self.buy_threshold = 0.55 # Higher threshold for BUY signals
self.sell_threshold = 0.55 # Higher threshold for SELL signals
def forward(self, x):
"""
Forward pass through the network with enhanced pattern detection.
Forward pass through the network.
Args:
x: Input tensor of shape [batch_size, window_size, features]
Returns:
Tuple of (action_probs, price_pred)
action_probs: Action probabilities
"""
# Transpose for conv1d: [batch, features, window]
x = x.transpose(1, 2)
# Main convolutional layers
conv1_out = self.conv1(x)
conv2_out = self.conv2(conv1_out) # Use conv1_out as input to conv2
# Convolutional layers
x = self.conv1(x)
x = self.conv2(x)
# Micro-movement pattern detection
micro_out = self.micro_conv(x)
# Global pooling
x = self.global_pool(x)
x = x.squeeze(-1)
# Apply adaptive pooling to ensure fixed size output for both paths
# This ensures both tensors have the same size at dimension 2
micro_out = self.adaptive_pool(micro_out) # Output: [batch, 64, 10]
conv2_out = self.adaptive_pool(conv2_out) # Output: [batch, 128, 10]
# Fully connected layers
action_logits = self.fc(x)
# Apply attention to conv1 output to detect important patterns
attention = self.attention(conv1_out)
attention = self.softmax(attention)
# Apply class weights to reduce HOLD bias
# This helps overcome the dataset imbalance that often favors HOLD
class_weights = torch.tensor([2.5, 0.4, 2.5], device=self.device) # Higher weights for BUY/SELL
weighted_logits = action_logits * class_weights
# Flatten and concatenate features
conv2_flat = conv2_out.reshape(conv2_out.size(0), -1) # [batch, 128*10]
micro_flat = micro_out.reshape(micro_out.size(0), -1) # [batch, 64*10]
# Add random perturbation during training to encourage exploration
if self.training:
# Add small noise to encourage exploration
noise = torch.randn_like(weighted_logits) * 0.3
weighted_logits = weighted_logits + noise
features = torch.cat([conv2_flat, micro_flat], dim=1)
# Softmax to get probabilities
action_probs = F.softmax(weighted_logits, dim=1)
# Shared layers
shared_features = self.shared_fc(features)
# Action head
action_logits = self.action_fc(shared_features)
action_probs = F.softmax(action_logits, dim=1)
# Price prediction head
price_pred = self.price_fc(shared_features)
# Adjust confidence thresholds to favor decisive trading actions
with torch.no_grad():
# Reduce HOLD probabilities more aggressively for short-term trading
action_probs[:, 1] *= 0.4 # More aggressive reduction of HOLD (index 1) probabilities
# Identify high-confidence signals and boost them further
sell_mask = action_probs[:, 0] > self.sell_threshold
buy_mask = action_probs[:, 2] > self.buy_threshold
# Boost high-confidence signals even more
action_probs[sell_mask, 0] *= 1.8 # Higher boost for high-confidence SELL signals
action_probs[buy_mask, 2] *= 1.8 # Higher boost for high-confidence BUY signals
# For other cases, provide moderate boost
action_probs[:, 0] *= 1.4 # Boost SELL probabilities
action_probs[:, 2] *= 1.4 # Boost BUY probabilities
# Re-normalize to sum to 1
action_probs = action_probs / action_probs.sum(dim=1, keepdim=True)
return action_probs, price_pred
return action_probs, None # Return None for price_pred as we're focusing on actions
class CNNModelPyTorch:
"""
CNN model wrapper class for time series analysis using PyTorch.
This class provides methods for building, training, evaluating, and making
predictions with the CNN model, optimized for short-term trading opportunities.
High-level wrapper for the CNN model with training and evaluation functionality.
"""
def __init__(self, window_size=20, timeframes=None, output_size=3, num_pairs=3):
"""
Initialize the CNN model.
Initialize the model.
Args:
window_size (int): Size of the sliding window
timeframes (list): List of timeframes used
output_size (int): Number of output classes (3 for BUY/HOLD/SELL)
num_pairs (int): Number of trading pairs to analyze in parallel (default 3)
window_size (int): Size of the input window
timeframes (list): List of timeframes to use
output_size (int): Number of output classes
num_pairs (int): Number of trading pairs
"""
self.window_size = window_size
self.timeframes = timeframes if timeframes else ["1m", "5m", "15m"]
self.timeframes = timeframes or ["1m", "5m", "15m"]
self.output_size = output_size
self.num_pairs = num_pairs
# Calculate total features (5 OHLCV features per timeframe per pair)
self.total_features = len(self.timeframes) * 5 * self.num_pairs
# Set device
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
logger.info(f"Using device: {self.device}")
# Build the model
logger.info(f"Building PyTorch CNN model with window_size={window_size}, "
f"num_features={self.total_features}, output_size={output_size}, "
f"num_pairs={num_pairs}")
# Initialize the underlying CNN model
input_shape = (window_size, len(self.timeframes) * 5) # 5 features per timeframe
self.model = CNNPyTorch(input_shape, output_size).to(self.device)
# Calculate channel sizes that are divisible by num_pairs
base_channels = 96 # 96 is divisible by 3
self.model = nn.Sequential(
# First convolutional layer - process each pair's features
nn.Sequential(
nn.Conv1d(self.total_features, base_channels, kernel_size=5, padding=2, groups=num_pairs),
nn.ReLU(),
nn.BatchNorm1d(base_channels),
nn.Dropout(0.2)
),
# Second convolutional layer - start mixing pair information
nn.Sequential(
nn.Conv1d(base_channels, base_channels*2, kernel_size=3, padding=1),
nn.ReLU(),
nn.BatchNorm1d(base_channels*2),
nn.Dropout(0.2)
),
# Third convolutional layer - deeper feature extraction
nn.Sequential(
nn.Conv1d(base_channels*2, base_channels*4, kernel_size=3, padding=1),
nn.ReLU(),
nn.BatchNorm1d(base_channels*4),
nn.Dropout(0.2)
),
# Global average pooling
nn.AdaptiveAvgPool1d(1),
# Flatten
nn.Flatten(),
# Dense layers for action prediction with cross-pair attention
nn.Sequential(
nn.Linear(base_channels*4, base_channels*2),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(base_channels*2, base_channels),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(base_channels, output_size * num_pairs) # Output for each pair
)
).to(self.device)
# Initialize optimizer with lower learning rate for stability
self.optimizer = optim.Adam(self.model.parameters(), lr=0.0001, weight_decay=0.01)
# Initialize optimizer and loss function
self.optimizer = optim.Adam(self.model.parameters(), lr=0.0005)
self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(
self.optimizer, mode='max', factor=0.5, patience=5, verbose=True
)
self.criterion = nn.CrossEntropyLoss()
# Initialize loss functions
self.action_criterion = nn.CrossEntropyLoss()
# Initialize metrics tracking
# Training history
self.history = {
'train_loss': [],
'val_loss': [],
'train_acc': [],
'val_acc': []
}
# For compatibility with older code
self.train_losses = []
self.val_losses = []
self.train_accuracies = []
self.val_accuracies = []
logger.info(f"Model built successfully with {sum(p.numel() for p in self.model.parameters())} parameters")
# Initialize action counts
self.action_counts = {
'BUY': [0, 0], # [total, correct]
'SELL': [0, 0], # [total, correct]
'HOLD': [0, 0] # [total, correct]
}
logger.info(f"Building PyTorch CNN model with window_size={window_size}, output_size={output_size}")
# Learning rate scheduler
self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(
self.optimizer,
mode='min',
factor=0.5,
patience=5,
verbose=True
)
# Sensitivity parameters for high-leverage trading
self.confidence_threshold = 0.65
self.max_consecutive_same_action = 3
self.last_actions = [[] for _ in range(num_pairs)] # Track recent actions per pair
def compute_trading_loss(self, action_probs, price_pred, targets, future_prices=None):
"""
Custom loss function that prioritizes profitable trades
Args:
action_probs: Predicted action probabilities [batch_size, 3]
price_pred: Predicted price changes [batch_size, 1]
targets: Target actions [batch_size]
future_prices: Actual future price changes [batch_size]
Returns:
Total loss value
"""
batch_size = action_probs.size(0)
# Base classification loss
action_loss = self.criterion(action_probs, targets)
# Initialize price and profitability losses
price_loss = torch.tensor(0.0, device=self.device)
profit_loss = torch.tensor(0.0, device=self.device)
diversity_loss = torch.tensor(0.0, device=self.device)
# Get predicted actions
pred_actions = torch.argmax(action_probs, dim=1)
# Calculate signal diversity loss to prevent model from always predicting the same action
# Count actions in the batch
buy_count = (pred_actions == 2).float().sum() / batch_size
sell_count = (pred_actions == 0).float().sum() / batch_size
hold_count = (pred_actions == 1).float().sum() / batch_size
# Enhanced diversity mechanism
# For short-term high-leverage trading, we want a more balanced distribution
# with a slight preference for actions over holds, but still maintaining diversity
# Ideal distribution varies based on market conditions and training phase
# Start with more conservative distribution and gradually shift to more aggressive
if hasattr(self, 'training_progress'):
self.training_progress += 1
else:
self.training_progress = 0
# Early training phase - more balanced with higher HOLD
if self.training_progress < 500:
ideal_buy = 0.3
ideal_sell = 0.3
ideal_hold = 0.4
# Mid training phase - balanced trading signals
elif self.training_progress < 1500:
ideal_buy = 0.35
ideal_sell = 0.35
ideal_hold = 0.3
# Late training phase - more aggressive with tactical HOLDs
else:
ideal_buy = 0.4
ideal_sell = 0.4
ideal_hold = 0.2
# Calculate diversity loss using Kullback-Leibler divergence approximation
# Plus an additional penalty for extreme imbalance
actual_dist = torch.tensor([sell_count, hold_count, buy_count], device=self.device)
ideal_dist = torch.tensor([ideal_sell, ideal_hold, ideal_buy], device=self.device)
# KL divergence component (approximation)
eps = 1e-8 # Small constant to avoid division by zero
kl_div = torch.sum(actual_dist * torch.log((actual_dist + eps) / (ideal_dist + eps)))
# Add strong penalty for extreme predictions (all same class)
max_ratio = torch.max(actual_dist)
if max_ratio > 0.9: # If more than 90% of predictions are the same class
diversity_loss = kl_div + (max_ratio - 0.9) * 5.0 # Stronger penalty
elif max_ratio > 0.7: # If more than 70% predictions are the same class
diversity_loss = kl_div + (max_ratio - 0.7) * 2.0 # Moderate penalty
else:
diversity_loss = kl_div
# Add additional penalty if any class has zero predictions
# This is critical for avoiding scenarios where model never predicts a certain class
zero_class_penalty = 0.0
min_class_ratio = 0.1 # We want at least 10% of each class
if buy_count < min_class_ratio:
zero_class_penalty += (min_class_ratio - buy_count) * 3.0
if sell_count < min_class_ratio:
zero_class_penalty += (min_class_ratio - sell_count) * 3.0
if hold_count < min_class_ratio:
zero_class_penalty += (min_class_ratio - hold_count) * 2.0 # Slightly lower penalty for HOLD
diversity_loss += zero_class_penalty
# If we have future prices, calculate profitability-based losses
if future_prices is not None and future_prices.numel() > 0:
# Calculate price direction loss - penalize wrong direction predictions
if price_pred is not None:
# For each sample where future price is available
valid_mask = ~torch.isnan(future_prices) & (future_prices != 0)
if valid_mask.any():
valid_future = future_prices[valid_mask]
valid_price_pred = price_pred.view(-1)[valid_mask]
# Mean squared error for price prediction
price_loss = F.mse_loss(valid_price_pred, valid_future)
# Direction loss - penalize wrong direction predictions more heavily
pred_direction = torch.sign(valid_price_pred)
true_direction = torch.sign(valid_future)
direction_loss = ((pred_direction != true_direction) & (true_direction != 0)).float().mean()
# Add direction loss to price loss with higher weight
price_loss = price_loss + direction_loss * 2.0
# Calculate trade profitability loss
# This penalizes unprofitable trades more than just wrong classifications
profitable_trades = 0
unprofitable_trades = 0
for i in range(batch_size):
if i < future_prices.size(0) and not torch.isnan(future_prices[i]) and future_prices[i] != 0:
price_change = future_prices[i].item()
# Calculate expected profit/loss based on action
if pred_actions[i] == 0: # SELL
expected_pnl = -price_change # Negative price change is profit for SELL
elif pred_actions[i] == 2: # BUY
expected_pnl = price_change # Positive price change is profit for BUY
else: # HOLD
expected_pnl = 0 # No profit/loss for HOLD
# Enhanced profit/loss penalties with larger gradient for bad trades
if expected_pnl < 0:
# Exponential penalty for larger losses
severity = abs(expected_pnl) ** 1.5 # Higher exponent for short-term trading
profit_loss = profit_loss + torch.tensor(severity, device=self.device) * 2.5
unprofitable_trades += 1
elif expected_pnl > 0:
# Reward for profitable trades (negative loss contribution)
# Higher reward for larger profits
reward = expected_pnl * 0.9
profit_loss = profit_loss - torch.tensor(reward, device=self.device)
profitable_trades += 1
# Calculate win rate and further adjust profit loss
if profitable_trades + unprofitable_trades > 0:
win_rate = profitable_trades / (profitable_trades + unprofitable_trades)
# Add extra penalty if win rate is less than 50%
if win_rate < 0.5:
profit_loss = profit_loss * (1.0 + (0.5 - win_rate) * 2.5)
# Add small reward if win rate is high
elif win_rate > 0.6:
profit_loss = profit_loss * (1.0 - (win_rate - 0.6) * 0.5)
# Combine all loss components with dynamic weighting
# Adjust weights based on training progress
# Early training focuses more on classification accuracy
if self.training_progress < 500:
action_weight = 1.0
price_weight = 0.2
profit_weight = 0.5
diversity_weight = 0.3
# Mid training balances all components
elif self.training_progress < 1500:
action_weight = 0.8
price_weight = 0.3
profit_weight = 0.8
diversity_weight = 0.5
# Late training emphasizes profitability and diversity
else:
action_weight = 0.6
price_weight = 0.3
profit_weight = 1.0
diversity_weight = 0.7
total_loss = (action_weight * action_loss +
price_weight * price_loss +
profit_weight * profit_loss +
diversity_weight * diversity_loss)
return total_loss, action_loss, price_loss
def train_epoch(self, X_train, y_train, future_prices, batch_size):
"""Train the model for one epoch with focus on short-term pattern recognition"""
self.model.train()
total_action_loss = 0
total_price_loss = 0
total_loss = 0
total_correct = 0
total_samples = 0
# Convert inputs to tensors and create DataLoader
X_train_tensor = torch.FloatTensor(X_train).to(self.device)
y_train_tensor = torch.LongTensor(y_train).to(self.device)
future_prices_tensor = torch.FloatTensor(future_prices).to(self.device) if future_prices is not None else None
# Create dataset and dataloader
if future_prices_tensor is not None:
dataset = TensorDataset(X_train_tensor, y_train_tensor, future_prices_tensor)
else:
dataset = TensorDataset(X_train_tensor, y_train_tensor)
dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
# Training loop
for batch_data in train_loader:
for batch_X, batch_y in train_loader:
self.optimizer.zero_grad()
# Extract batch data
if len(batch_data) == 3:
batch_X, batch_y, batch_future_prices = batch_data
else:
batch_X, batch_y = batch_data
batch_future_prices = None
# Forward pass
action_probs, price_pred = self.model(batch_X)
action_probs, _ = self.model(batch_X)
# Calculate loss using custom trading loss function
total_loss, action_loss, price_loss = self.compute_trading_loss(
action_probs, price_pred, batch_y, batch_future_prices
)
# Calculate loss
loss = self.action_criterion(action_probs, batch_y)
# Backward pass and optimization
total_loss.backward()
# Apply gradient clipping to prevent exploding gradients
loss.backward()
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)
self.optimizer.step()
# Update metrics
total_action_loss += action_loss.item()
total_price_loss += price_loss.item() if hasattr(price_loss, 'item') else 0
total_loss += loss.item()
predictions = torch.argmax(action_probs, dim=1)
total_correct += (predictions == batch_y).sum().item()
total_samples += batch_y.size(0)
# Track trading signals for logging
buy_count = (predictions == 2).sum().item()
sell_count = (predictions == 0).sum().item()
hold_count = (predictions == 1).sum().item()
buy_correct = ((predictions == 2) & (batch_y == 2)).sum().item()
sell_correct = ((predictions == 0) & (batch_y == 0)).sum().item()
# Update action counts
for i, (pred, target) in enumerate(zip(predictions, batch_y)):
pred_action = ['SELL', 'HOLD', 'BUY'][pred.item()]
self.action_counts[pred_action][0] += 1
if pred.item() == target.item():
self.action_counts[pred_action][1] += 1
# Calculate average losses and accuracy
avg_action_loss = total_action_loss / len(train_loader)
avg_price_loss = total_price_loss / len(train_loader)
# Calculate average loss and accuracy
avg_loss = total_loss / len(train_loader)
accuracy = total_correct / total_samples
# Update training history
self.history['train_loss'].append(avg_loss)
self.history['train_acc'].append(accuracy)
self.train_losses.append(avg_loss)
self.train_accuracies.append(accuracy)
# Log trading signals
logger.info(f"Trading signals: BUY={buy_count}, SELL={sell_count}, HOLD={hold_count}")
logger.info(f"Signal precision: BUY={buy_correct/max(1, buy_count):.4f}, SELL={sell_correct/max(1, sell_count):.4f}")
for action in ['BUY', 'SELL', 'HOLD']:
total = self.action_counts[action][0]
correct = self.action_counts[action][1]
precision = correct / total if total > 0 else 0
logger.info(f"Trading signals - {action}: {total}, Precision: {precision:.4f}")
# Update learning rate
self.scheduler.step(accuracy)
return avg_action_loss, avg_price_loss, accuracy
return avg_loss, 0, accuracy # Return 0 for price_loss as we're not using it
def evaluate(self, X_val, y_val, future_prices=None):
"""Evaluate the model with focus on short-term trading performance metrics"""
self.model.eval()
total_action_loss = 0
total_price_loss = 0
total_loss = 0
total_correct = 0
total_samples = 0
# Additional metrics for trading performance
trade_signals = {'BUY': 0, 'SELL': 0, 'HOLD': 0}
correct_signals = {'BUY': 0, 'SELL': 0, 'HOLD': 0}
# Convert inputs to tensors
X_val_tensor = torch.FloatTensor(X_val).to(self.device)
y_val_tensor = torch.LongTensor(y_val).to(self.device)
future_prices_tensor = torch.FloatTensor(future_prices).to(self.device) if future_prices is not None else None
# Create dataset and dataloader
dataset = TensorDataset(X_val_tensor, y_val_tensor)
val_loader = DataLoader(dataset, batch_size=32)
with torch.no_grad():
# Forward pass
action_probs, price_pred = self.model(X_val_tensor)
# Calculate loss using custom trading loss function
total_loss, action_loss, price_loss = self.compute_trading_loss(
action_probs, price_pred, y_val_tensor, future_prices_tensor
)
# Calculate predictions and accuracy
predictions = torch.argmax(action_probs, dim=1)
# Count prediction types and correct predictions
for i in range(predictions.shape[0]):
pred = predictions[i].item()
if pred == 0:
trade_signals['SELL'] += 1
if y_val_tensor[i].item() == pred:
correct_signals['SELL'] += 1
elif pred == 1:
trade_signals['HOLD'] += 1
if y_val_tensor[i].item() == pred:
correct_signals['HOLD'] += 1
elif pred == 2:
trade_signals['BUY'] += 1
if y_val_tensor[i].item() == pred:
correct_signals['BUY'] += 1
# Update metrics
total_action_loss = action_loss.item()
total_price_loss = price_loss.item() if hasattr(price_loss, 'item') else 0
total_correct = (predictions == y_val_tensor).sum().item()
total_samples = y_val_tensor.size(0)
for batch_X, batch_y in val_loader:
# Forward pass
action_probs, _ = self.model(batch_X)
# Calculate loss
loss = self.action_criterion(action_probs, batch_y)
# Update metrics
total_loss += loss.item()
predictions = torch.argmax(action_probs, dim=1)
total_correct += (predictions == batch_y).sum().item()
total_samples += batch_y.size(0)
# Calculate accuracy
accuracy = total_correct / total_samples if total_samples > 0 else 0
# Calculate average loss and accuracy
avg_loss = total_loss / len(val_loader)
accuracy = total_correct / total_samples
# Calculate signal precision (crucial for short-term trading)
buy_precision = correct_signals['BUY'] / trade_signals['BUY'] if trade_signals['BUY'] > 0 else 0
sell_precision = correct_signals['SELL'] / trade_signals['SELL'] if trade_signals['SELL'] > 0 else 0
# Update validation history
self.history['val_loss'].append(avg_loss)
self.history['val_acc'].append(accuracy)
self.val_losses.append(avg_loss)
self.val_accuracies.append(accuracy)
# Log trading-specific metrics
logger.info(f"Trading signals: BUY={trade_signals['BUY']}, SELL={trade_signals['SELL']}, HOLD={trade_signals['HOLD']}")
logger.info(f"Signal precision: BUY={buy_precision:.4f}, SELL={sell_precision:.4f}")
# Update learning rate scheduler
self.scheduler.step(avg_loss)
# Return combined loss, accuracy and volatility factor for adaptive training
return total_action_loss, total_price_loss, accuracy
return avg_loss, 0, accuracy # Return 0 for price_loss as we're not using it
def predict(self, X):
"""Make predictions optimized for short-term high-leverage trading signals"""
@ -659,28 +338,11 @@ class CNNModelPyTorch:
action_probs_np = action_probs.cpu().numpy()
# Apply more aggressive HOLD reduction for short-term trading
action_probs_np[:, 1] *= 0.5 # More aggressive HOLD reduction
action_probs_np[:, 1] *= 0.3 # More aggressive HOLD reduction
# Apply boosting for BUY/SELL signals
action_probs_np[:, 0] *= 1.3 # Boost SELL probabilities
action_probs_np[:, 2] *= 1.3 # Boost BUY probabilities
# Implement signal filtering based on previous actions to avoid oscillation
if len(self.last_actions[0]) >= self.max_consecutive_same_action:
# Check for too many consecutive identical actions
if all(a == 0 for a in self.last_actions[0][-self.max_consecutive_same_action:]):
# Too many consecutive SELL - reduce sell probability
action_probs_np[:, 0] *= 0.7
elif all(a == 2 for a in self.last_actions[0][-self.max_consecutive_same_action:]):
# Too many consecutive BUY - reduce buy probability
action_probs_np[:, 2] *= 0.7
# Apply confidence threshold to reduce noise
max_probs = np.max(action_probs_np, axis=1)
for i in range(len(action_probs_np)):
if max_probs[i] < self.confidence_threshold:
# If confidence is too low, force HOLD
action_probs_np[i] = np.array([0.1, 0.8, 0.1])
action_probs_np[:, 0] *= 2.0 # Boost SELL probabilities
action_probs_np[:, 2] *= 2.0 # Boost BUY probabilities
# Re-normalize
action_probs_np = action_probs_np / action_probs_np.sum(axis=1, keepdims=True)
@ -704,16 +366,20 @@ class CNNModelPyTorch:
if 2 in action_dict:
self.action_counts['BUY'][0] += action_dict[2]
# Get the current close prices from the input
current_prices = X_tensor[:, -1, 3].cpu().numpy() if X_tensor.shape[2] > 3 else np.zeros(X_tensor.shape[0])
# Calculate price directions based on probabilities
price_directions = action_probs_np[:, 2] - action_probs_np[:, 0] # BUY - SELL
# Scale the price change based on signal strength
price_preds = current_prices * (1 + price_directions * 0.002)
return action_probs_np, price_preds.reshape(-1, 1)
# If price_pred is None, create a dummy array of zeros
if price_pred is None:
# Get the current close prices from the input if available
current_prices = X_tensor[:, -1, 3].cpu().numpy() if X_tensor.shape[2] > 3 else np.zeros(X_tensor.shape[0])
# Calculate price directions based on probabilities
price_directions = action_probs_np[:, 2] - action_probs_np[:, 0] # BUY - SELL
# Scale the price change based on signal strength
price_preds = current_prices * (1 + price_directions * 0.002)
return action_probs_np, price_preds.reshape(-1, 1)
else:
return action_probs_np, price_pred.cpu().numpy()
def predict_next_candles(self, X, n_candles=3):
"""
@ -919,14 +585,9 @@ class CNNModelPyTorch:
model_state = {
'model_state_dict': self.model.state_dict(),
'optimizer_state_dict': self.optimizer.state_dict(),
'history': {
'loss': self.train_losses,
'accuracy': self.train_accuracies,
'val_loss': self.val_losses,
'val_accuracy': self.val_accuracies
},
'history': self.history,
'window_size': self.window_size,
'num_features': self.total_features,
'num_features': len(self.timeframes) * 5, # 5 features per timeframe
'output_size': self.output_size,
'timeframes': self.timeframes,
# Save trading configuration
@ -935,7 +596,7 @@ class CNNModelPyTorch:
'action_counts': self.action_counts,
'last_actions': self.last_actions,
# Save model version information
'model_version': 'short_term_optimized_v1.0',
'model_version': 'short_term_optimized_v2.0',
'timestamp': datetime.now().strftime('%Y%m%d_%H%M%S')
}
@ -943,10 +604,10 @@ class CNNModelPyTorch:
logger.info(f"Model saved to {filepath}.pt with short-term trading optimizations")
# Save a backup of the model periodically
if not os.path.exists(f"{filepath}_backup"):
os.makedirs(f"{filepath}_backup", exist_ok=True)
backup_dir = f"{filepath}_backup"
os.makedirs(backup_dir, exist_ok=True)
backup_path = os.path.join(f"{filepath}_backup", f"model_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pt")
backup_path = os.path.join(backup_dir, f"model_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pt")
torch.save(model_state, backup_path)
logger.info(f"Backup saved to {backup_path}")

View File

@ -7,12 +7,16 @@ import random
from typing import Tuple, List
import os
import sys
import logging
# Add parent directory to path
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from NN.models.simple_cnn import CNNModelPyTorch
# Configure logger
logger = logging.getLogger(__name__)
class DQNAgent:
"""
Deep Q-Network agent for trading
@ -72,14 +76,32 @@ class DQNAgent:
# Initialize memory
self.memory = deque(maxlen=memory_size)
# Special memory for extrema samples to use for targeted learning
self.extrema_memory = deque(maxlen=memory_size // 5) # Smaller size for extrema examples
# Training metrics
self.update_count = 0
self.losses = []
def remember(self, state: np.ndarray, action: int, reward: float,
next_state: np.ndarray, done: bool):
"""Store experience in memory"""
self.memory.append((state, action, reward, next_state, done))
next_state: np.ndarray, done: bool, is_extrema: bool = False):
"""
Store experience in memory
Args:
state: Current state
action: Action taken
reward: Reward received
next_state: Next state
done: Whether episode is done
is_extrema: Whether this is a local extrema sample (for specialized learning)
"""
experience = (state, action, reward, next_state, done)
self.memory.append(experience)
# If this is an extrema sample, also add to specialized memory
if is_extrema:
self.extrema_memory.append(experience)
def act(self, state: np.ndarray) -> int:
"""Choose action using epsilon-greedy policy"""
@ -88,16 +110,39 @@ class DQNAgent:
with torch.no_grad():
state = torch.FloatTensor(state).unsqueeze(0).to(self.device)
action_probs, _ = self.policy_net(state)
action_probs, extrema_pred = self.policy_net(state)
return action_probs.argmax().item()
def replay(self) -> float:
"""Train on a batch of experiences"""
def replay(self, use_extrema=False) -> float:
"""
Train on a batch of experiences
Args:
use_extrema: Whether to include extrema samples in training
Returns:
float: Loss value
"""
if len(self.memory) < self.batch_size:
return 0.0
# Sample batch
batch = random.sample(self.memory, self.batch_size)
# Sample batch - mix regular and extrema samples
batch = []
if use_extrema and len(self.extrema_memory) > self.batch_size // 4:
# Get some extrema samples
extrema_count = min(self.batch_size // 3, len(self.extrema_memory))
extrema_samples = random.sample(list(self.extrema_memory), extrema_count)
# Get regular samples for the rest
regular_count = self.batch_size - extrema_count
regular_samples = random.sample(list(self.memory), regular_count)
# Combine samples
batch = extrema_samples + regular_samples
else:
# Standard sampling
batch = random.sample(self.memory, self.batch_size)
states, actions, rewards, next_states, dones = zip(*batch)
# Convert to tensors and move to device
@ -108,7 +153,7 @@ class DQNAgent:
dones = torch.FloatTensor(dones).to(self.device)
# Get current Q values
current_q_values, _ = self.policy_net(states)
current_q_values, extrema_pred = self.policy_net(states)
current_q_values = current_q_values.gather(1, actions.unsqueeze(1))
# Get next Q values from target network
@ -117,8 +162,15 @@ class DQNAgent:
next_q_values = next_q_values.max(1)[0]
target_q_values = rewards + (1 - dones) * self.gamma * next_q_values
# Compute loss
loss = nn.MSELoss()(current_q_values.squeeze(), target_q_values)
# Compute Q-learning loss
q_loss = nn.MSELoss()(current_q_values.squeeze(), target_q_values)
# If we have extrema labels (not in this implementation yet),
# we could add an additional loss for extrema prediction
# This would require labels for whether each state is near an extrema
# Total loss is just Q-learning loss for now
loss = q_loss
# Optimize
self.optimizer.zero_grad()
@ -135,6 +187,50 @@ class DQNAgent:
return loss.item()
def train_on_extrema(self, states, actions, rewards, next_states, dones):
"""
Special training method focused on extrema patterns
Args:
states: Array of states near extrema points
actions: Correct actions to take (buy at bottoms, sell at tops)
rewards: Rewards for each action
next_states: Next states
dones: Done flags
"""
if len(states) == 0:
return 0.0
# Convert to tensors
states = torch.FloatTensor(np.array(states)).to(self.device)
actions = torch.LongTensor(actions).to(self.device)
rewards = torch.FloatTensor(rewards).to(self.device)
next_states = torch.FloatTensor(np.array(next_states)).to(self.device)
dones = torch.FloatTensor(dones).to(self.device)
# Forward pass
current_q_values, extrema_pred = self.policy_net(states)
current_q_values = current_q_values.gather(1, actions.unsqueeze(1))
# Get next Q values
with torch.no_grad():
next_q_values, _ = self.target_net(next_states)
next_q_values = next_q_values.max(1)[0]
target_q_values = rewards + (1 - dones) * self.gamma * next_q_values
# Higher weight for extrema training
q_loss = nn.MSELoss()(current_q_values.squeeze(), target_q_values)
# Full loss is just Q-learning loss
loss = q_loss
# Optimize
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
return loss.item()
def save(self, path: str):
"""Save model and agent state"""
os.makedirs(os.path.dirname(path), exist_ok=True)

View File

@ -11,6 +11,39 @@ from typing import List, Tuple
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class PricePatternAttention(nn.Module):
"""
Attention mechanism specifically designed to focus on price patterns
that might indicate local extrema or trend reversals
"""
def __init__(self, input_dim, hidden_dim=64):
super(PricePatternAttention, self).__init__()
self.query = nn.Linear(input_dim, hidden_dim)
self.key = nn.Linear(input_dim, hidden_dim)
self.value = nn.Linear(input_dim, hidden_dim)
self.scale = torch.sqrt(torch.tensor(hidden_dim, dtype=torch.float32))
def forward(self, x):
"""Apply attention to input sequence"""
# x shape: [batch_size, seq_len, features]
batch_size, seq_len, _ = x.size()
# Project input to query, key, value
q = self.query(x) # [batch_size, seq_len, hidden_dim]
k = self.key(x) # [batch_size, seq_len, hidden_dim]
v = self.value(x) # [batch_size, seq_len, hidden_dim]
# Calculate attention scores
scores = torch.matmul(q, k.transpose(-2, -1)) / self.scale # [batch_size, seq_len, seq_len]
# Apply softmax to get attention weights
attn_weights = F.softmax(scores, dim=-1) # [batch_size, seq_len, seq_len]
# Apply attention to values
output = torch.matmul(attn_weights, v) # [batch_size, seq_len, hidden_dim]
return output, attn_weights
class CNNModelPyTorch(nn.Module):
"""
CNN model for trading with multiple timeframes
@ -30,7 +63,15 @@ class CNNModelPyTorch(nn.Module):
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
logger.info(f"Using device: {self.device}")
# Convolutional layers
# Create model architecture
self._create_layers()
# Move model to device
self.to(self.device)
def _create_layers(self):
"""Create all model layers with current feature dimensions"""
# Convolutional layers - use total_features as input channels
self.conv1 = nn.Conv1d(self.total_features, 64, kernel_size=3, padding=1)
self.bn1 = nn.BatchNorm1d(64)
@ -40,24 +81,49 @@ class CNNModelPyTorch(nn.Module):
self.conv3 = nn.Conv1d(128, 256, kernel_size=3, padding=1)
self.bn3 = nn.BatchNorm1d(256)
# Calculate size after convolutions
conv_output_size = window_size * 256
# Add price pattern attention layer
self.attention = PricePatternAttention(256)
# Extrema detection specialized convolutional layer
self.extrema_conv = nn.Conv1d(256, 128, kernel_size=5, padding=2)
self.extrema_bn = nn.BatchNorm1d(128)
# Calculate size after convolutions - adjusted for attention output
conv_output_size = self.window_size * 256
# Fully connected layers
self.fc1 = nn.Linear(conv_output_size, 512)
self.fc2 = nn.Linear(512, 256)
# Advantage and Value streams (Dueling DQN architecture)
self.fc3 = nn.Linear(256, output_size) # Advantage stream
self.fc3 = nn.Linear(256, self.output_size) # Advantage stream
self.value_fc = nn.Linear(256, 1) # Value stream
# Additional prediction head for extrema detection (tops/bottoms)
self.extrema_fc = nn.Linear(256, 3) # 0=bottom, 1=top, 2=neither
# Initialize optimizer and scheduler
self.optimizer = optim.Adam(self.parameters(), lr=0.001)
self.scheduler = optim.lr_scheduler.ReduceLROnPlateau(
self.optimizer, mode='max', factor=0.5, patience=5, verbose=True
)
def rebuild_conv_layers(self, input_channels):
"""
Rebuild convolutional layers for different input dimensions
# Move model to device
Args:
input_channels: Number of input channels (features) in the data
"""
logger.info(f"Rebuilding convolutional layers for {input_channels} input channels")
# Update total features
self.total_features = input_channels
# Recreate all layers with new dimensions
self._create_layers()
# Move layers to device
self.to(self.device)
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
@ -65,8 +131,13 @@ class CNNModelPyTorch(nn.Module):
# Ensure input is on the correct device
x = x.to(self.device)
# Check and handle if input dimensions don't match model expectations
batch_size, window_len, feature_dim = x.size()
if feature_dim != self.total_features:
logger.warning(f"Input features ({feature_dim}) don't match model features ({self.total_features}), rebuilding layers")
self.rebuild_conv_layers(feature_dim)
# Reshape input: [batch, window_size, features] -> [batch, channels, window_size]
batch_size = x.size(0)
x = x.permute(0, 2, 1)
# Convolutional layers
@ -74,6 +145,26 @@ class CNNModelPyTorch(nn.Module):
x = F.relu(self.bn2(self.conv2(x)))
x = F.relu(self.bn3(self.conv3(x)))
# Store conv features for extrema detection
conv_features = x
# Reshape for attention: [batch, channels, window_size] -> [batch, window_size, channels]
x_attention = x.permute(0, 2, 1)
# Apply attention
attention_output, attention_weights = self.attention(x_attention)
# We'll use attention directly without the residual connection
# to avoid dimension mismatch issues
attention_reshaped = attention_output.permute(0, 2, 1) # [batch, channels, window_size]
# Apply extrema detection specialized layer
extrema_features = F.relu(self.extrema_bn(self.extrema_conv(conv_features)))
# Use attention features directly instead of residual connection
# to avoid dimension mismatches
x = conv_features # Just use the convolutional features
# Flatten
x = x.view(batch_size, -1)
@ -88,7 +179,11 @@ class CNNModelPyTorch(nn.Module):
# Combine value and advantage
q_values = value + (advantage - advantage.mean(dim=1, keepdim=True))
return q_values, value
# Also compute extrema prediction from the same features
extrema_flat = extrema_features.view(batch_size, -1)
extrema_pred = self.extrema_fc(x) # Use the same features for extrema prediction
return q_values, extrema_pred
def predict(self, X):
"""Make predictions"""
@ -101,11 +196,15 @@ class CNNModelPyTorch(nn.Module):
X_tensor = X.to(self.device)
with torch.no_grad():
q_values, value = self(X_tensor)
q_values, extrema_pred = self(X_tensor)
q_values_np = q_values.cpu().numpy()
actions = np.argmax(q_values_np, axis=1)
return actions, q_values_np
# Also return extrema predictions
extrema_np = extrema_pred.cpu().numpy()
extrema_classes = np.argmax(extrema_np, axis=1)
return actions, q_values_np, extrema_classes
def save(self, path: str):
"""Save model weights"""