From 0042581275943074692e88afd7535c270875453d Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 25 Mar 2025 13:38:25 +0200 Subject: [PATCH] new nn wip --- .gitignore | 1 + .vscode/tasks.json | 38 + NN/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 727 bytes NN/__pycache__/main.cpython-312.pyc | Bin 0 -> 12637 bytes NN/_notes.md | 13 + NN/main.py | 265 ++++++ NN/models/cnn_model.py | 560 +++++++++++++ NN/models/cnn_model_pytorch.py | 546 ++++++++++++ NN/models/transformer_model.py | 786 +++++++++++------- NN/models/transformer_model_pytorch.py | 653 +++++++++++++++ NN/requirements.txt | 27 +- NN/start_tensorboard.py | 88 ++ NN/utils/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 493 bytes .../data_interface.cpython-312.pyc | Bin 0 -> 16274 bytes NN/utils/data_interface.py | 390 +++++++++ run_nn.py | 232 ++++++ run_nn_in_conda.bat | 3 + run_pytorch_nn.bat | 50 ++ 18 files changed, 3358 insertions(+), 294 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 NN/__pycache__/__init__.cpython-312.pyc create mode 100644 NN/__pycache__/main.cpython-312.pyc create mode 100644 NN/_notes.md create mode 100644 NN/main.py create mode 100644 NN/models/cnn_model.py create mode 100644 NN/models/cnn_model_pytorch.py create mode 100644 NN/models/transformer_model_pytorch.py create mode 100644 NN/start_tensorboard.py create mode 100644 NN/utils/__pycache__/__init__.cpython-312.pyc create mode 100644 NN/utils/__pycache__/data_interface.cpython-312.pyc create mode 100644 NN/utils/data_interface.py create mode 100644 run_nn.py create mode 100644 run_nn_in_conda.bat create mode 100644 run_pytorch_nn.bat diff --git a/.gitignore b/.gitignore index b6f22db..02bbac2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ models/trading_agent_final.pt models/trading_agent_final.pt.backup *.pt *.backup +logs/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..e00bdfd --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,38 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Start TensorBoard", + "type": "shell", + "command": "python", + "args": [ + "-m", + "tensorboard.main", + "--logdir=NN/models/saved/logs", + "--port=6006", + "--host=localhost" + ], + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^.*$", + "file": 1, + "location": 2, + "message": 3 + }, + "background": { + "activeOnStart": true, + "beginsPattern": ".*TensorBoard.*", + "endsPattern": ".*TensorBoard.*" + } + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "runOptions": { + "runOn": "folderOpen" + } + } + ] +} \ No newline at end of file diff --git a/NN/__pycache__/__init__.cpython-312.pyc b/NN/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..442d4caf72fad9a72d8ce0ed767fd405cceb0e0b GIT binary patch literal 727 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

1: + merged = Concatenate()(conv_layers) + else: + merged = conv_layers[0] + + # Add another Conv1D layer after merging + x = Conv1D(filters=filters[-1], kernel_size=3, padding='same')(merged) + x = BatchNormalization()(x) + x = LeakyReLU(alpha=0.1)(x) + x = MaxPooling1D(pool_size=2, padding='same')(x) + x = Dropout(dropout_rate)(x) + + # Bidirectional LSTM for temporal dependencies + x = Bidirectional(LSTM(128, return_sequences=True))(x) + x = Dropout(dropout_rate)(x) + + # Attention mechanism to focus on important time steps + x = Bidirectional(LSTM(64, return_sequences=True))(x) + + # Global average pooling to reduce parameters + x = GlobalAveragePooling1D()(x) + x = Dropout(dropout_rate)(x) + + # Dense layers for final classification/regression + x = Dense(64, activation='relu')(x) + x = BatchNormalization()(x) + x = Dropout(dropout_rate)(x) + + # Output layer + if self.output_size == 1: + # Binary classification (up/down) + outputs = Dense(1, activation='sigmoid', name='output')(x) + loss = 'binary_crossentropy' + metrics = ['accuracy', AUC()] + elif self.output_size == 3: + # Multi-class classification (buy/hold/sell) + outputs = Dense(3, activation='softmax', name='output')(x) + loss = 'categorical_crossentropy' + metrics = ['accuracy'] + else: + # Regression + outputs = Dense(self.output_size, activation='linear', name='output')(x) + loss = 'mse' + metrics = ['mae'] + + # Create and compile model + self.model = Model(inputs=inputs, outputs=outputs) + + # Compile with Adam optimizer + self.model.compile( + optimizer=Adam(learning_rate=learning_rate), + loss=loss, + metrics=metrics + ) + + # Log model summary + self.model.summary(print_fn=lambda x: logger.info(x)) + + return self.model + + def train(self, X_train, y_train, batch_size=32, epochs=100, validation_split=0.2, + callbacks=None, class_weights=None): + """ + Train the CNN model on the provided data. + + Args: + X_train (numpy.ndarray): Training features + y_train (numpy.ndarray): Training targets + batch_size (int): Batch size + epochs (int): Number of epochs + validation_split (float): Fraction of data to use for validation + callbacks (list): List of Keras callbacks + class_weights (dict): Class weights for imbalanced datasets + + Returns: + History object containing training metrics + """ + if self.model is None: + self.build_model() + + # Default callbacks if none provided + if callbacks is None: + # Create a timestamp for model checkpoints + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + + callbacks = [ + EarlyStopping( + monitor='val_loss', + patience=10, + restore_best_weights=True + ), + ReduceLROnPlateau( + monitor='val_loss', + factor=0.5, + patience=5, + min_lr=1e-6 + ), + ModelCheckpoint( + filepath=os.path.join(self.model_dir, f"cnn_model_{timestamp}.h5"), + monitor='val_loss', + save_best_only=True + ) + ] + + # Check if y_train needs to be one-hot encoded for multi-class + if self.output_size == 3 and len(y_train.shape) == 1: + y_train = tf.keras.utils.to_categorical(y_train, num_classes=3) + + # Train the model + logger.info(f"Training CNN model with {len(X_train)} samples, batch size {batch_size}, epochs {epochs}") + self.history = self.model.fit( + X_train, y_train, + batch_size=batch_size, + epochs=epochs, + validation_split=validation_split, + callbacks=callbacks, + class_weight=class_weights, + verbose=2 + ) + + # Save the trained model + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + model_path = os.path.join(self.model_dir, f"cnn_model_final_{timestamp}.h5") + self.model.save(model_path) + logger.info(f"Model saved to {model_path}") + + # Save training history + history_path = os.path.join(self.model_dir, f"cnn_model_history_{timestamp}.json") + with open(history_path, 'w') as f: + # Convert numpy values to Python native types for JSON serialization + history_dict = {key: [float(value) for value in values] for key, values in self.history.history.items()} + json.dump(history_dict, f, indent=2) + + return self.history + + def evaluate(self, X_test, y_test, plot_results=False): + """ + Evaluate the model on test data. + + Args: + X_test (numpy.ndarray): Test features + y_test (numpy.ndarray): Test targets + plot_results (bool): Whether to plot evaluation results + + Returns: + dict: Evaluation metrics + """ + if self.model is None: + raise ValueError("Model has not been built or trained yet") + + # Convert y_test to one-hot encoding for multi-class + y_test_original = y_test.copy() + if self.output_size == 3 and len(y_test.shape) == 1: + y_test = tf.keras.utils.to_categorical(y_test, num_classes=3) + + # Evaluate model + logger.info(f"Evaluating CNN model on {len(X_test)} samples") + eval_results = self.model.evaluate(X_test, y_test, verbose=0) + + metrics = {} + for metric, value in zip(self.model.metrics_names, eval_results): + metrics[metric] = value + logger.info(f"{metric}: {value:.4f}") + + # Get predictions + y_pred_prob = self.model.predict(X_test) + + # Different processing based on output type + if self.output_size == 1: + # Binary classification + y_pred = (y_pred_prob > 0.5).astype(int).flatten() + + # Classification report + report = classification_report(y_test, y_pred) + logger.info(f"Classification Report:\n{report}") + + # Confusion matrix + cm = confusion_matrix(y_test, y_pred) + logger.info(f"Confusion Matrix:\n{cm}") + + # ROC curve and AUC + fpr, tpr, _ = roc_curve(y_test, y_pred_prob) + roc_auc = auc(fpr, tpr) + metrics['auc'] = roc_auc + + if plot_results: + self._plot_binary_results(y_test, y_pred, y_pred_prob, fpr, tpr, roc_auc) + + elif self.output_size == 3: + # Multi-class classification + y_pred = np.argmax(y_pred_prob, axis=1) + + # Classification report + report = classification_report(y_test_original, y_pred) + logger.info(f"Classification Report:\n{report}") + + # Confusion matrix + cm = confusion_matrix(y_test_original, y_pred) + logger.info(f"Confusion Matrix:\n{cm}") + + if plot_results: + self._plot_multiclass_results(y_test_original, y_pred, y_pred_prob) + + return metrics + + def predict(self, X): + """ + Make predictions on new data. + + Args: + X (numpy.ndarray): Input features + + Returns: + tuple: (y_pred, y_proba) where: + y_pred is the predicted class (0/1 for binary, 0/1/2 for multi-class) + y_proba is the class probability + """ + if self.model is None: + raise ValueError("Model has not been built or trained yet") + + # Ensure X has the right shape + if len(X.shape) == 2: + # Single sample, add batch dimension + X = np.expand_dims(X, axis=0) + + # Get predictions + y_proba = self.model.predict(X) + + # Process based on output type + if self.output_size == 1: + # Binary classification + y_pred = (y_proba > 0.5).astype(int).flatten() + return y_pred, y_proba.flatten() + elif self.output_size == 3: + # Multi-class classification + y_pred = np.argmax(y_proba, axis=1) + return y_pred, y_proba + else: + # Regression + return y_proba, y_proba + + def save(self, filepath=None): + """ + Save the model to disk. + + Args: + filepath (str): Path to save the model + + Returns: + str: Path where the model was saved + """ + if self.model is None: + raise ValueError("Model has not been built yet") + + if filepath is None: + # Create a default filepath with timestamp + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + filepath = os.path.join(self.model_dir, f"cnn_model_{timestamp}.h5") + + self.model.save(filepath) + logger.info(f"Model saved to {filepath}") + return filepath + + def load(self, filepath): + """ + Load a saved model from disk. + + Args: + filepath (str): Path to the saved model + + Returns: + The loaded model + """ + self.model = load_model(filepath) + logger.info(f"Model loaded from {filepath}") + return self.model + + def extract_hidden_features(self, X): + """ + Extract features from the last hidden layer of the CNN for transfer learning. + + Args: + X (numpy.ndarray): Input data + + Returns: + numpy.ndarray: Extracted features + """ + if self.model is None: + raise ValueError("Model has not been built or trained yet") + + # Create a new model that outputs the features from the layer before the output + feature_layer_name = self.model.layers[-2].name + feature_extractor = Model( + inputs=self.model.input, + outputs=self.model.get_layer(feature_layer_name).output + ) + + # Extract features + features = feature_extractor.predict(X) + + return features + + def _plot_binary_results(self, y_true, y_pred, y_proba, fpr, tpr, roc_auc): + """ + Plot evaluation results for binary classification. + + Args: + y_true (numpy.ndarray): True labels + y_pred (numpy.ndarray): Predicted labels + y_proba (numpy.ndarray): Prediction probabilities + fpr (numpy.ndarray): False positive rates for ROC curve + tpr (numpy.ndarray): True positive rates for ROC curve + roc_auc (float): Area under ROC curve + """ + plt.figure(figsize=(15, 5)) + + # Confusion Matrix + plt.subplot(1, 3, 1) + cm = confusion_matrix(y_true, y_pred) + plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues) + plt.title('Confusion Matrix') + plt.colorbar() + tick_marks = [0, 1] + plt.xticks(tick_marks, ['0', '1']) + plt.yticks(tick_marks, ['0', '1']) + plt.xlabel('Predicted Label') + plt.ylabel('True Label') + + # Add text annotations to confusion matrix + thresh = cm.max() / 2. + for i in range(cm.shape[0]): + for j in range(cm.shape[1]): + plt.text(j, i, format(cm[i, j], 'd'), + horizontalalignment="center", + color="white" if cm[i, j] > thresh else "black") + + # Histogram of prediction probabilities + plt.subplot(1, 3, 2) + plt.hist(y_proba[y_true == 0], alpha=0.5, label='Class 0') + plt.hist(y_proba[y_true == 1], alpha=0.5, label='Class 1') + plt.title('Prediction Probabilities') + plt.xlabel('Probability of Class 1') + plt.ylabel('Count') + plt.legend() + + # ROC Curve + plt.subplot(1, 3, 3) + plt.plot(fpr, tpr, label=f'ROC Curve (AUC = {roc_auc:.3f})') + plt.plot([0, 1], [0, 1], 'k--') + plt.xlim([0.0, 1.0]) + plt.ylim([0.0, 1.05]) + plt.xlabel('False Positive Rate') + plt.ylabel('True Positive Rate') + plt.title('Receiver Operating Characteristic') + plt.legend(loc="lower right") + + plt.tight_layout() + + # Save figure + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + fig_path = os.path.join(self.model_dir, f"cnn_evaluation_{timestamp}.png") + plt.savefig(fig_path) + plt.close() + + logger.info(f"Evaluation plots saved to {fig_path}") + + def _plot_multiclass_results(self, y_true, y_pred, y_proba): + """ + Plot evaluation results for multi-class classification. + + Args: + y_true (numpy.ndarray): True labels + y_pred (numpy.ndarray): Predicted labels + y_proba (numpy.ndarray): Prediction probabilities + """ + plt.figure(figsize=(12, 5)) + + # Confusion Matrix + plt.subplot(1, 2, 1) + cm = confusion_matrix(y_true, y_pred) + plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues) + plt.title('Confusion Matrix') + plt.colorbar() + classes = ['BUY', 'HOLD', 'SELL'] # Assumes classes are 0, 1, 2 + tick_marks = np.arange(len(classes)) + plt.xticks(tick_marks, classes) + plt.yticks(tick_marks, classes) + plt.xlabel('Predicted Label') + plt.ylabel('True Label') + + # Add text annotations to confusion matrix + thresh = cm.max() / 2. + for i in range(cm.shape[0]): + for j in range(cm.shape[1]): + plt.text(j, i, format(cm[i, j], 'd'), + horizontalalignment="center", + color="white" if cm[i, j] > thresh else "black") + + # Class probability distributions + plt.subplot(1, 2, 2) + for i, cls in enumerate(classes): + plt.hist(y_proba[y_true == i, i], alpha=0.5, label=f'Class {cls}') + plt.title('Class Probability Distributions') + plt.xlabel('Probability') + plt.ylabel('Count') + plt.legend() + + plt.tight_layout() + + # Save figure + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + fig_path = os.path.join(self.model_dir, f"cnn_multiclass_evaluation_{timestamp}.png") + plt.savefig(fig_path) + plt.close() + + logger.info(f"Multiclass evaluation plots saved to {fig_path}") + + def plot_training_history(self): + """ + Plot training history (loss and metrics). + + Returns: + str: Path to the saved plot + """ + if self.history is None: + raise ValueError("Model has not been trained yet") + + plt.figure(figsize=(12, 5)) + + # Plot loss + plt.subplot(1, 2, 1) + plt.plot(self.history.history['loss'], label='Training Loss') + if 'val_loss' in self.history.history: + plt.plot(self.history.history['val_loss'], label='Validation Loss') + plt.title('Model Loss') + plt.xlabel('Epoch') + plt.ylabel('Loss') + plt.legend() + + # Plot accuracy + plt.subplot(1, 2, 2) + + if 'accuracy' in self.history.history: + plt.plot(self.history.history['accuracy'], label='Training Accuracy') + if 'val_accuracy' in self.history.history: + plt.plot(self.history.history['val_accuracy'], label='Validation Accuracy') + plt.title('Model Accuracy') + plt.ylabel('Accuracy') + elif 'mae' in self.history.history: + plt.plot(self.history.history['mae'], label='Training MAE') + if 'val_mae' in self.history.history: + plt.plot(self.history.history['val_mae'], label='Validation MAE') + plt.title('Model MAE') + plt.ylabel('MAE') + + plt.xlabel('Epoch') + plt.legend() + + plt.tight_layout() + + # Save figure + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + fig_path = os.path.join(self.model_dir, f"cnn_training_history_{timestamp}.png") + plt.savefig(fig_path) + plt.close() + + logger.info(f"Training history plot saved to {fig_path}") + return fig_path \ No newline at end of file diff --git a/NN/models/cnn_model_pytorch.py b/NN/models/cnn_model_pytorch.py new file mode 100644 index 0000000..683c360 --- /dev/null +++ b/NN/models/cnn_model_pytorch.py @@ -0,0 +1,546 @@ +#!/usr/bin/env python3 +""" +CNN Model - PyTorch Implementation + +This module implements a CNN model using PyTorch for time series analysis. +The model consists of multiple convolutional pathways and LSTM layers. +""" + +import os +import logging +import numpy as np +import matplotlib.pyplot as plt +from datetime import datetime + +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader, TensorDataset +from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score + +# Configure logging +logger = logging.getLogger(__name__) + +class CNNPyTorch(nn.Module): + """PyTorch CNN model for time series analysis""" + + def __init__(self, input_shape, output_size=3): + """ + Initialize the CNN model. + + Args: + input_shape (tuple): Shape of input data (window_size, features) + output_size (int): Size of output (1 for regression, 3 for classification) + """ + super(CNNPyTorch, self).__init__() + + window_size, num_features = input_shape + + # Architecture parameters + filters = [32, 64, 128] + kernel_sizes = [3, 5, 7] + lstm_units = 100 + dense_units = 64 + dropout_rate = 0.3 + + # Create parallel convolutional pathways + self.conv_paths = nn.ModuleList() + + for f, k in zip(filters, kernel_sizes): + path = nn.Sequential( + nn.Conv1d(num_features, f, kernel_size=k, padding='same'), + nn.ReLU(), + nn.BatchNorm1d(f), + nn.MaxPool1d(kernel_size=2, stride=1, padding=1), + nn.Dropout(dropout_rate) + ) + self.conv_paths.append(path) + + # Calculate output size from conv paths + conv_output_size = sum(filters) * window_size + + # LSTM layer + self.lstm = nn.LSTM( + input_size=sum(filters), + hidden_size=lstm_units, + batch_first=True, + bidirectional=True + ) + + # Dense layers + self.flatten = nn.Flatten() + self.dense1 = nn.Sequential( + nn.Linear(lstm_units * 2 * window_size, dense_units), + nn.ReLU(), + nn.BatchNorm1d(dense_units), + nn.Dropout(dropout_rate) + ) + + # Output layer + self.output = nn.Linear(dense_units, output_size) + + # Activation based on output size + if output_size == 1: + self.activation = nn.Sigmoid() # Binary classification or regression + elif output_size > 1: + self.activation = nn.Softmax(dim=1) # Multi-class classification + else: + self.activation = nn.Identity() # No activation + + def forward(self, x): + """ + Forward pass through the network. + + Args: + x: Input tensor of shape [batch_size, window_size, features] + + Returns: + Output tensor of shape [batch_size, output_size] + """ + batch_size, window_size, num_features = x.shape + + # Transpose for conv1d: [batch, features, window] + x_t = x.transpose(1, 2) + + # Process through parallel conv paths + conv_outputs = [] + for path in self.conv_paths: + conv_outputs.append(path(x_t)) + + # Concatenate conv outputs + conv_concat = torch.cat(conv_outputs, dim=1) + + # Transpose back for LSTM: [batch, window, features] + conv_concat = conv_concat.transpose(1, 2) + + # LSTM processing + lstm_out, _ = self.lstm(conv_concat) + + # Flatten + flattened = self.flatten(lstm_out) + + # Dense processing + dense_out = self.dense1(flattened) + + # Output + output = self.output(dense_out) + + # Apply activation + return self.activation(output) + + +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. + """ + + def __init__(self, window_size, num_features, output_size=3, timeframes=None): + """ + Initialize the CNN model. + + Args: + window_size (int): Size of the input window + num_features (int): Number of features in the input data + output_size (int): Size of the output (1 for regression, 3 for classification) + timeframes (list): List of timeframes used (for logging) + """ + self.window_size = window_size + self.num_features = num_features + self.output_size = output_size + self.timeframes = timeframes or [] + + # Determine device (GPU or CPU) + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + logger.info(f"Using device: {self.device}") + + # Initialize model + self.model = None + self.build_model() + + # Initialize training history + self.history = { + 'loss': [], + 'val_loss': [], + 'accuracy': [], + 'val_accuracy': [] + } + + def build_model(self): + """Build the CNN model architecture""" + logger.info(f"Building PyTorch CNN model with window_size={self.window_size}, " + f"num_features={self.num_features}, output_size={self.output_size}") + + self.model = CNNPyTorch( + input_shape=(self.window_size, self.num_features), + output_size=self.output_size + ).to(self.device) + + # Initialize optimizer + self.optimizer = optim.Adam(self.model.parameters(), lr=0.001) + + # Initialize loss function based on output size + if self.output_size == 1: + self.criterion = nn.BCELoss() # Binary classification + elif self.output_size > 1: + self.criterion = nn.CrossEntropyLoss() # Multi-class classification + else: + self.criterion = nn.MSELoss() # Regression + + logger.info(f"Model built successfully with {sum(p.numel() for p in self.model.parameters())} parameters") + + def train(self, X_train, y_train, X_val=None, y_val=None, batch_size=32, epochs=100): + """ + Train the CNN model. + + Args: + X_train: Training input data + y_train: Training target data + X_val: Validation input data + y_val: Validation target data + batch_size: Batch size for training + epochs: Number of training epochs + + Returns: + Training history + """ + logger.info(f"Training PyTorch CNN model with {len(X_train)} samples, " + f"batch_size={batch_size}, epochs={epochs}") + + # Convert numpy arrays to PyTorch tensors + X_train_tensor = torch.tensor(X_train, dtype=torch.float32).to(self.device) + + # Handle different output sizes for y_train + if self.output_size == 1: + y_train_tensor = torch.tensor(y_train, dtype=torch.float32).to(self.device) + else: + y_train_tensor = torch.tensor(y_train, dtype=torch.long).to(self.device) + + # Create DataLoader for training data + train_dataset = TensorDataset(X_train_tensor, y_train_tensor) + train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) + + # Create DataLoader for validation data if provided + if X_val is not None and y_val is not None: + X_val_tensor = torch.tensor(X_val, dtype=torch.float32).to(self.device) + if self.output_size == 1: + y_val_tensor = torch.tensor(y_val, dtype=torch.float32).to(self.device) + else: + y_val_tensor = torch.tensor(y_val, dtype=torch.long).to(self.device) + + val_dataset = TensorDataset(X_val_tensor, y_val_tensor) + val_loader = DataLoader(val_dataset, batch_size=batch_size) + else: + val_loader = None + + # Training loop + for epoch in range(epochs): + # Training phase + self.model.train() + running_loss = 0.0 + correct = 0 + total = 0 + + for inputs, targets in train_loader: + # Zero the parameter gradients + self.optimizer.zero_grad() + + # Forward pass + outputs = self.model(inputs) + + # Calculate loss + if self.output_size == 1: + loss = self.criterion(outputs, targets.unsqueeze(1)) + else: + loss = self.criterion(outputs, targets) + + # Backward pass and optimize + loss.backward() + self.optimizer.step() + + # Statistics + running_loss += loss.item() + if self.output_size > 1: + _, predicted = torch.max(outputs, 1) + total += targets.size(0) + correct += (predicted == targets).sum().item() + + epoch_loss = running_loss / len(train_loader) + epoch_acc = correct / total if total > 0 else 0 + + # Validation phase + if val_loader is not None: + val_loss, val_acc = self._validate(val_loader) + + logger.info(f"Epoch {epoch+1}/{epochs} - " + f"loss: {epoch_loss:.4f} - acc: {epoch_acc:.4f} - " + f"val_loss: {val_loss:.4f} - val_acc: {val_acc:.4f}") + + # Update history + self.history['loss'].append(epoch_loss) + self.history['accuracy'].append(epoch_acc) + self.history['val_loss'].append(val_loss) + self.history['val_accuracy'].append(val_acc) + else: + logger.info(f"Epoch {epoch+1}/{epochs} - " + f"loss: {epoch_loss:.4f} - acc: {epoch_acc:.4f}") + + # Update history without validation + self.history['loss'].append(epoch_loss) + self.history['accuracy'].append(epoch_acc) + + logger.info("Training completed") + return self.history + + def _validate(self, val_loader): + """Validate the model using the validation set""" + self.model.eval() + val_loss = 0.0 + correct = 0 + total = 0 + + with torch.no_grad(): + for inputs, targets in val_loader: + # Forward pass + outputs = self.model(inputs) + + # Calculate loss + if self.output_size == 1: + loss = self.criterion(outputs, targets.unsqueeze(1)) + else: + loss = self.criterion(outputs, targets) + + val_loss += loss.item() + + # Calculate accuracy + if self.output_size > 1: + _, predicted = torch.max(outputs, 1) + total += targets.size(0) + correct += (predicted == targets).sum().item() + + return val_loss / len(val_loader), correct / total if total > 0 else 0 + + def evaluate(self, X_test, y_test): + """ + Evaluate the model on test data. + + Args: + X_test: Test input data + y_test: Test target data + + Returns: + dict: Evaluation metrics + """ + logger.info(f"Evaluating model on {len(X_test)} samples") + + # Convert to PyTorch tensors + X_test_tensor = torch.tensor(X_test, dtype=torch.float32).to(self.device) + + # Get predictions + self.model.eval() + with torch.no_grad(): + y_pred = self.model(X_test_tensor) + + if self.output_size > 1: + _, y_pred_class = torch.max(y_pred, 1) + y_pred_class = y_pred_class.cpu().numpy() + else: + y_pred_class = (y_pred.cpu().numpy() > 0.5).astype(int).flatten() + + # Calculate metrics + if self.output_size > 1: + accuracy = accuracy_score(y_test, y_pred_class) + precision = precision_score(y_test, y_pred_class, average='weighted') + recall = recall_score(y_test, y_pred_class, average='weighted') + f1 = f1_score(y_test, y_pred_class, average='weighted') + + metrics = { + 'accuracy': accuracy, + 'precision': precision, + 'recall': recall, + 'f1_score': f1 + } + else: + accuracy = accuracy_score(y_test, y_pred_class) + precision = precision_score(y_test, y_pred_class) + recall = recall_score(y_test, y_pred_class) + f1 = f1_score(y_test, y_pred_class) + + metrics = { + 'accuracy': accuracy, + 'precision': precision, + 'recall': recall, + 'f1_score': f1 + } + + logger.info(f"Evaluation metrics: {metrics}") + return metrics + + def predict(self, X): + """ + Make predictions with the model. + + Args: + X: Input data + + Returns: + Predictions + """ + # Convert to PyTorch tensor + X_tensor = torch.tensor(X, dtype=torch.float32).to(self.device) + + # Get predictions + self.model.eval() + with torch.no_grad(): + predictions = self.model(X_tensor) + + if self.output_size > 1: + # Multi-class classification + probs = predictions.cpu().numpy() + _, class_preds = torch.max(predictions, 1) + class_preds = class_preds.cpu().numpy() + return class_preds, probs + else: + # Binary classification or regression + preds = predictions.cpu().numpy() + if self.output_size == 1: + # Binary classification + class_preds = (preds > 0.5).astype(int) + return class_preds.flatten(), preds.flatten() + else: + # Regression + return preds.flatten(), None + + def save(self, filepath): + """ + Save the model to a file. + + Args: + filepath: Path to save the model + """ + # Create directory if it doesn't exist + os.makedirs(os.path.dirname(filepath), exist_ok=True) + + # Save the model state + model_state = { + 'model_state_dict': self.model.state_dict(), + 'optimizer_state_dict': self.optimizer.state_dict(), + 'history': self.history, + 'window_size': self.window_size, + 'num_features': self.num_features, + 'output_size': self.output_size, + 'timeframes': self.timeframes + } + + torch.save(model_state, f"{filepath}.pt") + logger.info(f"Model saved to {filepath}.pt") + + def load(self, filepath): + """ + Load the model from a file. + + Args: + filepath: Path to load the model from + """ + # Check if file exists + if not os.path.exists(f"{filepath}.pt"): + logger.error(f"Model file {filepath}.pt not found") + return False + + # Load the model state + model_state = torch.load(f"{filepath}.pt", map_location=self.device) + + # Update model parameters + self.window_size = model_state['window_size'] + self.num_features = model_state['num_features'] + self.output_size = model_state['output_size'] + self.timeframes = model_state['timeframes'] + + # Rebuild the model + self.build_model() + + # Load the model state + self.model.load_state_dict(model_state['model_state_dict']) + self.optimizer.load_state_dict(model_state['optimizer_state_dict']) + self.history = model_state['history'] + + logger.info(f"Model loaded from {filepath}.pt") + return True + + def plot_training_history(self): + """Plot the training history""" + if not self.history['loss']: + logger.warning("No training history to plot") + return + + plt.figure(figsize=(12, 4)) + + # Plot loss + plt.subplot(1, 2, 1) + plt.plot(self.history['loss'], label='Training Loss') + if 'val_loss' in self.history and self.history['val_loss']: + plt.plot(self.history['val_loss'], label='Validation Loss') + plt.title('Model Loss') + plt.ylabel('Loss') + plt.xlabel('Epoch') + plt.legend() + + # Plot accuracy + plt.subplot(1, 2, 2) + plt.plot(self.history['accuracy'], label='Training Accuracy') + if 'val_accuracy' in self.history and self.history['val_accuracy']: + plt.plot(self.history['val_accuracy'], label='Validation Accuracy') + plt.title('Model Accuracy') + plt.ylabel('Accuracy') + plt.xlabel('Epoch') + plt.legend() + + # Save the plot + os.makedirs('plots', exist_ok=True) + plt.savefig(os.path.join('plots', f"cnn_history_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png")) + plt.close() + + logger.info("Training history plots saved to plots directory") + + def extract_hidden_features(self, X): + """ + Extract hidden features from the model. + + Args: + X: Input data + + Returns: + Hidden features + """ + # Convert to PyTorch tensor + X_tensor = torch.tensor(X, dtype=torch.float32).to(self.device) + + # Forward pass through the model up to the last hidden layer + self.model.eval() + with torch.no_grad(): + # Get features before the output layer + x_t = X_tensor.transpose(1, 2) + + # Process through parallel conv paths + conv_outputs = [] + for path in self.model.conv_paths: + conv_outputs.append(path(x_t)) + + # Concatenate conv outputs + conv_concat = torch.cat(conv_outputs, dim=1) + + # Transpose back for LSTM + conv_concat = conv_concat.transpose(1, 2) + + # LSTM processing + lstm_out, _ = self.model.lstm(conv_concat) + + # Flatten + flattened = self.model.flatten(lstm_out) + + # Dense processing + hidden_features = self.model.dense1(flattened) + + return hidden_features.cpu().numpy() \ No newline at end of file diff --git a/NN/models/transformer_model.py b/NN/models/transformer_model.py index 326ab21..9d9bba1 100644 --- a/NN/models/transformer_model.py +++ b/NN/models/transformer_model.py @@ -1,45 +1,38 @@ +""" +Transformer Neural Network for timeseries analysis + +This module implements a Transformer model with attention mechanisms for cryptocurrency price analysis. +It also includes a Mixture of Experts model that combines predictions from multiple models. +""" + import os -import sys +import logging import numpy as np -import pandas as pd +import matplotlib.pyplot as plt import tensorflow as tf -from tensorflow.keras.models import Model +from tensorflow.keras.models import Model, load_model from tensorflow.keras.layers import ( - Input, Dense, Dropout, LayerNormalization, MultiHeadAttention, - GlobalAveragePooling1D, Concatenate, Add, Activation, Flatten + Input, Dense, Dropout, BatchNormalization, + Concatenate, Layer, LayerNormalization, MultiHeadAttention, + Add, GlobalAveragePooling1D, Conv1D, Reshape ) from tensorflow.keras.optimizers import Adam -from tensorflow.keras.callbacks import ( - EarlyStopping, ModelCheckpoint, ReduceLROnPlateau, - TensorBoard, CSVLogger -) -import matplotlib.pyplot as plt -import logging -import time +from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau import datetime +import json -# Configure logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.StreamHandler(), - logging.FileHandler('nn_transformer_model.log') - ] -) +logger = logging.getLogger(__name__) -logger = logging.getLogger('transformer_model') - -class TransformerBlock(tf.keras.layers.Layer): +class TransformerBlock(Layer): """ - Transformer block with multi-head self-attention and feed-forward network + Transformer block implementation with multi-head attention and feed-forward networks. """ def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1): super(TransformerBlock, self).__init__() self.att = MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim) self.ffn = tf.keras.Sequential([ Dense(ff_dim, activation="relu"), - Dense(embed_dim) + Dense(embed_dim), ]) self.layernorm1 = LayerNormalization(epsilon=1e-6) self.layernorm2 = LayerNormalization(epsilon=1e-6) @@ -47,33 +40,86 @@ class TransformerBlock(tf.keras.layers.Layer): self.dropout2 = Dropout(rate) def call(self, inputs, training=False): - # Normalization and attention attn_output = self.att(inputs, inputs) attn_output = self.dropout1(attn_output, training=training) out1 = self.layernorm1(inputs + attn_output) - - # Feed-forward network ffn_output = self.ffn(out1) ffn_output = self.dropout2(ffn_output, training=training) - - # Skip connection and normalization return self.layernorm2(out1 + ffn_output) + + def get_config(self): + config = super().get_config() + config.update({ + 'att': self.att, + 'ffn': self.ffn, + 'layernorm1': self.layernorm1, + 'layernorm2': self.layernorm2, + 'dropout1': self.dropout1, + 'dropout2': self.dropout2 + }) + return config + +class PositionalEncoding(Layer): + """ + Positional encoding layer to add position information to input embeddings. + """ + def __init__(self, position, d_model): + super(PositionalEncoding, self).__init__() + self.position = position + self.d_model = d_model + self.pos_encoding = self.positional_encoding(position, d_model) + + def get_angles(self, position, i, d_model): + angles = 1 / tf.pow(10000, (2 * (i // 2)) / tf.cast(d_model, tf.float32)) + return position * angles + + def positional_encoding(self, position, d_model): + angle_rads = self.get_angles( + position=tf.range(position, dtype=tf.float32)[:, tf.newaxis], + i=tf.range(d_model, dtype=tf.float32)[tf.newaxis, :], + d_model=d_model + ) + + # Apply sin to even indices in the array + sines = tf.math.sin(angle_rads[:, 0::2]) + + # Apply cos to odd indices in the array + cosines = tf.math.cos(angle_rads[:, 1::2]) + + pos_encoding = tf.concat([sines, cosines], axis=-1) + pos_encoding = pos_encoding[tf.newaxis, ...] + + return tf.cast(pos_encoding, tf.float32) + + def call(self, inputs): + return inputs + self.pos_encoding[:, :tf.shape(inputs)[1], :] + + def get_config(self): + config = super().get_config() + config.update({ + 'position': self.position, + 'd_model': self.d_model, + 'pos_encoding': self.pos_encoding + }) + return config class TransformerModel: """ - Transformer-based model for financial time series analysis. - This model processes both raw time series data and high-level features from the CNN model. + Transformer Neural Network for time series analysis. + + This model uses self-attention mechanisms to capture relationships between + different time points in the input data. """ - def __init__(self, ts_input_shape=(20, 5), feature_input_shape=128, output_size=3, model_dir='NN/models/saved'): + def __init__(self, ts_input_shape=(20, 5), feature_input_shape=64, output_size=1, model_dir="NN/models/saved"): """ - Initialize the Transformer model + Initialize the Transformer model. Args: - ts_input_shape: Shape of time series input data (sequence_length, features) - feature_input_shape: Shape of high-level feature input (from CNN) - output_size: Number of output classes or values - model_dir: Directory to save model files + ts_input_shape (tuple): Shape of time series input data (sequence_length, features) + feature_input_shape (int): Shape of additional feature input (e.g., from CNN) + output_size (int): Number of output classes (1 for binary, 3 for buy/hold/sell) + model_dir (str): Directory to save trained models """ self.ts_input_shape = ts_input_shape self.feature_input_shape = feature_input_shape @@ -83,341 +129,418 @@ class TransformerModel: self.history = None # Create model directory if it doesn't exist - os.makedirs(model_dir, exist_ok=True) + os.makedirs(self.model_dir, exist_ok=True) - logger.info(f"Initialized TransformerModel with time series input shape {ts_input_shape}, " + logger.info(f"Initialized Transformer model with TS input shape {ts_input_shape}, " f"feature input shape {feature_input_shape}, and output size {output_size}") - def build_model(self, embed_dim=64, num_heads=4, ff_dim=128, num_transformer_blocks=2, - dropout_rate=0.2, learning_rate=0.001): + def build_model(self, embed_dim=32, num_heads=4, ff_dim=64, num_transformer_blocks=2, dropout_rate=0.1, learning_rate=0.001): """ - Build the Transformer model architecture + Build the Transformer model architecture. Args: - embed_dim: Embedding dimension for the transformer - num_heads: Number of attention heads - ff_dim: Hidden layer size in the feed-forward network - num_transformer_blocks: Number of transformer blocks to stack - dropout_rate: Dropout rate for regularization - learning_rate: Learning rate for the optimizer + embed_dim (int): Embedding dimension for transformer + num_heads (int): Number of attention heads + ff_dim (int): Hidden dimension of the feed forward network + num_transformer_blocks (int): Number of transformer blocks + dropout_rate (float): Dropout rate for regularization + learning_rate (float): Learning rate for Adam optimizer Returns: - Compiled Keras model + The compiled model """ - # Time series input (price and volume data) - ts_inputs = Input(shape=self.ts_input_shape, name='time_series_input') + # Time series input + ts_inputs = Input(shape=self.ts_input_shape, name="ts_input") - # High-level feature input (from CNN or other sources) - feature_inputs = Input(shape=(self.feature_input_shape,), name='feature_input') + # Additional feature input (e.g., from CNN) + feature_inputs = Input(shape=(self.feature_input_shape,), name="feature_input") - # Process time series with transformer blocks - x = ts_inputs + # Process time series with transformer + # First, project the input to the embedding dimension + x = Conv1D(embed_dim, 1, activation="relu")(ts_inputs) + + # Add positional encoding + x = PositionalEncoding(self.ts_input_shape[0], embed_dim)(x) + + # Add transformer blocks for _ in range(num_transformer_blocks): x = TransformerBlock(embed_dim, num_heads, ff_dim, dropout_rate)(x) - # Global pooling to get fixed-size representation + # Global pooling to get a single vector representation x = GlobalAveragePooling1D()(x) + x = Dropout(dropout_rate)(x) - # Combine with the high-level features + # Combine with additional features combined = Concatenate()([x, feature_inputs]) - # Dense layers - dense1 = Dense(128, activation='relu')(combined) - dropout1 = Dropout(dropout_rate)(dense1) - dense2 = Dense(64, activation='relu')(dropout1) - dropout2 = Dropout(dropout_rate)(dense2) + # Dense layers for final classification/regression + x = Dense(64, activation="relu")(combined) + x = BatchNormalization()(x) + x = Dropout(dropout_rate)(x) # Output layer if self.output_size == 1: - # Binary classification - outputs = Dense(1, activation='sigmoid')(dropout2) + # Binary classification (up/down) + outputs = Dense(1, activation='sigmoid', name='output')(x) + loss = 'binary_crossentropy' + metrics = ['accuracy'] elif self.output_size == 3: - # For BUY/HOLD/SELL signals (3 classes) - outputs = Dense(3, activation='softmax')(dropout2) - else: - # Regression or multi-class classification - outputs = Dense(self.output_size, activation='linear')(dropout2) - - # Create and compile the model - model = Model(inputs=[ts_inputs, feature_inputs], outputs=outputs) - - if self.output_size == 1: - # Binary classification - model.compile( - optimizer=Adam(learning_rate=learning_rate), - loss='binary_crossentropy', - metrics=['accuracy'] - ) - elif self.output_size == 3: - # Multi-class classification for BUY/HOLD/SELL - model.compile( - optimizer=Adam(learning_rate=learning_rate), - loss='categorical_crossentropy', - metrics=['accuracy'] - ) + # Multi-class classification (buy/hold/sell) + outputs = Dense(3, activation='softmax', name='output')(x) + loss = 'categorical_crossentropy' + metrics = ['accuracy'] else: # Regression - model.compile( - optimizer=Adam(learning_rate=learning_rate), - loss='mse', - metrics=['mae'] - ) + outputs = Dense(self.output_size, activation='linear', name='output')(x) + loss = 'mse' + metrics = ['mae'] - self.model = model - logger.info(f"Model built with {model.count_params()} parameters") - model.summary(print_fn=logger.info) + # Create and compile model + self.model = Model(inputs=[ts_inputs, feature_inputs], outputs=outputs) - return model + # Compile with Adam optimizer + self.model.compile( + optimizer=Adam(learning_rate=learning_rate), + loss=loss, + metrics=metrics + ) + + # Log model summary + self.model.summary(print_fn=lambda x: logger.info(x)) + + return self.model def train(self, X_ts, X_features, y, batch_size=32, epochs=100, validation_split=0.2, - early_stopping_patience=20, reduce_lr_patience=10, verbose=1): + callbacks=None, class_weights=None): """ - Train the Transformer model + Train the Transformer model on the provided data. Args: - X_ts: Time series input data - X_features: High-level feature input data - y: Target values - batch_size: Batch size for training - epochs: Maximum number of epochs - validation_split: Fraction of data to use for validation - early_stopping_patience: Patience for early stopping - reduce_lr_patience: Patience for learning rate reduction - verbose: Verbosity level + X_ts (numpy.ndarray): Time series input features + X_features (numpy.ndarray): Additional input features + y (numpy.ndarray): Target labels + batch_size (int): Batch size + epochs (int): Number of epochs + validation_split (float): Fraction of data to use for validation + callbacks (list): List of Keras callbacks + class_weights (dict): Class weights for imbalanced datasets Returns: - Training history + History object containing training metrics """ if self.model is None: - logger.warning("Model not built yet, building with default parameters") self.build_model() - # Create a timestamp for this training run - timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - model_name = f"transformer_model_{timestamp}" + # Default callbacks if none provided + if callbacks is None: + # Create a timestamp for model checkpoints + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + + callbacks = [ + EarlyStopping( + monitor='val_loss', + patience=10, + restore_best_weights=True + ), + ReduceLROnPlateau( + monitor='val_loss', + factor=0.5, + patience=5, + min_lr=1e-6 + ), + ModelCheckpoint( + filepath=os.path.join(self.model_dir, f"transformer_model_{timestamp}.h5"), + monitor='val_loss', + save_best_only=True + ) + ] - # Set up callbacks - callbacks = [ - # Early stopping to prevent overfitting - EarlyStopping( - monitor='val_loss', - patience=early_stopping_patience, - restore_best_weights=True, - verbose=1 - ), - - # Reduce learning rate when training plateaus - ReduceLROnPlateau( - monitor='val_loss', - factor=0.5, - patience=reduce_lr_patience, - min_lr=1e-6, - verbose=1 - ), - - # Save the best model - ModelCheckpoint( - filepath=os.path.join(self.model_dir, f"{model_name}_best.h5"), - monitor='val_loss', - save_best_only=True, - verbose=1 - ), - - # TensorBoard logging - TensorBoard( - log_dir=os.path.join(self.model_dir, 'logs', model_name), - histogram_freq=1 - ), - - # CSV logging - CSVLogger( - filename=os.path.join(self.model_dir, f"{model_name}_training.csv"), - separator=',', - append=False - ) - ] + # Check if y needs to be one-hot encoded for multi-class + if self.output_size == 3 and len(y.shape) == 1: + y = tf.keras.utils.to_categorical(y, num_classes=3) # Train the model - logger.info(f"Starting training with {len(X_ts)} samples, {epochs} max epochs") - - start_time = time.time() - history = self.model.fit( + logger.info(f"Training Transformer model with {len(X_ts)} samples, batch size {batch_size}, epochs {epochs}") + self.history = self.model.fit( [X_ts, X_features], y, batch_size=batch_size, epochs=epochs, validation_split=validation_split, callbacks=callbacks, - verbose=verbose + class_weight=class_weights, + verbose=2 ) - # Calculate training time - training_time = time.time() - start_time - logger.info(f"Training completed in {training_time:.2f} seconds") - - # Save the final model - self.model.save(os.path.join(self.model_dir, f"{model_name}_final.h5")) - logger.info(f"Model saved to {os.path.join(self.model_dir, model_name + '_final.h5')}") + # Save the trained model + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + model_path = os.path.join(self.model_dir, f"transformer_model_final_{timestamp}.h5") + self.model.save(model_path) + logger.info(f"Model saved to {model_path}") # Save training history - hist_df = pd.DataFrame(history.history) - hist_df.to_csv(os.path.join(self.model_dir, f"{model_name}_history.csv"), index=False) + history_path = os.path.join(self.model_dir, f"transformer_model_history_{timestamp}.json") + with open(history_path, 'w') as f: + # Convert numpy values to Python native types for JSON serialization + history_dict = {key: [float(value) for value in values] for key, values in self.history.history.items()} + json.dump(history_dict, f, indent=2) - self.history = history - return history + return self.history - def predict(self, X_ts, X_features, threshold=0.5): + def evaluate(self, X_ts, X_features, y): """ - Make predictions with the model + Evaluate the model on test data. Args: - X_ts: Time series input data - X_features: High-level feature input data - threshold: Threshold for binary classification + X_ts (numpy.ndarray): Time series input features + X_features (numpy.ndarray): Additional input features + y (numpy.ndarray): Target labels Returns: - Predicted values or classes + dict: Evaluation metrics """ if self.model is None: - logger.error("Model not built or trained yet") - return None + raise ValueError("Model has not been built or trained yet") - # Get raw predictions - y_pred_proba = self.model.predict([X_ts, X_features]) + # Convert y to one-hot encoding for multi-class + if self.output_size == 3 and len(y.shape) == 1: + y = tf.keras.utils.to_categorical(y, num_classes=3) - # Format predictions based on output type + # Evaluate model + logger.info(f"Evaluating Transformer model on {len(X_ts)} samples") + eval_results = self.model.evaluate([X_ts, X_features], y, verbose=0) + + metrics = {} + for metric, value in zip(self.model.metrics_names, eval_results): + metrics[metric] = value + logger.info(f"{metric}: {value:.4f}") + + return metrics + + def predict(self, X_ts, X_features=None): + """ + Make predictions on new data. + + Args: + X_ts (numpy.ndarray): Time series input features + X_features (numpy.ndarray): Additional input features + + Returns: + tuple: (y_pred, y_proba) where: + y_pred is the predicted class (0/1 for binary, 0/1/2 for multi-class) + y_proba is the class probability + """ + if self.model is None: + raise ValueError("Model has not been built or trained yet") + + # Ensure X_ts has the right shape + if len(X_ts.shape) == 2: + # Single sample, add batch dimension + X_ts = np.expand_dims(X_ts, axis=0) + + # Ensure X_features has the right shape + if X_features is None: + # Create dummy features with zeros + X_features = np.zeros((X_ts.shape[0], self.feature_input_shape)) + elif len(X_features.shape) == 1: + # Single sample, add batch dimension + X_features = np.expand_dims(X_features, axis=0) + + # Get predictions + y_proba = self.model.predict([X_ts, X_features]) + + # Process based on output type if self.output_size == 1: # Binary classification - y_pred = (y_pred_proba > threshold).astype(int).flatten() - return y_pred, y_pred_proba.flatten() + y_pred = (y_proba > 0.5).astype(int).flatten() + return y_pred, y_proba.flatten() elif self.output_size == 3: - # Multi-class (BUY/HOLD/SELL) - y_pred = np.argmax(y_pred_proba, axis=1) - return y_pred, y_pred_proba + # Multi-class classification + y_pred = np.argmax(y_proba, axis=1) + return y_pred, y_proba else: # Regression - return y_pred_proba + return y_proba, y_proba - def save_model(self, filepath=None): + def save(self, filepath=None): """ - Save the model to a file + Save the model to disk. Args: - filepath: Path to save the model to + filepath (str): Path to save the model Returns: - Path to the saved model + str: Path where the model was saved """ if self.model is None: - logger.error("Model not built or trained yet") - return None + raise ValueError("Model has not been built yet") if filepath is None: - # Create a default filepath + # Create a default filepath with timestamp timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") filepath = os.path.join(self.model_dir, f"transformer_model_{timestamp}.h5") self.model.save(filepath) logger.info(f"Model saved to {filepath}") - return filepath - def load_model(self, filepath): + def load(self, filepath): """ - Load a model from a file + Load a saved model from disk. Args: - filepath: Path to load the model from + filepath (str): Path to the saved model Returns: - Loaded model + The loaded model """ - try: - self.model = tf.keras.models.load_model(filepath) - logger.info(f"Model loaded from {filepath}") - return self.model - except Exception as e: - logger.error(f"Error loading model: {str(e)}") - return None + # Register custom layers + custom_objects = { + 'TransformerBlock': TransformerBlock, + 'PositionalEncoding': PositionalEncoding + } + + self.model = load_model(filepath, custom_objects=custom_objects) + logger.info(f"Model loaded from {filepath}") + return self.model + + def plot_training_history(self): + """ + Plot training history (loss and metrics). + + Returns: + str: Path to the saved plot + """ + if self.history is None: + raise ValueError("Model has not been trained yet") + + plt.figure(figsize=(12, 5)) + + # Plot loss + plt.subplot(1, 2, 1) + plt.plot(self.history.history['loss'], label='Training Loss') + if 'val_loss' in self.history.history: + plt.plot(self.history.history['val_loss'], label='Validation Loss') + plt.title('Model Loss') + plt.xlabel('Epoch') + plt.ylabel('Loss') + plt.legend() + + # Plot accuracy + plt.subplot(1, 2, 2) + + if 'accuracy' in self.history.history: + plt.plot(self.history.history['accuracy'], label='Training Accuracy') + if 'val_accuracy' in self.history.history: + plt.plot(self.history.history['val_accuracy'], label='Validation Accuracy') + plt.title('Model Accuracy') + plt.ylabel('Accuracy') + elif 'mae' in self.history.history: + plt.plot(self.history.history['mae'], label='Training MAE') + if 'val_mae' in self.history.history: + plt.plot(self.history.history['val_mae'], label='Validation MAE') + plt.title('Model MAE') + plt.ylabel('MAE') + + plt.xlabel('Epoch') + plt.legend() + + plt.tight_layout() + + # Save figure + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + fig_path = os.path.join(self.model_dir, f"transformer_training_history_{timestamp}.png") + plt.savefig(fig_path) + plt.close() + + logger.info(f"Training history plot saved to {fig_path}") + return fig_path + class MixtureOfExpertsModel: """ - Mixture of Experts (MoE) model that combines predictions from multiple models. - This implementation focuses on combining CNN and Transformer models for financial analysis. + Mixture of Experts (MoE) model. + + This model combines predictions from multiple expert models (such as CNN and Transformer) + using a weighted ensemble approach. """ - def __init__(self, output_size=3, model_dir='NN/models/saved'): + def __init__(self, output_size=1, model_dir="NN/models/saved"): """ - Initialize the MoE model + Initialize the MoE model. Args: - output_size: Number of output classes or values - model_dir: Directory to save model files + output_size (int): Number of output classes (1 for binary, 3 for buy/hold/sell) + model_dir (str): Directory to save trained models """ self.output_size = output_size self.model_dir = model_dir - self.models = {} # Dictionary to store expert models - self.gating_model = None # Model to determine which expert to use - self.model = None # Combined MoE model + self.model = None + self.history = None + self.experts = {} # Create model directory if it doesn't exist - os.makedirs(model_dir, exist_ok=True) + os.makedirs(self.model_dir, exist_ok=True) - logger.info(f"Initialized MixtureOfExpertsModel with output size {output_size}") + logger.info(f"Initialized Mixture of Experts model with output size {output_size}") def add_expert(self, name, model): """ - Add an expert model to the MoE + Add an expert model to the MoE. Args: - name: Name of the expert - model: Expert model instance + name (str): Name of the expert model + model: The expert model instance Returns: None """ - self.models[name] = model + self.experts[name] = model logger.info(f"Added expert model '{name}' to MoE") def build_model(self, ts_input_shape=(20, 5), expert_weights=None, learning_rate=0.001): """ - Build the MoE model architecture + Build the MoE model by combining expert models. Args: - ts_input_shape: Shape of time series input data - expert_weights: Dictionary of expert weights (if None, equal weighting) - learning_rate: Learning rate for the optimizer + ts_input_shape (tuple): Shape of time series input data + expert_weights (dict): Weights for each expert model + learning_rate (float): Learning rate for Adam optimizer Returns: - Compiled Keras model + The compiled model """ - if not self.models: - logger.error("No expert models added to MoE") - return None - # Time series input - ts_inputs = Input(shape=ts_input_shape, name='time_series_input') + ts_inputs = Input(shape=ts_input_shape, name="ts_input") - # Get predictions from each expert + # Additional feature input (from CNN) + feature_inputs = Input(shape=(64,), name="feature_input") # Default size for features + + # Process with each expert model expert_outputs = [] expert_names = [] - for name, model in self.models.items(): - if hasattr(model, 'predict') and callable(model.predict): - expert_names.append(name) + for name, expert in self.experts.items(): + # Skip if expert model is not valid or doesn't have a call/predict method + if expert is None: + logger.warning(f"Expert model '{name}' is None, skipping") + continue + + try: + # Different handling based on model type if name == 'cnn': - # For CNN, we directly use the time series input - # We need to extract the raw prediction function from the model's predict method - # which typically returns both predictions and probabilities - expert_outputs.append(model.model(ts_inputs)) + # CNN model takes only time series input + expert_output = expert(ts_inputs) + expert_outputs.append(expert_output) + expert_names.append(name) elif name == 'transformer': - # For transformer, we need features from the CNN as well - # This is a simplification - in a real implementation, we would need to - # extract features from the CNN model and pass them to the transformer - # Here we just create dummy features - dummy_features = Dense(128, activation='relu')(Flatten()(ts_inputs)) - expert_outputs.append(model.model([ts_inputs, dummy_features])) + # Transformer model takes both time series and feature inputs + expert_output = expert([ts_inputs, feature_inputs]) + expert_outputs.append(expert_output) + expert_names.append(name) else: - logger.warning(f"Unknown model type: {name}, skipping") + logger.warning(f"Unknown expert model type: {name}") + except Exception as e: + logger.error(f"Error adding expert '{name}': {str(e)}") if not expert_outputs: logger.error("No valid expert models found") @@ -443,7 +566,7 @@ class MixtureOfExpertsModel: combined_output = Add()(weighted_outputs) # Create the MoE model - moe_model = Model(inputs=ts_inputs, outputs=combined_output) + moe_model = Model(inputs=[ts_inputs, feature_inputs], outputs=combined_output) # Compile the model if self.output_size == 1: @@ -469,83 +592,176 @@ class MixtureOfExpertsModel: ) self.model = moe_model - logger.info(f"MoE model built with experts: {expert_names}, weights: {weights}") - moe_model.summary(print_fn=logger.info) - return moe_model + # Log model summary + self.model.summary(print_fn=lambda x: logger.info(x)) + + logger.info(f"Built MoE model with weights: {weights}") + return self.model - def predict(self, X, threshold=0.5): + def train(self, X_ts, X_features, y, batch_size=32, epochs=100, validation_split=0.2, + callbacks=None, class_weights=None): """ - Make predictions with the MoE model + Train the MoE model on the provided data. Args: - X: Input data - threshold: Threshold for binary classification + X_ts (numpy.ndarray): Time series input features + X_features (numpy.ndarray): Additional input features + y (numpy.ndarray): Target labels + batch_size (int): Batch size + epochs (int): Number of epochs + validation_split (float): Fraction of data to use for validation + callbacks (list): List of Keras callbacks + class_weights (dict): Class weights for imbalanced datasets Returns: - Predicted values or classes + History object containing training metrics """ if self.model is None: - logger.error("MoE model not built yet") + logger.error("MoE model has not been built yet") return None - # Get raw predictions - y_pred_proba = self.model.predict(X) + # Default callbacks if none provided + if callbacks is None: + # Create a timestamp for model checkpoints + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + + callbacks = [ + EarlyStopping( + monitor='val_loss', + patience=10, + restore_best_weights=True + ), + ReduceLROnPlateau( + monitor='val_loss', + factor=0.5, + patience=5, + min_lr=1e-6 + ), + ModelCheckpoint( + filepath=os.path.join(self.model_dir, f"moe_model_{timestamp}.h5"), + monitor='val_loss', + save_best_only=True + ) + ] - # Format predictions based on output type + # Check if y needs to be one-hot encoded for multi-class + if self.output_size == 3 and len(y.shape) == 1: + y = tf.keras.utils.to_categorical(y, num_classes=3) + + # Train the model + logger.info(f"Training MoE model with {len(X_ts)} samples, batch size {batch_size}, epochs {epochs}") + self.history = self.model.fit( + [X_ts, X_features], y, + batch_size=batch_size, + epochs=epochs, + validation_split=validation_split, + callbacks=callbacks, + class_weight=class_weights, + verbose=2 + ) + + # Save the trained model + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + model_path = os.path.join(self.model_dir, f"moe_model_final_{timestamp}.h5") + self.model.save(model_path) + logger.info(f"Model saved to {model_path}") + + # Save training history + history_path = os.path.join(self.model_dir, f"moe_model_history_{timestamp}.json") + with open(history_path, 'w') as f: + # Convert numpy values to Python native types for JSON serialization + history_dict = {key: [float(value) for value in values] for key, values in self.history.history.items()} + json.dump(history_dict, f, indent=2) + + return self.history + + def predict(self, X_ts, X_features=None): + """ + Make predictions on new data. + + Args: + X_ts (numpy.ndarray): Time series input features + X_features (numpy.ndarray): Additional input features + + Returns: + tuple: (y_pred, y_proba) where: + y_pred is the predicted class (0/1 for binary, 0/1/2 for multi-class) + y_proba is the class probability + """ + if self.model is None: + raise ValueError("Model has not been built or trained yet") + + # Ensure X_ts has the right shape + if len(X_ts.shape) == 2: + # Single sample, add batch dimension + X_ts = np.expand_dims(X_ts, axis=0) + + # Ensure X_features has the right shape + if X_features is None: + # Create dummy features with zeros + X_features = np.zeros((X_ts.shape[0], 64)) # Default size + elif len(X_features.shape) == 1: + # Single sample, add batch dimension + X_features = np.expand_dims(X_features, axis=0) + + # Get predictions + y_proba = self.model.predict([X_ts, X_features]) + + # Process based on output type if self.output_size == 1: # Binary classification - y_pred = (y_pred_proba > threshold).astype(int).flatten() - return y_pred, y_pred_proba.flatten() + y_pred = (y_proba > 0.5).astype(int).flatten() + return y_pred, y_proba.flatten() elif self.output_size == 3: - # Multi-class (BUY/HOLD/SELL) - y_pred = np.argmax(y_pred_proba, axis=1) - return y_pred, y_pred_proba + # Multi-class classification + y_pred = np.argmax(y_proba, axis=1) + return y_pred, y_proba else: # Regression - return y_pred_proba + return y_proba, y_proba - def save_model(self, filepath=None): + def save(self, filepath=None): """ - Save the MoE model to a file + Save the model to disk. Args: - filepath: Path to save the model to + filepath (str): Path to save the model Returns: - Path to the saved model + str: Path where the model was saved """ if self.model is None: - logger.error("MoE model not built yet") - return None + raise ValueError("Model has not been built yet") if filepath is None: - # Create a default filepath + # Create a default filepath with timestamp timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") filepath = os.path.join(self.model_dir, f"moe_model_{timestamp}.h5") self.model.save(filepath) - logger.info(f"MoE model saved to {filepath}") - + logger.info(f"Model saved to {filepath}") return filepath - def load_model(self, filepath): + def load(self, filepath): """ - Load an MoE model from a file + Load a saved model from disk. Args: - filepath: Path to load the model from + filepath (str): Path to the saved model Returns: - Loaded model + The loaded model """ - try: - self.model = tf.keras.models.load_model(filepath) - logger.info(f"MoE model loaded from {filepath}") - return self.model - except Exception as e: - logger.error(f"Error loading MoE model: {str(e)}") - return None + # Register custom layers + custom_objects = { + 'TransformerBlock': TransformerBlock, + 'PositionalEncoding': PositionalEncoding + } + + self.model = load_model(filepath, custom_objects=custom_objects) + logger.info(f"Model loaded from {filepath}") + return self.model # Example usage: if __name__ == "__main__": diff --git a/NN/models/transformer_model_pytorch.py b/NN/models/transformer_model_pytorch.py new file mode 100644 index 0000000..6f99624 --- /dev/null +++ b/NN/models/transformer_model_pytorch.py @@ -0,0 +1,653 @@ +#!/usr/bin/env python3 +""" +Transformer Model - PyTorch Implementation + +This module implements a Transformer model using PyTorch for time series analysis. +The model consists of a Transformer encoder and a Mixture of Experts model. +""" + +import os +import logging +import numpy as np +import matplotlib.pyplot as plt +from datetime import datetime + +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader, TensorDataset +from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score + +# Configure logging +logger = logging.getLogger(__name__) + +class TransformerBlock(nn.Module): + """Transformer Block with self-attention mechanism""" + + def __init__(self, input_dim, num_heads=4, ff_dim=64, dropout=0.1): + super(TransformerBlock, self).__init__() + + self.attention = nn.MultiheadAttention( + embed_dim=input_dim, + num_heads=num_heads, + dropout=dropout, + batch_first=True + ) + + self.feed_forward = nn.Sequential( + nn.Linear(input_dim, ff_dim), + nn.ReLU(), + nn.Linear(ff_dim, input_dim) + ) + + self.layernorm1 = nn.LayerNorm(input_dim) + self.layernorm2 = nn.LayerNorm(input_dim) + self.dropout1 = nn.Dropout(dropout) + self.dropout2 = nn.Dropout(dropout) + + def forward(self, x): + # Self-attention + attn_output, _ = self.attention(x, x, x) + x = x + self.dropout1(attn_output) + x = self.layernorm1(x) + + # Feed forward + ff_output = self.feed_forward(x) + x = x + self.dropout2(ff_output) + x = self.layernorm2(x) + + return x + +class TransformerModelPyTorch(nn.Module): + """PyTorch Transformer model for time series analysis""" + + def __init__(self, input_shape, output_size=3, num_heads=4, ff_dim=64, num_transformer_blocks=2): + """ + Initialize the Transformer model. + + Args: + input_shape (tuple): Shape of input data (window_size, features) + output_size (int): Size of output (1 for regression, 3 for classification) + num_heads (int): Number of attention heads + ff_dim (int): Feed forward dimension + num_transformer_blocks (int): Number of transformer blocks + """ + super(TransformerModelPyTorch, self).__init__() + + window_size, num_features = input_shape + + # Positional encoding + self.pos_encoding = nn.Parameter( + torch.zeros(1, window_size, num_features), + requires_grad=True + ) + + # Transformer blocks + self.transformer_blocks = nn.ModuleList([ + TransformerBlock( + input_dim=num_features, + num_heads=num_heads, + ff_dim=ff_dim + ) for _ in range(num_transformer_blocks) + ]) + + # Global average pooling + self.global_avg_pool = nn.AdaptiveAvgPool1d(1) + + # Dense layers + self.dense = nn.Sequential( + nn.Linear(num_features, 64), + nn.ReLU(), + nn.BatchNorm1d(64), + nn.Dropout(0.3), + nn.Linear(64, output_size) + ) + + # Activation based on output size + if output_size == 1: + self.activation = nn.Sigmoid() # Binary classification or regression + elif output_size > 1: + self.activation = nn.Softmax(dim=1) # Multi-class classification + else: + self.activation = nn.Identity() # No activation + + def forward(self, x): + """ + Forward pass through the network. + + Args: + x: Input tensor of shape [batch_size, window_size, features] + + Returns: + Output tensor of shape [batch_size, output_size] + """ + # Add positional encoding + x = x + self.pos_encoding + + # Apply transformer blocks + for transformer_block in self.transformer_blocks: + x = transformer_block(x) + + # Global average pooling + x = x.transpose(1, 2) # [batch, features, window] + x = self.global_avg_pool(x) # [batch, features, 1] + x = x.squeeze(-1) # [batch, features] + + # Dense layers + x = self.dense(x) + + # Apply activation + return self.activation(x) + + +class TransformerModelPyTorchWrapper: + """ + Transformer model wrapper class for time series analysis using PyTorch. + + This class provides methods for building, training, evaluating, and making + predictions with the Transformer model. + """ + + def __init__(self, window_size, num_features, output_size=3, timeframes=None): + """ + Initialize the Transformer model. + + Args: + window_size (int): Size of the input window + num_features (int): Number of features in the input data + output_size (int): Size of the output (1 for regression, 3 for classification) + timeframes (list): List of timeframes used (for logging) + """ + self.window_size = window_size + self.num_features = num_features + self.output_size = output_size + self.timeframes = timeframes or [] + + # Determine device (GPU or CPU) + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + logger.info(f"Using device: {self.device}") + + # Initialize model + self.model = None + self.build_model() + + # Initialize training history + self.history = { + 'loss': [], + 'val_loss': [], + 'accuracy': [], + 'val_accuracy': [] + } + + def build_model(self): + """Build the Transformer model architecture""" + logger.info(f"Building PyTorch Transformer model with window_size={self.window_size}, " + f"num_features={self.num_features}, output_size={self.output_size}") + + self.model = TransformerModelPyTorch( + input_shape=(self.window_size, self.num_features), + output_size=self.output_size + ).to(self.device) + + # Initialize optimizer + self.optimizer = optim.Adam(self.model.parameters(), lr=0.001) + + # Initialize loss function based on output size + if self.output_size == 1: + self.criterion = nn.BCELoss() # Binary classification + elif self.output_size > 1: + self.criterion = nn.CrossEntropyLoss() # Multi-class classification + else: + self.criterion = nn.MSELoss() # Regression + + logger.info(f"Model built successfully with {sum(p.numel() for p in self.model.parameters())} parameters") + + def train(self, X_train, y_train, X_val=None, y_val=None, batch_size=32, epochs=100): + """ + Train the Transformer model. + + Args: + X_train: Training input data + y_train: Training target data + X_val: Validation input data + y_val: Validation target data + batch_size: Batch size for training + epochs: Number of training epochs + + Returns: + Training history + """ + logger.info(f"Training PyTorch Transformer model with {len(X_train)} samples, " + f"batch_size={batch_size}, epochs={epochs}") + + # Convert numpy arrays to PyTorch tensors + X_train_tensor = torch.tensor(X_train, dtype=torch.float32).to(self.device) + + # Handle different output sizes for y_train + if self.output_size == 1: + y_train_tensor = torch.tensor(y_train, dtype=torch.float32).to(self.device) + else: + y_train_tensor = torch.tensor(y_train, dtype=torch.long).to(self.device) + + # Create DataLoader for training data + train_dataset = TensorDataset(X_train_tensor, y_train_tensor) + train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) + + # Create DataLoader for validation data if provided + if X_val is not None and y_val is not None: + X_val_tensor = torch.tensor(X_val, dtype=torch.float32).to(self.device) + if self.output_size == 1: + y_val_tensor = torch.tensor(y_val, dtype=torch.float32).to(self.device) + else: + y_val_tensor = torch.tensor(y_val, dtype=torch.long).to(self.device) + + val_dataset = TensorDataset(X_val_tensor, y_val_tensor) + val_loader = DataLoader(val_dataset, batch_size=batch_size) + else: + val_loader = None + + # Training loop + for epoch in range(epochs): + # Training phase + self.model.train() + running_loss = 0.0 + correct = 0 + total = 0 + + for inputs, targets in train_loader: + # Zero the parameter gradients + self.optimizer.zero_grad() + + # Forward pass + outputs = self.model(inputs) + + # Calculate loss + if self.output_size == 1: + loss = self.criterion(outputs, targets.unsqueeze(1)) + else: + loss = self.criterion(outputs, targets) + + # Backward pass and optimize + loss.backward() + self.optimizer.step() + + # Statistics + running_loss += loss.item() + if self.output_size > 1: + _, predicted = torch.max(outputs, 1) + total += targets.size(0) + correct += (predicted == targets).sum().item() + + epoch_loss = running_loss / len(train_loader) + epoch_acc = correct / total if total > 0 else 0 + + # Validation phase + if val_loader is not None: + val_loss, val_acc = self._validate(val_loader) + + logger.info(f"Epoch {epoch+1}/{epochs} - " + f"loss: {epoch_loss:.4f} - acc: {epoch_acc:.4f} - " + f"val_loss: {val_loss:.4f} - val_acc: {val_acc:.4f}") + + # Update history + self.history['loss'].append(epoch_loss) + self.history['accuracy'].append(epoch_acc) + self.history['val_loss'].append(val_loss) + self.history['val_accuracy'].append(val_acc) + else: + logger.info(f"Epoch {epoch+1}/{epochs} - " + f"loss: {epoch_loss:.4f} - acc: {epoch_acc:.4f}") + + # Update history without validation + self.history['loss'].append(epoch_loss) + self.history['accuracy'].append(epoch_acc) + + logger.info("Training completed") + return self.history + + def _validate(self, val_loader): + """Validate the model using the validation set""" + self.model.eval() + val_loss = 0.0 + correct = 0 + total = 0 + + with torch.no_grad(): + for inputs, targets in val_loader: + # Forward pass + outputs = self.model(inputs) + + # Calculate loss + if self.output_size == 1: + loss = self.criterion(outputs, targets.unsqueeze(1)) + else: + loss = self.criterion(outputs, targets) + + val_loss += loss.item() + + # Calculate accuracy + if self.output_size > 1: + _, predicted = torch.max(outputs, 1) + total += targets.size(0) + correct += (predicted == targets).sum().item() + + return val_loss / len(val_loader), correct / total if total > 0 else 0 + + def evaluate(self, X_test, y_test): + """ + Evaluate the model on test data. + + Args: + X_test: Test input data + y_test: Test target data + + Returns: + dict: Evaluation metrics + """ + logger.info(f"Evaluating model on {len(X_test)} samples") + + # Convert to PyTorch tensors + X_test_tensor = torch.tensor(X_test, dtype=torch.float32).to(self.device) + + # Get predictions + self.model.eval() + with torch.no_grad(): + y_pred = self.model(X_test_tensor) + + if self.output_size > 1: + _, y_pred_class = torch.max(y_pred, 1) + y_pred_class = y_pred_class.cpu().numpy() + else: + y_pred_class = (y_pred.cpu().numpy() > 0.5).astype(int).flatten() + + # Calculate metrics + if self.output_size > 1: + accuracy = accuracy_score(y_test, y_pred_class) + precision = precision_score(y_test, y_pred_class, average='weighted') + recall = recall_score(y_test, y_pred_class, average='weighted') + f1 = f1_score(y_test, y_pred_class, average='weighted') + + metrics = { + 'accuracy': accuracy, + 'precision': precision, + 'recall': recall, + 'f1_score': f1 + } + else: + accuracy = accuracy_score(y_test, y_pred_class) + precision = precision_score(y_test, y_pred_class) + recall = recall_score(y_test, y_pred_class) + f1 = f1_score(y_test, y_pred_class) + + metrics = { + 'accuracy': accuracy, + 'precision': precision, + 'recall': recall, + 'f1_score': f1 + } + + logger.info(f"Evaluation metrics: {metrics}") + return metrics + + def predict(self, X): + """ + Make predictions with the model. + + Args: + X: Input data + + Returns: + Predictions + """ + # Convert to PyTorch tensor + X_tensor = torch.tensor(X, dtype=torch.float32).to(self.device) + + # Get predictions + self.model.eval() + with torch.no_grad(): + predictions = self.model(X_tensor) + + if self.output_size > 1: + # Multi-class classification + probs = predictions.cpu().numpy() + _, class_preds = torch.max(predictions, 1) + class_preds = class_preds.cpu().numpy() + return class_preds, probs + else: + # Binary classification or regression + preds = predictions.cpu().numpy() + if self.output_size == 1: + # Binary classification + class_preds = (preds > 0.5).astype(int) + return class_preds.flatten(), preds.flatten() + else: + # Regression + return preds.flatten(), None + + def save(self, filepath): + """ + Save the model to a file. + + Args: + filepath: Path to save the model + """ + # Create directory if it doesn't exist + os.makedirs(os.path.dirname(filepath), exist_ok=True) + + # Save the model state + model_state = { + 'model_state_dict': self.model.state_dict(), + 'optimizer_state_dict': self.optimizer.state_dict(), + 'history': self.history, + 'window_size': self.window_size, + 'num_features': self.num_features, + 'output_size': self.output_size, + 'timeframes': self.timeframes + } + + torch.save(model_state, f"{filepath}.pt") + logger.info(f"Model saved to {filepath}.pt") + + def load(self, filepath): + """ + Load the model from a file. + + Args: + filepath: Path to load the model from + """ + # Check if file exists + if not os.path.exists(f"{filepath}.pt"): + logger.error(f"Model file {filepath}.pt not found") + return False + + # Load the model state + model_state = torch.load(f"{filepath}.pt", map_location=self.device) + + # Update model parameters + self.window_size = model_state['window_size'] + self.num_features = model_state['num_features'] + self.output_size = model_state['output_size'] + self.timeframes = model_state['timeframes'] + + # Rebuild the model + self.build_model() + + # Load the model state + self.model.load_state_dict(model_state['model_state_dict']) + self.optimizer.load_state_dict(model_state['optimizer_state_dict']) + self.history = model_state['history'] + + logger.info(f"Model loaded from {filepath}.pt") + return True + +class MixtureOfExpertsModelPyTorch: + """ + Mixture of Experts model implementation using PyTorch. + + This model combines predictions from multiple models (experts) using a + learned weighting scheme. + """ + + def __init__(self, output_size=3, timeframes=None): + """ + Initialize the Mixture of Experts model. + + Args: + output_size (int): Size of the output (1 for regression, 3 for classification) + timeframes (list): List of timeframes used (for logging) + """ + self.output_size = output_size + self.timeframes = timeframes or [] + self.experts = {} + self.expert_weights = {} + + # Determine device (GPU or CPU) + self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + logger.info(f"Using device: {self.device}") + + # Initialize model and training history + self.model = None + self.history = { + 'loss': [], + 'val_loss': [], + 'accuracy': [], + 'val_accuracy': [] + } + + def add_expert(self, name, model): + """ + Add an expert model. + + Args: + name (str): Name of the expert + model: Expert model + """ + self.experts[name] = model + logger.info(f"Added expert: {name}") + + def predict(self, X): + """ + Make predictions using all experts and combine them. + + Args: + X: Input data + + Returns: + Combined predictions + """ + if not self.experts: + logger.error("No experts added to the MoE model") + return None + + # Get predictions from each expert + expert_predictions = {} + for name, expert in self.experts.items(): + pred, _ = expert.predict(X) + expert_predictions[name] = pred + + # Combine predictions based on weights + final_pred = None + for name, pred in expert_predictions.items(): + weight = self.expert_weights.get(name, 1.0 / len(self.experts)) + if final_pred is None: + final_pred = weight * pred + else: + final_pred += weight * pred + + # For classification, convert to class indices + if self.output_size > 1: + # Get class with highest probability + class_pred = np.argmax(final_pred, axis=1) + return class_pred, final_pred + else: + # Binary classification + class_pred = (final_pred > 0.5).astype(int) + return class_pred, final_pred + + def evaluate(self, X_test, y_test): + """ + Evaluate the model on test data. + + Args: + X_test: Test input data + y_test: Test target data + + Returns: + dict: Evaluation metrics + """ + logger.info(f"Evaluating MoE model on {len(X_test)} samples") + + # Get predictions + y_pred_class, _ = self.predict(X_test) + + # Calculate metrics + if self.output_size > 1: + accuracy = accuracy_score(y_test, y_pred_class) + precision = precision_score(y_test, y_pred_class, average='weighted') + recall = recall_score(y_test, y_pred_class, average='weighted') + f1 = f1_score(y_test, y_pred_class, average='weighted') + + metrics = { + 'accuracy': accuracy, + 'precision': precision, + 'recall': recall, + 'f1_score': f1 + } + else: + accuracy = accuracy_score(y_test, y_pred_class) + precision = precision_score(y_test, y_pred_class) + recall = recall_score(y_test, y_pred_class) + f1 = f1_score(y_test, y_pred_class) + + metrics = { + 'accuracy': accuracy, + 'precision': precision, + 'recall': recall, + 'f1_score': f1 + } + + logger.info(f"MoE evaluation metrics: {metrics}") + return metrics + + def save(self, filepath): + """ + Save the model weights to a file. + + Args: + filepath: Path to save the model + """ + # Create directory if it doesn't exist + os.makedirs(os.path.dirname(filepath), exist_ok=True) + + # Save the model state + model_state = { + 'expert_weights': self.expert_weights, + 'output_size': self.output_size, + 'timeframes': self.timeframes + } + + torch.save(model_state, f"{filepath}_moe.pt") + logger.info(f"MoE model saved to {filepath}_moe.pt") + + def load(self, filepath): + """ + Load the model from a file. + + Args: + filepath: Path to load the model from + """ + # Check if file exists + if not os.path.exists(f"{filepath}_moe.pt"): + logger.error(f"MoE model file {filepath}_moe.pt not found") + return False + + # Load the model state + model_state = torch.load(f"{filepath}_moe.pt", map_location=self.device) + + # Update model parameters + self.expert_weights = model_state['expert_weights'] + self.output_size = model_state['output_size'] + self.timeframes = model_state['timeframes'] + + logger.info(f"MoE model loaded from {filepath}_moe.pt") + return True \ No newline at end of file diff --git a/NN/requirements.txt b/NN/requirements.txt index d86e257..1b22494 100644 --- a/NN/requirements.txt +++ b/NN/requirements.txt @@ -1,13 +1,22 @@ -tensorflow>=2.5.0 +# Main dependencies numpy>=1.19.5 pandas>=1.3.0 matplotlib>=3.4.2 scikit-learn>=0.24.2 -tensorflow-addons>=0.13.0 -plotly>=5.1.0 -h5py>=3.1.0 -tqdm>=4.61.1 -pyyaml>=5.4.1 -tensorboard>=2.5.0 -ccxt>=1.50.0 -requests>=2.25.1 \ No newline at end of file + +# PyTorch (primary framework) +torch +torchvision + +# TensorFlow (optional) +# tensorflow>=2.5.0 +# tensorflow-addons>=0.13.0 + +# Additional dependencies +plotly +h5py +tqdm +pyyaml +tensorboard +ccxt +requests \ No newline at end of file diff --git a/NN/start_tensorboard.py b/NN/start_tensorboard.py new file mode 100644 index 0000000..ed27a1b --- /dev/null +++ b/NN/start_tensorboard.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +""" +Start TensorBoard for monitoring neural network training +""" + +import os +import sys +import subprocess +import webbrowser +from time import sleep + +def start_tensorboard(logdir="NN/models/saved/logs", port=6006, open_browser=True): + """ + Start TensorBoard in a subprocess + + Args: + logdir: Directory containing TensorBoard logs + port: Port to run TensorBoard on + open_browser: Whether to open a browser automatically + """ + # Make sure the log directory exists + os.makedirs(logdir, exist_ok=True) + + # Create command + cmd = [ + sys.executable, + "-m", + "tensorboard.main", + f"--logdir={logdir}", + f"--port={port}", + "--bind_all" + ] + + print(f"Starting TensorBoard with logs from {logdir} on port {port}") + print(f"Command: {' '.join(cmd)}") + + # Start TensorBoard in a subprocess + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True + ) + + # Wait for TensorBoard to start up + for line in process.stdout: + print(line.strip()) + if "TensorBoard" in line and "http://" in line: + # TensorBoard is running, extract the URL + url = None + for part in line.split(): + if part.startswith(("http://", "https://")): + url = part + break + + # Open browser if requested and URL found + if open_browser and url: + print(f"Opening TensorBoard in browser: {url}") + webbrowser.open(url) + + break + + # Return the process for the caller to manage + return process + +if __name__ == "__main__": + import argparse + + # Parse command line arguments + parser = argparse.ArgumentParser(description="Start TensorBoard for NN training visualization") + parser.add_argument("--logdir", default="NN/models/saved/logs", help="Directory containing TensorBoard logs") + parser.add_argument("--port", type=int, default=6006, help="Port to run TensorBoard on") + parser.add_argument("--no-browser", action="store_true", help="Don't open browser automatically") + + args = parser.parse_args() + + # Start TensorBoard + process = start_tensorboard(args.logdir, args.port, not args.no_browser) + + try: + # Keep the script running until Ctrl+C + print("TensorBoard is running. Press Ctrl+C to stop.") + while True: + sleep(1) + except KeyboardInterrupt: + print("Stopping TensorBoard...") + process.terminate() + process.wait() \ No newline at end of file diff --git a/NN/utils/__pycache__/__init__.cpython-312.pyc b/NN/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b59c7300d376284e7ab5ee4517d460a2e3079f0a GIT binary patch literal 493 zcmZvZF;4<96vx{;6mo{djRcoBnJ~z}NYq7R!g7-=ZNn2uYbMzDV8T>3R z&P+xpHx0Tl*`DCU8=CgL_wVoDzUO8$K|Tf#Z^aNJ^d1h|h&G#5+ixBbLnFj6izF_i zQG^kT3p|P!wa(t%carkCp%PN=Znc@h&wOIIRl}jQsIc~-@=Y6=) zO7YxTa2gD!(utD8R>neeK+TNKc?A;=#@g&+m6~yBlaKH{9lUG?(m%+=KAU!G%Q_)U z=Y%XSx}>O|Y63y3oBWO|wd$5ib^wknU`; qZk&1>bIH$57@!{=vMs-S#u($TXdl;p4iWCX9<*PQqx-sP_}VY#z?cI7 literal 0 HcmV?d00001 diff --git a/NN/utils/__pycache__/data_interface.cpython-312.pyc b/NN/utils/__pycache__/data_interface.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..193eeac387a0c0ac516bc1c2a3e86ab96eb156f0 GIT binary patch literal 16274 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 S_LA-^ji9B$Yl%4-= n_candles: + logger.info(f"Using cached data for {self.symbol} {timeframe} ({len(df)} candles)") + self.dataframes[timeframe] = df + return df.tail(n_candles) + except Exception as e: + logger.error(f"Error reading cached data: {str(e)}") + + # If we get here, we need to fetch data + # For now, we'll use a placeholder for fetching data from an exchange + try: + # In a real implementation, we would fetch data from an exchange or API here + # For this example, we'll create dummy data if we can't load from cache + logger.info(f"Fetching historical data for {self.symbol} {timeframe}") + + # Placeholder for real data fetching + # In a real implementation, this would be replaced with API calls + self._fetch_data_from_exchange(timeframe, n_candles) + + # Save to cache + if self.dataframes[timeframe] is not None: + self.dataframes[timeframe].to_csv(cache_file, index=False) + return self.dataframes[timeframe] + else: + # Create dummy data as fallback + logger.warning(f"Could not fetch data for {self.symbol} {timeframe}, using dummy data") + df = self._create_dummy_data(timeframe, n_candles) + self.dataframes[timeframe] = df + return df + except Exception as e: + logger.error(f"Error fetching data: {str(e)}") + return None + + def _fetch_data_from_exchange(self, timeframe, n_candles): + """ + Placeholder method for fetching data from an exchange. + In a real implementation, this would connect to an exchange API. + """ + # This is a placeholder - in a real implementation this would make API calls + # to a cryptocurrency exchange to fetch OHLCV data + + # For now, just generate dummy data + self.dataframes[timeframe] = self._create_dummy_data(timeframe, n_candles) + + def _create_dummy_data(self, timeframe, n_candles): + """ + Create dummy OHLCV data for testing purposes. + + Args: + timeframe (str): Timeframe to create data for + n_candles (int): Number of candles to create + + Returns: + pd.DataFrame: DataFrame with dummy OHLCV data + """ + # Map timeframe to seconds + tf_seconds = { + '1m': 60, + '5m': 300, + '15m': 900, + '1h': 3600, + '4h': 14400, + '1d': 86400 + } + seconds = tf_seconds.get(timeframe, 3600) # Default to 1h + + # Create timestamps + end_time = datetime.now() + timestamps = [end_time - timedelta(seconds=seconds * i) for i in range(n_candles)] + timestamps.reverse() # Oldest first + + # Generate random price data with realistic patterns + np.random.seed(42) # For reproducibility + + # Start price + price = 50000 # For BTC/USDT + prices = [] + volumes = [] + + for i in range(n_candles): + # Random walk with drift and volatility based on timeframe + drift = 0.0001 * seconds # Larger drift for larger timeframes + volatility = 0.01 * np.sqrt(seconds / 3600) # Scale volatility by sqrt of time + + # Daily/weekly patterns + if timeframe in ['1d', '4h']: + # Add some cyclical patterns + cycle = np.sin(i / 7 * np.pi) * 0.02 # Weekly cycle + else: + cycle = np.sin(i / 24 * np.pi) * 0.01 # Daily cycle + + # Calculate price change with random walk + cycles + price_change = price * (drift + volatility * np.random.randn() + cycle) + price += price_change + + # Generate OHLC from the price + open_price = price + high_price = price * (1 + abs(0.005 * np.random.randn())) + low_price = price * (1 - abs(0.005 * np.random.randn())) + close_price = price * (1 + 0.002 * np.random.randn()) + + # Ensure high >= open, close, low and low <= open, close + high_price = max(high_price, open_price, close_price) + low_price = min(low_price, open_price, close_price) + + # Generate volume (higher for larger price movements) + volume = abs(price_change) * (10000 + 5000 * np.random.rand()) + + prices.append((open_price, high_price, low_price, close_price)) + volumes.append(volume) + + # Update price for next iteration + price = close_price + + # Create DataFrame + df = pd.DataFrame( + [(t, o, h, l, c, v) for t, (o, h, l, c), v in zip(timestamps, prices, volumes)], + columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'] + ) + + return df + + def prepare_nn_input(self, timeframes=None, n_candles=500, window_size=20): + """ + Prepare input data for neural network models. + + Args: + timeframes (list): List of timeframes to use + n_candles (int): Number of candles to fetch for each timeframe + window_size (int): Size of the sliding window for feature creation + + Returns: + tuple: (X, y, timestamps) where: + X is the input features array with shape (n_samples, window_size, n_features) + y is the target array with shape (n_samples,) + timestamps is an array of timestamps for each sample + """ + if timeframes is None: + timeframes = self.timeframes + + # Get data for all requested timeframes + dfs = {} + for tf in timeframes: + df = self.get_historical_data(timeframe=tf, n_candles=n_candles) + if df is not None and not df.empty: + dfs[tf] = df + + if not dfs: + logger.error("No data available for feature creation") + return None, None, None + + # For simplicity, we'll use just one timeframe for now + # In a more complex implementation, we would merge multiple timeframes + primary_tf = timeframes[0] + if primary_tf not in dfs: + logger.error(f"Primary timeframe {primary_tf} not available") + return None, None, None + + df = dfs[primary_tf] + + # Create features + X, y, timestamps = self._create_features(df, window_size) + + return X, y, timestamps + + def _create_features(self, df, window_size): + """ + Create features from OHLCV data using a sliding window approach. + + Args: + df (pd.DataFrame): DataFrame with OHLCV data + window_size (int): Size of the sliding window + + Returns: + tuple: (X, y, timestamps) where: + X is the input features array + y is the target array + timestamps is an array of timestamps for each sample + """ + # Extract OHLCV columns + ohlcv = df[['open', 'high', 'low', 'close', 'volume']].values + + # Scale the data + scaler = MinMaxScaler() + ohlcv_scaled = scaler.fit_transform(ohlcv) + + # Store the scaler for later use + timeframe = next((tf for tf in self.timeframes if self.dataframes.get(tf) is not None and + self.dataframes[tf].equals(df)), 'unknown') + self.scalers[timeframe] = scaler + + # Create sliding windows + X = [] + y = [] + timestamps = [] + + for i in range(len(ohlcv_scaled) - window_size): + # Input: window_size candles of OHLCV data + X.append(ohlcv_scaled[i:i+window_size]) + + # Target: binary classification - price goes up (1) or down (0) + # 1 if close price increases in the next candle, 0 otherwise + price_change = ohlcv[i+window_size, 3] - ohlcv[i+window_size-1, 3] + y.append(1 if price_change > 0 else 0) + + # Store timestamp for reference + timestamps.append(df['timestamp'].iloc[i+window_size]) + + return np.array(X), np.array(y), np.array(timestamps) + + def generate_training_dataset(self, timeframes=None, n_candles=1000, window_size=20): + """ + Generate and save a training dataset for neural network models. + + Args: + timeframes (list): List of timeframes to use + n_candles (int): Number of candles to fetch for each timeframe + window_size (int): Size of the sliding window for feature creation + + Returns: + dict: Dictionary of dataset file paths + """ + if timeframes is None: + timeframes = self.timeframes + + # Prepare inputs + X, y, timestamps = self.prepare_nn_input(timeframes, n_candles, window_size) + + if X is None or y is None: + logger.error("Failed to prepare input data for dataset") + return None + + # Prepare output paths + timestamp_str = datetime.now().strftime("%Y%m%d_%H%M%S") + dataset_name = f"{self.symbol.replace('/', '_')}_{'_'.join(timeframes)}_{timestamp_str}" + + X_path = os.path.join(self.data_dir, f"{dataset_name}_X.npy") + y_path = os.path.join(self.data_dir, f"{dataset_name}_y.npy") + timestamps_path = os.path.join(self.data_dir, f"{dataset_name}_timestamps.npy") + metadata_path = os.path.join(self.data_dir, f"{dataset_name}_metadata.json") + + # Save arrays + np.save(X_path, X) + np.save(y_path, y) + np.save(timestamps_path, timestamps) + + # Save metadata + metadata = { + 'symbol': self.symbol, + 'timeframes': timeframes, + 'window_size': window_size, + 'n_samples': len(X), + 'feature_shape': X.shape[1:], + 'created_at': datetime.now().isoformat(), + 'dataset_name': dataset_name + } + + with open(metadata_path, 'w') as f: + json.dump(metadata, f, indent=2) + + # Save scalers + scaler_path = os.path.join(self.data_dir, f"{dataset_name}_scalers.pkl") + with open(scaler_path, 'wb') as f: + pickle.dump(self.scalers, f) + + # Return dataset info + dataset_info = { + 'X_path': X_path, + 'y_path': y_path, + 'timestamps_path': timestamps_path, + 'metadata_path': metadata_path, + 'scaler_path': scaler_path + } + + logger.info(f"Dataset generated and saved: {dataset_name}") + return dataset_info + + def prepare_realtime_input(self, timeframe='1h', n_candles=30, window_size=20): + """ + Prepare a single input sample from the most recent data for real-time inference. + + Args: + timeframe (str): Timeframe to use + n_candles (int): Number of recent candles to fetch + window_size (int): Size of the sliding window + + Returns: + tuple: (X, timestamp) where: + X is the input features array with shape (1, window_size, n_features) + timestamp is the timestamp of the most recent candle + """ + # Get recent data + df = self.get_historical_data(timeframe=timeframe, n_candles=n_candles, use_cache=False) + + if df is None or len(df) < window_size: + logger.error(f"Not enough data for inference (need at least {window_size} candles)") + return None, None + + # Extract features from the most recent window + ohlcv = df[['open', 'high', 'low', 'close', 'volume']].tail(window_size).values + + # Scale the data + if timeframe in self.scalers: + # Use existing scaler + scaler = self.scalers[timeframe] + else: + # Create new scaler + scaler = MinMaxScaler() + # Fit on all available data + all_data = df[['open', 'high', 'low', 'close', 'volume']].values + scaler.fit(all_data) + self.scalers[timeframe] = scaler + + ohlcv_scaled = scaler.transform(ohlcv) + + # Reshape to (1, window_size, n_features) + X = np.array([ohlcv_scaled]) + + # Get timestamp of the most recent candle + timestamp = df['timestamp'].iloc[-1] + + return X, timestamp \ No newline at end of file diff --git a/run_nn.py b/run_nn.py new file mode 100644 index 0000000..a4d0a7a --- /dev/null +++ b/run_nn.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +""" +Neural Network Training Runner Script + +This script runs the Neural Network Trading System with the existing conda environment. +It detects which deep learning framework is available (TensorFlow or PyTorch) and +adjusts the implementation accordingly. +""" + +import os +import sys +import subprocess +import argparse +import logging +from pathlib import Path + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger('nn_runner') + +def detect_framework(): + """Detect which deep learning framework is available in the environment""" + try: + import torch + torch_version = torch.__version__ + logger.info(f"PyTorch {torch_version} detected") + return "pytorch", torch_version + except ImportError: + logger.warning("PyTorch not found in environment") + try: + import tensorflow as tf + tf_version = tf.__version__ + logger.info(f"TensorFlow {tf_version} detected") + return "tensorflow", tf_version + except ImportError: + logger.error("Neither PyTorch nor TensorFlow is available in the environment") + return None, None + +def check_dependencies(): + """Check for required dependencies and return if they are met""" + required_packages = ["numpy", "pandas", "matplotlib", "scikit-learn"] + missing_packages = [] + + for package in required_packages: + try: + __import__(package) + except ImportError: + missing_packages.append(package) + + if missing_packages: + logger.warning(f"Missing required packages: {', '.join(missing_packages)}") + return False + + return True + +def create_run_command(args, framework): + """Create the command to run the neural network based on the available framework""" + cmd = ["python", "-m", "NN.main"] + + # Add mode + cmd.extend(["--mode", args.mode]) + + # Add symbol + if args.symbol: + cmd.extend(["--symbol", args.symbol]) + + # Add timeframes + if args.timeframes: + cmd.extend(["--timeframes"] + args.timeframes) + + # Add window size + if args.window_size: + cmd.extend(["--window-size", str(args.window_size)]) + + # Add output size + if args.output_size: + cmd.extend(["--output-size", str(args.output_size)]) + + # Add batch size + if args.batch_size: + cmd.extend(["--batch-size", str(args.batch_size)]) + + # Add epochs + if args.epochs: + cmd.extend(["--epochs", str(args.epochs)]) + + # Add model type + if args.model_type: + cmd.extend(["--model-type", args.model_type]) + + # Add framework-specific flag + cmd.extend(["--framework", framework]) + + return cmd + +def parse_arguments(): + """Parse command line arguments""" + parser = argparse.ArgumentParser(description='Neural Network Trading System Runner') + + parser.add_argument('--mode', type=str, choices=['train', 'predict', 'realtime'], default='train', + help='Mode to run (train, predict, realtime)') + parser.add_argument('--symbol', type=str, default='BTC/USDT', + help='Trading pair symbol') + parser.add_argument('--timeframes', type=str, nargs='+', default=['1h', '4h'], + help='Timeframes to use') + parser.add_argument('--window-size', type=int, default=20, + help='Window size for input data') + parser.add_argument('--output-size', type=int, default=3, + help='Output size (1 for binary, 3 for BUY/HOLD/SELL)') + parser.add_argument('--batch-size', type=int, default=32, + help='Batch size for training') + parser.add_argument('--epochs', type=int, default=100, + help='Number of epochs for training') + parser.add_argument('--model-type', type=str, choices=['cnn', 'transformer', 'moe'], default='cnn', + help='Model type to use') + parser.add_argument('--conda-env', type=str, default='gpt-gpu', + help='Name of conda environment to use') + parser.add_argument('--no-conda', action='store_true', + help='Do not use conda environment activation') + parser.add_argument('--framework', type=str, choices=['tensorflow', 'pytorch'], default='pytorch', + help='Deep learning framework to use (default: pytorch)') + + return parser.parse_args() + +def main(): + # Parse arguments + args = parse_arguments() + + # Check if we should run with conda + if not args.no_conda and args.conda_env: + # Create conda activation command + if sys.platform == 'win32': + conda_cmd = f"conda activate {args.conda_env} && " + else: + conda_cmd = f"source activate {args.conda_env} && " + + logger.info(f"Running with conda environment: {args.conda_env}") + + # Create the run script + script_path = Path("run_nn_in_conda.bat" if sys.platform == 'win32' else "run_nn_in_conda.sh") + + with open(script_path, 'w') as f: + if sys.platform == 'win32': + f.write("@echo off\n") + f.write(f"call conda activate {args.conda_env}\n") + f.write(f"python -m NN.main --mode {args.mode} --symbol {args.symbol}") + + if args.timeframes: + f.write(f" --timeframes {' '.join(args.timeframes)}") + + if args.window_size: + f.write(f" --window-size {args.window_size}") + + if args.output_size: + f.write(f" --output-size {args.output_size}") + + if args.batch_size: + f.write(f" --batch-size {args.batch_size}") + + if args.epochs: + f.write(f" --epochs {args.epochs}") + + if args.model_type: + f.write(f" --model-type {args.model_type}") + else: + f.write("#!/bin/bash\n") + f.write(f"source activate {args.conda_env}\n") + f.write(f"python -m NN.main --mode {args.mode} --symbol {args.symbol}") + + if args.timeframes: + f.write(f" --timeframes {' '.join(args.timeframes)}") + + if args.window_size: + f.write(f" --window-size {args.window_size}") + + if args.output_size: + f.write(f" --output-size {args.output_size}") + + if args.batch_size: + f.write(f" --batch-size {args.batch_size}") + + if args.epochs: + f.write(f" --epochs {args.epochs}") + + if args.model_type: + f.write(f" --model-type {args.model_type}") + + # Make script executable on Unix + if sys.platform != 'win32': + os.chmod(script_path, 0o755) + + # Run the script + logger.info(f"Created script: {script_path}") + logger.info("Run this script to execute the neural network with the conda environment") + + if sys.platform == 'win32': + print("\nTo run the neural network, execute the following command:") + print(f" {script_path}") + else: + print("\nTo run the neural network, execute the following command:") + print(f" ./{script_path}") + else: + # Run directly without conda + # First detect available framework + framework, version = detect_framework() + + if framework is None: + logger.error("Cannot run Neural Network - no deep learning framework available") + return + + # Check dependencies + if not check_dependencies(): + logger.error("Missing required dependencies - please install them first") + return + + # Create command + cmd = create_run_command(args, framework) + + # Run command + logger.info(f"Running command: {' '.join(cmd)}") + try: + subprocess.run(cmd, check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Error running neural network: {str(e)}") + except Exception as e: + logger.error(f"Error: {str(e)}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/run_nn_in_conda.bat b/run_nn_in_conda.bat new file mode 100644 index 0000000..a1b9a03 --- /dev/null +++ b/run_nn_in_conda.bat @@ -0,0 +1,3 @@ +@echo off +call conda activate gpt-gpu +python -m NN.main --mode train --symbol BTC/USDT --timeframes 1h 4h --window-size 20 --output-size 3 --batch-size 32 --epochs 100 --model-type cnn --framework pytorch \ No newline at end of file diff --git a/run_pytorch_nn.bat b/run_pytorch_nn.bat new file mode 100644 index 0000000..facfb2a --- /dev/null +++ b/run_pytorch_nn.bat @@ -0,0 +1,50 @@ +@echo off +echo ============================================================ +echo Neural Network Trading System - PyTorch Implementation +echo ============================================================ + +call conda activate gpt-gpu + +REM Parse command-line arguments +set MODE=train +set MODEL_TYPE=cnn +set SYMBOL=BTC/USDT +set EPOCHS=100 + +:parse +if "%~1"=="" goto endparse +if /i "%~1"=="--mode" ( + set MODE=%~2 + shift + shift + goto parse +) +if /i "%~1"=="--model" ( + set MODEL_TYPE=%~2 + shift + shift + goto parse +) +if /i "%~1"=="--symbol" ( + set SYMBOL=%~2 + shift + shift + goto parse +) +if /i "%~1"=="--epochs" ( + set EPOCHS=%~2 + shift + shift + goto parse +) +shift +goto parse +:endparse + +echo Running Neural Network in %MODE% mode with %MODEL_TYPE% model for %SYMBOL% for %EPOCHS% epochs + +python -m NN.main --mode %MODE% --symbol %SYMBOL% --timeframes 1h 4h --window-size 20 --output-size 3 --batch-size 32 --epochs %EPOCHS% --model-type %MODEL_TYPE% --framework pytorch + +echo ============================================================ +echo Run completed. +echo ============================================================ \ No newline at end of file