English Русский 中文 Español Deutsch 日本語 Português 한국어 Italiano Türkçe
preview
Comment utiliser les modèles ONNX dans MQL5

Comment utiliser les modèles ONNX dans MQL5

MetaTrader 5Machine learning | 16 novembre 2023, 10:59
1 170 0
MetaQuotes
MetaQuotes

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 :

2.10.0
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)
C:\Users\user\AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Files\

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 début des données = 2022-11-28 12:28:39.870685
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()

Graphique des prix de clôture de l’EURUSD en H1

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 du modèleProprié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()

dict_keys(['loss', 'root_mean_squared_error', 'val_loss', 'val_root_mean_squared_error'])

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()

Graphique des itérations de la fonction de perte

#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()


Graphique des itérations RMSE

#evaluate training data
model.evaluate(x_train,y_train, batch_size = 32)

48/48 [==============================] - 1s 22ms/pas - perte : 2.9911e-04 - root_mean_squared_error : 0,0173
[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)

48/48 [==============================] - 2s 18ms/pas

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()

Graphique des prédictions sur le jeu de données d'entraînement :

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()

Graphique des prédictions sur le jeu de données de test


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). 

Paramètres de test de l’EA


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 :

Résultats des tests de l’EA



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.

Paramètres d'entrée de l’EA utilisant le SL/TP


Les résultats des tests avec les niveaux SL/TP pour la même période :

Résultats des tests de l’Expert Advisor utilisant les niveaux SL et TP


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

Fichiers joints |
MQL5.zip (1243.88 KB)
Algorithmes d'optimisation de la population : Algorithme des Lucioles (Firefly Algorithm - FA) Algorithmes d'optimisation de la population : Algorithme des Lucioles (Firefly Algorithm - FA)
Dans cet article, je considérerai la méthode d'optimisation de l'Algorithme Firefly (FA). Grâce à la modification, l'algorithme est passé d'un outsider à un véritable leader du classement.
Développer un Expert Advisor de trading à partir de zéro (Partie 23) : Nouveau système d'ordres (VI) Développer un Expert Advisor de trading à partir de zéro (Partie 23) : Nouveau système d'ordres (VI)
Nous allons rendre le système d’ordres plus flexible. Nous examinerons ici les modifications à apporter au code pour le rendre plus flexible, ce qui nous permettra de modifier les niveaux d'arrêt des positions beaucoup plus rapidement.
Algorithmes d'optimisation de la population : Algorithme de la Chauve-Souris (BA) Algorithmes d'optimisation de la population : Algorithme de la Chauve-Souris (BA)
Dans cet article, j'examinerai l'algorithme de la Chauve-Souris, ou Bat (BA), qui présente une bonne convergence pour les fonctions lisses.
Développer un Expert Advisor de trading à partir de zéro (Partie 22) : Nouveau système d’ordres (V) Développer un Expert Advisor de trading à partir de zéro (Partie 22) : Nouveau système d’ordres (V)
Nous allons continuer aujourd’hui à développer le nouveau système d’ordres. Il n'est pas facile de mettre en œuvre un nouveau système, car nous rencontrons souvent des problèmes qui compliquent considérablement le processus. Lorsque ces problèmes apparaissent, nous devons nous arrêter et réanalyser la direction dans laquelle nous avançons.