
Um exemplo de como montar modelos ONNX em MQL5
Introdução
Para garantir uma negociação consistente e estável, é comum aconselhar a diversificação dos instrumentos negociados e das estratégias de negociação. Isso também se aplica aos modelos de aprendizado de máquina: a criação de diversos modelos mais simples é mais viável do que um único complexo. Contudo, pode ser desafiador combinar esses modelos em um único modelo ONNX.
Apesar disso, é factível unir vários modelos ONNX já treinados em um programa MQL5. Neste artigo, vamos explorar uma dessas combinações, conhecida como classificador de votação, e mostrar como implementá-la de forma simples e eficiente.
Modelos para o projeto
No exemplo que vamos explorar, usaremos dois modelos básicos: um modelo de previsão de preços de regressão e um modelo de previsão de movimentos de preços de classificação. A principal diferença entre os dois é que o modelo de regressão prevê quantidades, enquanto o de classificação prevê classes.
O primeiro modelo é de regressão.
Este foi treinado com dados EURUSD D1 de 2003 até o final de 2022, usando séries de 10 preços OHLC. Para aprimorar a treinabilidade do modelo, normalizamos os preços e dividimos o preço médio da série pelo seu desvio padrão. Desse modo, situamos a série dentro de um intervalo específico com uma média de 0 e um spread de 1, melhorando a convergência durante o treinamento.
O objetivo do modelo é prever o preço de fechamento do dia seguinte.
O modelo é bem simples e foi feito apenas para fins de ilustração.
# Copyright 2023, MetaQuotes Ltd. # https://www.mql5.com from datetime import datetime 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 tqdm import tqdm from sys import argv if not mt5.initialize(): print("initialize() failed, error code =",mt5.last_error()) quit() # we will save generated onnx-file near the our script 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) # input parameters inp_model_name = "model.eurusd.D1.10.onnx" inp_history_size = 10 inp_start_date = datetime(2003, 1, 1, 0) inp_end_date = datetime(2023, 1, 1, 0) # get data from client terminal eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_D1, inp_start_date, inp_end_date) df = pd.DataFrame(eurusd_rates) # # collect dataset subroutine # def collect_dataset(df: pd.DataFrame, history_size: int): """ Collect dataset for the following regression problem: - input: history_size consecutive H1 bars; - output: close price for the next bar. :param df: D1 bars for a range of dates :param history_size: how many bars should be considered for making a prediction :return: features and labels """ n = len(df) xs = [] ys = [] for i in tqdm(range(n - history_size)): w = df.iloc[i: i + history_size + 1] x = w[['open', 'high', 'low', 'close']].iloc[:-1].values y = w.iloc[-1]['close'] xs.append(x) ys.append(y) X = np.array(xs) y = np.array(ys) return X, y ### # get prices X, y = collect_dataset(df, history_size=inp_history_size) # normalize prices m = X.mean(axis=1, keepdims=True) s = X.std(axis=1, keepdims=True) X_norm = (X - m) / s y_norm = (y - m[:, 0, 3]) / s[:, 0, 3] # split data to train and test sets X_train, X_test, y_train, y_test = train_test_split(X_norm, y_norm, test_size=0.2, random_state=0) # define model model = tf.keras.Sequential([ tf.keras.layers.LSTM(64, input_shape=(inp_history_size, 4)), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dropout(0.1), tf.keras.layers.Dense(32, activation='relu'), tf.keras.layers.BatchNormalization(), tf.keras.layers.Dropout(0.1), tf.keras.layers.Dense(32, activation='relu'), tf.keras.layers.Dense(1) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) # model training for 50 epochs lr_reduction = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, min_lr=0.000001) history = model.fit(X_train, y_train, epochs=50, verbose=2, validation_split=0.15, callbacks=[lr_reduction]) # model evaluation test_loss, test_mae = model.evaluate(X_test, y_test) print(f"test_loss={test_loss:.3f}") print(f"test_mae={test_mae:.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()A execução de nosso modelo de regressão deve transformar o preço previsto resultante na classe "o preço cai, o preço não muda, o preço sobe". Isso é necessário para preparar o classificador de votação.
O segundo modelo é de classificação.
Este foi treinado com dados EURUSD D1 de 2010 até o final de 2022, usando séries de 63 preços de fechamento. O modelo deve determinar uma de três classes na saída: o preço vai cair, o preço vai ficar dentro de 10 pontos, ou o preço vai subir. Devido à existência dessa segunda classe, foi necessário treinar o modelo com dados a partir de 2010, pois em 2009 os mercados mudaram de uma precisão de 4 dígitos para 5 dígitos, fazendo com que cada ponto antigo se transformasse em dez novos pontos.
Semelhante ao modelo anterior, o preço neste modelo também é normalizado: dividimos o desvio do preço médio na série pelo desvio padrão na série. A intenção por trás deste modelo foi descrita no artigo "Previsão de séries temporais financeiras com MLP em Keras" (em russo). Vale ressaltar que ambos os modelos são simples e foram usados aqui apenas para fins de demonstração.
# Copyright 2023, MetaQuotes Ltd. # https://www.mql5.com # # Classification model # 0,0,1 - predict price down # 0,1,0 - predict price same # 1,0,0 - predict price up # from datetime import datetime 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 tqdm import tqdm from keras.models import Sequential from keras.layers import Dense, Activation,Dropout, BatchNormalization, LeakyReLU from keras.optimizers import SGD from keras import regularizers from sys import argv # initialize MetaTrader 5 client terminal if not mt5.initialize(): print("initialize() failed, error code =",mt5.last_error()) quit() # we will save the generated onnx-file near the our script 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) # input parameters inp_model_name = "model.eurusd.D1.63.onnx" inp_history_size = 63 inp_start_date = datetime(2010, 1, 1, 0) inp_end_date = datetime(2023, 1, 1, 0) # get data from the client terminal eurusd_rates = mt5.copy_rates_range("EURUSD", mt5.TIMEFRAME_D1, inp_start_date, inp_end_date) df = pd.DataFrame(eurusd_rates) # # collect dataset subroutine # def collect_dataset(df: pd.DataFrame, history_size: int): """ Collect dataset for the following regression problem: - input: history_size consecutive H1 bars; - output: close price for the next bar. :param df: H1 bars for a range of dates :param history_size: how many bars should be considered for making a prediction :return: features and labels """ n = len(df) xs = [] ys = [] for i in tqdm(range(n - history_size)): w = df.iloc[i: i + history_size + 1] x = w[['close']].iloc[:-1].values delta = x[-1] - w.iloc[-1]['close'] if np.abs(delta)<=0.0001: y = 0, 1, 0 else: if delta<0: y = 1, 0, 0 else: y = 0, 0, 1 xs.append(x) ys.append(y) X = np.array(xs) Y = np.array(ys) return X, Y ### # get prices X, Y = collect_dataset(df, history_size=inp_history_size) # normalize prices m = X.mean(axis=1, keepdims=True) s = X.std(axis=1, keepdims=True) X_norm = (X - m) / s # split data to train and test sets X_train, X_test, Y_train, Y_test = train_test_split(X_norm, Y, test_size=0.1, random_state=0) # define model model = Sequential() model.add(Dense(64, input_dim=inp_history_size, activity_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization()) model.add(LeakyReLU()) model.add(Dropout(0.3)) model.add(Dense(16, activity_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization()) model.add(LeakyReLU()) model.add(Dense(3)) model.add(Activation('softmax')) opt = SGD(learning_rate=0.01, momentum=0.9) model.compile(optimizer=opt, loss='categorical_crossentropy', metrics=['accuracy']) # model training for 300 epochs lr_reduction = tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.9, patience=5, min_lr=0.00001) history = model.fit(X_train, Y_train, epochs=300, validation_data=(X_test, Y_test), shuffle = True, batch_size=128, verbose=2, callbacks=[lr_reduction]) # model evaluation test_loss, test_accuracy = model.evaluate(X_test, Y_test) print(f"test_loss={test_loss:.3f}") print(f"test_accuracy={test_accuracy:.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()Os modelos foram treinados com dados até o final de 2022, deixando um período de tempo para testar seu funcionamento com o testador de estratégia.
Conjunto de modelos ONNX no EA MQL5
Para exemplificar o potencial de conjuntos de modelos, apresentamos a seguir um EA simples. Os princípios fundamentais do uso de modelos ONNX em MQL5 foram abordados na segunda parte do artigo anterior.
Declarações e definições
#include <Trade\Trade.mqh> input double InpLots = 1.0; // Lots amount to open position #resource "Python/model.eurusd.D1.10.onnx" as uchar ExtModel1[] #resource "Python/model.eurusd.D1.63.onnx" as uchar ExtModel2[] #define SAMPLE_SIZE1 10 #define SAMPLE_SIZE2 63 long ExtHandle1=INVALID_HANDLE; long ExtHandle2=INVALID_HANDLE; int ExtPredictedClass1=-1; int ExtPredictedClass2=-1; int ExtPredictedClass=-1; datetime ExtNextBar=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_D1) { Print("model must work with EURUSD,D1"); return(INIT_FAILED); } //--- create first model from static buffer ExtHandle1=OnnxCreateFromBuffer(ExtModel1,ONNX_DEFAULT); if(ExtHandle1==INVALID_HANDLE) { Print("First model 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 (OHLC) const long input_shape1[] = {1,SAMPLE_SIZE1,4}; if(!OnnxSetInputShape(ExtHandle1,0,input_shape1)) { Print("First model 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_shape1[] = {1,1}; if(!OnnxSetOutputShape(ExtHandle1,0,output_shape1)) { Print("First model OnnxSetOutputShape error ",GetLastError()); return(INIT_FAILED); } //--- create second model from static buffer ExtHandle2=OnnxCreateFromBuffer(ExtModel2,ONNX_DEFAULT); if(ExtHandle2==INVALID_HANDLE) { Print("Second model 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 const long input_shape2[] = {1,SAMPLE_SIZE2}; if(!OnnxSetInputShape(ExtHandle2,0,input_shape2)) { Print("Second model 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 classes (up, same or down) const long output_shape2[] = {1,3}; if(!OnnxSetOutputShape(ExtHandle2,0,output_shape2)) { Print("Second model OnnxSetOutputShape error ",GetLastError()); return(INIT_FAILED); } //--- ok return(INIT_SUCCEEDED); }
Nós só rodaremos essa função com EURUSD, D1. Isso ocorre porque os modelos são treinados com preços diários, enquanto utilizamos os dados do período-símbolo atual.
Os modelos são inclusos no EA como recursos.
É crucial definir de maneira explícita as formas dos dados de entrada e saída.
Função OnTick
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- check new bar if(TimeCurrent()<ExtNextBar) return; //--- set next bar time ExtNextBar=TimeCurrent(); ExtNextBar-=ExtNextBar%PeriodSeconds(); ExtNextBar+=PeriodSeconds(); //--- predict price movement Predict(); //--- check trading according to prediction if(ExtPredictedClass>=0) if(PositionSelect(_Symbol)) CheckForClose(); else CheckForOpen(); }
As operações de negociação são realizadas apenas no início do dia.
Função de previsão
//+------------------------------------------------------------------+ //| Voting classification | //+------------------------------------------------------------------+ void Predict(void) { //--- evaluate first model ExtPredictedClass1=PredictPrice(ExtHandle1,SAMPLE_SIZE1); //--- evaluate second model ExtPredictedClass2=PredictPriceMovement(ExtHandle2,SAMPLE_SIZE2); //--- vote if(ExtPredictedClass1==ExtPredictedClass2) ExtPredictedClass=ExtPredictedClass1; else ExtPredictedClass=-1; }
Quando ambos os modelos indicam a mesma classe, consideramos que essa classe foi selecionada. Isso configura um voto por maioria. E, como temos apenas dois modelos no conjunto, voto por maioria aqui significa "unanimidade".
Previsão do preço de fechamento do dia a partir de 10 preços OHLC anteriores
//+------------------------------------------------------------------+ //| Predict next price (first model) | //+------------------------------------------------------------------+ int PredictPrice(const long handle,const int sample_size) { static matrixf input_data(sample_size,4); // matrix for prepared input data static vectorf output_data(1); // vector to get result static matrix mm(sample_size,4); // matrix of horizontal vectors Mean static matrix ms(sample_size,4); // matrix of horizontal vectors Std static matrix x_norm(sample_size,4); // matrix for prices normalize //--- prepare input data matrix rates; //--- request last bars if(!rates.CopyRates(_Symbol,_Period,COPY_RATES_OHLC,1,sample_size)) return(-1); //--- get series Mean vector m=rates.Mean(1); //--- get series Std vector s=rates.Std(1); //--- prepare matrices for prices normalization for(int i=0; i<sample_size; i++) { mm.Row(m,i); ms.Row(s,i); } //--- the input of the model must be a set of vertical OHLC vectors x_norm=rates.Transpose(); //--- normalize prices x_norm-=mm; x_norm/=ms; //--- run the inference input_data.Assign(x_norm); if(!OnnxRun(handle,ONNX_NO_CONVERSION,input_data,output_data)) return(-1); //--- denormalize the price from the output value double predicted=output_data[0]*s[3]+m[3]; //--- classify predicted price movement int predicted_class=-1; double delta=rates[3][sample_size-1]-predicted; if(fabs(delta)<=0.0001) predicted_class=PRICE_SAME; else { if(delta<0) predicted_class=PRICE_UP; else predicted_class=PRICE_DOWN; } return(predicted_class); }
Os dados de entrada devem ser preparados seguindo as mesmas regras aplicadas no treinamento do modelo. Após a execução do modelo, o valor resultante é revertido para o preço. A classe é calculada com base na diferença entre o último preço de fechamento na série e o preço resultante.
Previsão do movimento de preços com base em uma série de 63 fechamentos diários.
//+------------------------------------------------------------------+ //| Predict price movement (second model) | //+------------------------------------------------------------------+ int PredictPriceMovement(const long handle,const int sample_size) { static vectorf input_data(sample_size); // vector for prepared input data static vectorf output_data(3); // vector to get result //--- request last bars if(!input_data.CopyRates(_Symbol,_Period,COPY_RATES_CLOSE,1,sample_size)) return(-1); //--- get series Mean float m=input_data.Mean(); //--- get series Std float s=input_data.Std(); //--- normalize prices input_data-=m; input_data/=s; //--- run the inference if(!OnnxRun(handle,ONNX_NO_CONVERSION,input_data,output_data)) return(-1); //--- evaluate prediction return(int(output_data.ArgMax())); }
A normalização do preço é realizada de acordo com as mesmas regras do primeiro modelo. Mas o código é mais compacto porque a entrada é um vetor em vez de uma matriz. A classe é escolhida pelo valor máximo de três probabilidades.
A estratégia de negociação é simples. As operações de negociação são realizadas no início de cada dia. Se a previsão indicar que "o preço subirá", então fazemos uma compra; se indicar que "o preço cairá", fazemos uma venda.
//+------------------------------------------------------------------+ //| 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)) ExtTrade.PositionOpen(_Symbol,signal,InpLots, SymbolInfoDouble(_Symbol,signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK), 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(); } }
Nós treinamos nosso modelo com dados até o início de 2023. Portanto, vamos definir o intervalo de teste a partir do início desse ano.
Aqui estão os resultados dos testes baseados nos dados desde o início do ano.
Seria interessante conhecer os resultados dos testes de cada modelo individual.
Para fazer isso, modificamos o código-fonte do EA.
enum EnModels { USE_FIRST_MODEL, // Use first model only USE_SECOND_MODEL, // Use second model only USE_BOTH_MODELS // Use both models }; input EnModels InpModels = USE_BOTH_MODELS; // Models using input double InpLots = 1.0; // Lots amount to open position ... //+------------------------------------------------------------------+ //| Voting classification | //+------------------------------------------------------------------+ void Predict(void) { //--- evaluate first model if(InpModels==USE_BOTH_MODELS || InpModels==USE_FIRST_MODEL) ExtPredictedClass1=PredictPrice(ExtHandle1,SAMPLE_SIZE1); //--- evaluate second model if(InpModels==USE_BOTH_MODELS || InpModels==USE_SECOND_MODEL) ExtPredictedClass2=PredictPriceMovement(ExtHandle2,SAMPLE_SIZE2); //--- check predictions switch(InpModels) { case USE_FIRST_MODEL : ExtPredictedClass=ExtPredictedClass1; break; case USE_SECOND_MODEL : ExtPredictedClass=ExtPredictedClass2; break; case USE_BOTH_MODELS : if(ExtPredictedClass1==ExtPredictedClass2) ExtPredictedClass=ExtPredictedClass1; else ExtPredictedClass=-1; } }
E habilitamos o parâmetro "Usar apenas o primeiro modelo".
Seguem os resultados dos testes do primeiro modelo.
Agora seguem os testes do segundo modelo.
O segundo modelo se mostrou muito mais robusto do que o primeiro. Os resultados confirmam a teoria de que modelos mais fracos precisam ser combinados. Contudo, este artigo trata da aplicação prática, e não da teoria de combinação de modelos.
Nota importante: Os modelos usados no artigo servem apenas para demonstrar o uso de modelos ONNX com a linguagem MQL5. O Expert Advisor não deve ser usado para negociações em contas reais.
Considerações finais
Apresentamos um exemplo simples, porém elucidativo, de um conjunto de dois modelos ONNX. O número de modelos usados simultaneamente é limitado, não podendo exceder 256. No entanto, o uso de mais de dois modelos exigirá uma abordagem de programação do EA diferente, baseada em programação orientada a objetos.
Mas isso fica para um próximo artigo.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/12433





- 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