Un exemple d'assemblage de modèles ONNX dans MQL5
Introduction
Pour un trading stable, il est généralement recommandé de diversifier à la fois les instruments négociés et les stratégies de trading. Il en va de même pour les modèles d'apprentissage automatique : il est plus facile de créer plusieurs modèles simples qu'un seul modèle complexe. Mais il peut être difficile d'assembler ces modèles en un seul modèle ONNX.
Mais il est possible de combiner plusieurs modèles ONNX entraînés dans un programme MQL5. Dans cet article, nous examinerons l'un de ces ensembles, appelé le classificateur par vote. Nous allons vous montrer comment il est facile de mettre en œuvre un tel ensemble.
Modèles pour le projet
Pour notre exemple, nous utiliserons 2 modèles simples : un modèle de prédiction des prix par régression et un modèle de prédiction des mouvements de prix par classification. La principale différence entre les modèles est que la régression prédit la quantité, alors que la classification prédit la classe.
Le premier modèle est celui de la régression.
Il est entraîné à l'aide des données de l’EURUSD en D1 de 2003 à fin 2022. L'entraînement est réalisé à partir d'une série de 10 prix OHLC. Pour améliorer la capacité d'apprentissage du modèle, nous normalisons les prix et divisons le prix moyen de la série par l'écart-type de la série. Nous plaçons ainsi une série dans un certain intervalle avec une moyenne de 0 et un écart de 1, ce qui améliore la convergence pendant l'apprentissage.
Le modèle devrait donc prédire le prix de clôture du jour suivant.
Le modèle est très simple. Il n'est fourni ici qu'à des fins de démonstration.
# 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()En supposant que notre modèle de régression soit exécuté, le prix prédit qui en résulte devrait être transformé dans la classe suivante : le prix baisse, le prix ne change pas, le prix augmente. Cela est nécessaire pour organiser le classificateur de vote.
Le deuxième modèle est celui de la classification.
Il est entraîné sur l'EURUSD en D1 de 2010 à fin 2022. L'entraînement est effectué à l'aide d'une série de 63 prix de clôture. L'une des trois classes suivantes doit être définie à la sortie : le prix baissera, le prix restera dans les 10 points ou le prix augmentera. C'est à cause de la deuxième classe que nous avons dû entraîner le modèle en utilisant des données depuis 2010. Avant cela, en 2009, les marchés sont passés d'une précision de 4 chiffres à une précision à 5 chiffres. Un ancien point est ainsi transformé en 10 nouveaux points.
Comme dans le modèle précédent, le prix est normalisé. La normalisation est la même : nous divisons l'écart par rapport au prix moyen de la série par l'écart type de la série. L'idée de ce modèle a été décrite dans l'article timeseries forecasting with MLP in Keras" (en russe). Ce modèle est également conçu à des fins de démonstration uniquement.
# 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()Les modèles ont été entraînés avec des données jusqu'à la fin de l'année 2022, ce qui laisse le temps de démontrer leur fonctionnement dans le testeur de stratégie.
Un ensemble de modèles ONNX dans l’Expert Advisor MQL5
Vous trouverez ci-dessous un Expert Advisor simple qui illustre les possibilités offertes par les ensembles de modèles. Les grands principes de l'utilisation des modèles ONNX dans MQL5 ont été décrits dans la deuxième partie de l'article précédent.
Déclarations et définitions prospectives
#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
Fonction OnInit
//+------------------------------------------------------------------+ //| 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); }
Nous ne l'utiliserons qu'avec l’EURUSD, en D1. Cela s'explique par le fait que nous utilisons les données de la période du symbole en cours, alors que les modèles sont formés à partir des prix quotidiens.
Les modèles sont inclus dans l’Expert Advisor en tant que ressources.
Il est important de définir explicitement les formes de données d'entrée et de sortie.
Fonction 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(); }
Toutes les opérations de trading ne sont effectuées qu'en début de journée.
Fonction de prédiction
//+------------------------------------------------------------------+ //| 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; }
Une classe est considérée comme sélectionnée lorsque les deux modèles ont reçu la même classe. Il s'agit d'un vote à la majorité. Et comme il n'y a que deux modèles dans l'ensemble, le vote à la majorité est "unanime".
Prédiction du prix de clôture du jour à partir des 10 prix OHLC précédents
//+------------------------------------------------------------------+ //| 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); }
Les données d'entrée doivent être préparées selon les mêmes règles que pour l'entraînement du modèle. Après l'exécution du modèle, la valeur obtenue est reconvertie en prix. La classe est calculée sur la base de la différence entre le dernier prix de clôture de la série et le prix obtenu.
La prédiction du mouvement des prix est basée sur une série de 63 prix de clôture quotidiens :
//+------------------------------------------------------------------+ //| 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())); }
Les prix sont normalisés selon les mêmes règles que dans le premier modèle. Mais cette fois-ci, le code est plus compact car l'entrée est un vecteur et non une matrice. La classe est sélectionnée par la valeur maximale des 3 probabilités.
La stratégie de trading est simple. Les opérations de trading sont effectuées au début de chaque journée. Si la prédiction est "le prix va augmenter", nous achetons ; si elle est "le prix va baisser", nous vendons.
//+------------------------------------------------------------------+ //| 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(); } }
Nous avons entraîné notre modèle avec les données jusqu'au début de l'année 2023. Fixons donc l'intervalle de test à partir du début de l'année.
Voici le résultat du test basé sur les données depuis le début de l'année.
Il serait intéressant de connaître les résultats des tests pour chaque modèle.
Pour ce faire, modifions le code source de l'EA comme suit :
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; } }
Activons le paramètre "Utiliser uniquement le premier modèle".
Résultats des tests du premier modèle
Testons maintenant le second modèle. Voici les résultats du test du deuxième modèle.
Le deuxième modèle s'est avéré beaucoup plus solide que le premier. Les résultats confirment la théorie selon laquelle les modèles faibles doivent être assemblés. Toutefois, cet article ne porte pas sur la théorie de l'assemblage, mais sur son application pratique.
Remarque importante : Veuillez noter que les modèles utilisés dans l'article sont présentés uniquement pour démontrer comment travailler avec les modèles ONNX en utilisant le langage MQL5. L'Expert Advisor n'est pas destiné à être utilisé sur des comptes réels.
Conclusion
Nous avons présenté un exemple très simple mais illustratif d'un ensemble de 2 modèles ONNX. Le nombre de modèles utilisés simultanément est limité et ne peut excéder 256 modèles. Mais même l'utilisation de plus de 2 modèles nécessitera une approche différente de la programmation de l’Expert Advisor, à savoir une programmation orientée objet.
Mais c'est le sujet d'un autre article.
Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/12433
- Applications de trading gratuites
- Plus de 8 000 signaux à copier
- Actualités économiques pour explorer les marchés financiers
Vous acceptez la politique du site Web et les conditions d'utilisation