English Русский Español Deutsch 日本語 Português
preview
掌握ONNX:MQL5交易者的游戏规则改变者

掌握ONNX:MQL5交易者的游戏规则改变者

MetaTrader 5交易系统 | 28 三月 2024, 10:08
460 0
Omega J Msigwa
Omega J Msigwa

“以ONNX格式导出和导入人工智能模型的能力简化了开发过程,在将人工智能集成到不同的语言生态系统中时节省了时间和资源。”


概述

不可否认,我们正处于人工智能和机器学习的时代,每天都有一种新的基于人工智能的技术部署在金融、艺术和游戏、教育以及生活的更多方面。

对我们这些交易员来说,学习利用人工智能的力量可以让我们在市场上占据优势,让我们检测到肉眼看不到的模式和关系。

尽管人工智能看起来很酷很神奇,但在这些模型背后,有一些复杂的数学运算,如果你要从头开始实现这些机器学习模型,需要大量的工作、高度的准确性和专注力才能正确地计算和实现,而这要归功于开源

如今,你甚至不需要是数学和编程天才就可以构建和实现人工智能模型。你需要对你想在项目中使用的某种编程语言或工具有一个基本的了解,在某些情况下,你甚至不用拥有电脑,这要归功于Google Colab等服务,你可以使用python免费编写、构建和运行人工智能模型的代码。

尽管使用Python和其他流行且成熟的编程语言实现机器学习模型很容易,但老实说,在MQL5中实现它并不容易。除非你想通过在MQL5中从头开始创建机器学习模型来重新发明轮子——这是我们在系列文章中所做的事情——否则我强烈建议使用ONNX将python中构建的人工智能模型集成到MQL5。ONNX现在在MQL5中得到支持,我很兴奋,我相信你也应该这样做。

  onnx-mql5

理解本文内容需要对人工智能和机器学习有基本的了解,请参阅>>这篇文章,还有这篇文章。 


什么是ONNX?

ONNX代表开放神经网络交换(Open Neural Network Exchange),是一种用于表示机器学习和深度学习模型的开源格式。它允许您将在一个深度学习框架中训练的模型转换为可在其他框架中使用的通用格式,从而更容易在不同平台和工具中使用模型。

这意味着您可以使用MQL5以外的任何支持机器学习的语言构建机器学习模型,然后将该模型转换为ONNX格式,然后可以在MQL5程序中使用该ONNX模型。

在这篇文章中,我将使用Python来构建机器学习,因为我很熟悉它,我被告知你也可以使用其他语言,我不确定。顺便说一句,整个ONNX文档似乎是基于Python的,我相信目前ONNX是为Python而设计的,这是有道理的,因为我认为除了Python之外,没有任何语言具有高级的基于AI的库和工具。


ONNX的基本概念:

在深入研究ONNX之前,您应该熟悉一些关键概念:

  • ONNX模型:ONNX模型是机器学习模型的表现形式,它由一个计算图组成,其中节点表示运算(例如卷积、加法),边表示运算之间的数据流。
  • Nodes:ONNX图中的节点(Node)表示应用于输入数据的操作或函数。这些节点可以是卷积、加法或自定义操作等操作。
  • Tensors:张量(Tensor)是表示计算图中节点之间流动的数据的多维数组。它们可以是输入、输出或中间数据。
  • Operators:运算符(Operators)是应用于ONNX中张量的函数。每个运算符表示一个特定的运算,如矩阵乘法或逐元素加法。

用Python构建模型并使用ONNX在MQL5中部署

要在Python中成功构建机器学习模型,请在MQL5中的EA、指标或脚本中部署该模型;它需要的不仅仅是模型的Python代码,下面是要遵循的关键步骤,这样你不仅可以得到ONNX模型,还可以得到一个可以给出你想要的准确预测的模型;

  1. 数据收集
  2. MQL5侧的数据规范化 
  3. 用Python构建模型
  4. 在MQL5中获取已构建的ONNX模型
  5. 实时运行模型


01:数据收集

数据收集是MQL5程序中必须完成的第一件事,我认为最好在MQL5计划中收集所有数据,以与我们收集训练数据和实时交易或实时运行模型期间使用的数据的方式一致。请记住,数据收集可能会因您试图解决的问题的性质而异。在本文中,我们将试图解决回归问题。我们将使用OHLC(开盘价、最高价、最低价、收盘价)市场信息作为我们的主要数据集,其中开盘价(Open)最高价(High)最低价(Low)将用作自变量,而收盘价(Close)价格值将用作目标变量。

在 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;
 } 

该函数的作用是收集自变量OHL和目标变量CLOSE。在有监督的机器学习中,需要指定目标变量并将其提供给模型,以便它能够在此基础上学习并理解目标变量和其他变量之间的模式,在我们的例子中,模型将试图了解这些指标读数是如何导致上涨或下跌的。 

在部署模型时,我们需要以相同的方式收集数据,只是这次我们将在没有目标变量的情况下进行收集,因为这是我们希望经过训练的模型能够解决的问题。可以说是预测。

这就是为什么有一个名为GetLiveData的不同函数,用于加载新数据,以便在市场上进行流畅的预测。

在 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;
 }

收集训练数据

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

输出:

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]

获取实时数据

获取当前柱的OHL信息。

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

输出:

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

当为时间序列预测模型准备数据时,实时数据的加载方式可能略有不同,如RNNGRULSTM

注意我是如何使用matrixf的,它代表浮点型矩阵,而不仅仅是矩阵!!这是为了确保MQL5和Python之间的数据类型兼容性:确保输入数据的数据类型与ONNX模型所期望的数据类型兼容。如果ONNX模型需要float32输入,请确保输入数据的类型也为float32。ONNX与float32float64兼容,如果出错,可能会导致类似的错误;
error 2023.09.18 18:03:53.212   ONNX: invalid parameter size, expected 1044480 bytes instead of 32640


02:MQL5侧的数据规范化

对于机器学习模型将要使用的数据集,数据规范化是最关键的事情之一。

请记住,用于准备训练数据的规范化技术需要与用于准备测试和实时数据的技术相同。这意味着,如果所使用的技术是MinMaxScaler,则必须使用在准备训练数据时使用的minmax值来保持将由模型在其他地方处理的新数据的标准化。为了实现这种一致性,我们必须将每个规范化技术的变量保存到csv文件中:

数据规范化仅适用于自变量,无论您试图解决哪种问题,都不必规范化目标变量

我们将使用此处找到的预处理(Preprocessing)类 。

在 ONNX get data.mq5 脚本文件中

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

输出:

规范化参数

当在csv文件中使用 Standardization Scaler 时,参数如下所示;


请注意,规范化也集成在GetData函数中,因为规范化非常重要,所以负责收集数据的两个函数返回的每个数据矩阵都必须是具有规范化价格值的矩阵。

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

 最后,数据保存在CSV中,以便与Python代码共享。


03:用Python构建模型

我将建立一个多层感知器神经网络,但你可以建立任何你选择的模型。您不限于此特定类型的模型。如果你还没有在系统中安装Python,请先安装。之后,通过从Windows CMD运行以下命令安装virtualenv,不要与Powershell混淆

$ pip3 install virtualenv

在运行之后,

$ virtualenv venv

这将为您的Windows机器创建一个Python虚拟环境,我相信我们大多数人都在Windows上,对于Mac用户和Linux用户来说,这个过程可能有点不同。之后,通过运行以下命令启动一个虚拟环境

$ venv\Scripts\activate

之后,通过运行安装本教程中使用的所有依赖项

$ pip install -r requirements.txt #This txt file is found at https://github.com/MegaJoctan/ONNX-MQL5/blob/main/requirements.txt

通过创建虚拟环境来隔离项目总是很重要的,以避免模块和Python版本之间的冲突,并使项目易于共享

导入和初始化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"

我们需要检查路径是否存在,如果它不存在,那意味着我们还没有收集到MT5方面的数据。

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

构建多层感知器神经网络(MLP

我们将把一个MLP NN封装在一个类中,使我们的代码成为可读的部分;

01:类初始化

数据被收集并划分为训练和测试样本,同时重要变量被声明为可供整个类使用,

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"
                

输出:

pd head

02:建立神经网络模型

我们的单层神经网络是由给定数量的神经元定义的。

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

输出:

顺序模型摘要

03:训练和测试神经网络模型

    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

输出:

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

训练精度

神经网络模型在训练期间的准确率为93%,在测试期间的准确度约为95%,可能过拟合,但我们无论如何都会继续进行。

04:保存ONNX模型。

一旦训练成功完成,并且您对模型在训练和样本外验证中的性能感到满意,通常保存模型是一种很好的做法。我们需要在类中的train_network函数中添加ONNX运行时代码来保存模型。首先,我们需要安装两个库,onnxtf2onnx

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}')

输出:

onnx已保存

您可能已经注意到,我选择将ONNX模型保存在 Files 父目录下,为什么是这个目录?这是因为将 ONNX 文件作为资源(如专家顾问或指示器)包含在我们的MQL5程序中更容易。


04:在MQL5中获取已构建的ONNX模型

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

这将导入ONNX模型并将其存储在RNNModel uchar数组中。

我们需要做的下一件事是将ONNX句柄定义为全局变量,并在OnInit函数内创建句柄

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

输出:

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

获取实时数据

正如我之前所说,实时数据必须从市场上获得,并以收集训练数据时的规范化方式进行规范化。

在 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;
 }

为了使norm_x类实例工作,它是在OnInit内的LoadNormParams()函数内声明的,该函数从相应的CSV文件加载保存的规范化参数。

在 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:实时运行模型

要在OnTick函数中使用模型,只需调用OnnxRun函数,并将ONNX句柄向量矩阵float值传递给它,即可用于输入和预测。

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

需要调整输出数据矢量或float矩阵的大小,以避免出现代表ERR_ONNX_INVALID_PARAMETER的错误代码5805由于我在神经网络中只有一个输出,这就是为什么我调整了这个向量的大小,使其大小为1如果我使用矩阵,那么我应该将其调整为1行1列

输出:

图表

太好了,一切都很好。我们现在正在使用MetaTrader5中使用Python制作和训练的神经网络模型,这个过程并没有那么困难,干杯


在MQL5中使用ONNX的优势

  1. 互操作性:ONNX提供了一种表示深度学习模型的通用格式。这种格式允许在一个深度学习框架(如TensorFlowPyTorchscikit learn)中训练的模型在MQL5中使用,而无需大量的模型重新实现。这可以节省大量时间,因为我们不再需要从头开始硬编码模型以使其在MQL5中工作
  2. 灵活性:ONNX支持广泛的深度学习模型类型,从传统的前馈神经网络到更复杂的模型,如递归神经网络(RNN)和卷积神经网络(CNNs)。这种灵活性使其适用于各种应用。
  3. 效率:ONNX型号可以进行优化,以便在不同的硬件和平台上进行高效部署。这意味着您可以在边缘设备、移动设备、云服务器甚至专用硬件加速器上部署模型。
  4. 社区支持:ONNX获得了大量的社区支持。主要的深度学习框架,如TensorFlow、PyTorch和scikit-learn,支持将模型导出为ONNX格式,而各种运行时引擎,如ONNX runtime,使ONNX模型的部署变得容易。
  5. 广泛的生态系统:ONNX集成到各种软件包中,并且有广泛的工具用于处理ONNX模型。您可以使用这些工具转换、优化和运行ONNX格式的模型。
  6. 跨平台兼容性:ONNX被设计为跨平台,这意味着以ONNX格式导出的模型可以在外部和硬件的不同操作系统上运行,而无需修改。 
  7. 模型演进:ONNX支持模型版本控制和演进。您可以随着时间的推移改进和扩展您的模型,同时保持与以前版本的兼容性。
  8. 标准化:ONNX正在成为不同深度学习框架之间互操作性的事实标准,使社区更容易共享模型和工具。 


最后的想法

ONNX在需要跨不同框架利用模型、在各种平台上部署模型或与可能使用不同深度学习工具的其他人合作的场景中尤其有价值。它简化了使用深度学习模型的过程,随着生态系统的不断发展,ONNX的优势变得更加显著。在本文中,我们看到了开始使用工作模型所需遵循的5个重要步骤。至少可以说,您可以扩展此代码以满足您的需求。此外,为了让程序在策略测试器上工作,需要在测试器内部读取规范化CSV文件,这是我在本文中没有涉及的。 

顺致敬意

文件 用途
 neuralnet.py  主python脚本文件,包含所有用python语言实现的神经网络
ONNX mt5.mq5  EA 交易,展示如何在交易情况下使用ONNX模型
 ONNX get data.mq5   用于收集和准备要与python脚本共享的数据的脚本


本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/13394

附加的文件 |
MQL5-CODE.zip (9.14 KB)
将您自己的LLM集成到EA中(第1部分):硬件和环境部署 将您自己的LLM集成到EA中(第1部分):硬件和环境部署
随着人工智能的快速发展,大型语言模型(LLM)成为人工智能的重要组成部分,因此我们应该思考如何将强大的语言模型集成到我们的算法交易中。对大多数人来说,很难根据他们的需求对这些强大的模型进行微调,在本地部署,然后将其应用于算法交易。本系列文章将采取循序渐进的方法来实现这一目标。
开发回放系统 — 市场模拟(第 21 部分):外汇(II) 开发回放系统 — 市场模拟(第 21 部分):外汇(II)
我们将继续构建一个在外汇市场工作的系统。为了解决这个问题,我们必须在加载以前的柱线之前首先声明加载跳价。这解决了问题,但同时迫使用户遵循配置文件中的某些结构,就个人而言,这对我来说没有多大意义。原因是,通过设计一个负责分析和执行配置文件中内容的程序,我们可以允许用户按任何顺序声明他需要的元素。
了解如何在MQL5中处理日期和时间 了解如何在MQL5中处理日期和时间
这是一篇关于一个新的重要话题的新文章,这个话题是关于日期和时间的。作为交易工具的交易员或程序员,了解如何很好、有效地处理日期和时间这两个方面至关重要。因此,我将分享一些重要信息,关于我们如何处理日期和时间,以便顺利、简单地创建有效的交易工具。
从外汇市场的季节性获益 从外汇市场的季节性获益
我们都熟悉季节性的概念,例如,我们都习惯于冬季新鲜蔬菜价格的上涨或严重霜冻期间燃料价格的上涨,但很少有人知道外汇市场也存在类似的模式。