Files
AlphaFlow-MT5-ML-DL-Trading…/notebooks/strategies/ml/backtesting/momentum_strategy.ipynb
T

908 KiB

In [1]:
import sys
import os
import warnings
from pathlib import Path
import datetime

import numpy as np
import pandas as pd
import MetaTrader5 as mt5
import joblib
import optuna

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
from tensorflow.keras.optimizers import Adam
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import StandardScaler

warnings.filterwarnings("ignore")

# 1) PROJECT ROOT
project_root = Path.cwd().parent.parent
sys.path.append(str(project_root))
os.chdir(str(project_root))

# 2) IMPORTS FROM YOUR MODULES
from data.data_loader import get_data_mt5
from features.feature_engineering import add_momentum_indicators
from features.labeling_schemes import create_labels_momentum_classification

###############################################################################
# A) LOAD & PREPARE DATA
###############################################################################
if not mt5.initialize():
    print("Failed to initialize MT5")
else:
    data = get_data_mt5(symbol="BTCUSD", timeframe=mt5.TIMEFRAME_H4, n_bars=3000, start_pos=3000)
    mt5.shutdown()

# Suppose you have a function that adds short/long momentum, MA, Hurst
df = add_momentum_indicators(data)

# Suppose you have a labeling function that sets 'momentum_label' in {0,1,2}
# 0 => Sell, 1 => Hold, 2 => Buy
df = create_labels_momentum_classification(df, horizon=10, threshold=0.0)
df.dropna(inplace=True)

# Choose features
feature_cols = ["mom_10", "mom_30", "ma_90", "hurst"]
X_full = df[feature_cols].values  # shape: (num_samples, num_features)
y_full = df["momentum_label"].values  # shape: (num_samples,)

print("X_full shape:", X_full.shape)
print("Unique y labels:", np.unique(y_full))

# We'll define a lookback for the LSTM
lookback = 10

###############################################################################
# B) CREATE SEQUENCES FOR LSTM CLASSIFICATION
###############################################################################
def create_sequences_classification(X, y, lookback):
    X_seq, y_seq = [], []
    for i in range(len(X) - lookback):
        X_seq.append(X[i : i + lookback])
        y_seq.append(y[i + lookback])
    return np.array(X_seq), np.array(y_seq)

###############################################################################
# C) BUILD LSTM MODEL FOR CLASSIFICATION
###############################################################################
def build_lstm_model_classification(n_units, dropout_rate, learning_rate, lookback, n_features, n_classes=3):
    model = Sequential([
        LSTM(n_units, return_sequences=True, input_shape=(lookback, n_features)),
        Dropout(dropout_rate),
        LSTM(n_units),
        Dense(n_classes, activation='softmax')  # 3 classes: 0,1,2
    ])
    optimizer = Adam(learning_rate=learning_rate)
    model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer, metrics=["accuracy"])
    return model

###############################################################################
# D) OPTUNA OBJECTIVE (With TensorBoard logging)
###############################################################################
def objective(trial):
    n_units = trial.suggest_int("n_units", 32, 128, step=32)
    dropout_rate = trial.suggest_float("dropout_rate", 0.0, 0.5, step=0.1)
    learning_rate = trial.suggest_float("learning_rate", 1e-4, 1e-2, log=True)
    epochs = trial.suggest_int("epochs", 10, 50, step=10)
    batch_size = trial.suggest_int("batch_size", 16, 64, step=16)

    tscv = TimeSeriesSplit(n_splits=3)
    accuracies = []

    for fold_idx, (train_idx, test_idx) in enumerate(tscv.split(X_full)):
        X_train_raw, X_test_raw = X_full[train_idx], X_full[test_idx]
        y_train_raw, y_test_raw = y_full[train_idx], y_full[test_idx]

        # Scale
        scaler = StandardScaler()
        X_train_scaled = scaler.fit_transform(X_train_raw)
        X_test_scaled = scaler.transform(X_test_raw)

        # Create sequences
        X_train_seq, y_train_seq = create_sequences_classification(X_train_scaled, y_train_raw, lookback)
        X_test_seq, y_test_seq = create_sequences_classification(X_test_scaled, y_test_raw, lookback)

        # Build model
        model = build_lstm_model_classification(
            n_units, dropout_rate, learning_rate, 
            lookback=lookback,
            n_features=len(feature_cols),
            n_classes=3
        )

        # TensorBoard callback for each fold
        log_dir = f"logs/optuna/trial_{trial.number}_fold_{fold_idx}_{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
        tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=log_dir, histogram_freq=1)

        # Train
        model.fit(
            X_train_seq, y_train_seq,
            epochs=epochs,
            batch_size=batch_size,
            verbose=0,
            callbacks=[tensorboard_callback]
        )

        # Evaluate
        preds = model.predict(X_test_seq, verbose=0)
        pred_classes = np.argmax(preds, axis=1)

        fold_acc = accuracy_score(y_test_seq, pred_classes)
        accuracies.append(fold_acc)

    # We'll do direction="maximize" in create_study if we want to maximize accuracy
    # but if direction="minimize", we can return negative accuracy
    return -np.mean(accuracies)

###############################################################################
# E) RUN OPTUNA STUDY
###############################################################################
study = optuna.create_study(direction="minimize")  # or direction="maximize" + invert in objective
study.optimize(objective, n_trials=10, timeout=1800)

best_params = study.best_trial.params
best_value = study.best_trial.value

print("\nBest Trial => Accuracy:", -best_value)  # because we used negative
print("Params:", best_params)

###############################################################################
# F) VISUALIZE OPTUNA RESULTS
###############################################################################
import optuna.visualization as ov
ov.plot_optimization_history(study).show()
ov.plot_param_importances(study).show()

###############################################################################
# G) RETRAIN ON FULL DATA WITH BEST PARAMS
###############################################################################
n_units = best_params["n_units"]
dropout_rate = best_params["dropout_rate"]
learning_rate = best_params["learning_rate"]
epochs = best_params["epochs"]
batch_size = best_params["batch_size"]

# Scale entire dataset
scaler_final = StandardScaler()
X_full_scaled = scaler_final.fit_transform(X_full)

X_all_seq, y_all_seq = create_sequences_classification(X_full_scaled, y_full, lookback)

final_model = build_lstm_model_classification(
    n_units, dropout_rate, learning_rate,
    lookback=lookback,
    n_features=len(feature_cols),
    n_classes=3
)

final_model.fit(
    X_all_seq, y_all_seq,
    epochs=epochs,
    batch_size=batch_size,
    verbose=1
)

final_model.save("models/saved_models/best_lstm_momentum_classification.h5")
joblib.dump(scaler_final, "models/saved_models/scaler_momentum_classification.pkl")

print("Saved final momentum classification model + scaler!")
[I 2025-03-03 14:40:04,398] A new study created in memory with name: no-name-cc179a91-a63f-47d4-882c-453c9329a6df
X_full shape: (2900, 4)
Unique y labels: [0 2]
[I 2025-03-03 14:42:20,519] Trial 0 finished with value: -0.5300699300699301 and parameters: {'n_units': 32, 'dropout_rate': 0.1, 'learning_rate': 0.00011602366876197863, 'epochs': 30, 'batch_size': 16}. Best is trial 0 with value: -0.5300699300699301.
[I 2025-03-03 14:43:40,011] Trial 1 finished with value: -0.5016317016317017 and parameters: {'n_units': 96, 'dropout_rate': 0.30000000000000004, 'learning_rate': 0.0033250450966077795, 'epochs': 20, 'batch_size': 64}. Best is trial 0 with value: -0.5300699300699301.
[I 2025-03-03 14:45:39,973] Trial 2 finished with value: -0.48205128205128206 and parameters: {'n_units': 32, 'dropout_rate': 0.1, 'learning_rate': 0.0005971822072690268, 'epochs': 50, 'batch_size': 64}. Best is trial 0 with value: -0.5300699300699301.
[I 2025-03-03 14:46:43,028] Trial 3 finished with value: -0.48531468531468525 and parameters: {'n_units': 32, 'dropout_rate': 0.0, 'learning_rate': 0.002735665026261543, 'epochs': 20, 'batch_size': 48}. Best is trial 0 with value: -0.5300699300699301.
[I 2025-03-03 14:47:34,050] Trial 4 finished with value: -0.509090909090909 and parameters: {'n_units': 96, 'dropout_rate': 0.1, 'learning_rate': 0.00043159819905881804, 'epochs': 10, 'batch_size': 48}. Best is trial 0 with value: -0.5300699300699301.
[I 2025-03-03 14:49:45,200] Trial 5 finished with value: -0.468997668997669 and parameters: {'n_units': 64, 'dropout_rate': 0.5, 'learning_rate': 0.0004901443178213349, 'epochs': 40, 'batch_size': 48}. Best is trial 0 with value: -0.5300699300699301.
[I 2025-03-03 14:51:57,727] Trial 6 finished with value: -0.48391608391608393 and parameters: {'n_units': 32, 'dropout_rate': 0.4, 'learning_rate': 0.0034168043049860155, 'epochs': 50, 'batch_size': 48}. Best is trial 0 with value: -0.5300699300699301.
[I 2025-03-03 14:56:32,870] Trial 7 finished with value: -0.4983682983682984 and parameters: {'n_units': 64, 'dropout_rate': 0.1, 'learning_rate': 0.0014203347077598569, 'epochs': 50, 'batch_size': 16}. Best is trial 0 with value: -0.5300699300699301.
[I 2025-03-03 14:59:48,218] Trial 8 finished with value: -0.5156177156177156 and parameters: {'n_units': 128, 'dropout_rate': 0.0, 'learning_rate': 0.0026601325707652657, 'epochs': 50, 'batch_size': 32}. Best is trial 0 with value: -0.5300699300699301.
[I 2025-03-03 15:01:45,146] Trial 9 finished with value: -0.5114219114219115 and parameters: {'n_units': 128, 'dropout_rate': 0.30000000000000004, 'learning_rate': 0.00010039375777589028, 'epochs': 30, 'batch_size': 48}. Best is trial 0 with value: -0.5300699300699301.
Best Trial => Accuracy: 0.5300699300699301
Params: {'n_units': 32, 'dropout_rate': 0.1, 'learning_rate': 0.00011602366876197863, 'epochs': 30, 'batch_size': 16}
[Data output - unsupported data type map[string]interface {} for mime type application/vnd.plotly.v1+json]
[Data output - unsupported data type map[string]interface {} for mime type application/vnd.plotly.v1+json]
Epoch 1/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 6s 15ms/step - accuracy: 0.3219 - loss: 1.0889
Epoch 2/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 16ms/step - accuracy: 0.5326 - loss: 0.9527
Epoch 3/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 17ms/step - accuracy: 0.5517 - loss: 0.7641
Epoch 4/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 2s 12ms/step - accuracy: 0.5736 - loss: 0.6994
Epoch 5/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 17ms/step - accuracy: 0.5621 - loss: 0.6922
Epoch 6/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 18ms/step - accuracy: 0.5588 - loss: 0.6865
Epoch 7/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 16ms/step - accuracy: 0.5771 - loss: 0.6800
Epoch 8/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 1s 6ms/step - accuracy: 0.5904 - loss: 0.6815
Epoch 9/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 1s 6ms/step - accuracy: 0.5716 - loss: 0.6837
Epoch 10/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 2s 14ms/step - accuracy: 0.5797 - loss: 0.6742
Epoch 11/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 17ms/step - accuracy: 0.5989 - loss: 0.6712
Epoch 12/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 15ms/step - accuracy: 0.5714 - loss: 0.6758
Epoch 13/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 2s 9ms/step - accuracy: 0.5814 - loss: 0.6762
Epoch 14/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 15ms/step - accuracy: 0.5677 - loss: 0.6767
Epoch 15/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 16ms/step - accuracy: 0.5865 - loss: 0.6739
Epoch 16/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 15ms/step - accuracy: 0.6010 - loss: 0.6671
Epoch 17/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 16ms/step - accuracy: 0.5886 - loss: 0.6709
Epoch 18/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 14ms/step - accuracy: 0.5902 - loss: 0.6653
Epoch 19/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 17ms/step - accuracy: 0.5861 - loss: 0.6696
Epoch 20/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 17ms/step - accuracy: 0.6019 - loss: 0.6635
Epoch 21/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 13ms/step - accuracy: 0.6062 - loss: 0.6616
Epoch 22/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 2s 8ms/step - accuracy: 0.6127 - loss: 0.6602
Epoch 23/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 16ms/step - accuracy: 0.5901 - loss: 0.6640
Epoch 24/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 1s 7ms/step - accuracy: 0.6150 - loss: 0.6557
Epoch 25/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 2s 10ms/step - accuracy: 0.6079 - loss: 0.6584
Epoch 26/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 2s 13ms/step - accuracy: 0.5999 - loss: 0.6534
Epoch 27/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 17ms/step - accuracy: 0.6013 - loss: 0.6555
Epoch 28/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 13ms/step - accuracy: 0.6146 - loss: 0.6522
Epoch 29/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 17ms/step - accuracy: 0.6169 - loss: 0.6478
Epoch 30/30
181/181 ━━━━━━━━━━━━━━━━━━━━ 3s 16ms/step - accuracy: 0.6089 - loss: 0.6477
WARNING:absl:You are saving your model as an HDF5 file via `model.save()` or `keras.saving.save_model(model)`. This file format is considered legacy. We recommend using instead the native Keras format, e.g. `model.save('my_model.keras')` or `keras.saving.save_model(model, 'my_model.keras')`. 
Saved final momentum classification model + scaler!
In [1]:
import sys
import os
import warnings
from pathlib import Path

# ---------------------------------------------------------------------------
# 1) SET PROJECT ROOT AND UPDATE PATH/WORKING DIRECTORY
# ---------------------------------------------------------------------------
project_root = Path.cwd().parent.parent
sys.path.append(str(project_root))
os.chdir(str(project_root))
warnings.filterwarnings("ignore")
import joblib
import numpy as np
import pandas as pd
import MetaTrader5 as mt5
import vectorbt as vbt
import tensorflow as tf
from tensorflow.keras.models import load_model
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
from tensorflow.keras.optimizers import Adam

# Our modules
from data.data_loader import get_data_mt5
from features.feature_engineering import add_momentum_indicators
from features.labeling_schemes import create_labels_momentum_classification

###########################################################
# 1) DATA LOADING & FEATURE ENGINEERING
###########################################################
if not mt5.initialize():
    print("Failed to initialize MT5")
else:
    # Fetch 2000 most recent bars for backtesting
    data = get_data_mt5(symbol="BTCUSD", timeframe=mt5.TIMEFRAME_H4, n_bars=2000, start_pos=0)
    mt5.shutdown()

# 1) Add momentum indicators (e.g., short/long momentum, MA, Hurst)
df = add_momentum_indicators(data)

# 2) Create multi-class momentum labels (0=Sell, 1=Hold, 2=Buy)
df = create_labels_momentum_classification(df, horizon=10, threshold=0.0)
df.dropna(inplace=True)
df.sort_index(inplace=True)  # Ensure chronological order

# Choose the features used in training
feature_cols = ["mom_10", "mom_30", "ma_90", "hurst"]
X = df[feature_cols].values
y = df["momentum_label"].values  # 0 => Sell, 1 => Hold, 2 => Buy

print(f"Full Dataset Size: {len(X)} bars")
print("Features used:", feature_cols)
print("Unique momentum classes:", np.unique(y))

###########################################################
# 2) LOAD BEST LSTM MOMENTUM MODEL & SCALER
###########################################################
model_path = "models/saved_models/best_lstm_momentum_classification.h5"
best_model = load_model(model_path)
# For classification, we compile with 'sparse_categorical_crossentropy'
best_model.compile(
    optimizer=Adam(learning_rate=0.001),
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"]
)

# Load the saved scaler from training
scaler_path = "models/saved_models/scaler_momentum_classification.pkl"
if os.path.exists(scaler_path):
    scaler = joblib.load(scaler_path)
    print(f"Loaded scaler from {scaler_path}")
else:
    print("No saved scaler found. Fitting a new one on the entire dataset (not recommended for live usage).")
    scaler = StandardScaler()
    scaler.fit(X)

# Scale the entire dataset
X_scaled = scaler.transform(X)

# We'll use the same lookback as in training
lookback = 10
fees = 0.0002  # e.g., 0.02% transaction cost

###########################################################
# 3) CREATE SEQUENCES FOR CLASSIFICATION
###########################################################
def create_sequences_classification(X_data, y_data, lookback):
    X_seq, y_seq = [], []
    for i in range(len(X_data) - lookback):
        X_seq.append(X_data[i : i + lookback])
        y_seq.append(y_data[i + lookback])
    return np.array(X_seq), np.array(y_seq)

X_seq, y_seq = create_sequences_classification(X_scaled, y, lookback)
print(f"Total Sequences Created: {len(X_seq)}")

###########################################################
# 4) PREDICT CLASSES & CONVERT TO TRADING SIGNALS
###########################################################
print("\nPredicting momentum classes on the entire dataset (No retraining)...")
pred_probs = best_model.predict(X_seq)
pred_classes = np.argmax(pred_probs, axis=1)  # 0 => Sell, 1 => Hold, 2 => Buy

# (Optional) Evaluate final accuracy
acc = accuracy_score(y_seq, pred_classes)
print(f"Classification Accuracy (entire dataset after lookback): {acc:.4f}")

# Convert classes to signals:
# 0 => Sell => -1
# 1 => Hold => 0
# 2 => Buy  => +1
class_to_signal = {0: -1, 1: 0, 2: 1}
signals = np.array([class_to_signal[c] for c in pred_classes])

# Align signals with dataset index (after lookback)
df_test = df.iloc[lookback:].copy()
close_prices = df_test["close"]

if len(signals) < len(close_prices):
    pad_len = len(close_prices) - len(signals)
    signals = np.append(signals, [0]*pad_len)

signals_s = pd.Series(signals, index=close_prices.index)

###########################################################
# 5) RUN FULL BACKTEST USING VECTORBT
###########################################################
print("\nRunning Full Backtest with Momentum Classification LSTM...")

pf = vbt.Portfolio.from_signals(
    close_prices,
    entries=signals_s > 0,  # class 2 => buy
    exits=signals_s < 0,    # class 0 => sell
    init_cash=10000,
    freq='4H',
    fees=fees
)

total_return = pf.total_return()
sharpe_ratio = pf.sharpe_ratio()

# Print final results
print("\nFull Backtest Results:")
print(f"Accuracy={acc:.2f}, Return={total_return:.2f}%, Sharpe={sharpe_ratio:.2f}")
print(pf.stats())

# Plot the backtest results
fig = pf.plot()
fig.show()
WARNING:absl:Compiled the loaded model, but the compiled metrics have yet to be built. `model.compile_metrics` will be empty until you train or evaluate the model.
Full Dataset Size: 1900 bars
Features used: ['mom_10', 'mom_30', 'ma_90', 'hurst']
Unique momentum classes: [0 2]
Loaded scaler from models/saved_models/scaler_momentum_classification.pkl
Total Sequences Created: 1890

Predicting momentum classes on the entire dataset (No retraining)...
60/60 ━━━━━━━━━━━━━━━━━━━━ 1s 7ms/step
Classification Accuracy (entire dataset after lookback): 0.4937

Running Full Backtest with Momentum Classification LSTM...

Full Backtest Results:
Accuracy=0.49, Return=0.21%, Sharpe=0.78
Start                               2024-04-21 04:00:00
End                                 2025-03-02 00:00:00
Period                                315 days 00:00:00
Start Value                                     10000.0
End Value                                   12138.43894
Total Return [%]                              21.384389
Benchmark Return [%]                          32.581465
Max Gross Exposure [%]                            100.0
Total Fees Paid                              220.932334
Max Drawdown [%]                              25.183279
Max Drawdown Duration                 114 days 08:00:00
Total Trades                                         52
Total Closed Trades                                  51
Total Open Trades                                     1
Open Trade PnL                              -327.382351
Win Rate [%]                                  49.019608
Best Trade [%]                                33.733774
Worst Trade [%]                              -11.226602
Avg Winning Trade [%]                          3.586734
Avg Losing Trade [%]                          -2.317658
Avg Winning Trade Duration              3 days 00:48:00
Avg Losing Trade Duration     2 days 07:41:32.307692307
Profit Factor                                  1.382234
Expectancy                                    48.349437
Sharpe Ratio                                   0.783246
Calmar Ratio                                   0.999722
Omega Ratio                                    1.079804
Sortino Ratio                                  1.127532
dtype: object
[Data output - unsupported data type map[string]interface {} for mime type application/vnd.plotly.v1+json]