Ein Beispiel für die Zusammenstellung von ONNX-Modellen in MQL5
Einführung
Für einen stabilen Handel ist es in der Regel empfehlenswert, sowohl die gehandelten Instrumente als auch die Handelsstrategien zu diversifizieren. Das Gleiche gilt für Modelle des maschinellen Lernens: Es ist leichter, mehrere einfachere Modelle zu erstellen als ein komplexes. Es kann jedoch schwierig sein, diese Modelle zu einem ONNX-Modell zusammenzufügen.
Es ist jedoch möglich, mehrere trainierte ONNX-Modelle in einem MQL5-Programm zu kombinieren. In diesem Artikel betrachten wir eines aus so einem Ensemble, den sogenannten Abstimmungsklassifikator. Wir werden Ihnen zeigen, wie einfach es ist, ein solches Ensemble zu realisieren.
Modelle für das Projekt
Für unser Beispiel verwenden wir zwei einfache Modelle: ein Regressionsmodell zur Preisvorhersage und ein Klassifikationsmodell zur Vorhersage von Preisbewegungen. Der Hauptunterschied zwischen den Modellen besteht darin, dass die Regression das Ausmaß vorhersagt, während die Klassifikation die Klasse vorhersagt.
Das erste Modell ist die Regression.
Es wird mit den Daten des EURUSD D1 von 2003 bis Ende 2022 trainiert. Das Training wird mit einer Serie von 10 OHLC-Preisen durchgeführt. Um die Trainierbarkeit des Modells zu verbessern, normalisieren wir die Preise und teilen den Durchschnittspreis der Reihe durch die Standardabweichung der Reihe. So legen wir eine Reihe in einen bestimmten Bereich mit einem Mittelwert von 0 und einer Streuung von 1, was die Konvergenz beim Training verbessert.
Infolgedessen sollte das Modell den Schlusskurs für den nächsten Tag vorhersagen.
Das Modell ist sehr einfach. Sie wird hier nur zu Demonstrationszwecken bereitgestellt.
# Copyright 2023, MetaQuotes Ltd. # https://www.mql5.com from datetime import datetime import MetaTrader5 as mt5 import tensorflow as tf import numpy as np import pandas as pd import tf2onnx from sklearn.model_selection import train_test_split from tqdm import tqdm from sys import argv if not mt5.initialize(): print("initialize() failed, error code =",mt5.last_error()) quit() # we will save generated onnx-file near the our script data_path=argv[0] last_index=data_path.rfind("\\")+1 data_path=data_path[0:last_index] print("data path to save onnx model",data_path) # input parameters inp_model_name = "model.eurusd.D1.10.onnx" inp_history_size = 10 inp_start_date = datetime(2003, 1, 1, 0) inp_end_date = datetime(2023, 1, 1, 0) # get data from client terminal eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_D1, inp_start_date, inp_end_date) df = pd.DataFrame(eurusd_rates) # # collect dataset subroutine # def collect_dataset(df: pd.DataFrame, history_size: int): """ Collect dataset for the following regression problem: - input: history_size consecutive H1 bars; - output: close price for the next bar. :param df: D1 bars for a range of dates :param history_size: how many bars should be considered for making a prediction :return: features and labels """ n = len(df) xs = [] ys = [] for i in tqdm(range(n - history_size)): w = df.iloc[i: i + history_size + 1] x = w[['open', 'high', 'low', 'close']].iloc[:-1].values y = w.iloc[-1]['close'] xs.append(x) ys.append(y) X = np.array(xs) y = np.array(ys) return X, y ### # get prices X, y = collect_dataset(df, history_size=inp_history_size) # normalize prices m = X.mean(axis=1, keepdims=True) s = X.std(axis=1, keepdims=True) X_norm = (X - m) / s y_norm = (y - m[:, 0, 3]) / s[:, 0, 3] # split data to train and test sets X_train, X_test, y_train, y_test = train_test_split(X_norm, y_norm, test_size=0.2, random_state=0) # define model model = tf.keras.Sequential([ tf.keras.layers.LSTM(64, input_shape=(inp_history_size, 4)), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dropout(0.1), tf.keras.layers.Dense(32, activation='relu'), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dropout(0.1), tf.keras.layers.Dense(32, activation='relu'), tf.keras.layers.Dense(1) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) # model training for 50 epochs lr_reduction = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, min_lr=0.000001) history = model.fit(X_train, y_train, epochs=50, verbose=2, validation_split=0.15, callbacks=[lr_reduction]) # model evaluation test_loss, test_mae = model.evaluate(X_test, y_test) print(f"test_loss={test_loss:.3f}") print(f"test_mae={test_mae:.3f}") # save model to onnx output_path = data_path+inp_model_name onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path) print(f"saved model to {output_path}") # finish mt5.shutdown()Es wird davon ausgegangen, dass unser Regressionsmodell ausgeführt wird und der sich daraus ergebende vorhergesagte Preis in die folgenden Klassen umgewandelt werden sollte: Preis geht nach unten, Preis ändert sich nicht, Preis geht nach oben. Dies ist erforderlich, um den Wahlklassifikator zu organisieren.
Das zweite Modell ist das Klassifizierungsmodell.
Es wird auf EURUSD D1 von 2010 bis Ende 2022 trainiert. Das Training wird mit einer Serie von 63 Schlusskursen durchgeführt. Bei der Ausgabe muss eine von drei Klassen definiert werden: Der Preis wird sinken, der Preis wird innerhalb von 10 Punkten bleiben oder der Preis wird steigen. Wegen der zweiten Klasse mussten wir das Modell mit Daten aus dem Jahr 2010 trainieren. Davor, im Jahr 2009, wechselten die Märkte von vierstelliger zu fünfstelliger Genauigkeit. So wurden aus einem alten Punkt zehn neue Punkte.
Wie im vorherigen Modell ist der Preis normalisiert. Die Normalisierung ist dieselbe: Wir teilen die Abweichung vom Durchschnittspreis in der Reihe durch die Standardabweichung in der Reihe. Die Idee dieses Modells wurde in dem Artikel „Finanzielle Zeitreihenprognose mit MLP in Keras“ (auf Russisch) beschrieben. Auch dieses Modell ist nur für Demonstrationszwecke gedacht.
# Copyright 2023, MetaQuotes Ltd. # https://www.mql5.com # # Classification model # 0,0,1 - predict price down # 0,1,0 - predict price same # 1,0,0 - predict price up # from datetime import datetime import MetaTrader5 as mt5 import tensorflow as tf import numpy as np import pandas as pd import tf2onnx from sklearn.model_selection import train_test_split from tqdm import tqdm from keras.models import Sequential from keras.layers import Dense, Activation,Dropout, BatchNormalization, LeakyReLU from keras.optimizers import SGD from keras import regularizers from sys import argv # initialize MetaTrader 5 client terminal if not mt5.initialize(): print("initialize() failed, error code =",mt5.last_error()) quit() # we will save the generated onnx-file near the our script data_path=argv[0] last_index=data_path.rfind("\\")+1 data_path=data_path[0:last_index] print("data path to save onnx model",data_path) # input parameters inp_model_name = "model.eurusd.D1.63.onnx" inp_history_size = 63 inp_start_date = datetime(2010, 1, 1, 0) inp_end_date = datetime(2023, 1, 1, 0) # get data from the client terminal eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_D1, inp_start_date, inp_end_date) df = pd.DataFrame(eurusd_rates) # # collect dataset subroutine # def collect_dataset(df: pd.DataFrame, history_size: int): """ Collect dataset for the following regression problem: - input: history_size consecutive H1 bars; - output: close price for the next bar. :param df: H1 bars for a range of dates :param history_size: how many bars should be considered for making a prediction :return: features and labels """ n = len(df) xs = [] ys = [] for i in tqdm(range(n - history_size)): w = df.iloc[i: i + history_size + 1] x = w[['close']].iloc[:-1].values delta = x[-1] - w.iloc[-1]['close'] if np.abs(delta)<=0.0001: y = 0, 1, 0 else: if delta<0: y = 1, 0, 0 else: y = 0, 0, 1 xs.append(x) ys.append(y) X = np.array(xs) Y = np.array(ys) return X, Y ### # get prices X, Y = collect_dataset(df, history_size=inp_history_size) # normalize prices m = X.mean(axis=1, keepdims=True) s = X.std(axis=1, keepdims=True) X_norm = (X - m) / s # split data to train and test sets X_train, X_test, Y_train, Y_test = train_test_split(X_norm, Y, test_size=0.1, random_state=0) # define model model = Sequential() model.add(Dense(64, input_dim=inp_history_size, activity_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization()) model.add(LeakyReLU()) model.add(Dropout(0.3)) model.add(Dense(16, activity_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization()) model.add(LeakyReLU()) model.add(Dense(3)) model.add(Activation('softmax')) opt = SGD(learning_rate=0.01, momentum=0.9) model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy']) # model training for 300 epochs lr_reduction = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.9, patience=5, min_lr=0.00001) history = model.fit(X_train, Y_train, epochs=300, validation_data=(X_test, Y_test), shuffle = True, batch_size=128, verbose=2, callbacks=[lr_reduction]) # model evaluation test_loss, test_accuracy = model.evaluate(X_test, Y_test) print(f"test_loss={test_loss:.3f}") print(f"test_accuracy={test_accuracy:.3f}") # save model to onnx output_path = data_path+inp_model_name onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path) print(f"saved model to {output_path}") # finish mt5.shutdown()Die Modelle wurden mit Daten bis Ende 2022 trainiert, sodass noch genügend Zeit blieb, um ihre Funktionsweise im Strategietester zu demonstrieren.
Ein Ensemble von ONNX-Modellen im MQL5 Expert Advisor
Nachfolgend finden Sie einen einfachen Expert Advisor, der die Möglichkeiten von Modell-Ensembles demonstriert. Die Hauptprinzipien der Verwendung von ONNX-Modellen in MQL5 wurden im zweiten Teil des vorherigen Artikels beschrieben.
Forward-Deklarationen und Definitionen
#include <Trade\Trade.mqh> input double InpLots = 1.0; // Lots amount to open position #resource "Python/model.eurusd.D1.10.onnx" as uchar ExtModel1[] #resource "Python/model.eurusd.D1.63.onnx" as uchar ExtModel2[] #define SAMPLE_SIZE1 10 #define SAMPLE_SIZE2 63 long ExtHandle1=INVALID_HANDLE; long ExtHandle2=INVALID_HANDLE; int ExtPredictedClass1=-1; int ExtPredictedClass2=-1; int ExtPredictedClass=-1; datetime ExtNextBar=0; CTrade ExtTrade; //--- price movement prediction #define PRICE_UP 0 #define PRICE_SAME 1 #define PRICE_DOWN 2
OnInit-Funktion
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(_Symbol!="EURUSD" || _Period!=PERIOD_D1) { Print("model must work with EURUSD,D1"); return(INIT_FAILED); } //--- create first model from static buffer ExtHandle1=OnnxCreateFromBuffer(ExtModel1,ONNX_DEFAULT); if(ExtHandle1==INVALID_HANDLE) { Print("First model OnnxCreateFromBuffer error ",GetLastError()); return(INIT_FAILED); } //--- since not all sizes defined in the input tensor we must set them explicitly //--- first index - batch size, second index - series size, third index - number of series (OHLC) const long input_shape1[] = {1,SAMPLE_SIZE1,4}; if(!OnnxSetInputShape(ExtHandle1,0,input_shape1)) { Print("First model OnnxSetInputShape error ",GetLastError()); return(INIT_FAILED); } //--- since not all sizes defined in the output tensor we must set them explicitly //--- first index - batch size, must match the batch size of the input tensor //--- second index - number of predicted prices (we only predict Close) const long output_shape1[] = {1,1}; if(!OnnxSetOutputShape(ExtHandle1,0,output_shape1)) { Print("First model OnnxSetOutputShape error ",GetLastError()); return(INIT_FAILED); } //--- create second model from static buffer ExtHandle2=OnnxCreateFromBuffer(ExtModel2,ONNX_DEFAULT); if(ExtHandle2==INVALID_HANDLE) { Print("Second model OnnxCreateFromBuffer error ",GetLastError()); return(INIT_FAILED); } //--- since not all sizes defined in the input tensor we must set them explicitly //--- first index - batch size, second index - series size const long input_shape2[] = {1,SAMPLE_SIZE2}; if(!OnnxSetInputShape(ExtHandle2,0,input_shape2)) { Print("Second model OnnxSetInputShape error ",GetLastError()); return(INIT_FAILED); } //--- since not all sizes defined in the output tensor we must set them explicitly //--- first index - batch size, must match the batch size of the input tensor //--- second index - number of classes (up, same or down) const long output_shape2[] = {1,3}; if(!OnnxSetOutputShape(ExtHandle2,0,output_shape2)) { Print("Second model OnnxSetOutputShape error ",GetLastError()); return(INIT_FAILED); } //--- ok return(INIT_SUCCEEDED); }
Wir werden es nur mit EURUSD, D1 ausführen. Das liegt daran, dass wir die Daten der aktuellen Symbolperiode verwenden, während die Modelle mit Tagespreisen trainiert werden.
Die Modelle sind im Expert Advisor als Ressourcen enthalten.
Es ist wichtig, die Formen der Eingangs- und Ausgangsdaten explizit zu definieren.
Die Funktion OnTick
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- check new bar if(TimeCurrent()<ExtNextBar) return; //--- set next bar time ExtNextBar=TimeCurrent(); ExtNextBar-=ExtNextBar%PeriodSeconds(); ExtNextBar+=PeriodSeconds(); //--- predict price movement Predict(); //--- check trading according to prediction if(ExtPredictedClass>=0) if(PositionSelect(_Symbol)) CheckForClose(); else CheckForOpen(); }
Alle Handelsoperationen werden nur zu Beginn des Tages durchgeführt.
Funktion der Vorhersage
//+------------------------------------------------------------------+ //| Voting classification | //+------------------------------------------------------------------+ void Predict(void) { //--- evaluate first model ExtPredictedClass1=PredictPrice(ExtHandle1,SAMPLE_SIZE1); //--- evaluate second model ExtPredictedClass2=PredictPriceMovement(ExtHandle2,SAMPLE_SIZE2); //--- vote if(ExtPredictedClass1==ExtPredictedClass2) ExtPredictedClass=ExtPredictedClass1; else ExtPredictedClass=-1; }
Eine Klasse gilt als ausgewählt, wenn beide Modelle für die gleiche Klasse gestimmt haben. Dies ist eine Mehrheitsentscheidung. Und da es nur zwei Modelle im Ensemble gibt, bedeutet die Mehrheitsentscheidung „einstimmig“.
Vorhersage des Tagesschlusskurses aus 10 vorherigen OHLC-Kursen
//+------------------------------------------------------------------+ //| Predict next price (first model) | //+------------------------------------------------------------------+ int PredictPrice(const long handle,const int sample_size) { static matrixf input_data(sample_size,4); // matrix for prepared input data static vectorf output_data(1); // vector to get result static matrix mm(sample_size,4); // matrix of horizontal vectors Mean static matrix ms(sample_size,4); // matrix of horizontal vectors Std static matrix x_norm(sample_size,4); // matrix for prices normalize //--- prepare input data matrix rates; //--- request last bars if(!rates.CopyRates(_Symbol,_Period,COPY_RATES_OHLC,1,sample_size)) return(-1); //--- get series Mean vector m=rates.Mean(1); //--- get series Std vector s=rates.Std(1); //--- prepare matrices for prices normalization for(int i=0; i<sample_size; i++) { mm.Row(m,i); ms.Row(s,i); } //--- the input of the model must be a set of vertical OHLC vectors x_norm=rates.Transpose(); //--- normalize prices x_norm-=mm; x_norm/=ms; //--- run the inference input_data.Assign(x_norm); if(!OnnxRun(handle,ONNX_NO_CONVERSION,input_data,output_data)) return(-1); //--- denormalize the price from the output value double predicted=output_data[0]*s[3]+m[3]; //--- classify predicted price movement int predicted_class=-1; double delta=rates[3][sample_size-1]-predicted; if(fabs(delta)<=0.0001) predicted_class=PRICE_SAME; else { if(delta<0) predicted_class=PRICE_UP; else predicted_class=PRICE_DOWN; } return(predicted_class); }
Die Eingabedaten sollten nach denselben Regeln aufbereitet werden wie beim Training des Modells. Nach der Ausführung des Modells wird der resultierende Wert wieder in den Preis umgerechnet. Die Klasse wird auf der Grundlage der Differenz zwischen dem letzten Schlusskurs in der Serie und dem resultierenden Kurs berechnet.
Die Vorhersage der Kursentwicklung basiert auf einer Reihe von 63 täglichen Schlusskursen:
//+------------------------------------------------------------------+ //| Predict price movement (second model) | //+------------------------------------------------------------------+ int PredictPriceMovement(const long handle,const int sample_size) { static vectorf input_data(sample_size); // vector for prepared input data static vectorf output_data(3); // vector to get result //--- request last bars if(!input_data.CopyRates(_Symbol,_Period,COPY_RATES_CLOSE,1,sample_size)) return(-1); //--- get series Mean float m=input_data.Mean(); //--- get series Std float s=input_data.Std(); //--- normalize prices input_data-=m; input_data/=s; //--- run the inference if(!OnnxRun(handle,ONNX_NO_CONVERSION,input_data,output_data)) return(-1); //--- evaluate prediction return(int(output_data.ArgMax())); }
Die Preise werden nach den gleichen Regeln wie im ersten Modell normalisiert. Dieses Mal ist der Code jedoch kompakter, da die Eingabe ein Vektor und keine Matrix ist. Die Klasse wird durch den Maximalwert der drei Wahrscheinlichkeiten ausgewählt.
Die Handelsstrategie ist einfach. Die Handelsgeschäfte werden zu Beginn eines jeden Tages durchgeführt. Wenn die Vorhersage lautet „der Preis wird steigen“, dann kaufen wir; wenn sie lautet „der Preis wird fallen“, dann verkaufen wir.
//+------------------------------------------------------------------+ //| Check for open position conditions | //+------------------------------------------------------------------+ void CheckForOpen(void) { ENUM_ORDER_TYPE signal=WRONG_VALUE; //--- check signals if(ExtPredictedClass==PRICE_DOWN) signal=ORDER_TYPE_SELL; // sell condition else { if(ExtPredictedClass==PRICE_UP) signal=ORDER_TYPE_BUY; // buy condition } //--- open position if possible according to signal if(signal!=WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) ExtTrade.PositionOpen(_Symbol,signal,InpLots, SymbolInfoDouble(_Symbol,signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK), 0,0); } //+------------------------------------------------------------------+ //| Check for close position conditions | //+------------------------------------------------------------------+ void CheckForClose(void) { bool bsignal=false; //--- position already selected before long type=PositionGetInteger(POSITION_TYPE); //--- check signals if(type==POSITION_TYPE_BUY && ExtPredictedClass==PRICE_DOWN) bsignal=true; if(type==POSITION_TYPE_SELL && ExtPredictedClass==PRICE_UP) bsignal=true; //--- close position if possible if(bsignal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { ExtTrade.PositionClose(_Symbol,3); //--- open opposite CheckForOpen(); } }
Wir haben unser Modell mit den Daten bis Anfang 2023 trainiert. Legen wir also das Prüfintervall vom Jahresanfang an fest.
Hier ist das Testergebnis auf der Grundlage der Daten seit Anfang des Jahres.
Es wäre interessant, die Testergebnisse für jedes einzelne Modell zu kennen.
Dazu müssen wir den EA-Quellcode wie folgt ändern:
enum EnModels { USE_FIRST_MODEL, // Use first model only USE_SECOND_MODEL, // Use second model only USE_BOTH_MODELS // Use both models }; input EnModels InpModels = USE_BOTH_MODELS; // Models using input double InpLots = 1.0; // Lots amount to open position ... //+------------------------------------------------------------------+ //| Voting classification | //+------------------------------------------------------------------+ void Predict(void) { //--- evaluate first model if(InpModels==USE_BOTH_MODELS || InpModels==USE_FIRST_MODEL) ExtPredictedClass1=PredictPrice(ExtHandle1,SAMPLE_SIZE1); //--- evaluate second model if(InpModels==USE_BOTH_MODELS || InpModels==USE_SECOND_MODEL) ExtPredictedClass2=PredictPriceMovement(ExtHandle2,SAMPLE_SIZE2); //--- check predictions switch(InpModels) { case USE_FIRST_MODEL : ExtPredictedClass=ExtPredictedClass1; break; case USE_SECOND_MODEL : ExtPredictedClass=ExtPredictedClass2; break; case USE_BOTH_MODELS : if(ExtPredictedClass1==ExtPredictedClass2) ExtPredictedClass=ExtPredictedClass1; else ExtPredictedClass=-1; } }
Aktivieren Sie den Parameter „Use first model only“ (‚Nur das erste Modell verwenden‘).
Die Ergebnisse des ersten Modelltests
Nun wollen wir das zweite Modell testen. Hier sind die Ergebnisse des zweiten Modelltests.
Das zweite Modell erwies sich als viel stärker als das erste. Die Ergebnisse bestätigen die Theorie, dass schwache Modelle zusammengesetzt werden müssen. In diesem Artikel ging es jedoch nicht um die Theorie des Bildens von Ensemble, sondern um die praktische Anwendung.
Wichtiger Hinweis: Bitte beachten Sie, dass die in diesem Artikel verwendeten Modelle nur zur Demonstration der Arbeit mit ONNX-Modellen in der Sprache MQL5 dienen. Der Expert Advisor ist nicht für den Handel auf realen Konten vorgesehen.
Schlussfolgerung
Wir haben ein sehr einfaches, aber anschauliches Beispiel für ein Ensemble aus zwei ONNX-Modellen vorgestellt. Die Anzahl der gleichzeitig verwendeten Modelle ist begrenzt und kann 256 Modelle nicht überschreiten. Aber auch die Verwendung von mehr als zwei Modellen erfordert einen anderen Ansatz bei der Programmierung von Expert Advisors, nämlich die objektorientierte Programmierung.
Aber das ist ein Thema für einen anderen Artikel.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/12433
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.