enhancements
This commit is contained in:
@ -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}")
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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"""
|
||||
|
Reference in New Issue
Block a user