English Русский 日本語
preview
Trendvorhersage mit LSTM für Trendfolgestrategien

Trendvorhersage mit LSTM für Trendfolgestrategien

MetaTrader 5Handelssysteme |
121 2
Zhuo Kai Chen
Zhuo Kai Chen

Einführung

Long Short-Term Memory (LSTM) ist eine Art rekurrentes neuronales Netz (RNN), das für die Modellierung sequenzieller Daten entwickelt wurde, indem es langfristige Abhängigkeiten effektiv erfasst und das Problem des verschwindenden Gradienten löst. In diesem Artikel werden wir untersuchen, wie LSTM zur Vorhersage zukünftiger Trends eingesetzt werden kann, um die Leistung von Trendfolgestrategien zu verbessern. Der Artikel behandelt die Einführung von Schlüsselkonzepten und die Motivation hinter der Entwicklung, das Abrufen von Daten aus dem MetaTrader 5, die Verwendung dieser Daten zum Trainieren des Modells in Python, die Integration des maschinellen Lernmodells in MQL5 und die Reflexion der Ergebnisse und zukünftigen Bestrebungen auf der Grundlage von statistischem Backtesting.


Motivation

Intuitiv betrachtet, profitieren Trendfolgestrategien von Gewinnen in Märkten, die sich im Aufwärtstrend befinden, schneiden aber in unruhigen Märkten schlecht ab, in denen die Strategie mit einem Aufschlag gekauft und mit einem Abschlag verkauft wird. Wissenschaftliche Untersuchungen haben gezeigt, dass klassische Trendfolgestrategien, wie das Goldene Kreuz, über lange Zeiträume hinweg auf verschiedenen Märkten und Zeitrahmen funktionieren. Diese Strategien sind zwar nicht hochprofitabel, haben aber beständige Gewinne gezeigt. Trendfolgestrategien profitieren in der Regel von extremen Ausreißern, die deutlich höhere Gewinne als die durchschnittlichen Verluste erzielen. Der enge Stop-Loss und der Ansatz, Gewinne laufen zu lassen, führen zu einer niedrigen Gewinnrate, aber zu einem hohen Chance-Risiko-Verhältnis pro Handel.

LSTM (Long Short-Term Memory) ist ein spezieller Typ eines rekurrenten neuronalen Netzes (RNN), das für die Erfassung langfristiger Abhängigkeiten in sequentiellen Daten entwickelt wurde. Es nutzt Speicherzellen, die Informationen über lange Zeiträume hinweg aufrechterhalten können, und überwindet so das Problem des verschwindenden Gradienten, von dem herkömmliche RNNs betroffen sind. Diese Fähigkeit, Informationen aus einer früheren Sequenz zu speichern und darauf zuzugreifen, macht LSTM besonders effektiv für Aufgaben wie Zeitreihenprognosen und Trendvorhersagen. Bei Regressionsproblemen kann das LSTM die zeitlichen Beziehungen zwischen den Eingabemerkmalen modellieren und kontinuierliche Ausgaben mit hoher Genauigkeit vorhersagen, was es ideal für Vorhersageanwendungen macht.

Die Motivation für diesen Artikel ist es, die Leistungsfähigkeit von LSTM für die Trendregression zu nutzen, um zukünftige Handelsgeschäfte vorherzusagen und potenziell schlechte Trades herauszufiltern, die aus einer geringen Trendstärke resultieren. Diese Motivation beruht auf der Hypothese, dass Trendfolgestrategien auf trendigen Märkten besser abschneiden als auf Märkten mit einem abweichenden Trend.

Wir werden den ADX (Average Directional Index) verwenden, um die Trendstärke anzuzeigen, da er einer der beliebtesten Indikatoren für die Beurteilung der aktuellen Trendstärke ist. Wir versuchen, seinen zukünftigen Wert vorherzusagen, anstatt seinen aktuellen Wert zu verwenden, da ein hoher ADX in der Regel darauf hinweist, dass ein Trend bereits eingetreten ist oder zu Ende geht, sodass unser Einstiegspunkt zu spät ist, um davon zu profitieren.

Der ADX wird wie folgt berechnet:

ADX-Gleichung


Datenaufbereitung und Vorverarbeitung

Bevor wir Daten abrufen, müssen wir zunächst klären, welche Daten benötigt werden. Wir planen, mehrere Merkmale zu verwenden, um ein Regressionsmodell zu trainieren, das zukünftige ADX-Werte vorhersagt. Zu diesen Merkmalen gehören der RSI, der die aktuelle relative Stärke des Marktes anzeigt, der prozentuale Ertrag der letzten Kerze, der als stationärer Wert des Schlusskurses dient, und der ADX selbst, der für den Wert, den wir vorhersagen wollen, direkt relevant ist. Wir haben soeben die Intuition erklärt, die hinter der Auswahl dieser Merkmale steht. Sie können die Merkmale selbst bestimmen, aber achten Sie darauf, dass sie angemessen und stationär sind. Wir planen, das Modell mit stündlichen Daten von 2020.1.1 bis 2024.1.1 zu trainieren und die Leistung des Modells von 2024.1.1-2025.1.1 als Out-of-Sample-Test zu testen.

Nachdem wir nun geklärt haben, welche Daten wir abrufen wollen, können wir einen Expertenberater zum Abrufen dieser Daten erstellen.

Wir werden die in diesem Artikel vorgestellte Klasse CFileCSV verwenden, um das Array als String in einer CSV-Datei zu speichern. Der Code für diesen Vorgang ist recht einfach, wie unten gezeigt.

#include <FileCSV.mqh>
CFileCSV csvFile;

int barsTotal = 0;
int handleRsi;
int handleAdx;
string headers[] = {
    "time",
    "ADX",
    "RSI",
    "Stationary"
};
string data[1000000][4];
int indexx = 0;
vector xx;

input string fileName = "XAU-1h-2020-2024.csv";
input bool SaveData = true;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {//Initialize model
   handleRsi = iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE);
   handleAdx = iADX(_Symbol,PERIOD_CURRENT,14);
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if (!SaveData) return;
   if(csvFile.Open(fileName, FILE_WRITE|FILE_ANSI))
     {
      //Write the header
      csvFile.WriteHeader(headers);
      //Write data rows
      csvFile.WriteLine(data);
      //Close the file
      csvFile.Close();
     }
   else
     {
      Print("File opening error!");
     }

  }
  
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);
  
  if (barsTotal!= bars){
     barsTotal = bars;
     double rsi[];
     double adx[];
     CopyBuffer(handleAdx,0,1,1,adx);
     CopyBuffer(handleRsi,0,1,1,rsi);
     data[indexx][0] =(string)TimeTradeServer();
   data[indexx][1] = DoubleToString(adx[0], 2); 
   data[indexx][2] = DoubleToString(rsi[0], 2); 
   data[indexx][3] = DoubleToString((iClose(_Symbol,PERIOD_CURRENT,1)-iOpen(_Symbol,PERIOD_CURRENT,1))/iClose(_Symbol,PERIOD_CURRENT,1),3);
   indexx++;
   }
 }

Dieser Expert Advisor (EA) ist so konzipiert, dass er die Werte des Relative Strength Index (RSI) und des Average Directional Index (ADX) für ein bestimmtes Symbol verfolgt und aufzeichnet. Der EA verwendet die Funktionen iRSI und iADX, um die aktuellen Werte von RSI und ADX zu ermitteln und sie zusammen mit einem Zeitstempel in einer CSV-Datei zu speichern. Die CSV-Datei wird mit Überschriften für „time“, „ADX“, „RSI“ und „Stationary“ erstellt. Wenn die Option SaveData aktiviert ist, werden die Daten bei der Deinitialisierung in eine Datei (angegeben in fileName) geschrieben. Es erfasst neue Daten bei jedem Tick und speichert sie, wenn sich die Anzahl der Balken ändert.

Führen Sie den Expert Advisor (EA) im Strategy Tester mit einem einzigen Test aus. Nach der Durchführung des Tests sollte die Datei in folgendem Dateipfad gespeichert werden: /Tester/Agent-sth000/MQL5/Files.

Als Nächstes wechseln wir zu Python für die Datenvorverarbeitung als Vorbereitung für das Training unseres maschinellen Lernmodells.

Wir planen, einen überwachten Lernansatz zu verwenden, bei dem das Modell so trainiert wird, dass es ein gewünschtes Ergebnis auf der Grundlage markierter Daten vorhersagt. Der Trainingsprozess beinhaltet die Anpassung der Gewichte verschiedener Operationen, die auf die Merkmale angewendet werden, um den Fehlerverlust zu minimieren und die endgültige Ausgabe zu erzeugen. 

Für die Bezeichnung schlage ich vor, den mittleren ADX-Wert der nächsten 10 ADX-Werte zu verwenden. Dieser Ansatz stellt sicher, dass der Trend nicht bereits vollständig etabliert ist, wenn wir in den Handel einsteigen, und verhindert gleichzeitig, dass der Trend zu weit vom aktuellen Signal entfernt ist. Die Verwendung des Mittelwerts der nächsten 10 ADX-Werte ist eine gute Möglichkeit, um sicherzustellen, dass der Trend in den nächsten Balken aktiv bleibt, sodass unser Einstieg die Gewinne aus den bevorstehenden Richtungswechseln mitnehmen kann.

import pandas as pd
data = pd.read_csv('XAU-1h-2020-2024.csv', sep=';')
data= data.dropna().set_index('time')
data['output'] = data['ADX'].shift(-10)
data = data[:-10]
data['output']= data['output'].rolling(window=10).mean()
data = data[9:]

Dieser Code liest die CSV-Datei und trennt die Daten in verschiedene Spalten, da die Werte durch ein Semikolon (;) gruppiert sind. Anschließend werden alle leeren Zeilen entfernt und die Spalte „time“ als Index festgelegt, um sicherzustellen, dass die Daten für das Training chronologisch geordnet sind. Anschließend wird eine neue Spalte mit der Bezeichnung „output“ erstellt, die den Mittelwert der nächsten 10 ADX-Werte berechnet. Danach lässt der Code alle verbleibenden leeren Zeilen fallen, da einige Zeilen möglicherweise nicht genügend zukünftige ADX-Werte aufweisen, um die Ausgabe zu berechnen.


Modelltraining

LSTM-Diagramm

Dies Diagramm veranschaulicht, was das LSTM während unseres Trainingsprozesses zu erreichen versucht. Die Eingabe soll die Form (sample_amount, time_steps, feature_amount) haben, wobei time_step angibt, wie viele frühere Zeitpunkte der Daten wir zur Vorhersage des nächsten Wertes verwenden wollen. Wir könnten zum Beispiel Daten von Montag bis Donnerstag verwenden, um ein Ergebnis für Freitag vorherzusagen. LSTM nutzt Algorithmen, um Muster in den Zeitreihen und die Beziehung zwischen den Merkmalen und dem Ergebnis zu erkennen. Es erstellt eine oder mehrere Schichten neuronaler Netze, die jeweils aus vielen Gewichtungseinheiten (Neuronen) bestehen, die auf jedes Merkmal und jeden Zeitschritt Gewichte anwenden, um schließlich die endgültige Vorhersage auszugeben.

Der Einfachheit halber können Sie einfach den folgenden Code ausführen, der den Trainingsprozess für Sie übernimmt.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense

# Assume data is your DataFrame already loaded with the specified columns and a time-based index
# data.columns should include ['ADX', 'RSI', 'Stationary', 'output']

# --- Step 1: Data Preparation ---
time_step = 5

# Select features and target
features = ['ADX', 'RSI', 'Stationary']
target = 'output'

# --- Step 2: Create sequences for LSTM input ---
def create_sequences(data, target_col, time_step):
    """
    Create sequences of length time_step from the DataFrame.
    data: DataFrame of input features and target.
    target_col: Name of the target column.
    Returns: X, y arrays suitable for LSTM.
    """
    X, y = [], []
    feature_cols = data.columns.drop(target_col)
    for i in range(len(data) - time_step):
        seq_x = data.iloc[i:i+time_step][feature_cols].values
        # predict target at the next time step after the sequence
        seq_y = data.iloc[i+time_step][target_col]
        X.append(seq_x)
        y.append(seq_y)
    return np.array(X), np.array(y)

# Create sequences
X, y = create_sequences(data, target_col=target, time_step=time_step)

# --- Step 3: Split into training and evaluation sets ---
# Use a simple 80/20 split for training and evaluation
X_train, X_eval, y_train, y_eval = train_test_split(X, y, test_size=0.2, shuffle=False)

# --- Step 4: Build the LSTM model ---
n_features = len(features)  # number of features per time step

model = Sequential()
model.add(LSTM(50, input_shape=(time_step, n_features)))  # LSTM layer with 50 units
model.add(Dense(1))  # output layer for regression

model.compile(optimizer='adam', loss='mse')

model.summary()

# --- Step 5: Train the model ---
epochs = 50
batch_size = 100

history = model.fit(
    X_train, y_train,
    epochs=epochs,
    batch_size=batch_size,
    validation_data=(X_eval, y_eval)
)

Hier sind einige wichtige Überlegungen für die Ausbildung mit dem oben genannten Code:

  1. Vorverarbeitung der Daten: Stellen Sie sicher, dass Ihre Daten in der Vorverarbeitungsphase in zeitlicher Reihenfolge vorliegen. Andernfalls kann es zu einer Verzerrung der Vorausschau kommen, wenn die Daten in Zeitschritte aufgeteilt werden.

  2. Datenaufteilung in Training und Test: Wenn wir die Daten in Trainings- und Testsätze aufteilen, dürfen Sie die Daten nicht mischen. Die zeitliche Reihenfolge muss beibehalten werden, um eine Verzerrung durch die Vorausschau zu vermeiden.

  3. Komplexität der Modelle: Für die Zeitreihenanalyse, insbesondere bei begrenzten Datenpunkten, ist es nicht notwendig, zu viele Schichten, Neuronen oder Epochen zu konstruieren. Eine Überkomplizierung des Modells könnte zu einer Überanpassung oder zu stark verzerrten Parametern führen. Die im Beispiel verwendeten Einstellungen sollten ausreichend sein. 

Anschließend können wir die Genauigkeit des Modells anhand des Evaluierungssatzes bewerten, um seine Leistung bei ungesehenen Daten zu beurteilen.

    # --- Step 6: Evaluate the model ---
    eval_loss = model.evaluate(X_eval, y_eval)
    print(f"Evaluation Loss: {eval_loss}")
    # --- Step 7: Generate Predictions and Plot ---
    
    # Generate predictions on the evaluation set
    predictions = model.predict(X_eval).flatten()
    
    # Create a plot for predictions vs actual values
    plt.figure(figsize=(12, 6))
    plt.plot(predictions, label='Predicted Output', color='red')
    plt.plot(y_eval, label='Actual Output', color='blue')
    plt.title('LSTM Predictions vs Actual Output')
    plt.xlabel('Sample Index')
    plt.ylabel('Output Value')
    plt.legend()
    plt.show()

    Dieser Code sollte den mittleren quadratischen Fehler der Auswertungsmenge im Vergleich zur Vorhersage des Modells wie folgt ausgeben.

    Auswertung Visualisierung

    Evaluation Loss: 57.405677795410156

    Sie wird berechnet durch:

    Mittlerer quadratischer Fehler

    Dabei ist n der Stichprobenumfang, yi der vorausgesagte Wert für jede Stichprobe und y^i der tatsächliche Wert für jedes Bewertungsergebnis.

    Wie Sie aus der Berechnung ersehen können, können Sie den Verlust des Modells mit dem Quadrat der Mittelwerte der Dinge, die Sie vorhersagen, vergleichen, um zu prüfen, ob der relative Verlust übermäßig hoch ist. Vergewissern Sie sich auch, dass der Verlust ähnlich wie bei den Trainingsdatensätzen ist, was darauf hindeutet, dass das Modell nicht zu stark an die Trainingsdaten angepasst ist.

    Um das Modell schließlich mit MQL5 kompatibel zu machen, wollen wir es im ONNX-Format speichern. Da LSTM-Modelle ONNX-Übergänge nicht direkt unterstützen, müssen wir das Modell zunächst als funktionales Modell speichern und dabei das Format seiner Ein- und Ausgabe explizit festlegen. Danach können wir sie als ONNX-Datei speichern, sodass sie für die zukünftige Verwendung mit MQL5 geeignet ist.

    import tensorflow as tf
    import tf2onnx
    
    # Define the input shape based on your LSTM requirements: (time_step, n_features)
    time_step = 5
    n_features = 3
    
    # Create a new Keras Input layer matching the shape of your data
    inputs = tf.keras.Input(shape=(time_step, n_features), name="input")
    
    # Pass the input through your existing sequential model
    outputs = model(inputs)  
    functional_model = tf.keras.Model(inputs=inputs, outputs=outputs)
    
    # Create an input signature that matches the defined input shape
    input_signature = (
        tf.TensorSpec((None, time_step, n_features), dtype=tf.float32, name="input"),
    )
    
    output_path = "regression2024.onnx"
    
    # Convert the functional model to ONNX format
    onnx_model, _ = tf2onnx.convert.from_keras(
        functional_model,
        input_signature=input_signature,  # matching the input signature
        opset=15,                         
        output_path=output_path
    )
    
    print(f"Model successfully converted to ONNX at {output_path}")

    Beachten Sie, dass „None“ als Eingabeformat hier bedeutet, dass das Modell eine beliebige Anzahl von Stichproben akzeptieren kann. Es gibt automatisch die entsprechenden Vorhersagen für jede Probe aus und ist damit flexibel für unterschiedliche Chargengrößen.


    Aufbau eines Expert Advisors

    Nachdem wir nun die ONNX-Modelldatei gespeichert haben, wollen wir sie zur späteren Verwendung in das Verzeichnis /MQL5/Files kopieren.

    Wir kehren zum MetaEditor zurück. Wir werden auf einer klassischen Trendfolgestrategie aufbauen, die auf der Logik des goldenen Kreuzes basiert. Dies ist dieselbe, die ich in meinem vorherigen Artikel über maschinelles Lernen implementiert habe. Die grundlegende Logik beinhaltet zwei gleitende Durchschnitte: einen schnellen und einen langsamen. Ein Handelssignal wird generiert, wenn sich die beiden MAs kreuzen und die Handelsrichtung dem schnellen gleitenden Durchschnitt folgt, daher der Begriff „trendfolgend“. Das Ausstiegssignal tritt ein, wenn der Kurs den langsamen gleitenden Durchschnitt überschreitet, was mehr Spielraum für Trailing-Stops bietet. Der vollständige Code lautet wie folgt:

    #include <Trade/Trade.mqh>
    //XAU - 1h.
    CTrade trade;
    
    input int MaPeriodsFast = 15;
    input int MaPeriodsSlow = 25;
    input int MaPeriods = 200;
    input double lott = 0.01;
    
    ulong buypos = 0, sellpos = 0;
    input int Magic = 0;
    int barsTotal = 0;
    int handleMaFast;
    int handleMaSlow;
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {
       trade.SetExpertMagicNumber(Magic);
       handleMaFast =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsFast,0,MODE_SMA,PRICE_CLOSE);
       handleMaSlow =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsSlow,0,MODE_SMA,PRICE_CLOSE);  
       return(INIT_SUCCEEDED);
      }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
      {
    
      }
      
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
      {
      int bars = iBars(_Symbol,PERIOD_CURRENT);
      //Beware, the last element of the buffer list is the most recent data, not [0]
      if (barsTotal!= bars){
         barsTotal = bars;
         double maFast[];
         double maSlow[];
         CopyBuffer(handleMaFast,BASE_LINE,1,2,maFast);
         CopyBuffer(handleMaSlow,BASE_LINE,1,2,maSlow);
         double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         double lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
         //The order below matters
         if(buypos>0&& lastClose<maSlow[1]) trade.PositionClose(buypos);
         if(sellpos>0 &&lastClose>maSlow[1])trade.PositionClose(sellpos);   
         if (maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&buypos ==sellpos)executeBuy(); 
         if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos ==buypos) executeSell();
         if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
          buypos = 0;
          }
         if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
          sellpos = 0;
          }
        }
     }
    
    //+------------------------------------------------------------------+
    //| Expert trade transaction handling function                       |
    //+------------------------------------------------------------------+
    void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) {
        if (trans.type == TRADE_TRANSACTION_ORDER_ADD) {
            COrderInfo order;
            if (order.Select(trans.order)) {
                if (order.Magic() == Magic) {
                    if (order.OrderType() == ORDER_TYPE_BUY) {
                        buypos = order.Ticket();
                    } else if (order.OrderType() == ORDER_TYPE_SELL) {
                        sellpos = order.Ticket();
                    }
                }
            }
        }
    }
    
    //+------------------------------------------------------------------+
    //| Execute sell trade function                                      |
    //+------------------------------------------------------------------+
    void executeSell() {      
           double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
           bid = NormalizeDouble(bid,_Digits);
           trade.Sell(lott,_Symbol,bid);  
           sellpos = trade.ResultOrder();  
           }    
    
    //+------------------------------------------------------------------+
    //| Execute buy trade function                                       |
    //+------------------------------------------------------------------+
    void executeBuy() {
           double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
           ask = NormalizeDouble(ask,_Digits);
           trade.Buy(lott,_Symbol,ask);
           buypos = trade.ResultOrder();
    }

    Ich werde nicht weiter auf die Validierung und die Vorschläge zur Auswahl Ihrer Backtest-Strategie eingehen. Weitere Einzelheiten finden Sie in meinem früheren Artikel über maschinelles Lernen, der hier verlinkt ist.

    Nun werden wir versuchen, unser LSTM-Modell auf der Grundlage dieses Rahmens zu betreiben.

    Zunächst deklarieren wir die globalen Variablen, die die Form unserer Ein- und Ausgabe spezifizieren, sowie zwei Multi-Arrays zum Speichern der Ein- und Ausgabedaten. Zusätzlich deklarieren wir ein Modell-Handle, das den Prozess des Abrufs von Daten in das Modell und die Extraktion von Vorhersagen aus diesem verwaltet. Dadurch werden ein ordnungsgemäßer Datenfluss und die Interaktion zwischen dem Modell und den Eingabe-/Ausgabevariablen gewährleistet.

    #resource "\\Files\\regression2024.onnx" as uchar lstm_onnx[]
    
    float data[1][5][3];
    float out[1][1];
    long lstmHandle = INVALID_HANDLE;
    const long input_shape[] = {1,5,3};
    const long output_shape[]={1,1};

    Als Nächstes werden in der Funktion OnInit() die relevanten Indikatoren, wie RSI und ADX, sowie das ONNX-Modell initialisiert. Während dieser Initialisierung wird überprüft, ob die in MQL5 deklarierte Eingabe- und Ausgabeform mit den zuvor im Python-Funktionsmodell angegebenen übereinstimmt. Dieser Schritt gewährleistet die Konsistenz und verhindert Fehler bei der Modellinitialisierung, sodass das Modell die Daten im erwarteten Format korrekt verarbeiten kann.

    int handleMaFast;
    int handleMaSlow;
    int handleAdx;     // Average Directional Movement Index - 3
    int handleRsi;
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
      {//Initialize model
       trade.SetExpertMagicNumber(Magic);
       handleMaFast =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsFast,0,MODE_SMA,PRICE_CLOSE);
       handleMaSlow =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsSlow,0,MODE_SMA,PRICE_CLOSE);   
       handleAdx=iADX(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index - 3
       handleRsi = iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE);
        // Load the ONNX model
       lstmHandle = OnnxCreateFromBuffer(lstm_onnx, ONNX_DEFAULT);
       //--- specify the shape of the input data
       if(!OnnxSetInputShape(lstmHandle,0,input_shape))
         {
          Print("OnnxSetInputShape failed, error ",GetLastError());
          OnnxRelease(lstmHandle);
          return(-1);
         }
    //--- specify the shape of the output data
       if(!OnnxSetOutputShape(lstmHandle,0,output_shape))
         {
          Print("OnnxSetOutputShape failed, error ",GetLastError());
          OnnxRelease(lstmHandle);
          return(-1);
         }
       if (lstmHandle == INVALID_HANDLE)
       {
          Print("Error creating model OnnxCreateFromBuffer ", GetLastError());
          return(INIT_FAILED);
       }
       return(INIT_SUCCEEDED);
      }

    Als Nächstes deklarieren wir eine Funktion, die die Eingabedaten mit jedem neuen Balken aktualisiert. Diese Funktion führt eine Schleife durch den Zeitschritt (in diesem Fall 5) durch, um die entsprechenden Daten im globalen Multi-Array zu speichern. Er konvertiert die Daten in den Typ Float, um sicherzustellen, dass sie die vom ONNX-Modell erwartete 32-Bit-Anforderung erfüllen. Darüber hinaus stellt die Funktion sicher, dass die Reihenfolge des Multi-Arrays korrekt ist, d. h. ältere Daten zuerst und neuere Daten nacheinander hinzugefügt werden. Dadurch wird sichergestellt, dass die Daten in der richtigen zeitlichen Reihenfolge in das Modell eingespeist werden.

    void getData(){
         double rsi[];
         double adx[];
         CopyBuffer(handleAdx,0,1,5,adx);
         CopyBuffer(handleRsi,0,1,5,rsi);
         for (int i =0; i<5; i++){
         data[0][i][0] = (float)adx[i]; 
         data[0][i][1] = (float)rsi[i]; 
         data[0][i][2] = (float)((iClose(_Symbol,PERIOD_CURRENT,5-i)-iOpen(_Symbol,PERIOD_CURRENT,5-i))/iClose(_Symbol,PERIOD_CURRENT,5-i));
         }
    }

    Schließlich wird in der Funktion OnTick() die Handelslogik implementiert.

    Diese Funktion stellt sicher, dass die nachfolgende Handelslogik nur überprüft wird, wenn sich ein neuer Balken gebildet hat. Dies verhindert unnötige Neuberechnungen oder Handelsaktionen während desselben Taktes und stellt sicher, dass die Vorhersagen des Modells auf vollständigen Daten für jeden neuen Zeitschritt beruhen.

    int bars = iBars(_Symbol,PERIOD_CURRENT);
    if (barsTotal!= bars){
       barsTotal = bars;

    Dieser Code setzt die Variablen buypos und sellpos auf 0 zurück, wenn keine Positionen mit der magischen Zahl des EA mehr vorhanden sind. Die Variablen buypos und sellpos werden verwendet, um sicherzustellen, dass sowohl Kauf- als auch Verkaufspositionen leer sind, bevor ein Einstiegssignal erzeugt wird. Indem wir diese Variablen zurücksetzen, wenn keine Positionen offen sind, stellen wir sicher, dass das System nicht versehentlich versucht, neue Positionen zu eröffnen, wenn bereits eine existiert.

    if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
     buypos = 0;
     }
    if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
     sellpos = 0;
     }

    Wir verwenden diese Codezeile, um das ONNX-Modell auszuführen, das die Eingabedaten aufnimmt und die Vorhersage in das Out-Array ausgibt. Dieser Vorgang wird nur ausgeführt, wenn das erste Einstiegssignal gebildet wird, und nicht bei jedem neuen Balken. Dieser Ansatz hilft, Rechenleistung zu sparen und macht den Backtest effizienter, da wir unnötige Modellevaluierungen in Zeiten vermeiden, in denen kein Einstiegssignal vorhanden ist.

    OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out);

    Die Handelslogik sieht nun so aus, dass wir das Modell ausführen, um den vorhergesagten ADX-Wert zu erhalten, wenn das MA-Cross eintritt und keine aktuelle Position eröffnet ist. Wenn der Wert unter einem bestimmten Schwellenwert liegt, betrachten wir ihn als wenig trendig und vermeiden den Handel, und wenn er höher ist, steigen wir ein. Hier ist die gesamte Funktion OnTick():

    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
      {
      int bars = iBars(_Symbol,PERIOD_CURRENT);
      if (barsTotal!= bars){
         barsTotal = bars;
         double maFast[];
         double maSlow[];
         double adx[];
         CopyBuffer(handleMaFast,BASE_LINE,1,2,maFast);
         CopyBuffer(handleMaSlow,BASE_LINE,1,2,maSlow);
         CopyBuffer(handleAdx,0,1,1,adx);
         double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         double lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
         //The order below matters
         if(buypos>0&& lastClose<maSlow[1]) trade.PositionClose(buypos);
         if(sellpos>0 &&lastClose>maSlow[1])trade.PositionClose(sellpos);   
         if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos == buypos){
            getData();
            OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out);
            if(out[0][0]>threshold)executeSell();}
         if(maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&sellpos == buypos){
            getData();
            OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out);
            if(out[0][0]>threshold)executeBuy();}
         if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
          buypos = 0;
          }
         if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){
          sellpos = 0;
          }
        }
     }


    Statistischer Backtest

    Nachdem wir alles implementiert haben, können wir nun den EA kompilieren und die Ergebnisse im Strategietester testen. Wir werden einen Out-of-Sample-Test für XAUUSD auf dem 1-Stunden-Zeitfenster vom 1. Januar 2024 bis zum 1. Januar 2025 durchführen. Zunächst lassen wir unsere ursprüngliche Backtest-Strategie als Ausgangsbasis laufen. Wir gehen davon aus, dass der EA mit der LSTM-Implementierung in diesem Zeitraum besser abschneidet als das Grundmodell. 

    Backtest Einstellung

    Backtest Parameter

    Backtest Kapitalkurve

    Backtest Ergebnisse

    Führen wir nun den Backtest für den EA mit der LSTM-Implementierung durch und verwenden dabei einen Schwellenwert von 30, da ein ADX von 30 allgemein als Indikator für eine starke Trendstärke gilt.

    LSTM-Einstellungen

    LSTM-Parameter

    LSTM-Kapitalkurve

    LSTM-Ergebnisse

    Der Vergleich der beiden Ergebnisse zeigt, dass die LSTM-Implementierung etwa 70 % der ursprünglichen Handelsgeschäfte herausfiltert und den Gewinnfaktor von 1,48 auf 1,52 verbessert. Sie wies auch eine höhere LR-Korrelation auf als die Grundlinie, was darauf hindeutet, dass sie zu einer stabileren Gesamtleistung beiträgt.

    Bei den Backtests von Modellen des maschinellen Lernens ist es wichtig zu erkennen, dass die internen Parameter des Modells die entscheidenden Faktoren sind, im Gegensatz zu einfacheren Strategien, bei denen die Parameter weniger Einfluss haben. Folglich können unterschiedliche Trainingsdaten zu sehr unterschiedlichen Parameterergebnissen führen. Außerdem ist es nicht ideal, den gesamten historischen Datensatz auf einmal zu trainieren, da dies zu viele Stichproben zur Folge hätte, von denen die meisten nicht mehr aktuell sind. Aus diesem Grund empfehle ich in solchen Fällen die Verwendung der Methode der gleitenden Fenster für die Backtests. Wenn wir während der gesamten Backtest-Historie nur begrenzte Stichproben haben, wie in meinem vorherigen Artikel über das CatBoost-Modell beschrieben, ist ein Backtest mit expandierenden Fenster besser geeignet.

    Hier sind die Bilder zur Demonstration:

    Gleitendes Fenster

    Beim Backtest der gleitende Fenster wird ein festes Fenster historischer Daten verwendet, das sich zeitlich nach vorne bewegt. Wenn neue Datenpunkte hinzukommen, werden die ältesten Datenpunkte gelöscht, sodass eine konstante Größe des Datenfensters beibehalten wird, um die Leistung der Strategie über verschiedene Zeiträume zu testen.

    Expandierende Fenster

    Beim Backtest expandierender Fenster wird mit einem anfänglichen Datenfenster fester Größe begonnen. Sobald jedoch neue Datenpunkte zur Verfügung stehen, wird das Fenster um die neuen Daten erweitert, sodass die Strategie im Laufe der Zeit mit einem immer größeren Datensatz getestet wird.

    Um den gleitenden Backtest durchzuführen, wiederholen wir einfach den in diesem Artikel beschriebenen Prozess und führen die Ergebnisse in einem einzigen Datensatz zusammen. Hier sehen Sie das Ergebnis des Backtests mit einem gleitenden Fenster vom 1. Januar 2015 bis zum 1. Januar 2025:

    Backtest gleitendes Fenster

    Kennzahlen:

    Profit Factor: 1.24
    Maximum Drawdown: -250.56
    Average Win: 12.02
    Average Loss: -5.20
    Win Rate: 34.81%

    Das Ergebnis ist beeindruckend, kann aber noch weiter verbessert werden.


    Reflexion

    Die Leistung des EA steht in direktem Zusammenhang mit der Vorhersagbarkeit des Modells. Um Ihren EA zu verbessern, sind einige Schlüsselfaktoren zu beachten:

    1. Das Herzstück Ihrer Backtest-Strategie: Letztendlich müssen die meisten Ihrer ursprünglichen Signale einen Vorteil haben, um eine weitere Filterung zu rechtfertigen.
    2. Die verwendeten Daten: Marktineffizienzen werden aufgedeckt, indem die Bedeutung der einzelnen Merkmale analysiert und weniger bekannte Merkmale identifiziert werden, die einen Vorteil bieten könnten.
    3. Das Modell, das Sie zum Trainieren verwenden: Überlegen Sie, ob es sich um ein Klassifizierungs- oder Regressionsproblem handelt, das Sie zu lösen versuchen. Und auch die Wahl der richtigen Trainingsparameter ist entscheidend.
    4. Die Dinge, die Sie vorhersagen wollen: Anstatt das Ergebnis eines Handels direkt vorherzusagen, sollten Sie sich auf etwas konzentrieren, das indirekt mit dem Endergebnis zusammenhängt, wie ich in diesem Artikel zeige.

    In meinen früheren Artikeln habe ich mit verschiedenen Techniken des maschinellen Lernens experimentiert, die auch für Privatanleger zugänglich sind. Mein Ziel ist es, die Leser zu inspirieren, diese Ideen zu übernehmen und ihre eigenen innovativen Ansätze zu entwickeln, denn der Kreativität in diesem Bereich sind keine Grenzen gesetzt. Maschinelles Lernen ist nicht von Natur aus komplex oder unerreichbar - es ist eine Denkweise. Es geht darum, die Grenzen zu verstehen, Vorhersagemodelle zu erstellen und Hypothesen rigoros zu testen. Wenn Sie weiter experimentieren, wird dieses Verständnis allmählich klarer werden.



    Schlussfolgerung

    In diesem Artikel haben wir zunächst die Motivation für den Einsatz von LSTM zur Trendvorhersage erläutert und die Konzepte hinter ADX und LSTM erklärt. Als Nächstes haben wir Daten aus dem MetaTrader 5 geholt, sie verarbeitet und das Modell in Python trainiert. Anschließend gingen wir den Prozess der Erstellung des Expert Advisors durch und prüften die Backtest-Ergebnisse. Zum Schluss werden die Konzepte des Backtests mit gleitenden Fenstern und des Backtests mit expandierenden Fenstern vorgestellt, und der Artikel wird mit einigen Überlegungen abgeschlossen.


    Datei-Tabelle

    Datei Name Dateiverwendung
    FileCSV.mqh Die Include-Datei zum Speichern von Daten in CSV
    LSTM_Demonstration.ipynb Die Python-Datei für das Training des LSTM-Modells
    LSTM-TF-XAU.mq5 Der Handels-EA mit LSTM-Implementierung
    OHLC Getter.mq5 Der EA zum Abrufen von Daten


    Übersetzt aus dem Englischen von MetaQuotes Ltd.
    Originalartikel: https://www.mql5.com/en/articles/16940

    Beigefügte Dateien |
    LSTM-Trend.zip (132.27 KB)
    Letzte Kommentare | Zur Diskussion im Händlerforum (2)
    an_tar
    an_tar | 17 Juli 2025 in 15:47
    Ich verstehe das nicht: Wo ist das Modell regression2024.onnx selbst im Zip-Archiv?
    Zhuo Kai Chen
    Zhuo Kai Chen | 17 Juli 2025 in 16:01
    an_tar #:
    Ich verstehe es nicht: wo ist das regression2024.onnx Modell selbst im Zip-Archiv?

    Hallo an_tar.

    Wie im Artikel erwähnt, soll diese Art von System durch einen Rolling-Window-Backtest validiert werden. Ich wollte nicht mein ganzes trainiertes Modell seit 2008 einbeziehen, um die Datei nicht zu schwer zu machen.

    Es ist ratsam, das im Artikel vorgestellte Framework zu verwenden, um Ihr eigenes Modell zu trainieren, damit es mit Ihrer persönlichen Validierungsmethode kompatibel ist.

    MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 54): Verstärkungslernen mit hybriden SAC und Tensoren MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 54): Verstärkungslernen mit hybriden SAC und Tensoren
    Soft Actor Critic ist ein Reinforcement Learning-Algorithmus, den wir bereits in einem früheren Artikel vorgestellt haben, in dem wir auch Python und ONNX als effiziente Ansätze für das Training von Netzwerken vorgestellt haben. Wir überarbeiten den Algorithmus mit dem Ziel, Tensoren, Berechnungsgraphen, die häufig in Python verwendet werden, zu nutzen.
    Erstellen eines Keltner-Kanal-Indikators mit nutzerdefinierten Canvas-Grafiken in MQL5 Erstellen eines Keltner-Kanal-Indikators mit nutzerdefinierten Canvas-Grafiken in MQL5
    In diesem Artikel bauen wir einen Keltner-Kanal-Indikator mit nutzerdefinierten Canvas-Grafiken in MQL5. Wir erläutern die Integration von gleitenden Durchschnitten, ATR-Berechnungen und verbesserter Chartvisualisierung. Wir behandeln auch die Backtests, um die Leistung des Indikators für praktische Handelseinblicke zu bewerten.
    Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 13): RSI-Sentinel-Tool Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 13): RSI-Sentinel-Tool
    Die Kursentwicklung kann durch die Identifizierung von Divergenzen effektiv analysiert werden, wobei technische Indikatoren wie der RSI wichtige Bestätigungssignale liefern. Im folgenden Artikel erläutern wir, wie eine automatisierte RSI-Divergenzanalyse Trendfortsetzungen und -umkehrungen erkennen kann und damit wertvolle Einblicke in die Marktstimmung bietet.
    Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 12): External Flow (III) TrendMap Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 12): External Flow (III) TrendMap
    Das Marktgeschehen wird von den Kräften zwischen Bullen und Bären bestimmt. Es gibt bestimmte Niveaus, die der Markt aufgrund der auf ihn wirkenden Kräfte einhält. Fibonacci- und VWAP-Levels sind besonders wirkungsvoll, um das Marktverhalten zu beeinflussen. Begleiten Sie mich in diesem Artikel bei der Erforschung einer Strategie, die auf VWAP und Fibonacci-Levels zur Signalgenerierung basiert.