English Русский 中文 Español 日本語 Português 한국어 Français Italiano Türkçe
preview
Wie man ONNX-Modelle in MQL5 verwendet

Wie man ONNX-Modelle in MQL5 verwendet

MetaTrader 5Maschinelles Lernen | 21 April 2023, 12:48
409 0
MetaQuotes
MetaQuotes

Einführung

Die Autoren des Artikels 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 Seiten, 2020) verglichen verschiedene Modelle zur Prognose von Aktienkursen:

Aktienkursdaten weisen die Merkmale von Zeitreihen auf.

Gleichzeitig schlagen wir auf der Grundlage des maschinellen Lernens mit Langzeitgedächtnis (LSTM), das die Vorteile der Analyse von Beziehungen zwischen Zeitreihendaten durch seine Gedächtnisfunktion hat, eine auf CNN-LSTM basierende Methode zur Vorhersage von Aktienkursen vor.

In der Zwischenzeit verwenden wir MLP, CNN, RNN, LSTM, CNN-RNN und andere Prognosemodelle, um den Aktienkurs nacheinander vorherzusagen. Außerdem werden die Vorhersageergebnisse dieser Modelle analysiert und verglichen. 
Die in dieser Untersuchung verwendeten Daten betreffen die täglichen Aktienkurse vom 1. Juli 1991 bis zum 31. August 2020, welches 7127 Handelstage bedeutet.

Für die historischen Daten wählen wir acht Merkmale aus, darunter Eröffnungskurs, Höchst-, Tiefst- und Schlusskurs, Volumen, Umsatz, Auf- und Abwärtsbewegungen und Veränderungen. 
Zunächst setzen wir CNN ein, um effizient Merkmale aus den Daten zu extrahieren, nämlich die Artikel der letzten 10 Tage. Dann verwenden wir LSTM, um den Aktienkurs mit den extrahierten Merkmalsdaten vorherzusagen.

Den experimentellen Ergebnissen zufolge kann das CNN-LSTM eine zuverlässige Aktienkursprognose mit der höchsten Vorhersagegenauigkeit liefern.
Diese Prognosemethode liefert nicht nur eine neue Forschungsidee für die Aktienkursprognose, sondern auch praktische Erfahrungen für Wissenschaftler, die Finanzzeitreihendaten untersuchen.

Von allen betrachteten Modellen haben die CNN-LSTM-Modelle bei den Experimenten die besten Ergebnisse erzielt. In diesem Artikel werden wir uns ansehen, wie man ein solches Modell zur Prognose von Finanzzeitreihen erstellt und wie man das erstellte ONNX-Modell in einem MQL5 Expert Advisor verwendet.


1. Aufbau eines Modells

Python bietet eine Reihe von Spezialbibliotheken und damit umfangreiche Möglichkeiten für die Arbeit mit ML-Modellen. Solche Bibliotheken erleichtern die Datenaufbereitung und -verarbeitung erheblich.

Wir empfehlen die Verwendung von GPU-Ressourcen, um die Effizienz von ML-Projekten zu maximieren. Viele Windows-Nutzer hatten Probleme bei der Installation der aktuellen TensorFlow-Version (siehe Kommentare zur Videoanleitung und seiner Textversion). Wir haben TensorFlow 2.10.0 getestet und empfehlen die Verwendung dieser Version. Die GPU-Berechnungen wurden auf einer NVIDIA GeForce RTX 2080 Ti Grafikkarte unter Verwendung der Bibliotheken CUDA 11.2 und CUDNN 8.1.0.7 durchgeführt.


1.1. Installation von Python und Bibliotheken

Wenn Sie Python nicht haben, sollten Sie es installieren. Wir haben die Version 3.9.16 verwendet.

Installieren Sie auch die Bibliotheken (wenn Sie Conda/Anaconda verwenden, führen Sie diese Befehle in Anaconda Prompt aus):

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. Überprüfen der TensorFlow-Version und der GPU

Der folgende Code prüft die installierte TensorFlow-Version und verifiziert, ob es möglich ist, die GPU zur Berechnung der Modelle zu verwenden:

#check tensorflow version
print(tf.__version__)
#check GPU support
print(len(tf.config.list_physical_devices('GPU'))>0)

Wenn die erforderliche Version korrekt installiert ist, wird folgendes Ergebnis angezeigt:

2.10.0
True

Wir haben ein Python-Skript verwendet, um das Modell zu erstellen und zu trainieren. Die einzelnen Schritte dieses Prozesses werden im Folgenden kurz beschrieben.


1.3. Aufbau und Training des Modells

Das Skript beginnt mit dem Import der Python-Bibliotheken, die im Modell verwendet werden sollen.

#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

Prüfen Sie die TensorFlow-Version und die Verfügbarkeit der GPU:

#check tensorflow version
print(tf.__version__)

2.10.0

#check GPU support
print(len(tf.config.list_physical_devices('GPU'))>0)

True

Initialisieren Sie MetaTrader 5 für Operationen aus Python:

#initialize MetaTrader5 for history data
if not mt5.initialize():
    print("initialize() failed, error code =",mt5.last_error())
    quit()

Informationen über das MetaTrader 5-Terminal:

#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.', name='MetaTrader 5', language='English', path='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\

Geben Sie den Pfad zum Speichern des Modells aus (in diesem Beispiel wird das Skript in Jupyter Notebook ausgeführt):

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

Datenpfad zum Speichern des Onnx-Modells C:\Users\user\AppData\Roaming\Python\Python39\site-packages\

Vorbereitung der Zeitangaben zur Abfrage historischer Daten. In unserem Beispiel fordern wir EURUSD H1-Balken für 120 ab dem aktuellen Datum an:

#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)
data start date= 2022-11-28 12:28:39.870685
data end date= 2023-03-28 12:28:39.870685

Anforderung der historische Daten von 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)

Ausgabe der heruntergeladenen Daten:

#check
print(eurusd_rates)


#create dataframe
df = pd.DataFrame(eurusd_rates)

Anzeige von Anfang und Ende des Datenrahmens:

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

Nur Schlusskurse auswählen:

#prepare close prices only
data = df.filter(['close']).values

Ausgabe der Daten:

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

EURUSD H1 Schlusskurs-Chart

Skalierung der Quellpreisdaten auf den Bereich [0,1] mit MinMaxScaler:

#scale data using MinMaxScaler
from sklearn.preprocessing import MinMaxScaler
scaler=MinMaxScaler(feature_range=(0,1))
scaled_data = scaler.fit_transform(data)

Die ersten 80 % der Daten werden für das Training verwendet.

#training size is 80% of the data
training_size = int(len(scaled_data)*0.80) 
print("training size:",training_size)

Trainingsgröße: 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

Die folgende Funktion erstellt Trainingssequenzen:

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

Erstellen der Einstellungen:

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

Tensorformen für Training und Test:

#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

Einstellungen des Modells:

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

Anzeige der Modelleigenschaften:

#show model
model.summary()

Eigenschaften des Modells

Training des Modells:

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

Epoch 1/300
48/48 [==============================] - 8s 49ms/step - loss: 0.0129 - root_mean_squared_error: 0.1136 - val_loss: 0.0065 - val_root_mean_squared_error: 0.0804

...

Epoch 299/300
48/48 [==============================] - 2s 35ms/step - loss: 4.5197e-04 - root_mean_squared_error: 0.0213 - val_loss: 4.2535e-04 - val_root_mean_squared_error: 0.0206
Epoch 300/300
48/48 [==============================] - 2s 32ms/step - loss: 4.2967e-04 - root_mean_squared_error: 0.0207 - val_loss: 4.4040e-04 - val_root_mean_squared_error: 0.0210

fit time = 467.4918096065521  seconds.

Das Training dauerte etwa 8 Minuten.

#show training history keys
history.history.keys()

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

Optimierungsdynamik in den Trainings- und Testdatensätzen:

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

Iterationsdiagramm der Verlustfunktion

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


RMSE-Iterationsdiagramm

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

48/48 [==============================] - 1s 22ms/step - loss: 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/step - loss: 4.4040e-04 - root_mean_squared_error: 0.0210

[0.00044039846397936344, 0.020985672250390053]

Erstellen der Vorhersage auf der Grundlage des Trainingsdaten:

#prediction using training data
train_predict = model.predict(x_train)
plot_y_train = y_train.reshape(-1,1)

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

Ausgabe der tatsächlichen und vorhergesagten Graphen für das Trainingsintervall:

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

Diagramm der Vorhersagen für den Trainingsdatensatz:

Erstellung der Vorhersage für den Testdatensatz:

#prediction using testing data
test_predict = model.predict(x_test)
plot_y_test = y_test.reshape(-1,1)

11/11 [==============================] - 0s 11ms/step

Zur Berechnung der Metriken müssen wir die Daten aus dem Intervall [0,1] umrechnen. Auch hier verwenden wir 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
R2 score     : 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()

Diagramm der Vorhersagen für den Testdatensatz:


Exportieren des Modells in eine onnx-Datei:

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

Der vollständige Python-Skriptcode ist als Jupyter Notebook dem Artikel beigefügt.

In dem Artikel A CNN-LSTM-Based Model to Forecast Stock Prices wurde das beste Ergebnis R^2=0,9646 für Modelle mit der CNN-LSTM-Architektur erzielt. In unserem Beispiel hat das CNN-LSTM-Netzwerk das beste Ergebnis von R^2=0,9684 erzielt. Die Ergebnisse zeigen, dass Modelle dieser Art bei der Lösung von Vorhersageproblemen effizient sein können.

Wir haben ein Beispiel für ein Python-Skript betrachtet, das CNN-LSTM-Modelle zur Vorhersage von Finanzzeitreihen erstellt und trainiert.


2. Verwendung des Modells in MetaTrader 5

2.1. Gut zu wissen, ehe man anfängt

Es gibt zwei Möglichkeiten, ein Modell zu erstellen: Sie können OnnxCreate verwenden, um ein Modell aus einer Onnx-Datei zu erstellen, oder OnnxCreateFromBuffer, um es aus einem Datenfeld zu erstellen.

Wenn ein ONNX-Modell als Ressource in einem EA verwendet wird, müssen Sie den EA jedes Mal neu kompilieren, wenn Sie das Modell ändern.

Nicht alle Modelle haben vollständig definierte Größen für den Eingangs- und/oder Ausgangstensor. Dies ist normalerweise die erste Dimension, die für die Paketgröße verantwortlich ist. Vor dem Ausführen eines Modells, müssen die Größen mit den Funktionen OnnxSetInputShape und OnnxSetOutputShape explizit angeben werden. Die Eingabedaten des Modells sollten in der gleichen Weise aufbereitet werden, wie dies beim Training des Modells geschehen ist.

Für die Eingabe- und Ausgabedaten empfehlen wir die Verwendung von Arrays, Matrizen und/oder Vektoren desselben Typs, die im Modell verwendet werden. In diesem Fall müssen Sie die Daten bei der Ausführung des Modells nicht konvertieren. Wenn die Daten nicht in dem gewünschten Typ dargestellt werden können, werden sie automatisch konvertiert.

Verwenden Sie OnnxRun zur Inferenz (Ausführung) Ihres Modells. Beachten Sie, dass ein Modell mehrfach ausgeführt werden kann. Nach der Verwendung des Modells, kann es mit der Funktion OnnxRelease freigegeben werden.

Vollständige Dokumentation für ONNX-Modelle in MQL5.


2.2. Lesen einer onnx-Datei und Abrufen von Informationen über Eingänge und Ausgänge

Um unser Modell verwenden zu können, müssen wir den Speicherort des Modells, den Typ und die Form der Eingabedaten sowie den Typ und die Form der Ausgabedaten kennen. Laut dem zuvor erstellten Skript befindet sich model.eurusd.H1.120.onnx im selben Ordner wie das Python-Skript, das die onnx-Datei erzeugt hat. Input ist float32, 120 normalisierte Close-Preise (für die Arbeit mit der Losgröße 1); Output ist float32, ein normalisierter Preis, der vom Modell vorhergesagt wird.

Wir haben auch die onnx-Datei im Ordner MQL5\Files erstellt, um die Eingabe- und Ausgabedaten des Modells mithilfe eines MQL5-Skripts zu erhalten.

//+------------------------------------------------------------------+
//|                                                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);
  }
//+------------------------------------------------------------------+

Im Dateiauswahlfenster wählten wir die in MQL5\Files gespeicherte onnx-Datei aus, erstellten mit OnnxCreate ein Modell aus der Datei und erhielten die folgenden Informationen.

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

Da der Debugging-Modus aktiviert wurde

   long session_handle=OnnxCreate(file_names[0],ONNX_DEBUG_LOGS);

gibt es Protokolle mit dem Präfix ONNX.

Wir sehen, dass das Modell tatsächlich eine Eingabe und eine Ausgabe hat. Hier sind die erste Dimension des Eingangstensors und die erste Dimension des Ausgangstensors nicht definiert. Es wird angenommen, dass diese Dimensionen für die Losgröße verantwortlich sind. Daher müssen wir vor der Inferenz des Modells ausdrücklich angeben, mit welchen Größen wir arbeiten wollen (OnnxSetInputShape und OnnxSetOutputShape). In der Regel wird nur ein Datensatz in das Modell eingegeben. Ein detailliertes Beispiel finden Sie im nächsten Abschnitt „Ein Beispiel für die Verwendung eines ONNX-Modells in einem Trading EA“.

Bei der Aufbereitung der Daten ist es nicht notwendig, ein Array mit den Abmessungen [1, 120, 1] zu verwenden. Wir können ein eindimensionales Array oder einen Vektor mit 120-Elementen eingeben.


2.3. Ein Beispiel für die Verwendung eines ONNX-Modells in einem Handels-EA

Erklärungen und Definitionen

#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

OnInit-Funktion

//+------------------------------------------------------------------+
//| 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);
  }

Wir arbeiten nur mit EURUSD, H1, weil wir die Daten des aktuellen Symbols/des aktuellen Zeitrahmens verwenden.

Unser Modell ist in dem EA als Ressource enthalten. Der EA ist völlig autark und muss keine externe onnx-Datei lesen. Ein Modell wird aus dem Ressourcen-Array erstellt.

Die Formen der Eingangs- und Ausgangsdaten müssen explizit definiert werden.

Die Funktion 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();
  }

Wir definieren den Beginn eines neuen Tages. Der Tagesbeginn wird verwendet, um die Tiefst- und Höchstwerte der letzten 120 Tage zu aktualisieren und die Preise in der 120-Stunden-Sequenz zu normalisieren. Das Modell wurde unter diesen Bedingungen trainiert, die wir bei der Vorbereitung der Eingabedaten beachten müssen.

//+------------------------------------------------------------------+
//| 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();
  }

Falls erforderlich, können wir Low und High im Laufe des Tages ändern.

Vorhersagefunktion:

//+------------------------------------------------------------------+
//| 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;
     }
  }

Zunächst prüfen wir, ob wir normalisieren können. Die Normalisierung ist wie bei der Python-Funktion MinMaxScaler implementiert.

#scale data
from sklearn.preprocessing import MinMaxScaler
scaler=MinMaxScaler(feature_range=(0,1))
scaled_data = scaler.fit_transform(data)

Der Normalisierungscode ist also sehr einfach und überschaubar.

Die Vektoren für die Eingabe von Daten und für den Empfang des Ergebnisses sind statisch organisiert. Dies garantiert einen nicht verschiebbaren Puffer, der während der gesamten Programmlaufzeit existiert. Daher werden die Eingangs- und Ausgangstensoren des ONNX-Modells nicht jedes Mal neu erstellt, wenn wir das Modell ausführen.

Die Schlüsselfunktion ist OnnxRun. Das Flag ONNX_NO_CONVERSION zeigt an, dass die Ein- und Ausgabedaten nicht konvertiert werden müssen, da der MQL5-Float-Typ genau dem ONNX_DATA_TYPE_FLOAT entspricht. Das ONNX_DEBUG-Flag ist nicht gesetzt.

Danach denormalisieren wir die erhaltenen Daten in den vorhergesagten Preis und bestimmen die Klasse: ob der Preis steigen, fallen oder sich nicht verändern wird.


Die Handelsstrategie ist einfach. Zu Beginn jeder Stunde überprüfen wir die Preisprognose für das Ende dieser Stunde. Wenn der prognostizierte Preis steigt, kaufen wir. Wenn das Modell eine Abwärtsbewegung vorhersagt, verkaufen wir.

//+------------------------------------------------------------------+
//| Check for open position conditions                               |
//+------------------------------------------------------------------+
void CheckForOpen(void)
  {
   ENUM_ORDER_TYPE signal=WRONG_VALUE;
//--- check signals
   if(ExtPredictedClass==PRICE_DOWN)
      signal=ORDER_TYPE_SELL;    // sell condition
   else
     {
      if(ExtPredictedClass==PRICE_UP)
         signal=ORDER_TYPE_BUY;  // buy condition
     }

//--- open position if possible according to signal
   if(signal!=WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
      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();
     }
  }

Nun wollen wir die Leistung des EA im Strategy Tester überprüfen. Um den EA von Anfang des Jahres an zu testen, sollte das Modell mit früheren Daten trainiert werden. Daher haben wir das Python-Skript leicht verändert, indem wir ungenutzte Teile entfernt und das Enddatum des Trainings so geändert haben, dass es sich nicht mit dem Testzeitraum überschneidet.

Das Skript ONNX.eurusd.H1.120.Training.py befindet sich im Unterordner Python und wird direkt in MetaEditor ausgeführt. Das resultierende ONNX-Modell wird im gleichen Python-Unterordner gespeichert und bei der EA-Kompilierung als Ressource verwendet.

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


Testen eines auf dem ONNX-Modell basierenden EA

Nun wollen wir den EA mit historischen Daten im Strategy Tester testen. Wir geben die gleichen Parameter an, die wir zum Trainieren des Modells verwendet haben: das EURUSD-Symbol und den H1-Zeitrahmen.

Das Testintervall umfasst nicht den Trainingszeitraum: Es beginnt mit dem Jahresanfang (01/01/2023). 

EA-Testeinstellungen


Gemäß der Strategie werden die Signale einmal zu Beginn jeder Stunde überprüft (der EA überwacht das Entstehen eines neuen Balkens), daher spielt der Tick-Modellierungsmodus keine Rolle. OnTick wird im Tester einmal pro Balken verarbeitet.

//+------------------------------------------------------------------+
//| 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();
  }


Mit diesem Verfahren dauert der dreimonatige Test nur wenige Sekunden. Nachstehend sind die Ergebnisse aufgeführt.

EA-Testergebnisse



Ändern wir nun die Handelsstrategie so, dass die Position durch ein Signal eröffnet und durch Stop Loss (SL) oder Take Profit (TP) geschlossen wird.

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, was bedeutet, dass SL- und TP-Level bei Positionseröffnung gesetzt werden.

EA-Eingangsparameter mit SL/TP


Die Ergebnisse der Tests mit SL/TP-Werten für den gleichen Zeitraum:

Testergebnisse des Expert Advisors bei Verwendung von SL- und TP-Level


Der vollständige Quellcode des EA und das trainierte Modell (bis zum Beginn des Jahres 2023) sind im Anhang enthalten.


Schlussfolgerung

Der Artikel zeigt, dass es nicht schwierig ist, ONNX-Modelle in MQL5-Programmen zu verwenden. Eigentlich ist die Anwendung von Modellen der einfachste Teil, während es viel schwieriger ist, ein angemessenes ONNX-Modell zu erhalten.

Bitte beachten Sie, dass das in diesem Artikel verwendete Modell nur zu Demonstrationszwecken zur Verfügung gestellt wird, um zu zeigen, wie man mit ONNX-Modellen in der Sprache MQL5 arbeitet. Der in diesem Artikel vorgestellte Expert Advisor ist nicht für den realen Handel bestimmt.

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/12373

Beigefügte Dateien |
MQL5.zip (1243.88 KB)
Backpropagation von Neuronalen Netze mit MQL5-Matrizen Backpropagation von Neuronalen Netze mit MQL5-Matrizen
Der Artikel beschreibt die Theorie und Praxis der Anwendung des Backpropagation-Algorithmus in MQL5 unter Verwendung von Matrizen. Es bietet vorgefertigte Klassen zusammen mit Beispielen von Skripten, Indikatoren und Expert Advisors.
Erstellung einer umfassenden Owl-Strategie des Handels Erstellung einer umfassenden Owl-Strategie des Handels
Meine Strategie basiert auf den klassischen Handelsgrundlagen und der Verfeinerung von Indikatoren, die in allen Arten von Märkten weit verbreitet sind. Es handelt sich um ein fertiges Instrument, mit dem die neue profitable Handelsstrategie voll ausgeschöpft werden kann.
Ein Beispiel für die Zusammenstellung von ONNX-Modellen in MQL5 Ein Beispiel für die Zusammenstellung von ONNX-Modellen in MQL5
ONNX (Open Neural Network eXchange) ist ein offenes Format zur Darstellung neuronaler Netze. In diesem Artikel zeigen wir Ihnen, wie Sie zwei ONNX-Modelle gleichzeitig in einem Expert Advisor verwenden können.
Algorithmen zur Optimierung mit Populationen: der Gravitationssuchalgorithmus (GSA) Algorithmen zur Optimierung mit Populationen: der Gravitationssuchalgorithmus (GSA)
GSA ist ein von der unbelebten Natur inspirierter Populationsoptimierungsalgorithmus. Dank des in den Algorithmus implementierten Newton'schen Gravitationsgesetzes können wir dank der hohen Zuverlässigkeit der Modellierung der Interaktion physikalischer Körper den bezaubernden Tanz von Planetensystemen und Galaxienhaufen beobachten. In diesem Artikel möchte ich einen der interessantesten und originellsten Optimierungsalgorithmen vorstellen. Der Simulator für die Bewegung von Raumobjekten ist ebenfalls vorhanden.