Comment utiliser les modèles ONNX dans MQL5
Introduction
Les auteurs de l'article A CNN-LSTM-Based Model to Forecast Stock Prices (Wenjie Lu, Jiazheng Li, Yifan Li, Aijun Sun, Jingyang Wang, Complexity magazine, vol. 2020, article ID 6622927, 10 pages, 2020) a comparé différents modèles de prévision des cours boursiers :
Les données boursières présentent les caractéristiques des séries chronologiques.
Parallèlement, basée sur la mémoire à long terme et à court terme (LSTM) d'apprentissage automatique qui présente l'avantage d'analyser les relations entre les données de séries chronologiques grâce à sa fonction de mémoire, nous proposons une méthode de prévision du cours des actions basée sur CNN-LSTM.
En attendant, nous utilisons MLP, CNN, RNN, LSTM, CNN-RNN et d'autres modèles de prévision pour prédire le cours des actions un par un. Les résultats de prévision de ces modèles sont également analysés et comparés.
Les données utilisées dans cette recherche concernent les cours quotidiens des actions du 1er juillet 1991 au 31 août 2020, incluant au total 7127 jours de bourse.
En termes de données historiques, nous choisissons 8 caractéristiques : le prix d'ouverture, le prix le plus élevé, le prix le plus bas, le prix de clôture, le volume, les retournements, les hauts et les bas et les changements.
Premièrement, nous adoptons CNN pour extraire efficacement les fonctionnalités des données, qui sont les éléments des 10 jours précédents. Puis, nous adoptons LSTM pour prédire le cours de l'action avec les données de fonctionnalités extraites.
Selon les résultats expérimentaux, le CNN-LSTM peut fournir une prévision fiable du cours des actions avec la plus grande précision de prévision.
Cette méthode de prévision fournit non seulement une nouvelle idée de recherche pour la prévision du cours des actions, mais offre également aux chercheurs une expérience pratique pour étudier les données de séries chronologiques financières.
Parmi tous les modèles considérés, les modèles CNN-LSTM ont généré les meilleurs résultats lors des expérimentations. Dans cet article, nous verrons comment créer un tel modèle pour prévoir des séries temporelles financières et comment utiliser le modèle ONNX créé dans un Expert Advisor MQL5.
1. Construire un modèle
Python propose un ensemble de bibliothèques spécialisées et offre ainsi des fonctionnalités étendues pour travailler avec des modèles ML. Les bibliothèques facilitent grandement la préparation et le traitement des données.
Nous vous recommandons d'utiliser des ressources GPU pour maximiser l'efficacité des projets ML. De nombreux utilisateurs Windows ont rencontré des problèmes en essayant d'installer la version actuelle de TensorFlow (voir les commentaires sur le guide vidéo et sa version texte). Nous avons donc testé TensorFlow 2.10.0 et nous recommandons d'utiliser cette version. Les calculs GPU ont été effectués sur une carte graphique NVIDIA GeForce RTX 2080 Ti avec les bibliothèques CUDA 11.2 et CUDNN 8.1.0.7.
1.1. Installation de Python et des bibliothèques
Si vous n'avez pas Python, vous devez l'installer. Nous avons utilisé la version 3.9.16.
Installez également les bibliothèques (si vous utilisez Conda/Anaconda, exécutez ces commandes dans Anaconda Prompt) :
python.exe -m pip install --upgrade pip pip install --upgrade pandas pip install --upgrade scikit-learn pip install --upgrade matplotlib pip install --upgrade tqdm pip install --upgrade metatrader5 pip install --upgrade onnx==1.12 pip install --upgrade tf2onnx pip install --upgrade tensorflow==2.10.0
1.2. Vérification de la version de TensorFlow et du GPU
Le code ci-dessous vérifie la version de TensorFlow installée et vérifie s'il est possible d'utiliser le GPU pour calculer les modèles :
#check tensorflow version print(tf.__version__) #check GPU support print(len(tf.config.list_physical_devices('GPU'))>0)
Si la version requise est correctement installée, vous verrez le résultat suivant :
True
Nous avons utilisé un script Python pour créer et entraîner le modèle. Les étapes de ce processus sont brièvement décrites ci-dessous.
1.3. Construire et entraîner le modèle
Le script commence par importer les bibliothèques Python qui seront utilisées dans le modèle.
#Python libraries import matplotlib.pyplot as plt 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 sys import argv
Vérifiez la version de TensorFlow et la disponibilité du GPU :
#check tensorflow version print(tf.__version__)
2.10.0
#check GPU support print(len(tf.config.list_physical_devices('GPU'))>0)
True
Initialisez MetaTrader 5 pour les opérations depuis Python :
#initialize MetaTrader5 for history data if not mt5.initialize(): print("initialize() failed, error code =",mt5.last_error()) quit()
Informations sur le terminal MetaTrader 5 :
#show terminal info terminal_info=mt5.terminal_info() print(terminal_info)
TerminalInfo(community_account=True, community_connection=True,connected=True, dlls_allowed=False, trade_allowed=False, tradeapi_disabled=False, email_enabled=False, ftp_enabled=False, notifications_enabled=False, mqid=False, build=3640, maxbars=100000, codepage=0, ping_last=58768, community_balance=1.0, retransmission=0.015296317559440137, company='MetaQuotes Software Corp.', nom='MetaTrader 5', langue='Anglais', chemin='C:\\Program Files\\MetaTrader 5', data_path='C:\\Users\\user\\AppData\\Roaming\\MetaQuotes\\Terminal\\D0E8209F77C8CF37AD8BF550E51FF075', commondata_path='C:\\Users\\user\\AppData\\Roaming\\ MetaQuotes\\Terminal\\Common')
#show file path file_path=terminal_info.data_path+"\\MQL5\\Files\\" print(file_path)
Affichez le chemin pour enregistrer le modèle (dans cet exemple, le script s'exécute dans Jupyter Notebook) :
#data path to save the model 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)
chemin pour sauvegarder le modèle onnx : C:\Users\user\AppData\Roaming\Python\Python39\site-packages\
Préparez les dates pour demander les données historiques. Dans notre exemple, nous demandons des barres EURUSD H1 pour 120 à partir de la date actuelle :
#set start and end dates for history data from datetime import timedelta,datetime end_date = datetime.now() start_date = end_date - timedelta(days=120) #print start and end dates print("data start date=",start_date) print("data end date=",end_date)
date de fin des données = 2023-03-28 12:28:39.870685
Demandez les données historiques sur l’EURUSD :
#get EURUSD rates (H1) from start_date to end_date eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, start_date, end_date)
Affichez les données téléchargées :
#check print(eurusd_rates)
#create dataframe df = pd.DataFrame(eurusd_rates)
Affichez le début et la fin du dataframe :
#show dataframe head df.head()
#show dataframe tail df.tail()
#show dataframe shape (the number of rows and columns in the data set) df.shape
(2045, 8)
Sélectionnez les Prix de Clôture uniquement :
#prepare close prices only data = df.filter(['close']).values
Affichez les données :
#show close prices plt.figure(figsize = (18,10)) plt.plot(data,'b',label = 'Original') plt.xlabel("Hours") plt.ylabel("Price") plt.title("EURUSD_H1") plt.legend()
Mettez à l'échelle les données de prix source dans l’intervalle [0,1] à l'aide de MinMaxScaler :
#scale data using MinMaxScaler from sklearn.preprocessing import MinMaxScaler scaler=MinMaxScaler(feature_range=(0,1)) scaled_data = scaler.fit_transform(data)
Les premiers 80% des données seront utilisées pour l’entraînement.
#training size is 80% of the data training_size = int(len(scaled_data)*0.80) print("training size:",training_size)
taille de l’entraînement : 1636
#create train data and check size train_data_initial = scaled_data[0:training_size,:] print(len(train_data_initial))
1636
#create test data and check size test_data_initial= scaled_data[training_size:,:1] print(len(test_data_initial))
409
La fonction suivante crée des séquences d'entraînement :
#split a univariate sequence into samples def split_sequence(sequence, n_steps): X, y = list(), list() for i in range(len(sequence)): #find the end of this pattern end_ix = i + n_steps #check if we are beyond the sequence if end_ix > len(sequence)-1: break #gather input and output parts of the pattern seq_x, seq_y = sequence[i:end_ix], sequence[end_ix] X.append(seq_x) y.append(seq_y) return np.array(X), np.array(y)
Construisez les ensembles :
#split into samples time_step = 120 x_train, y_train = split_sequence(train_data_initial, time_step) x_test, y_test = split_sequence(test_data_initial, time_step) #reshape input to be [samples, time steps, features] which is required for LSTM x_train =x_train.reshape(x_train.shape[0],x_train.shape[1],1) x_test = x_test.reshape(x_test.shape[0],x_test.shape[1],1)
Formes tensorielles pour l’entraînement et les tests :
#show shape of train data x_train.shape
(1516, 120, 1)
#show shape of test data x_test.shape
(289, 120, 1)
#import keras libraries for the model import math from keras.models import Sequential from keras.layers import Dense,Activation,Conv1D,MaxPooling1D,Dropout from keras.layers import LSTM from keras.utils.vis_utils import plot_model from keras.metrics import RootMeanSquaredError as rmse from keras import optimizers
Définissez le modèle :
#define the model model = Sequential() model.add(Conv1D(filters=256, kernel_size=2,activation='relu',padding = 'same',input_shape=(120,1))) model.add(MaxPooling1D(pool_size=2)) model.add(LSTM(100, return_sequences = True)) model.add(Dropout(0.3)) model.add(LSTM(100, return_sequences = False)) model.add(Dropout(0.3)) model.add(Dense(units=1, activation = 'sigmoid')) model.compile(optimizer='adam', loss= 'mse' , metrics = [rmse()])
Affichez les propriétés du modèle :
#show model model.summary()
Propriétés
Entraînement du modèle :
#measure time import time time_calc_start = time.time() #fit model with 300 epochs history=model.fit(x_train,y_train,epochs=300,validation_data=(x_test,y_test),batch_size=32,verbose=1) #calculate time fit_time_seconds = time.time() - time_calc_start print("fit time =",fit_time_seconds," seconds.")
Epoque 1/300
48/48 [==============================] - 8s 49ms/pas - perte : 0,0129 - root_mean_squared_error : 0,1136 - val_loss : 0,0065 - val_root_mean_squared_error : 0,0804
...
Époque 299/300
48/48 [==============================] - 2s 35ms/pas - perte : 4.5197e-04 - root_mean_squared_error : 0,0213 - val_loss : 4.2535e-04 - val_root_mean_squared_error : 0,0206
Époque 300/300
48/48 [==============================] - 2s 32ms/pas - perte : 4.2967e-04 - root_mean_squared_error : 0,0207 - val_loss : 4.4040e-04 - val_root_mean_squared_error : 0,0210
temps d'ajustement = 467,4918096065521 secondes.
La formation a duré environ 8 minutes.
#show training history keys history.history.keys()
Dynamique d'optimisation dans les ensembles de données de formation et de test :
#show iteration-loss graph for training and validation plt.figure(figsize = (18,10)) plt.plot(history.history['loss'],label='Training Loss',color='b') plt.plot(history.history['val_loss'],label='Validation-loss',color='g') plt.xlabel("Iteration") plt.ylabel("Loss") plt.title("LOSS") plt.legend()
#show iteration-rmse graph for training and validation plt.figure(figsize = (18,10)) plt.plot(history.history['root_mean_squared_error'],label='Training RMSE',color='b') plt.plot(history.history['val_root_mean_squared_error'],label='Validation-RMSE',color='g') plt.xlabel("Iteration") plt.ylabel("RMSE") plt.title("RMSE") plt.legend()
#evaluate training data model.evaluate(x_train,y_train, batch_size = 32)
[0,00029911252204328775, 0,01729486882686615]
#evaluate testing data model.evaluate(x_test,y_test, batch_size = 32)
10/10 [==============================] - 0s 31ms/pas - perte : 4.4040e-04 - root_mean_squared_error : 0,0210
[0,00044039846397936344, 0,020985672250390053]
Formez la prédiction sur l'ensemble de données d'entraînement :
#prediction using training data train_predict = model.predict(x_train) plot_y_train = y_train.reshape(-1,1)
Générez des graphiques réels et prévus pour l'intervalle d'entraînement :
#show actual vs predicted (training) graph plt.figure(figsize=(18,10)) plt.plot(scaler.inverse_transform(plot_y_train),color = 'b', label = 'Original') plt.plot(scaler.inverse_transform(train_predict),color='red', label = 'Predicted') plt.title("Prediction Graph Using Training Data") plt.xlabel("Hours") plt.ylabel("Price") plt.legend() plt.show()
Former la prédiction sur l'ensemble de données de test :
#prediction using testing data test_predict = model.predict(x_test) plot_y_test = y_test.reshape(-1,1)
11/11 [==============================] - 0s 11ms/pas
Pour calculer les métriques, nous devons convertir les données de l'intervalle [0,1]. Encore une fois, nous utilisons MinMaxScaler.
#calculate metrics from sklearn import metrics from sklearn.metrics import r2_score #transform data to real values value1=scaler.inverse_transform(plot_y_test) value2=scaler.inverse_transform(test_predict) #calc score score = np.sqrt(metrics.mean_squared_error(value1,value2)) print("RMSE : {}".format(score)) print("MSE :", metrics.mean_squared_error(value1,value2)) print("R2 score :",metrics.r2_score(value1,value2))
RMSE : 0,0015151631684117558
MSE : 2.295719426911551e-06
Note R2 : 0.9683533377809039
#show actual vs predicted (testing) graph plt.figure(figsize=(18,10)) plt.plot(scaler.inverse_transform(plot_y_test),color = 'b', label = 'Original') plt.plot(scaler.inverse_transform(test_predict),color='g', label = 'Predicted') plt.title("Prediction Graph Using Testing Data") plt.xlabel("Hours") plt.ylabel("Price") plt.legend() plt.show()
Exportez le modèle vers un fichier onnx :
# save model to ONNX output_path = data_path+"model.eurusd.H1.120.onnx" onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path) print(f"model saved to {output_path}") output_path = file_path+"model.eurusd.H1.120.onnx" onnx_model = tf2onnx.convert.from_keras(model, output_path=output_path) print(f"saved model to {output_path}") # finish mt5.shutdown()
Le code complet du script Python est joint à l'article dans un Jupyter Notebook.
Dans l'article A CNN-LSTM-Based Model to Forecast Stock Prices, le meilleur résultat R^2=0.9646 a été obtenu pour les modèles avec l’architecture CNN-LSTM. Dans notre exemple, le réseau CNN-LSTM a généré le meilleur résultat de R^2=0,9684. Selon les résultats, les modèles de ce type peuvent être efficaces pour résoudre des problèmes de prédiction.
Nous avons considéré un exemple de script Python qui construit et entraîne des modèles CNN-LSTM pour prédire des séries temporelles financières.
2. Utiliser le modèle dans MetaTrader 5
2.1. A savoir avant de commencer
Il existe 2 manières de créer un modèle : vous pouvez utiliser OnnxCreate pour créer un modèle à partir d'un fichier onnx, ou OnnxCreateFromBuffer pour le créer à partir d'un tableau de données.
Si un modèle ONNX est utilisé comme ressource dans un EA, vous devrez recompiler l'EA à chaque fois que vous modifiez le modèle.
Tous les modèles n'ont pas de tailles d'entrée et/ou de tenseur de sortie entièrement définis. Il s'agit normalement de la première dimension responsable de la taille du package. Avant d'exécuter un modèle, vous devez spécifier explicitement les tailles à l'aide des fonctions OnnxSetInputShape et OnnxSetOutputShape . Les données d'entrée du modèle doivent être préparées de la même manière que lors de l’entraînement du modèle.
Pour les données d'entrée et de sortie, nous recommandons d'utiliser les tableaux, des matrices et/ou des vecteurs du même type que ceux qui sont utilisés dans le modèle. Dans ce cas, vous n'aurez pas à convertir les données lors de l'exécution du modèle. Si les données ne peuvent pas être représentées dans le type requis, les données seront automatiquement converties.
Utilisez OnnxRun pour déduire (exécuter) votre modèle. Notez qu'un modèle peut être exécuté plusieurs fois. Après avoir utilisé le modèle, libérez-le à l'aide de la fonction OnnxRelease .
Documentation complète pour les modèles ONNX dans MQL5.
2.2. Lire un fichier onnx et obtenir des informations sur les entrées et sorties
Afin d'utiliser notre modèle, nous devons connaître l'emplacement du modèle, le type et la forme des données d'entrée, ainsi que le type et la forme des données de sortie. Selon le script créé précédemment, model.eurusd.H1.120.onnx se trouve dans le même dossier que le script Python qui a généré le fichier onnx. L'entrée est un ensemble de 120 prix de clôture normalisés (pour travailler avec une taille de lot égale à 1) en float32. La sortie est un float32, qui est un prix normalisé prédit par le modèle.
Nous avons également créé le fichier onnx dans le dossier MQL5\Files afin d'obtenir les données d'entrée et de sortie du modèle à l'aide d'un script MQL5.
//+------------------------------------------------------------------+ //| OnnxModelInfo.mq5 | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #define UNDEFINED_REPLACE 1 //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { string file_names[]; if(FileSelectDialog("Open ONNX model",NULL,"ONNX files (*.onnx)|*.onnx|All files (*.*)|*.*",FSD_FILE_MUST_EXIST,file_names,NULL)<1) return; PrintFormat("Create model from %s with debug logs",file_names[0]); long session_handle=OnnxCreate(file_names[0],ONNX_DEBUG_LOGS); if(session_handle==INVALID_HANDLE) { Print("OnnxCreate error ",GetLastError()); return; } OnnxTypeInfo type_info; long input_count=OnnxGetInputCount(session_handle); Print("model has ",input_count," input(s)"); for(long i=0; i<input_count; i++) { string input_name=OnnxGetInputName(session_handle,i); Print(i," input name is ",input_name); if(OnnxGetInputTypeInfo(session_handle,i,type_info)) PrintTypeInfo(i,"input",type_info); } long output_count=OnnxGetOutputCount(session_handle); Print("model has ",output_count," output(s)"); for(long i=0; i<output_count; i++) { string output_name=OnnxGetOutputName(session_handle,i); Print(i," output name is ",output_name); if(OnnxGetOutputTypeInfo(session_handle,i,type_info)) PrintTypeInfo(i,"output",type_info); } OnnxRelease(session_handle); } //+------------------------------------------------------------------+ //| PrintTypeInfo | //+------------------------------------------------------------------+ void PrintTypeInfo(const long num,const string layer,const OnnxTypeInfo& type_info) { Print(" type ",EnumToString(type_info.type)); Print(" data type ",EnumToString(type_info.element_type)); if(type_info.dimensions.Size()>0) { bool dim_defined=(type_info.dimensions[0]>0); string dimensions=IntegerToString(type_info.dimensions[0]); for(long n=1; n<type_info.dimensions.Size(); n++) { if(type_info.dimensions[n]<=0) dim_defined=false; dimensions+=", "; dimensions+=IntegerToString(type_info.dimensions[n]); } Print(" shape [",dimensions,"]"); //--- not all dimensions defined if(!dim_defined) PrintFormat(" %I64d %s shape must be defined explicitly before model inference",num,layer); //--- reduce shape uint reduced=0; long dims[]; for(long n=0; n<type_info.dimensions.Size(); n++) { long dimension=type_info.dimensions[n]; //--- replace undefined dimension if(dimension<=0) dimension=UNDEFINED_REPLACE; //--- 1 can be reduced if(dimension>1) { ArrayResize(dims,reduced+1); dims[reduced++]=dimension; } } //--- all dimensions assumed 1 if(reduced==0) { ArrayResize(dims,1); dims[reduced++]=1; } //--- shape was reduced if(reduced<type_info.dimensions.Size()) { dimensions=IntegerToString(dims[0]); for(long n=1; n<dims.Size(); n++) { dimensions+=", "; dimensions+=IntegerToString(dims[n]); } string sentence=""; if(!dim_defined) sentence=" if undefined dimension set to "+(string)UNDEFINED_REPLACE; PrintFormat(" shape of %s data can be reduced to [%s]%s",layer,dimensions,sentence); } } else PrintFormat("no dimensions defined for %I64d %s",num,layer); } //+------------------------------------------------------------------+
Dans la fenêtre de sélection de fichier, nous avons sélectionné le fichier onnx enregistré dans MQL5\Files, créé un modèle à partir du fichier à l'aide de OnnxCreate et obtenu les informations suivantes :
Create model from model.eurusd.H1.120.onnx with debug logs ONNX: Creating and using per session threadpools since use_per_session_threads_ is true ONNX: Dynamic block base set to 0 ONNX: Initializing session. ONNX: Adding default CPU execution provider. ONNX: Total shared scalar initializer count: 0 ONNX: Total fused reshape node count: 0 ONNX: Removing NodeArg 'Gather_out0'. It is no longer used by any node. ONNX: Removing NodeArg 'Gather_token_1_out0'. It is no longer used by any node. ONNX: Total shared scalar initializer count: 0 ONNX: Total fused reshape node count: 0 ONNX: Removing initializer 'sequential/conv1d/Conv1D/ExpandDims_1:0'. It is no longer used by any node. ONNX: Use DeviceBasedPartition as default ONNX: Saving initialized tensors. ONNX: Done saving initialized tensors ONNX: Session successfully initialized. model has 1 input(s) 0 input name is conv1d_input type ONNX_TYPE_TENSOR data type ONNX_DATA_TYPE_FLOAT shape [-1, 120, 1] 0 input shape must be defined explicitly before model inference shape of input data can be reduced to [120] if undefined dimension set to 1 model has 1 output(s) 0 output name is dense type ONNX_TYPE_TENSOR data type ONNX_DATA_TYPE_FLOAT shape [-1, 1] 0 output shape must be defined explicitly before model inference shape of output data can be reduced to [1] if undefined dimension set to 1
Puisque nous avons activé le mode de debug :
long session_handle=OnnxCreate(file_names[0],ONNX_DEBUG_LOGS);
nous avons des journaux avec le préfixe ONNX.
Nous voyons que le modèle a en réalité une entrée et une sortie. Ici, la première dimension du tenseur d'entrée et la première dimension du tenseur de sortie ne sont pas définies. On suppose que ces dimensions sont responsables de la taille du lot. Par conséquent, avant de déduire le modèle, nous devons spécifier explicitement avec quelles tailles nous allons travailler (OnnxSetInputShape et OnnxSetOutputShape). Habituellement, un seul ensemble de données est entré dans le modèle. Un exemple détaillé est fourni dans le paragraphe suivant « Exemple d'utilisation d'un modèle ONNX dans un EA de trading ».
Lors de la préparation des données, il n'est pas nécessaire d'utiliser un tableau de dimensions [1, 120, 1]. Nous pouvons saisir un tableau unidimensionnel ou un vecteur de 120 éléments.
2.3. Exemple d'utilisation d'un modèle ONNX dans un EA de trading
Déclarations et définitions
#include <Trade\Trade.mqh> input double InpLots = 1.0; // Lots amount to open position #resource "Python/model.120.H1.onnx" as uchar ExtModel[] #define SAMPLE_SIZE 120 long ExtHandle=INVALID_HANDLE; int ExtPredictedClass=-1; datetime ExtNextBar=0; datetime ExtNextDay=0; float ExtMin=0.0; float ExtMax=0.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_H1) { Print("model must work with EURUSD,H1"); return(INIT_FAILED); } //--- create a model from static buffer ExtHandle=OnnxCreateFromBuffer(ExtModel,ONNX_DEFAULT); if(ExtHandle==INVALID_HANDLE) { Print("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 (only Close) const long input_shape[] = {1,SAMPLE_SIZE,1}; if(!OnnxSetInputShape(ExtHandle,ONNX_DEFAULT,input_shape)) { Print("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_shape[] = {1,1}; if(!OnnxSetOutputShape(ExtHandle,0,output_shape)) { Print("OnnxSetOutputShape error ",GetLastError()); return(INIT_FAILED); } //--- return(INIT_SUCCEEDED); }
Nous travaillons uniquement avec EURUSD et en H1, car nous utilisons les données actuelles du symbole/période.
Notre modèle est inclus dans l’EA en tant que ressource. L'EA est totalement autonome et ne nécessite pas de lire un fichier onnx externe. Un modèle est créé à partir du tableau de ressources.
Les formes des données d'entrée et de sortie doivent être explicitement définies.
La fonction OnTick :
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- check new day if(TimeCurrent()>=ExtNextDay) { GetMinMax(); //--- set next day time ExtNextDay=TimeCurrent(); ExtNextDay-=ExtNextDay%PeriodSeconds(PERIOD_D1); ExtNextDay+=PeriodSeconds(PERIOD_D1); } //--- check new bar if(TimeCurrent()<ExtNextBar) return; //--- set next bar time ExtNextBar=TimeCurrent(); ExtNextBar-=ExtNextBar%PeriodSeconds(); ExtNextBar+=PeriodSeconds(); //--- check min and max double close=iClose(_Symbol,_Period,0); if(ExtMin>close) ExtMin=close; if(ExtMax<close) ExtMax=close; //--- predict next price PredictPrice(); //--- check trading according to prediction if(ExtPredictedClass>=0) if(PositionSelect(_Symbol)) CheckForClose(); else CheckForOpen(); }
Nous définissons le début d'une nouvelle journée. Le début de la journée est utilisé pour mettre à jour les valeurs Low et High de la séquence de 120 jours afin de normaliser les prix dans la séquence de 120 heures. Le modèle a été formé dans ces conditions. Nous devons donc les respecter lors de la préparation des données d'entrée.
//+------------------------------------------------------------------+ //| Get minimal and maximal Close for last 120 days | //+------------------------------------------------------------------+ void GetMinMax(void) { vectorf close; close.CopyRates(_Symbol,PERIOD_D1,COPY_RATES_CLOSE,0,SAMPLE_SIZE); ExtMin=close.Min(); ExtMax=close.Max(); }
Si besoin, nous pouvons modifier Low et High tout au long de la journée.
Fonction de prédiction :
//+------------------------------------------------------------------+ //| Predict next price | //+------------------------------------------------------------------+ void PredictPrice(void) { static vectorf output_data(1); // vector to get result static vectorf x_norm(SAMPLE_SIZE); // vector for prices normalize //--- check for normalization possibility if(ExtMin>=ExtMax) { ExtPredictedClass=-1; return; } //--- request last bars if(!x_norm.CopyRates(_Symbol,_Period,COPY_RATES_CLOSE,1,SAMPLE_SIZE)) { ExtPredictedClass=-1; return; } float last_close=x_norm[SAMPLE_SIZE-1]; //--- normalize prices x_norm-=ExtMin; x_norm/=(ExtMax-ExtMin); //--- run the inference if(!OnnxRun(ExtHandle,ONNX_NO_CONVERSION,x_norm,output_data)) { ExtPredictedClass=-1; return; } //--- denormalize the price from the output value float predicted=output_data[0]*(ExtMax-ExtMin)+ExtMin; //--- classify predicted price movement float delta=last_close-predicted; if(fabs(delta)<=0.00001) ExtPredictedClass=PRICE_SAME; else { if(delta<0) ExtPredictedClass=PRICE_UP; else ExtPredictedClass=PRICE_DOWN; } }
Nous vérifions en premier si nous pouvons normaliser. La normalisation est implémentée comme dans la fonction Python MinMaxScaler.
#scale data from sklearn.preprocessing import MinMaxScaler scaler=MinMaxScaler(feature_range=(0,1)) scaled_data = scaler.fit_transform(data)
Le code de normalisation est donc très simple et direct.
Les vecteurs pour les données d'entrée et de sortie sont organisés de manière statique. Cela garantit un buffer non déplaçable qui existe pendant toute la durée de vie du programme. Ainsi, les tenseurs d'entrée et de sortie du modèle ONNX ne sont pas recréés à chaque fois que nous exécutons le modèle.
La fonction clé est OnnxRun. L'indicateur ONNX_NO_CONVERSION indique que les données d'entrée et de sortie ne doivent pas être converties, puisque le type float MQL5 correspond exactement à ONNX_DATA_TYPE_FLOAT. L'indicateur ONNX_DEBUG n'est pas défini.
Nous dénormalisons ensuite les données obtenues en prix prédit et nous déterminons la classe : si le prix va augmenter, baisser ou ne changera pas.
La stratégie de trading est simple. Au début de chaque heure, nous vérifions les prévisions de prix pour la fin de cette heure. Si le prix prévu augmente, nous achetons. Si le modèle prédit un mouvement baissier, 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)) { double price; double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID); double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK); if(signal==ORDER_TYPE_SELL) price=bid; else price=ask; ExtTrade.PositionOpen(_Symbol,signal,InpLots,price,0.0,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(); } }
Vérifions maintenant les performances de l’EA dans le Testeur de Stratégie. Afin de tester l'EA dès le début de l'année, le modèle doit être entraîné à l'aide de données antérieures. Par conséquent, nous avons légèrement modifié le script Python en supprimant les parties inutilisées et en modifiant la date de fin de l’entraînement pour qu'il ne chevauche pas la période de test.
Le script ONNX.eurusd.H1.120.Training.py se trouve dans le sous-dossier Python et s'exécute directement dans MetaEditor. Le modèle ONNX résultant sera enregistré dans le même sous-dossier Python et sera utilisé comme ressource lors de la compilation de l’EA.
# Copyright 2023, MetaQuotes Ltd. # https://www.mql5.com # python libraries import MetaTrader5 as mt5 import tensorflow as tf import numpy as np import pandas as pd import tf2onnx # input parameters inp_model_name = "model.eurusd.H1.120.onnx" inp_history_size = 120 if not mt5.initialize(): print("initialize() failed, error code =",mt5.last_error()) quit() # we will save generated onnx-file near our script to use as resource from sys import argv 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) # set start and end dates for history data from datetime import timedelta, datetime #end_date = datetime.now() end_date = datetime(2023, 1, 1, 0) start_date = end_date - timedelta(days=inp_history_size) # print start and end dates print("data start date =",start_date) print("data end date =",end_date) # get rates eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_H1, start_date, end_date) # create dataframe df = pd.DataFrame(eurusd_rates) # get close prices only data = df.filter(['close']).values # scale data from sklearn.preprocessing import MinMaxScaler scaler=MinMaxScaler(feature_range=(0,1)) scaled_data = scaler.fit_transform(data) # training size is 80% of the data training_size = int(len(scaled_data)*0.80) print("Training_size:",training_size) train_data_initial = scaled_data[0:training_size,:] test_data_initial = scaled_data[training_size:,:1] # split a univariate sequence into samples def split_sequence(sequence, n_steps): X, y = list(), list() for i in range(len(sequence)): # find the end of this pattern end_ix = i + n_steps # check if we are beyond the sequence if end_ix > len(sequence)-1: break # gather input and output parts of the pattern seq_x, seq_y = sequence[i:end_ix], sequence[end_ix] X.append(seq_x) y.append(seq_y) return np.array(X), np.array(y) # split into samples time_step = inp_history_size x_train, y_train = split_sequence(train_data_initial, time_step) x_test, y_test = split_sequence(test_data_initial, time_step) # reshape input to be [samples, time steps, features] which is required for LSTM x_train =x_train.reshape(x_train.shape[0],x_train.shape[1],1) x_test = x_test.reshape(x_test.shape[0],x_test.shape[1],1) # define model from keras.models import Sequential from keras.layers import Dense, Activation, Conv1D, MaxPooling1D, Dropout, Flatten, LSTM from keras.metrics import RootMeanSquaredError as rmse model = Sequential() model.add(Conv1D(filters=256, kernel_size=2, activation='relu',padding = 'same',input_shape=(inp_history_size,1))) model.add(MaxPooling1D(pool_size=2)) model.add(LSTM(100, return_sequences = True)) model.add(Dropout(0.3)) model.add(LSTM(100, return_sequences = False)) model.add(Dropout(0.3)) model.add(Dense(units=1, activation = 'sigmoid')) model.compile(optimizer='adam', loss= 'mse' , metrics = [rmse()]) # model training for 300 epochs history = model.fit(x_train, y_train, epochs = 300 , validation_data = (x_test,y_test), batch_size=32, verbose=2) # evaluate training data train_loss, train_rmse = model.evaluate(x_train,y_train, batch_size = 32) print(f"train_loss={train_loss:.3f}") print(f"train_rmse={train_rmse:.3f}") # evaluate testing data test_loss, test_rmse = model.evaluate(x_test,y_test, batch_size = 32) print(f"test_loss={test_loss:.3f}") print(f"test_rmse={test_rmse:.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()
Test de l’EA basé sur le modèle ONNX
Maintenant, testons l'EA sur des données historiques dans le Testeur de Stratégie. Nous spécifions les mêmes paramètres que ceux que nous avons utilisés pour entraîner le modèle : le symbole EURUSD et la période H1.
L'intervalle de tests n'inclut pas la période d’entraînement : elle commence dès le début de l'année (01/01/2023).
Selon la stratégie, les signaux sont vérifiés une fois, au début de chaque heure (l'EA surveille l'arrivée d'une nouvelle barre), le mode de modélisation des ticks n'a donc pas d'importance. OnTick sera traité dans le testeur une fois par barre.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- check new day if(TimeCurrent()>=ExtNextDay) { GetMinMax(); //--- set next day time ExtNextDay=TimeCurrent(); ExtNextDay-=ExtNextDay%PeriodSeconds(PERIOD_D1); ExtNextDay+=PeriodSeconds(PERIOD_D1); } //--- check new bar if(TimeCurrent()<ExtNextBar) return; //--- set next bar time ExtNextBar=TimeCurrent(); ExtNextBar-=ExtNextBar%PeriodSeconds(); ExtNextBar+=PeriodSeconds(); //--- check min and max float close=(float)iClose(_Symbol,_Period,0); if(ExtMin>close) ExtMin=close; if(ExtMax<close) ExtMax=close; //--- predict next price PredictPrice(); //--- check trading according to prediction if(ExtPredictedClass>=0) if(PositionSelect(_Symbol)) CheckForClose(); else CheckForOpen(); }
Avec ce traitement, les tests sur une période de trois mois ne prennent que quelques secondes. Voici les résultats :
Modifions maintenant la stratégie de trading pour permettre l'ouverture d’une position par un signal et la clôture par le Stop Loss (SL) ou par le Take Profit (TP).
input double InpLots = 1.0; // Lots amount to open position input bool InpUseStops = true; // Use stops in trading input int InpTakeProfit = 500; // TakeProfit level input int InpStopLoss = 500; // StopLoss level //+------------------------------------------------------------------+ //| 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)) { double price,sl=0,tp=0; double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID); double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK); if(signal==ORDER_TYPE_SELL) { price=bid; if(InpUseStops) { sl=NormalizeDouble(bid+InpStopLoss*_Point,_Digits); tp=NormalizeDouble(ask-InpTakeProfit*_Point,_Digits); } } else { price=ask; if(InpUseStops) { sl=NormalizeDouble(ask-InpStopLoss*_Point,_Digits); tp=NormalizeDouble(bid+InpTakeProfit*_Point,_Digits); } } ExtTrade.PositionOpen(_Symbol,signal,InpLots,price,sl,tp); } } //+------------------------------------------------------------------+ //| Check for close position conditions | //+------------------------------------------------------------------+ void CheckForClose(void) { //--- position should be closed by stops if(InpUseStops) return; 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(); } }
InpUseStops = true, ce qui signifie que les niveaux SL et TP sont définis à l'ouverture de la position.
Les résultats des tests avec les niveaux SL/TP pour la même période :
Le code source complet de l'EA et du modèle formé (jusqu'au début de l'année 2023) sont fournis dans la pièce jointe.
Conclusion
L'article montre qu'il n'y a rien de difficile à utiliser les modèles ONNX dans les programmes MQL5. En fait, l’application des modèles est la partie la plus simple. Il est beaucoup plus difficile d’obtenir un modèle ONNX adéquat.
Veuillez noter que le modèle utilisé dans l'article est fourni à des fins de démonstration uniquement, pour montrer comment travailler avec les modèles ONNX à l'aide du langage MQL5. L’Expert Advisor présenté dans cet article n’est pas destiné au trading réel.
Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/12373
- 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