Uso de modelos ONNX em MQL5
Introdução
Os autores do artigo 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) compararam vários modelos de previsão de preços de ações:
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.
Entre todos os modelos considerados, os modelos CNN-LSTM geraram os melhores resultados durante os experimentos. Neste artigo, veremos como criar um modelo desse tipo para prever séries temporais financeiras e como usar o modelo ONNX criado em um Expert Advisor MQL5.
1. Construção do modelo
O Python conta com um conjunto de bibliotecas especializadas e, nesse sentido, oferece amplos recursos para trabalhar com modelos de aprendizado de máquina. Essas bibliotecas facilitam muito a preparação e o processamento de dados.
Recomendamos o uso de recursos de GPU para maximizar a eficiência dos projetos de aprendizado de máquina. Muitos usuários do Windows tiveram problemas ao tentar instalar a versão atual do TensorFlow (veja os comentários sobre o guia em vídeo e sua versão em texto). Por isso, testamos o TensorFlow 2.10.0 e recomendamos o uso dessa versão. Os cálculos de GPU foram executados na placa de vídeo NVIDIA GeForce RTX 2080 Ti usando as bibliotecas CUDA 11.2 e CUDNN 8.1.0.7.
1.1. Instalação do Python e das bibliotecas
Se você não tiver o Python, deverá instalá-lo. Usamos a versão 3.9.16.
Além disso, instale as bibliotecas (se estiver usando o Conda/Anaconda, execute esses comandos no prompt do Anaconda):
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. Verificação da versão do TensorFlow e da GPU
O código abaixo verifica a versão instalada do TensorFlow e confirma se é possível usar a GPU para computar os modelos:
#check tensorflow version print(tf.__version__) #check GPU support print(len(tf.config.list_physical_devices('GPU'))>0)
Se tudo estiver configurado corretamente, o resultado deverá ser o seguinte
True
A criação e o treinamento do modelo são feitos por um script Python, e as etapas desse processo são discutidas brevemente a seguir.
1.3. Criação e treinamento do modelo
O script começa importando as bibliotecas Python a serem usadas.
#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
Verifica a versão do TensorFlow e a disponibilidade da GPU:
#check tensorflow version print(tf.__version__)
2.10.0
#check GPU support print(len(tf.config.list_physical_devices('GPU'))>0)
True
Inicialize o MetaTrader 5 para operações a partir do Python:
#initialize MetaTrader5 for history data if not mt5.initialize(): print("initialize() failed, error code =",mt5.last_error()) quit()
Informações sobre o 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)
Imprima o caminho para salvar o modelo (neste exemplo, o script é executado no 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)
Caminho de dados para salvar o modelo onnx C:\Users\user\AppData\Roaming\Python\Python39\site-packages\
Prepare as datas para solicitar dados históricos. Em nosso exemplo, solicitamos barras H1 do EURUSD para 120 a partir da data atual:
#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 end date= 2023-03-28 12:28:39.870685
Solicite dados 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)
Exibimos os dados baixados:
#check print(eurusd_rates)
#create dataframe df = pd.DataFrame(eurusd_rates)
Mostramos o início e o fim do 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)
Selecionamos apenas os preços de fechamento:
#prepare close prices only data = df.filter(['close']).values
Exibição de dados:
#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()
Dimensione os dados de preço brutos para o intervalo [0,1] usando MinMaxScaler:
#scale data using MinMaxScaler from sklearn.preprocessing import MinMaxScaler scaler=MinMaxScaler(feature_range=(0,1)) scaled_data = scaler.fit_transform(data)
Os primeiros 80% dos dados serão usados para treinamento.
#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
A função a seguir cria sequências de treinamento:
#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)
Constru]imos os conjuntos:
#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 tensor para treinamento e teste:
#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
Definimos o 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()])
Exibimos as propriedades do modelo:
#show model model.summary()
Treinamento do 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.
Nesse caso, o treinamento levou cerca de 8 minutos.
#show training history keys history.history.keys()
Dinâmica de otimização nos conjuntos de treinamento e teste:
#show iteration-loss graph for training and validation plt.figure(figsize = (18,10)) plt.plot(history.history['loss'],label='Training Loss',color='b') plt.plot(history.history['val_loss'],label='Validation-loss',color='g') plt.xlabel("Iteration") plt.ylabel("Loss") plt.title("LOSS") plt.legend()
#show iteration-rmse graph for training and validation plt.figure(figsize = (18,10)) plt.plot(history.history['root_mean_squared_error'],label='Training RMSE',color='b') plt.plot(history.history['val_root_mean_squared_error'],label='Validation-RMSE',color='g') plt.xlabel("Iteration") plt.ylabel("RMSE") plt.title("RMSE") plt.legend()
#evaluate training data model.evaluate(x_train,y_train, batch_size = 32)
[0.00029911252204328775, 0.01729486882686615]
#evaluate testing data model.evaluate(x_test,y_test, batch_size = 32)
10/10 [==============================] - 0s 31ms/step - loss: 4.4040e-04 - root_mean_squared_error: 0.0210
[0.00044039846397936344, 0.020985672250390053]
Geração de uma previsão em uma amostra de treinamento:
#prediction using training data train_predict = model.predict(x_train) plot_y_train = y_train.reshape(-1,1)
Exibe gráficos reais e previstos para o intervalo de treinamento:
#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()
Geração de previsão na amostra de teste:
#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 as métricas, precisamos converter os dados do intervalo [0,1]; também usamos o MinMaxScaler para fazer isso.
#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()
Exportamos o modelo para um arquivo 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()
O código completo do script Python como Jupyter Notebook está anexado ao artigo.
No artigoCNN-LSTM-Based Model to Forecast Stock Prices", o melhor valor R^2=0,9646 foi obtido para modelos com arquitetura CNN-LSTM; em nosso exemplo, a rede CNN-LSTM apresentou o melhor resultado de R^2=0,9684. De acordo com os resultados, modelos desse tipo podem ser eficientes na solução de problemas de previsão.
Consideramos um exemplo de um script Python que cria e treina modelos CNN-LSTM para prever séries temporais financeiras.
2. Usando o modelo no MetaTrader 5
2.1. Antes de começar a usá-lo. O que você precisa saber
Há duas maneiras de criar um modelo: Você pode usar o OnnxCreate para criar um modelo a partir de um arquivo ONNX ou o OnnxCreateFromBuffer para criá-lo a partir de um array de dados.
Se o modelo ONNX for usado como recurso em um EA, será necessário recompilar o EA sempre que o modelo for alterado.
Nem todos os modelos têm tensor de entrada e/ou saída de tamanhos totalmente definidos. Normalmente, essa é a primeira dimensão responsável pelo tamanho do pacote. Antes de executar um modelo, você deve especificar explicitamente os tamanhos usando as funções OnnxSetInputShape e OnnxSetOutputShape.
Os dados de entrada do modelo devem ser preparados da mesma forma que foram feitos no treinamento do modelo.
Para os dados de entrada e saída, recomendamos o uso de matrizes, vetores e/ou matrizes do mesmo tipo que são usados no modelo. Nesse caso, você não precisará converter os dados ao executar o modelo. Se os dados não puderem ser representados no tipo necessário, eles serão automaticamente convertidos.
Use o OnnxRun para inferir (executar) seu modelo. Observe que um modelo pode ser executado várias vezes.
Depois de usar o modelo, libere-o usando a função OnnxRelease.
Documentação completa dos modelos ONNX em MQL5..
2.2. Leitura do arquivo onnx e recuperação de informações de entrada e saída
Para usar nosso modelo, precisamos saber a localização do modelo, o tipo e a forma dos dados de entrada, bem como o tipo e a forma dos dados de saída.De acordo com o script criado anteriormente, model.eurusd.H1.120.onnx está localizado na mesma pasta com o script Python que gerou o arquivo onnx.A entrada é float32, 120 preços de fechamento normalizados (para trabalhar com o tamanho do lote igual a 1); a saída é float32, que é um preço normalizado previsto pelo modelo.
Também criamos o arquivo onnx na pasta MQL5\Files para obter os dados de entrada e saída do modelo usando um 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); } //+------------------------------------------------------------------+
Na janela de seleção de arquivos, selecionamos o arquivo onnx salvo em MQL5\Files, criamos um modelo a partir do arquivo usando o OnnxCreate e obtivemos essas informações.
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
Como solicitamos as informações de depuração,
long session_handle=OnnxCreate(file_names[0],ONNX_DEBUG_LOGS);
recebemos várias mensagens com o prefixo ONNX.
Vemos que o modelo realmente tem uma entrada e uma saída. Aqui, a primeira dimensão do tensor de entrada e a primeira dimensão do tensor de saída não estão definidas. Supõe-se que essas dimensões sejam responsáveis pelo tamanho do lote. Portanto, antes de inferir o modelo, devemos especificar explicitamente com quais tamanhos vamos trabalhar (OnnxSetInputShape e OnnxSetOutputShape). Normalmente, o modelo é alimentado apenas com um conjunto de dados. Um exemplo detalhado é dado no parágrafo seguinte "Exemplo de uso do modelo ONNX em um EA".
Ao preparar os dados, não é necessário usar um array com dimensões [1, 120, 1]. Podemos inserir um array unidimensional ou um vetor de 120 elementos.
2.3. Exemplo de uso do modelo ONNX em um EA
Declarações e definições
#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
Função 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); }
Trabalhamos somente com EURUSD, H1, porque usamos os dados do símbolo/período atual.
Nosso modelo está incluído no EA como um recurso. O EA é totalmente autossuficiente e não requer a leitura de um arquivo onnx externo. Um modelo é criado a partir do array de recursos.
As formas de dados de entrada e saída devem ser definidas explicitamente.
Função 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 o início de um novo dia. O início do dia é usado para atualizar os valores mínimo e máximo da sequência de 120 dias para normalizar os preços na sequência de 120 horas. O modelo foi treinado nessas condições, que devemos seguir ao preparar os dados 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(); }
Se necessário, podemos modificar o Low e o High ao longo do dia.
Função de previsão:
//+------------------------------------------------------------------+ //| 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; } }
Primeiro, verificamos se podemos normalizar. A normalização é implementada como na função MinMaxScaler do Python.
#scale data from sklearn.preprocessing import MinMaxScaler scaler=MinMaxScaler(feature_range=(0,1)) scaled_data = scaler.fit_transform(data)
Portanto, o código de normalização é muito simples e direto.
Os vetores para dados de entrada e para receber o resultado são definidos como estáticos. Isso garante um buffer não realocável que existe durante todo o tempo de vida do programa. Assim, os tensores de entrada e saída do modelo ONNX não são recriados toda vez que o modelo é executado.
A função principal é OnnxRun. O sinalizador ONNX_NO_CONVERSION indica que os dados de entrada e saída não devem ser convertidos, pois o tipo float MQL5 corresponde exatamente a ONNX_DATA_TYPE_FLOAT. O sinalizador ONNX_DEBUG não é definido.
Depois disso, desnormalizamos os dados obtidos no preço previsto e determinamos a classe: se o preço subirá, descerá ou não mudará.
A estratégia de negociação é simples. No início de cada hora, verificamos a previsão de preço para o final dessa hora. Se o preço previsto subir, compramos. Se o modelo prevê um movimento de queda, vendemos.
//+------------------------------------------------------------------+ //| 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(); } }
Agora, vamos verificar o desempenho do EA no testador de estratégias. Para testar o EA desde o início do ano, o modelo deve ser treinado usando dados anteriores. Portanto, modificamos ligeiramente o script Python, removendo partes não utilizadas e alterando a data de término do treinamento para que não se sobreponha ao período de teste.
O script ONNX.eurusd.H1.120.Training.py está localizado na subpasta Python e é executado diretamente no MetaEditor. O modelo ONNX resultante será salvo na mesma subpasta Python e será usado como um recurso durante a compilação do 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()
Teste de um EA baseado no modelo ONNX
Agora, vamos testar o EA com dados históricos no testador de estratégias. Especificamos os mesmos parâmetros que usamos para treinar o modelo: o símbolo EURUSD e o período de tempo H1.
O intervalo de teste não inclui o período de treinamento: ele começa no início do ano (01/01/2023).
Como, de acordo com a estratégia, os sinais são verificados uma vez no início de cada hora (o Expert Advisor verifica o aparecimento de uma nova barra), o modo de simulação de ticks não importa - o OnTick será trabalhado no testador apenas uma 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(); }
Com esse tipo de teste, o teste de 3 meses leva alguns segundos. Obtemos resultados imediatos.
Agora, vamos modificar a estratégia de negociação para permitir a abertura de posição por um sinal e o fechamento por Stop Loss (SL) ou 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(); } }
Vamos especificar o parâmetro InpUseStops = true, o que significa que os níveis TP e SL são definidos quando uma posição é aberta.
Os resultados do teste usando os níveis de SL/TP do início do ano são mostrados abaixo.
O código-fonte completo do EA e o modelo treinado para o início de 2023 estão anexados ao artigo.
Considerações finais
O artigo mostra que não há nada difícil em usar modelos ONNX em programas MQL5. Na verdade, a aplicação de modelos é a parte mais fácil, enquanto é muito mais difícil obter um modelo ONNX adequado.
Observe que o modelo usado no artigo é disponibilizado apenas para fins de demonstração, para mostrar como trabalhar com modelos ONNX usando a linguagem MQL5. O Expert Advisor apresentado neste artigo não se destina a negociações em contas reais.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/12373
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
Novo artigo Uso de modelos ONNX em MQL5 foi publicado:
Autor: MetaQuotes
Olá senhores.
Alguém pode me ajudar, pois no final do processo em python, sai o seguinte erro: AttributeError: 'Sequential' object has no attribute 'output_names'. Não sei muito de python, nem de programação. Portanto qualquer ajuda será bem vinda! Obrigado.
Olá, Alberto, por favor, poste o trecho do seu código [utilizando o botão do CÓDIGO (Alt -S)] onde está ocorrendo o erro que alguém que conheça Python (MetaTrader para Python | Modelos ONNX) poderá apontar a solução...