English Русский 中文 Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
Uso de modelos ONNX en MQL5

Uso de modelos ONNX en MQL5

MetaTrader 5Aprendizaje automático | 10 julio 2023, 16:57
486 0
MetaQuotes
MetaQuotes

Introducción

En el artículo "A CNN-LSTM-Based Model to Forecast Stock Prices" (autores Wenjie Lu, Jiazheng Li, Yifan Li, Aijun Sun, Jingyang Wang, revista Complexity, vol. II. 2020, Article ID 6622927, 10 pages, 2020), se compararon distintos modelos de previsión de precios de acciones.

Stock price data have the characteristics of time series.

At the same time, based on machine learning long short-term memory (LSTM) which has the advantages of analyzing relationships among time series data through its memory function, we propose a forecasting method of stock price based on CNN-LSTM.

In the meanwhile, we use MLP, CNN, RNN, LSTM, CNN-RNN, and other forecasting models to predict the stock price one by one. Moreover, the forecasting results of these models are analyzed and compared. 
The data utilized in this research concern the daily stock prices from July 1, 1991, to August 31, 2020, including 7127 trading days.

In terms of historical data, we choose eight features, including opening price, highest price, lowest price, closing price, volume, turnover, ups and downs, and change. 
Firstly, we adopt CNN to efficiently extract features from the data, which are the items of the previous 10 days.  And then, we adopt LSTM to predict the stock price with the extracted feature data.

According to the experimental results, the CNN-LSTM can provide a reliable stock price forecasting with the highest prediction accuracy.
This forecasting method not only provides a new research idea for stock price forecasting but also provides practical experience for scholars to study financial time series data.

De esta forma, entre los modelos analizados, los de tipo CNN-LSTM mostraron los mejores resultados. En este artículo, consideraremos el proceso de creación de un modelo de este tipo para pronosticar series temporales financieras, y también el uso del modelo ONNX creado en un asesor experto MQL5.


1. Construyendo el modelo

Gracias a sus bibliotecas especializadas, Python proporciona una amplia gama de posibilidades para trabajar con modelos de aprendizaje automático. Las bibliotecas facilitan enormemente la preparación y el tratamiento de los datos.

Para proyectos de aprendizaje automático completos, se recomienda usar las capacidades de la GPU. Muchos usuarios de Windows han encontrado problemas al instalar la versión actual de TensorFlow (Ver comentarios sobre las instrucciones de vídeo y la versión de texto), por lo que hemos probado y recomendamos usar TensorFlow 2.10.0. Los cálculos en la GPU se ha realizado en una tarjeta gráfica NVIDIA GeForce RTX 2080 Ti utilizando las bibliotecas CUDA 11.2 y CUDNN 8.1.0.7.


1.1. Instalación de Python y las bibliotecas

Si el lenguaje Python no está instalado, deberá hacerlo (nosotros hemos utilizado la versión 3.9.16).

A continuación, instale las bibliotecas (si usa Conda/Anaconda, estos comandos deberán ejecutarse en 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. Comprobación de TensorFlow y la versión de la GPU

Código para comprobar la versión instalada de TensorFlow y la capacidad de uso de la GPU para calcular modelos:

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

Si todo está correctamente configurado, el resultado debería ser el siguiente

2.10.0
True

La creación y el entrenamiento del modelo se realizan con la ayuda de un script de Python; los pasos de este proceso se comentan brevemente a continuación.


1.3. Creación y entrenamiento del modelo

El script comienza importando las bibliotecas Python que se van a utilizar.

#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

Comprobando la versión de TensorFlow y la disponibilidad de la GPU:

#check tensorflow version
print(tf.__version__)

2.10.0

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

True

Inicializando MetaTrader 5 para trabajar desde Python:

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

Información sobre el 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.', 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\

Mostramos la ruta para guardar el modelo (en este ejemplo, el script se ha ejecutado en 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)

data path to save onnx model C:\Users\user\AppData\Roaming\Python\Python39\site-packages\

Preparamos las fechas para solicitar los datos históricos. En este caso, barras de hora de EURUSD durante 120 días a partir de la fecha actual:

#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

Solicitamos los datos históricos para 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)

Mostramos los datos descargados:

#check
print(eurusd_rates)


#create dataframe
df = pd.DataFrame(eurusd_rates)

Indicamos el principio y el final del frame de datos:

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

Muestra solo de los precios close:

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

Visualización de datos:

#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 gráfico de precios de cierre

Convertimos los datos originales del precio al rango [0,1] utilizando MinMaxScaler:

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

El primer 80% de los datos se usará para el entrenamiento.

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

training size: 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

Función para crear secuencias de entrenamiento:

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

Realizamos su construcción:

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

Formas de los tensores para el entrenamiento y la prueba:

#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

Configura el modelo:

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

Da salida a las propiedades del modelo:

#show model
model.summary()

Propiedades del modelo

Entrenamiento del modelo:

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

En este caso, el entrenamiento ha durado unos 8 minutos.

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

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

Dinámica de optimización con los conjuntos de entrenamiento y prueba:

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

Gráfico de iteraciones de la función LOSS

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


Gráfico de iteraciones RMSE

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

Generación de un pronóstico con la muestra de entrenamiento:

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

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

Representación de los gráficos (modelo real y predictivo) en el intervalo de entrenamiento:

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

Gráfico de pronóstico de la muestra de entrenamiento

Generación de pronósticos con la muestra de prueba:

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

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

Para calcular las métricas necesitamos convertir los datos del intervalo [0,1], también usaremos MinMaxScaler para hacer esto.

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

Gráfico de pronóstico de la muestra de entrenamiento


Exportación del modelo a un archivo 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()

El código completo del script Python en forma de Jupyter Notebook se adjunta al artículo.

En el artículo "A CNN-LSTM-Based Model to Forecast Stock Prices" se obtuvo un mejor valor de R^2=0,9646 para los modelos con arquitectura CNN-LSTM, en nuestro ejemplo, la red CNN-LSTM ha mostrado un mejor resultado de R^2=0,9684. Así pues, los modelos de este tipo pueden resolver con eficacia los problemas de pronóstico.

Asimismo, se presenta un ejemplo de script en Python para crear y entrenar modelos CNN-LSTM para el pronóstico de series temporales financieras.


2. Uso del modelo en MetaTrader 5

2.1. Antes de empezar a usarlo. Lo que debe saber

Hay dos formas de crear un modelo: OnnxCreate para crear un modelo a partir de un archivo onnx y OnnxCreateFromBuffer para crearlo a partir de un array de datos.

Si el modelo ONNX se utiliza como recurso en un asesor, este deberá recompilarse tras cada cambio.

No todos los modelos tienen tamaños de tensor de entrada y/o salida totalmente definidos. Como norma general, la primera dimensionalidad es responsable del tamaño del paquete. Antes de iniciar el modelo, deberán especificarse explícitamente las dimensiones que se van a utilizar. Para ello, se necesitarán las funciones OnnxSetInputShape y OnnxSetOutputShape.

Los datos de entrada del modelo deberán prepararse del mismo modo en que se usaron para entrenar el modelo.

Para los datos de entrada y salida, le recomendamos utilizar arrays y/o vectores del mismo tipo que en el modelo. En este caso, no tendrá que usar la conversión de datos al ejecutar el modelo. Si los datos no pueden representarse en el tipo requerido, se aplicará la conversión automática.

El modelo (o inferencia) se inicia con la función OnnxRun. El modelo puede ejecutarse varias veces.

Una vez usado el modelo, deberá liberarse mediante la función OnnxRelease.

Documentación completa sobre los modelos ONNX en MQL5.


2.2. Lectura desde un archivo onnx y recuperación de los datos de entrada y salida

Por lo tanto, debemos aplicar el modelo que hemos derivado. Para ello, tenemos que saber dónde tomar el modelo, el tipo y la dimensionalidad de los datos de entrada y el tipo y la dimensionalidad de los datos de salida. Nosotros mismos hemos escrito el script de entrenamiento, por lo que sabemos que model.eurusd.H1.120.onnx se encuentra junto al script python que generó el archivo onnx. Los datos de entrada son de tipo float32, 120 precios Close normalizados (si trabajamos con un tamaño de paquete igual a 1), los datos de salida son del tipo float32, un precio normalizado predicho por el modelo.

También hemos creado a propósito un archivo onnx en la carpeta MQL5\Files, de forma que podamos utilizar el script MQL5 para obtener información completa sobre la entrada y salida del modelo.

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

En la ventana de selección de archivos, hemos tomado nuestro archivo onnx almacenado en la carpeta MQL5\Files, hemos creado un modelo a partir del archivo (OnnxCreate) y obtenido esta información.

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

Desde que solicitamos la información de depuración,

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

hemos obtenido varios mensajes con el prefijo ONNX.

Podemos ver que el modelo tiene realmente una entrada y una salida. La primera dimensionalidad del tensor de entrada y la primera dimensionalidad del tensor de salida no están definidas. Se supone que estas dimensionalidades son las responsables del tamaño del lote. Por lo tanto, antes de iniciar el modelo para su ejecución (inferencia), necesitamos especificar explícitamente con qué dimensiones vamos a trabajar (OnnxSetInputShape, OnnxSetOutputShape). Por regla general, solo suministramos un lote de datos en la entrada. En el siguiente párrafo de "Ejemplo de utilización del modelo ONNX en un asesor comercial" se ofrece un ejemplo detallado.

Además, no necesitamos utilizar un array de dimensionalidades [1, 120, 1] para preparar los datos de entrada. Podemos introducir un array unidimensional o un vector de 120 elementos


2.3. Ejemplo de uso del modelo ONNX en un asesor comercial

Declaraciones y definiciones preliminares

#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

Función 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,0,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);
  }

Solo trabajaremos con EURUSD,H1. Simplemente porque estamos utilizando datos del símbolo-periodo actual.

Nuestro modelo se incluye en el asesor experto como recurso. El asesor es autosuficiente y no hay necesidad de leer el archivo onnx desde el exterior. Creamos el modelo directamente a partir del array de recursos.

Asegúrese de definir explícitamente los formularios de los datos de entrada y salida.

Función 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();
  }

Definimos el comienzo de un nuevo día. Necesitamos esto para actualizar el mínimo y el máximo de la secuencia de 120 días para normalizar los precios de la secuencia de 120 horas. Hemos entrenado el modelo en estas condiciones, por lo que debemos seguir las normas de preparación de los datos de entrada.

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

Luego modificaremos el mínimo y el máximo durante el día según sea necesario.

Función de pronóstico:

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

Primero comprobamos si podemos realizar la normalización. La normalización se efectúa de forma similar a MinMaxScaler de Python.

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

Como consecuencia, el código de normalización resulta muy obvio y sencillo.

Los vectores para los datos de entrada y para obtener el resultado han sido especialmente organizados como estáticos. De esta forma se garantiza la existencia de un búfer inamovible en la memoria durante toda la vida del programa. Así, en el modelo ONNX, los tensores de entrada y salida no se crearán nuevamente cada vez que se ejecute el modelo.

La función central será OnnxRun. La bandera ONNX_NO_CONVERSION indica que los datos, tanto de entrada como de salida, no deben ser convertidos, ya que el tipo float en MQL5 coincide exactamente con el tipo ONNX_DATA_TYPE_FLOAT. El indicador ONNX_DEBUG no ha sido establecido.

A continuación, diferenciamos los datos en el precio previsto y determinamos si el precio subirá, bajará o se mantendrá igual.


La estrategia comercial es sencilla. Al principio de cada hora, comprobamos el pronóstico de los precios al final de la hora. Si se prevé que el precio subirá, compraremos. Por el contrario, si se prevé que el precio bajará, venderemos.

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

Ahora debemos comprobar que el experto funciona. Esto puede hacerse en el simulador. Pero para probar el asesor desde principios de año, deberemos entrenar el modelo con los datos de antes de principios de año. Así que modificaremos ligeramente el script de Python eliminando todo lo superfluo y sustituyendo la fecha final.

El script ONNX.eurusd.H1.120.Training.py se encuentra en la subcarpeta Python y se ejecuta directamente en el MetaEditor. El modelo ONNX resultante se escribirá junto a él en la misma subcarpeta de Python y se utilizará como recurso al compilar el asesor.

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


Prueba del asesor basado en el modelo ONNX

Es hora de poner a prueba el asesor con datos históricos en el simulador de estrategias. Estableceremos los parámetros con los que se ha entrenado el modelo: el símbolo EURUSD y el marco temporal H1.

A continuación, estableceremos el intervalo más allá del periodo de entrenamiento del modelo (partiendo del principio del año, desde el 01.01.2023) e iniciaremos las pruebas. 

Ajustes de prueba del asesor


Dado que, según la estrategia, las señales se comprueban una vez al principio de cada hora (el asesor experto comprueba la aparición de una nueva barra), el modo de simulación de ticks no importará: OnTick se procesará en el simulador solo una vez por barra.

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


Con este tipo de prueba, la simulación de 3 meses durará unos segundos, y obtendremos los resultados de inmediato.

Resultado de la prueba del asesor



Ahora cambiaremos la estrategia comercial, es decir, abriremos una posición por señal y cerraremos la posición por stop loss o take profit.

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

Después estableceremos el parámetro InpUseStops = true, esto indica que los niveles TP y SL se establecen al abrirse una posición.

Parámetros de entrada para un asesor que utiliza SL/TP


A continuación le mostramos los resultados de las pruebas con los niveles SL/TP de principios de año.

Resultados de la prueba del asesor experto con niveles SL y TP


Adjuntamos al artículo el código fuente completo del asesor y el modelo entrenado a inicios de 2023.


Conclusión

En el artículo de hoy, hemos demostrado que no hay nada complicado en el uso de modelos ONNX en programas MQL5, al contrario, es algo muy sencillo. Resulta mucho más difícil conseguir un modelo ONNX adecuado.

Tenga en cuenta que el modelo usado en este artículo es de carácter exclusivamente demostrativo, para trabajar con modelos ONNX utilizando MQL5. El asesor no ha sido diseñado para operar en cuentas reales.

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/12373

Archivos adjuntos |
MQL5.zip (1243.88 KB)
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 06): Primeras mejoras (I) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 06): Primeras mejoras (I)
En este artículo empezaremos a estabilizar todo el sistema, porque sin eso corremos el riesgo de no poder cumplir los siguientes pasos.
Algoritmos de optimización de la población: Algoritmo electromagnético (ElectroMagnetism-like algorithm, ЕМ) Algoritmos de optimización de la población: Algoritmo electromagnético (ElectroMagnetism-like algorithm, ЕМ)
El artículo describe los principios, métodos y posibilidades del uso del algoritmo electromagnético (EM) en diversos problemas de optimización. El algoritmo EM es una herramienta de optimización eficiente capaz de trabajar con grandes cantidades de datos y funciones multidimensionales.
Teoría de Categorías en MQL5 (Parte 5): Ecualizadores Teoría de Categorías en MQL5 (Parte 5): Ecualizadores
La teoría de categorías es un apartado diverso y en expansión de las matemáticas, que solo recientemente ha comenzado a ser trabajado por la comunidad MQL5. Esta serie de artículos tiene por objetivo repasar algunos de sus conceptos para crear una biblioteca abierta y seguir usando este maravilloso apartado en la creación de estrategias comerciales.
Teoría de categorías en MQL5 (Parte 4): Intervalos, experimentos y composiciones Teoría de categorías en MQL5 (Parte 4): Intervalos, experimentos y composiciones
La teoría de categorías es una rama de las matemáticas diversa y en expansión, relativamente inexplorada aún en la comunidad MQL5. Esta serie de artículos tiene como objetivo describir algunos de sus conceptos para crear una biblioteca abierta y seguir utilizando esta maravillosa sección para crear estrategias comerciales.