English Русский 中文 Español Deutsch 日本語
preview
Dominando o ONNX: Ponto de virada para traders MQL5

Dominando o ONNX: Ponto de virada para traders MQL5

MetaTrader 5Sistemas de negociação | 22 março 2024, 10:36
150 0
Omega J Msigwa
Omega J Msigwa

"A capacidade de exportar e importar modelos de IA no formato ONNX simplifica o processo de desenvolvimento, economizando tempo e recursos ao integrar IA em ecossistemas escritos em diferentes linguagens".


Introdução

Não se pode negar que vivemos na era da inteligência artificial (IA) e do aprendizado de máquina: todos os dias, novas tecnologias baseadas em IA são aplicadas nas finanças, arte, jogos, educação e muitos outros aspectos da vida.

Para nós, traders, a IA é útil porque nos permite descobrir padrões e relações que não conseguimos ver a olho nu.

Apesar do aparente poder e magia, por trás dos modelos de IA existem operações matemáticas complexas que exigem um enorme volume de trabalho e um alto grau de precisão para sua correta compreensão e implementação. Felizmente, o código aberto simplifica significativamente a tarefa, eliminando a necessidade de desenvolver um modelo de aprendizado de máquina do zero.

Agora, não é necessário ser um gênio da matemática e da programação para criar e implementar modelos de IA. É necessário apenas um entendimento básico de uma determinada linguagem de programação ou das ferramentas que você deseja usar em seu projeto. Às vezes, nem é necessário ter um computador. Graças a serviços como o Google Colab, você pode codificar, criar e executar modelos de IA usando Python gratuitamente.

Implementar modelos de aprendizado de máquina usando Python e outras linguagens de programação populares e avançadas é relativamente simples, o que não se pode dizer sobre MQL5. A menos que você queira reinventar a roda, criando modelos de aprendizado de máquina em MQL5 do zero, o que estamos fazendo nesta série de artigos, eu recomendaria fortemente o uso do ONNX para integrar modelos de IA construídos com Python. Felizmente, o ONNX agora é suportado no MQL5.

  onnx-mql5

Para entender o artigo, é necessário ter uma compreensão básica sobre IA e aprendizado de máquina. Você pode encontrar informações aqui e aqui.


O que é ONNX?

ONNX (Open Neural Network Exchange) é uma biblioteca aberta para representação de modelos de aprendizado de máquina e profundo. Ela permite converter modelos treinados em um framework de aprendizado profundo para um formato comum, que pode ser usado em outros frameworks, simplificando o trabalho com modelos em diferentes plataformas e ferramentas.

Isso significa que você pode criar modelos de aprendizado de máquina usando qualquer linguagem que o suporte, exceto MQL5, depois converter esse modelo para o formato ONNX e usá-lo dentro do seu programa MQL5.

Neste artigo, estarei usando Python para criar aprendizado de máquina, pois estou familiarizado com ele. De fato, é possível usar outras linguagens, embora eu não esteja seguro disso. Aliás, parece que toda a documentação do ONNX é baseada em Python. Acredito que, no momento, o ONNX foi criado para Python. Isso faz sentido, pois não creio que exista outra linguagem, além do Python, com bibliotecas e ferramentas avançadas baseadas em IA.


Conceitos fundamentais no ONNX

Antes de mergulhar no ONNX, você deve estar familiarizado com alguns conceitos-chave:

  • Modelo ONNX: Um modelo ONNX é uma representação de um modelo de aprendizado de máquina. Consiste em um grafo de computação, onde os nós representam operações (por exemplo, convolução, adição), e as arestas representam o fluxo de dados entre as operações.
  • Nós: Os nós do grafo ONNX representam operações ou funções aplicadas aos dados de entrada. Esses nós podem ser operações como convolução, adição ou operações personalizadas.
  • Tensores: Os tensores são arrays multidimensionais que representam os dados transmitidos entre os nós do grafo computacional. Eles podem ser dados de entrada, saída ou intermediários.
  • Operadores: Os operadores são funções aplicadas aos tensores no ONNX. Cada operador representa uma operação específica, como multiplicação de matrizes ou adição elemento a elemento.

Criando modelos em Python e implementando em MQL5 usando ONNX

Para construir com sucesso um modelo de aprendizado de máquina em Python e implementá-lo dentro do seu Expert Advisor, indicador ou script em MQL5, é necessário mais do que apenas código Python. Aqui estão os passos importantes que você deve seguir para acabar não só com um modelo ONNX, mas também com um modelo que realmente forneça as previsões precisas de que você precisa:

  1. Coleta de dados
  2. Normalização de dados do lado do MQL5 
  3. Construção de modelos em Python
  4. Incorporação do modelo ONNX em MQL5
  5. Execução do modelo em tempo real


01: Coleta de dados

A coleta de dados é o primeiro passo a ser feito diretamente dentro do seu programa MQL5. Acredito que é melhor coletar todos os dados dentro do seu programa para que eles correspondam à maneira como coletamos dados de treinamento e os dados usados durante a negociação real ou a execução do modelo em tempo real. Lembre-se de que a coleta de dados pode variar dependendo da natureza do problema que você está tentando resolver. Neste artigo, estaremos abordando um problema de regressão. Usaremos dados OHLC (Open, High, Low, Close) como nosso principal conjunto de dados, onde Open, High e Low serão usados como variáveis independentes, e os valores de Close serão usados como a variável alvo.

Inside ONNX get data.mq5

matrixf GetTrainData(uint start, uint total)
 {
   matrixf return_matrix(total, 3);
   
   ulong last_col;
   
   
    OPEN.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_OPEN, start, total);
    HIGH.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_HIGH, start, total);
    LOW.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_LOW, start, total);
    CLOSE.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_CLOSE, start, total);
    
    return_matrix.Col(OPEN, 0);
    return_matrix.Col(HIGH, 1);
    return_matrix.Col(LOW, 2);
    
    
    csv_name_ = Symbol()+"."+EnumToString(Period())+"."+string(total_bars);
    
       
      x_vars = "OPEN,HIGH,LOW";
      
       
       return_matrix.Resize(total, 4); //if we are collecting the train data collect the target variable also
       
       last_col = return_matrix.Cols()-1; //Column located at the last index is the last column
       
       return_matrix.Col(CLOSE, last_col); //put the close price information in the last column of a matrix
       
       
       csv_name_ +=".targ=CLOSE";
       
       csv_header = x_vars + ",CLOSE";
         
       if (!WriteCsv("ONNX Datafolder\\"+csv_name_+".csv", return_matrix, csv_header))
         Print("Failed to Write to a csv file");
       else
         Print("Data saved to a csv file successfully");
     
    
   return return_matrix;
 } 

Esta função coleta as variáveis independentes OHL e a variável alvo CLOSE. No aprendizado supervisionado, é necessário especificar e passar a variável alvo para o modelo, para que ele possa aprender com ela e entender os padrões entre a variável alvo e as outras variáveis. No nosso caso, o modelo tentará entender como esses indicadores levam a um movimento altista ou baixista. 

Ao implementar o modelo, precisamos coletar os dados da mesma maneira, exceto que desta vez coletaremos os dados sem a variável alvo, porque queremos que nosso modelo treinado descubra isso. Basicamente, estamos falando de previsão.

É por isso que existe uma função separada GetLiveData para carregar novos dados para previsões rápidas no mercado.

Inside ONNX mt5.mq5

matrixf GetLiveData(uint start, uint total)
 {
   matrixf return_matrix(total, 3);
   
   
    OPEN.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_OPEN, start, total);
    HIGH.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_HIGH, start, total);
    LOW.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_LOW, start, total);
        
    return_matrix.Col(OPEN, 0);
    return_matrix.Col(HIGH, 1);
    return_matrix.Col(LOW, 2);
          
      
   return return_matrix;
 }

Coleta de dados de treinamento

    matrixf dataset = GetTrainData(start_bar, total_bars);
    
    Print("Train data\n",dataset);

Resultados

DK      0       23:10:54.837    ONNX get data (EURUSD,H1)       Train data
PR      0       23:10:54.837    ONNX get data (EURUSD,H1)       [[1.4243405,1.4130603,1.4215617,1.11194]
HF      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.3976599,1.3894916,1.4053394,1.11189]
RK      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.402994,1.3919021,1.397626,1.11123]
PM      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.3848507,1.3761013,1.3718294,1.11022]
FF      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.3597701,1.3447646,1.3545419,1.1097701]
CH      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.3461626,1.3522644,1.3433729,1.1106]
NL      0       23:10:54.837    ONNX get data (EURUSD,H1)        [1.3683074,1.3525325,1.3582669,1.10996]

Recuperando dados reais

Assim sãon obtidos os dados OHL atuais por barra.

matrixf live_data = GetLiveData(0,1);
   
Print("Live data\n",live_data);

Resultados

MN      0       23:15:47.167    ONNX mt5 (EURUSD,H1)    Live data
KS      0       23:15:47.167    ONNX mt5 (EURUSD,H1)    [[-0.21183228,-0.23540309,-0.20334835]]

A maneira de carregar dados em tempo real pode variar um pouco ao preparar dados para modelos de previsão de séries temporais, como RNN, GRU e LSTM.

Note que eu usei matrixf, que significa matriz de ponto flutuante, em vez de matrix comum!! Isso é necessário para garantir a compatibilidade dos tipos de dados entre MQL5 e Python. Certifique-se de que o tipo de entrada de dados seja compatível com o tipo de dados esperado pelo modelo ONNX. Se o seu modelo ONNX espera dados de entrada float32, garanta que seus dados de entrada também sejam do tipo float32. ONNX é compatível com float32 e float64. A incompatibilidade de tipos leva aos seguintes erros;
error 2023.09.18 18:03:53.212   ONNX: invalid parameter size, expected 1044480 bytes instead of 32640


02: Normalização de dados do lado do MQL5

A normalização de dados é uma das coisas mais importantes que precisam ser feitas corretamente para o conjunto de dados que será usado pelo modelo de aprendizado de máquina.

Lembre-se de que o método de normalização usado para preparar os dados de treinamento deve ser o mesmo usado para preparar os dados de teste e reais. Isso significa que ao usar o método MinMaxScaler, os valores de min e max, que são variáveis fundamentais na equação do MinMaxScaler usada na preparação dos dados de treinamento, devem ser usados para continuar a normalização dos novos dados que serão processados pelo modelo em outro lugar. Para alcançar tal consistência, precisamos salvar as variáveis para cada método de normalização em um arquivo csv:

A normalização de dados destina-se apenas às variáveis independentes. Independentemente do problema que você está tentando resolver, não há necessidade de normalizar a variável alvo.

Vamos usar a classe Preprocessing, apresentada aqui.

Inside ONNX get data.mq5 script

 //--- Saving the normalization prameters
 
 switch(NORM)
   {
    case  NORM_MEAN_NORM:
      
       //--- saving the mean
       
       norm_params.Assign(norm_x.mean_norm_scaler.mean);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.mean.csv",norm_params,x_vars);
       
       //--- saving the min
       
       norm_params.Assign(norm_x.mean_norm_scaler.min);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.min.csv",norm_params,x_vars);
       
       //--- saving the max
       
       norm_params.Assign(norm_x.mean_norm_scaler.max);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.max.csv",norm_params,x_vars);
           
      break;
      
    case NORM_MIN_MAX_SCALER:
       
       //--- saving the min
       
       norm_params.Assign(norm_x.min_max_scaler.min);
       WriteCsv(normparams_folder+csv_name_+".min_max_scaler.min.csv",norm_params,x_vars);
       
       //--- saving the max
       
       norm_params.Assign(norm_x.min_max_scaler.max);
       WriteCsv(normparams_folder+csv_name_+".min_max_scaler.max.csv",norm_params,x_vars);
       
       
       break;
       
    case NORM_STANDARDIZATION:

       //--- saving the mean
       
       norm_params.Assign(norm_x.standardization_scaler.mean);             
       WriteCsv(normparams_folder+csv_name_+".standardization_scaler.mean.csv",norm_params,x_vars);
       
       //--- saving the std
       
       norm_params.Assign(norm_x.standardization_scaler.std);
       WriteCsv(normparams_folder+csv_name_+".standardization_scaler.std.csv",norm_params,x_vars);
       
       break;
   } 

Resultados

parâmetros de normalização

Quando o Standardization Scaler foi usado dentro dos arquivos csv, os parâmetros apareceram da seguinte forma:


Note que a normalização também está integrada dentro da função GetData. Como a normalização é muito importante, cada matriz de dados retornada por ambas as funções responsáveis pela coleta de dados deve ser uma matriz com valores de preços normalizados.

Inside ONNX get data.mq5 script

matrixf GetTrainData(uint start, uint total)
 {
   matrixf return_matrix(total, 3);
   
   ulong last_col;
   
   
    OPEN.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_OPEN, start, total);
    HIGH.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_HIGH, start, total);
    LOW.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_LOW, start, total);
    CLOSE.CopyRates(Symbol(), PERIOD_CURRENT, COPY_RATES_CLOSE, start, total);
    
    return_matrix.Col(OPEN, 0);
    return_matrix.Col(HIGH, 1);
    return_matrix.Col(LOW, 2);
    
    matrixf norm_params = {};
    
    csv_name_ = Symbol()+"."+EnumToString(Period())+"."+string(total_bars);
    
       
      x_vars = "OPEN,HIGH,LOW";
      
       while (CheckPointer(norm_x) != POINTER_INVALID)
         delete (norm_x);
         
       norm_x = new CPreprocessing<vectorf, matrixf>(return_matrix, NORM);
    
       
 
 //--- Saving the normalization prameters
 
 switch(NORM)
   {
    case  NORM_MEAN_NORM:
      
       //--- saving the mean
       
       norm_params.Assign(norm_x.mean_norm_scaler.mean);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.mean.csv",norm_params,x_vars);
       
       //--- saving the min
       
       norm_params.Assign(norm_x.mean_norm_scaler.min);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.min.csv",norm_params,x_vars);
       
       //--- saving the max
       
       norm_params.Assign(norm_x.mean_norm_scaler.max);
       WriteCsv(normparams_folder+csv_name_+".mean_norm_scaler.max.csv",norm_params,x_vars);
           
      break;
      
    case NORM_MIN_MAX_SCALER:
       
       //--- saving the min
       
       norm_params.Assign(norm_x.min_max_scaler.min);
       WriteCsv(normparams_folder+csv_name_+".min_max_scaler.min.csv",norm_params,x_vars);
       
       //--- saving the max
       
       norm_params.Assign(norm_x.min_max_scaler.max);
       WriteCsv(normparams_folder+csv_name_+".min_max_scaler.max.csv",norm_params,x_vars);
       
       
       break;
       
    case NORM_STANDARDIZATION:

       //--- saving the mean
       
       norm_params.Assign(norm_x.standardization_scaler.mean);             
       WriteCsv(normparams_folder+csv_name_+".standardization_scaler.mean.csv",norm_params,x_vars);
       
       //--- saving the std
       
       norm_params.Assign(norm_x.standardization_scaler.std);
       WriteCsv(normparams_folder+csv_name_+".standardization_scaler.std.csv",norm_params,x_vars);
       
       break;
   }
 
       return_matrix.Resize(total, 4); //if we are collecting the train data collect the target variable also
       
       last_col = return_matrix.Cols()-1; //Column located at the last index is the last column
       
       return_matrix.Col(CLOSE, last_col); //put the close price information in the last column of a matrix
       
       
       csv_name_ +=".targ=CLOSE";
       
       csv_header = x_vars + ",CLOSE";
         
       if (!WriteCsv("ONNX Datafolder\\"+csv_name_+".csv", return_matrix, csv_header))
         Print("Failed to Write to a csv file");
       else
         Print("Data saved to a csv file successfully");
     
    
   return return_matrix;
 } 

 Finalmente, os dados foram salvos em formato CSV para que pudessem ser utilizados em conjunto com o código Python.


03: Construção de modelos em Python

Eu vou construir uma rede neural de perceptron multicamadas, mas você pode construir qualquer modelo de sua escolha. Você não está limitado a este tipo específico de modelo. Instale o Python, se você ainda não o tem. Em seguida, instale o virtualenv, executando os seguintes comandos do Windows CMD (não confunda com o Powershell!!)

$ pip3 install virtualenv

Depois de iniciado,

$ virtualenv venv

Isso criará um ambiente virtual Python para o seu computador com Windows. Este processo pode variar um pouco para usuários de Mac e Linux. Depois, inicie o ambiente virtual executando este comando:

$ venv\Scripts\activate

Em seguida, instale todas as dependências usadas neste guia, executando

$ pip install -r requirements.txt #Este arquivo txt está disponível em https://github.com/MegaJoctan/ONNX-MQL5/blob/main/requirements.txt

É sempre importante isolar o projeto criando um ambiente virtual para evitar conflitos entre módulos e versões do Python e facilitar o compartilhamento do projeto.

Importação e inicialização do MT5

import MetaTrader5 as mt5

if not mt5.initialize(): #This will open MT5 app in your pc

   print("initialize() failed, error code =",mt5.last_error())

   quit()

# program logic and ML code will be here



mt5.shutdown() #This closes the program

# Getting the data we stored in the Files path on Metaeditor

data_path = terminal_info.data_path

dataset_path = data_path + "\\MQL5\\Files\\ONNX Datafolder"

Precisamos verificar se o caminho existe. Se não existir, isso significa que não coletamos dados do MetaTrader 5.

import os if not os.path.exists(dataset_path):

print("Dataset folder doesn't exist | Be sure you are referring to the correct path and the data is collected from MT5 side of things")
quit()

Criação de uma rede neural perceptron de múltiplas camadas(MLP)

Vamos encapsular a NN MLP dentro de uma classe para tornar nosso código em seções legíveis;

01: Inicialização da classe

Os dados são coletados e divididos em conjuntos de treino e teste, enquanto variáveis importantes são declaradas disponíveis para uso por toda a classe,

class NeuralNetworkClass():
    def __init__(self, csv_name, target_column, batch_size=32):

    # Loading the dataset and storing to a variable Array        
        self.data = pd.read_csv(dataset_path+"\\"+csv_name)

        if self.data.empty:
            print(f"No such dataset or Empty dataset csv = {csv_name}")
            quit() # quit the program
        

        print(self.data.head()) # Print 5 first rows of a given data

        self.target_column = target_column
        # spliting the data into training and testing samples

        X = self.data.drop(columns=self.target_column).to_numpy() # droping the targeted column, the rest is x variables
        Y = self.data[self.target_column].to_numpy() # We convert data arrays to numpy arrays compartible with sklearn and tensorflow
                
        
        self.train_x, self.test_x, self.train_y, self.test_y = train_test_split(X, Y, test_size=0.3, random_state=42) # splitting the data into training and testing samples 
        
        print(f"train x shape {self.train_x.shape}\ntest x shape {self.test_x.shape}")
                
        self.input_size = self.train_x.shape[-1] # obtaining the number of columns in x variable as our inputs
        
        self.output_size = 1 # We are solving for a regression problem we need to have a single output neuron
        
        self.batch_size = batch_size
        
        self.model = None # Object to store the model
        
        self.plots_directory = "Plots"
        self.models_directory = "Models"
                

Resultados

pd head

02: Construção da rede neural

Nossa rede neural de uma camada é definida pelo número especificado de neurônios.

    def BuildNeuralNetwork(self, activation_function='relu', neurons = 10):

        # Create a Feedforward Neural Network model
        self.model = keras.Sequential([
            keras.layers.Input(shape=(self.input_size,)),  # Input layer
            keras.layers.Dense(units=neurons, activation=activation_function, activity_regularizer=l2(0.01), kernel_initializer="he_uniform"),  # Hidden layer with an activation function
            keras.layers.Dense(units=self.output_size, activation='linear', activity_regularizer=l2(0.01), kernel_initializer="he_uniform")  
        ])

        # Print a summary of the model's architecture.
        self.model.summary()

Resultados

descrição sequencial do modelo

03: Treinamento e teste do modelo da rede neural

    def train_network(self, epochs=100, learning_rate=0.001, loss='mean_squared_error'):

        early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True) # Early stoppage mechanism | stop training when there is no major change in loss in the last to epochs, defined by the variable patience

        adam = optimizers.Adam(learning_rate=learning_rate) # Adam optimizer >> https://machinelearningmastery.com/adam-optimization-algorithm-for-deep-learning/
    
        # Compile the model: Specify the loss function, optimizer, and evaluation metrics.
        self.model.compile(loss=loss, optimizer=adam, metrics=['mae'])    

        # One hot encode the validation and train target variables
         
        validation_y = self.test_y
        y = self.train_y

        history = self.model.fit(self.train_x, y, epochs=epochs, batch_size=self.batch_size, validation_data=(self.test_x, validation_y), callbacks=[early_stopping], verbose=2)
        
        if not os.path.exists(self.plots_directory): #create plots path if it doesn't exist for saving the train-test plots
            os.makedirs(self.plots_directory)
        
        # save the loss and validation loss plot
        
        plt.figure(figsize=(12, 6))
        plt.plot(history.history['loss'], label='Training Loss')
        plt.plot(history.history['val_loss'], label='Validation Loss')
        plt.xlabel('Epochs')
        plt.ylabel('Loss')
        plt.legend()
        title = 'Training and Validation Loss Curves'
        plt.title(title)
        plt.savefig(fname=f"{self.plots_directory}\\"+title)

        
        # use the trained model to make predictions on the trained data 
        
        pred = self.model.predict(self.train_x)

        acc = metrics.r2_score(self.train_y, pred)

        # Plot actual & pred
        count = [i*0.1 for i in range(len(self.train_y))]

        title = f'MLP {self.target_column} - Train'
        
        # Saving the plot containing information about predictions and actual values
        
        plt.figure(figsize=(7, 5))
        plt.plot(count, self.train_y, label = "Actual")
        plt.plot(count, pred,  label = "forecast")
        plt.xlabel('Actuals')
        plt.ylabel('Preds')
        plt.title(title+f" | Train acc={acc}")
        plt.legend()
        plt.savefig(fname=f"{self.plots_directory}\\"+title)    

        self.model.save(f"Models\\lstm-pat.{self.target_column}.h5") #saving the model in h5 format, this will help us to easily convert this model to onnx later


    def test_network(self):
        # Plot actual & pred
        
        count = [i*0.1 for i in range(len(self.test_y))]

        title = f'MLP {self.target_column} - Test'
        

        pred = self.model.predict(self.test_x)

        acc = metrics.r2_score(self.test_y, pred)

        
        # Saving the plot containing information about predictions and actual values
        
        plt.figure(figsize=(7, 5))
        plt.plot(count, self.test_y, label = "Actual")
        plt.plot(count, pred,  label = "forecast")
        plt.xlabel('Actuals')
        plt.ylabel('Preds')
        plt.title(title+f" | Train acc={acc}")
        plt.legend()
        plt.savefig(fname=f"{self.plots_directory}\\"+title)    
        
        if not os.path.exists(self.plots_directory): #create plots path if it doesn't exist for saving the train-test plots
            os.makedirs(self.plots_directory)
        
        plt.savefig(fname=f"{self.plots_directory}\\"+title)    
        
        return acc

Resultados

Epoch 1/50
219/219 - 2s - loss: 1.2771 - mae: 0.3826 - val_loss: 0.1153 - val_mae: 0.0309 - 2s/epoch - 8ms/step
Epoch 2/50
219/219 - 1s - loss: 0.0836 - mae: 0.0305 - val_loss: 0.0582 - val_mae: 0.0291 - 504ms/epoch - 2ms/step
Epoch 3/50
219/219 - 1s - loss: 0.0433 - mae: 0.0283 - val_loss: 0.0323 - val_mae: 0.0284 - 515ms/epoch - 2ms/step
Epoch 4/50
219/219 - 0s - loss: 0.0262 - mae: 0.0272 - val_loss: 0.0218 - val_mae: 0.0270 - 482ms/epoch - 2ms/step
Epoch 5/50
...
...
Epoch 48/50
219/219 - 0s - loss: 0.0112 - mae: 0.0106 - val_loss: 0.0112 - val_mae: 0.0121 - 490ms/epoch - 2ms/step
Epoch 49/50
219/219 - 0s - loss: 0.0112 - mae: 0.0106 - val_loss: 0.0112 - val_mae: 0.0109 - 486ms/epoch - 2ms/step
Epoch 50/50
219/219 - 1s - loss: 0.0112 - mae: 0.0106 - val_loss: 0.0112 - val_mae: 0.0097 - 501ms/epoch - 2ms/step
219/219 [==============================] - 0s 2ms/step
C:\Users\Omega Joctan\OneDrive\Documents\onnx article\ONNX python\venv\Lib\site-packages\keras\src\engine\training.py:3079: UserWarning: You are saving your model as an HDF5 file via `model.save()`. This file format is considered legacy. We recommend using instead the native Keras format, e.g. `model.save('my_model.keras')`.
  saving_api.save_model(
94/94 [==============================] - 0s 2ms/step
Test accuracy = 0.9336617822086006

acurácia do treinamento

O modelo da rede neural teve uma acurácia de 93% durante o treinamento e cerca de 95% durante o teste. Há um risco de fitting, mas continuaremos a trabalhar com ele.

04: Salvando o modelo ONNX

É recomendável salvar o modelo após a conclusão bem-sucedida do treinamento e depois de verificar o desempenho do modelo tanto no treinamento quanto na validação fora da amostra. Precisamos adicionar o código de tempo de execução do ONNX para salvar o modelo durante a função train_network na nossa classe. Primeiro, precisamos instalar duas bibliotecas - onnx e tf2onnx

def train_network(self, epochs=100, learning_rate=0.001, loss='mean_squared_error'):
# at the end of this function
# ....

    self.model.save(f"Models\\MLP.REG.{self.target_column}.{self.data.shape[0]}.h5") #saving the model in h5 format, this will help us to easily convert this model to onnx later
    self.saveONNXModel()


def saveONNXModel(self, folder="ONNX Models"):
    
    path = data_path + "\\MQL5\\Files\\" + folder 
    
    if not os.path.exists(path): # create this path if it doesn't exist
        os.makedirs(path)
    
    onnx_model_name = f"MLP.REG.{self.target_column}.{self.data.shape[0]}.onnx"
    path +=  "\\" + onnx_model_name
    
    
    loaded_keras_model = load_model(f"Models\\MLP.REG.{self.target_column}.{self.data.shape[0]}.h5") 
    
    onnx_model, _ = tf2onnx.convert.from_keras(loaded_keras_model, output_path=path)

    onnx.save(onnx_model, path )
    
    print(f'Saved model to {path}')

Resultados

onnx salva

Você pode ter notado que escolhi salvar o modelo ONNX no diretório pai Files. Por que lá? Isso é porque o arquivo ONNX é mais fácil de ser incorporado em nosso programa MQL5 como um recurso, como um Expert Advisor ou indicador.


04: Incorporação do modelo ONNX em MQL5

#resource "\\Files\\ONNX Models\\MLP.REG.CLOSE.10000.onnx" as uchar RNNModel[]

O comando importa o modelo ONNX e o salva dentro de um array RNNModel uchar.

A próxima coisa que precisamos fazer é definir um handle ONNX como uma variável global e criar um handle dentro da função OnInit.

Inside ONNX mt5.mq5 EA

long mlp_onnxhandle;

#include <MALE5\preprocessing.mqh>
CPreprocessing<vectorf, matrixf> *norm_x;

int inputs[], outputs[];

vectorf OPEN,
       HIGH, 
       LOW;
       
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
  if (!LoadNormParams()) //Load the normalization parameters we saved once
    {
      Print("Normalization parameters csv files couldn't be found \nEnsure you are collecting data and Normalizing them using [ONNX get data.ex5] Script \nTrain the Python model again if necessary");
      return INIT_FAILED;
    }
   
//--- ONNX SETTINGS
 
  mlp_onnxhandle = OnnxCreateFromBuffer(RNNModel, MQLInfoInteger(MQL_DEBUG) ? ONNX_DEBUG_LOGS : ONNX_DEFAULT); //creating onnx handle buffer | rUN DEGUG MODE during debug mode
  
  if (mlp_onnxhandle == 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)
   
   OnnxTypeInfo type_info; //Getting onnx information for Reference In case you forgot what the loaded ONNX is all about

   long input_count=OnnxGetInputCount(mlp_onnxhandle);
   Print("model has ",input_count," input(s)");
   for(long i=0; i<input_count; i++)
     {
      string input_name=OnnxGetInputName(mlp_onnxhandle,i);
      Print(i," input name is ",input_name);
      if(OnnxGetInputTypeInfo(mlp_onnxhandle,i,type_info))
        {
          PrintTypeInfo(i,"input",type_info);
          ArrayCopy(inputs, type_info.tensor.dimensions);
        }
     }

   long output_count=OnnxGetOutputCount(mlp_onnxhandle);
   Print("model has ",output_count," output(s)");
   for(long i=0; i<output_count; i++)
     {
      string output_name=OnnxGetOutputName(mlp_onnxhandle,i);
      Print(i," output name is ",output_name);
      if(OnnxGetOutputTypeInfo(mlp_onnxhandle,i,type_info))
       {
         PrintTypeInfo(i,"output",type_info);
         ArrayCopy(outputs, type_info.tensor.dimensions);
       }
     }
   
//---

   if (MQLInfoInteger(MQL_DEBUG))
    {
      Print("Inputs & Outputs");
      ArrayPrint(inputs);
      ArrayPrint(outputs);
    }
   
   const long input_shape[] = {batch_size, 3};
   
   if (!OnnxSetInputShape(mlp_onnxhandle, 0, input_shape)) //Giving the Onnx handle the input shape
     {
       printf("Failed to set the input shape Err=%d",GetLastError());
       return INIT_FAILED;
     }
   
   const long output_shape[] = {batch_size, 1};
   
   if (!OnnxSetOutputShape(mlp_onnxhandle, 0, output_shape)) //giving the onnx handle the output shape
     {
       printf("Failed to set the input shape Err=%d",GetLastError());
       return INIT_FAILED;
     }
   
//---

   return(INIT_SUCCEEDED);
  }

Resultados

PR      0       18:57:10.265    ONNX mt5 (EURUSD,H1)    ONNX: Creating and using per session threadpools since use_per_session_threads_ is true
CN      0       18:57:10.265    ONNX mt5 (EURUSD,H1)    ONNX: Dynamic block base set to 0
EE      0       18:57:10.266    ONNX mt5 (EURUSD,H1)    ONNX: Initializing session.
IM      0       18:57:10.266    ONNX mt5 (EURUSD,H1)    ONNX: Adding default CPU execution provider.
JN      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    ONNX: Use DeviceBasedPartition as default
QK      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    ONNX: Saving initialized tensors.
GR      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    ONNX: Done saving initialized tensors
RI      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    ONNX: Session successfully initialized.
JF      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    model has 1 input(s)
QR      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    0 input name is input_1
NF      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       type ONNX_TYPE_TENSOR
PM      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       data type ONNX_TYPE_TENSOR
HI      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       shape [-1, 3]
FS      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       0 input shape must be defined explicitly before model inference
NE      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       shape of input data can be reduced to [3] if undefined dimension set to 1
GD      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    model has 1 output(s)
GQ      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    0 output name is dense_1
LJ      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       type ONNX_TYPE_TENSOR
NQ      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       data type ONNX_TYPE_TENSOR
LF      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       shape [-1, 1]
KQ      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       0 output shape must be defined explicitly before model inference
CO      0       18:57:10.269    ONNX mt5 (EURUSD,H1)       shape of output data can be reduced to [1] if undefined dimension set to 1
GR      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    Inputs & Outputs
IE      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    -1  3
CK      0       18:57:10.269    ONNX mt5 (EURUSD,H1)    -1  1

Recuperando dados reais

Como eu mencionei anteriormente, os dados ao vivo devem ser obtidos do mercado e normalizados da mesma forma que foram normalizados ao coletar dados para treinamento.

Inside ONNX mt5.mq5 EA

matrixf GetLiveData(uint start, uint total)
 {
   matrixf return_matrix(total, 3);
   
   
    OPEN.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_OPEN, start, total);
    HIGH.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_HIGH, start, total);
    LOW.CopyRates(Symbol(), PERIOD_CURRENT,COPY_RATES_LOW, start, total);
        
    return_matrix.Col(OPEN, 0);
    return_matrix.Col(HIGH, 1);
    return_matrix.Col(LOW, 2);
     
     if (!norm_x.Normalization(return_matrix))
        Print("Failed to Normalize");  
      
   return return_matrix;
 }

Uma instância de trabalho da classe norm_x foi declarada dentro da função LoadNormParams() em OnInit. A função carrega os parâmetros de normalização salvos do respectivo arquivo CSV.

Inside ONNX mt5.mq5 EA

bool LoadNormParams()
 {
    vectorf min = {}, max ={}, mean={} , std = {};
    
    csv_name_ = Symbol()+"."+EnumToString(Period())+"."+string(total_bars);
    
    switch(NORM)
      {
       case  NORM_MEAN_NORM:
          
          mean = ReadCsvVector(normparams_folder+csv_name_+".mean_norm_scaler.mean.csv"); //--- Loading the mean
          min = ReadCsvVector(normparams_folder+csv_name_+".mean_norm_scaler.min.csv"); //--- Loading the min 
          max = ReadCsvVector(normparams_folder+csv_name_+".mean_norm_scaler.max.csv"); //--- Loading the max
          
          if (MQLInfoInteger(MQL_DEBUG))
              Print(EnumToString(NORM),"\nMean ",mean,"\nMin ",min,"\nMax ",max);
          
          norm_x = new CPreprocessing<vectorf,matrixf>(max, mean, min);
           
          if (mean.Sum()<=0 && min.Sum()<=0 && max.Sum() <=0)
              return false;  

         break;
         
       case NORM_MIN_MAX_SCALER:
          
          min = ReadCsvVector(normparams_folder+csv_name_+".min_max_scaler.min.csv"); //--- Loading the min
          max = ReadCsvVector(normparams_folder+csv_name_+".min_max_scaler.max.csv"); //--- Loading the max  
       
           
          if (MQLInfoInteger(MQL_DEBUG))
              Print(EnumToString(NORM),"\nMin ",min,"\nMax ",max);
              
          norm_x = new CPreprocessing<vectorf,matrixf>(max, min);
          
          
          if (min.Sum()<=0 && max.Sum() <=0)
            return false;
            
          break;
          
       case NORM_STANDARDIZATION:
          
          mean = ReadCsvVector(normparams_folder+csv_name_+".standardization_scaler.mean.csv"); //--- Loading the mean
          std = ReadCsvVector(normparams_folder+csv_name_+".standardization_scaler.std.csv"); //--- Loading the std
         
          if (MQLInfoInteger(MQL_DEBUG))
              Print(EnumToString(NORM),"\nMean ",mean,"\nStd ",std);
             
           norm_x = new CPreprocessing<vectorf,matrixf>(mean, std, NORM_STANDARDIZATION);
            
          if (mean.Sum()<=0 && std.Sum() <=0)
            return false;
            
          break;
      }
      
   return true;
 }


05: Executando o modelo em tempo real

Para usar o modelo dentro da função OnTick, é necessário chamar a função OnnxRun e passar a ela o handle ONNX, o vetor ou a matriz de valores float tanto para dados de entrada quanto para previsões.

Inside ONNX mt5.mq5 EA

void OnTick()
  {
//---
   matrixf input_data = GetLiveData(0,1);
   vectorf output_data(1); //It is very crucial to resize this vector or matrix
   
   
   if (!OnnxRun(mlp_onnxhandle, ONNX_NO_CONVERSION, input_data, output_data))
     {
       Print("Failed to Get the Predictions Err=",GetLastError());
       ExpertRemove();
       return;
     }
   
   Comment("inputs_data\n",input_data,"\npredictions\n",output_data);
}

O vetor de saída ou matriz de float precisa ser ajustado para evitar o erro 5805 (ERR_ONNX_INVALID_PARAMETER). Como eu tenho apenas uma saída na rede neural, eu ajustei o tamanho desse vetor para ter tamanho 1. Se eu estivesse usando uma matriz, teria que ajustar seu tamanho para 1 linha e 1 coluna.

Resultados

gráfico

Tudo está funcionando normalmente. Agora estamos usando um modelo de rede neural, criado e treinado usando Python dentro do MetaTrader5. O procedimento não é tão complicado.


Vantagens de usar ONNX em MQL5

  1. Interoperabilidade: ONNX fornece um formato comum para representar modelos de aprendizado profundo. Este formato permite que modelos treinados em um ambiente de aprendizado profundo (como TensorFlow, PyTorch ou scikit-learn) sejam usados em MQL5 sem a necessidade de uma extensa re-implementação do modelo. Isso pode economizar muito tempo, já que não precisaremos mais codificar modelos do zero para fazê-los funcionar em MQL5.
  2. Flexibilidade: ONNX suporta uma ampla gama de tipos de modelos de aprendizado profundo: desde redes neurais tradicionais de feedforward até modelos mais complexos, como redes neurais recorrentes (RNN) e redes neurais convolucionais (CNN). Essa flexibilidade o torna adequado para uma variedade de aplicações.
  3. Eficiência: modelos ONNX podem ser otimizados para implantação eficiente em diferentes equipamentos e plataformas. Isso significa que você pode implantar modelos em dispositivos periféricos, dispositivos móveis, servidores na nuvem e até em aceleradores de hardware especializados.
  4. Suporte da comunidade: ONNX conta com um suporte significativo da comunidade. Plataformas de aprendizado profundo líderes, como TensorFlow, PyTorch e scikit-learn, suportam a exportação de modelos para o formato ONNX, enquanto várias engines de execução, como ONNX Runtime, facilitam a implantação de modelos ONNX.
  5. Ampla ecossistema: ONNX está integrado em diversos pacotes de software, existindo um extenso conjunto de ferramentas para trabalhar com modelos ONNX. Com essas ferramentas, você pode converter, otimizar e executar modelos no formato ONNX.
  6. Compatibilidade entre plataformas: Modelos exportados no formato ONNX podem ser executados em diferentes sistemas operacionais e hardware sem modificações. 
  7. Evolução do Modelo: ONNX suporta o gerenciamento de versões e a evolução do modelo. Você pode melhorar e expandir seus modelos ao longo do tempo, mantendo a compatibilidade com versões anteriores.
  8. Padronização: ONNX está se tornando o padrão de facto para interação entre diferentes plataformas de aprendizado profundo, simplificando para a comunidade a troca de modelos e ferramentas. 


Considerações finais

ONNX é particularmente valioso em cenários onde você precisa usar modelos em diferentes plataformas, implantar modelos em diversas plataformas ou colaborar com outras pessoas que podem estar usando diferentes ferramentas de aprendizado profundo. Ele simplifica o processo de trabalhar com modelos de aprendizado profundo e, à medida que o ecossistema continua a crescer, os benefícios do ONNX se tornam ainda mais significativos. Neste artigo, examinamos cinco etapas importantes que você precisa realizar para começar com um modelo operacional. Pelo menos, você pode expandir esse código de acordo com suas necessidades. Além disso, para que o programa funcione com o testador de estratégias, os arquivos CSV de normalização precisam ser lidos dentro do testador. Isso não foi abordado no artigo atual. 

Obrigado pela atenção!

Arquivo Aplicação
neuralnet.py  Arquivo principal do script Python. Contém toda a implementação da rede neural em Python.
ONNX mt5.mq5  Expert Advisor que demonstra como usar o modelo ONNX na negociação.
ONNX get data.mq5  Script para coletar e preparar dados para uso conjunto com o script Python.


Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/13394

Arquivos anexados |
MQL5-CODE.zip (9.14 KB)
Adicionando um LLM personalizado a um robô investidor (Parte 1): Implantação de equipamentos e ambiente Adicionando um LLM personalizado a um robô investidor (Parte 1): Implantação de equipamentos e ambiente
Os modelos de linguagem são uma parte importante da inteligência artificial que evolui rapidamente, por isso devemos pensar em como integrar LLMs poderosos em nossa negociação algorítmica. Para a maioria das pessoas, é desafiador configurar esses poderosos modelos de acordo com suas necessidades, implementá-los localmente e, em seguida, aplicá-los à negociação algorítmica. Esta série de artigos explorará uma abordagem passo a passo para alcançar esse objetivo.
Desenvolvendo um sistema de Replay (Parte 44): Projeto do Chart Trade (III) Desenvolvendo um sistema de Replay (Parte 44): Projeto do Chart Trade (III)
No artigo anterior, expliquei como você pode manipular os dados do template a fim de usá-los em um OBJ_CHART. Mas lá apenas introduzi a questão, mas sem entrar em muitos detalhes, já que naquela versão o trabalho foi feito de uma maneira bem simplificada. No entanto, ela foi feita daquela forma, justamente para facilitar a explicação do conteúdo. Pois apesar de parecer simples fazer certas coisas, algumas não são tão evidentes, e sem compreender a parte mais simples e básica, você não irá de fato entender o que estou fazendo.
Trabalho com datas e horas no MQL5 Trabalho com datas e horas no MQL5
É muito importante que os operadores e desenvolvedores de ferramentas de negociação entendam como manusear datas e horas de forma adequada e eficiente. Neste artigo, mostrarei como podemos trabalhar com datas e horas ao criar ferramentas de negociação eficientes.
A sazonalidade no mercado de moedas e suas possibilidades de uso A sazonalidade no mercado de moedas e suas possibilidades de uso
Todo indivíduo moderno está familiarizado com o conceito de sazonalidade, por exemplo, todos nós estamos acostumados com o aumento dos preços de vegetais frescos no inverno ou o aumento do preço dos combustíveis durante fortes geadas, mas poucos sabem que existem padrões semelhantes no mercado de moedas.