Русский 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
Wrapping ONNX models in classes

Wrapping ONNX models in classes

MetaTrader 5Examples | 23 August 2023, 15:43
3 435 3
MetaQuotes
MetaQuotes

Introduction

In the previous article, we used two ONNX models to arrange the voting classifier. The entire source text was organized as a single MQ5 file. The entire code was divided into functions. But what if we try to swap models? Or add another model? The original text will become even bigger. Let's try the object-oriented approach.


1. What models are we going to use?

In the previous voting classifier, we used one classification model and one regression model. In the regression model, instead of the predicted price movement (down, up, does not change), we get the predicted price used to calculate the class. However, in this case, we do not have a probability distribution by class, which does not allow for the so-called "soft voting".

We have prepared 3 classification models. Two models have already been used in the article "An example of how to ensemble ONNX models in MQL5". The first model (regression) was converted into a classification model. Training was conducted on a series of 10 OHLC prices. The second model is the classification one. Training was conducted on a series of 63 Close prices.

Finally, there is one more model. The classification model was trained on a series of 30 Close prices and a series of simple moving averages with averaging periods of 21 and 34. We did not make any assumptions about the intersection of the moving averages with the Close chart and among themselves - all patterns will be calculated and remembered by the network in the form of coefficient matrices between layers.

All models were trained on MetaQuotes-Demo server data, EURUSD D1 from 2010.01.01 to 2023.01.01. Training scripts for all three models are written in Python and are attached to this article. We will not provide their source codes here so as not to distract the reader's attention from the main topic of our article.


2. One base class for all models is needed

There are three models. Each differs from others in the size and preparation of the input data. All models feature the same interface. The classes of all models must be inherited from the same base class.

Let's try to represent the base class.

//+------------------------------------------------------------------+
//|                                            ModelSymbolPeriod.mqh |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+

//--- price movement prediction
#define PRICE_UP   0
#define PRICE_SAME 1
#define PRICE_DOWN 2

//+------------------------------------------------------------------+
//| Base class for models based on trained symbol and period         |
//+------------------------------------------------------------------+
class CModelSymbolPeriod
  {
protected:
   long              m_handle;           // created model session handle
   string            m_symbol;           // symbol of trained data
   ENUM_TIMEFRAMES   m_period;           // timeframe of trained data
   datetime          m_next_bar;         // time of next bar (we work at bar begin only)
   double            m_class_delta;      // delta to recognize "price the same" in regression models

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
   CModelSymbolPeriod(const string symbol,const ENUM_TIMEFRAMES period,const double class_delta=0.0001)
     {
      m_handle=INVALID_HANDLE;
      m_symbol=symbol;
      m_period=period;
      m_next_bar=0;
      m_class_delta=class_delta;
     }

   //+------------------------------------------------------------------+
   //| Destructor                                                       |
   //+------------------------------------------------------------------+
   ~CModelSymbolPeriod(void)
     {
      Shutdown();
     }

   //+------------------------------------------------------------------+
   //| virtual stub for Init                                            |
   //+------------------------------------------------------------------+
   virtual bool Init(const string symbol,const ENUM_TIMEFRAMES period)
     {
      return(false);
     }

   //+------------------------------------------------------------------+
   //| Check for initialization, create model                           |
   //+------------------------------------------------------------------+
   bool CheckInit(const string symbol,const ENUM_TIMEFRAMES period,const uchar& model[])
     {
      //--- check symbol, period
      if(symbol!=m_symbol || period!=m_period)
        {
         PrintFormat("Model must work with %s,%s",m_symbol,EnumToString(m_period));
         return(false);
        }

      //--- create a model from static buffer
      m_handle=OnnxCreateFromBuffer(model,ONNX_DEFAULT);
      if(m_handle==INVALID_HANDLE)
        {
         Print("OnnxCreateFromBuffer error ",GetLastError());
         return(false);
        }

      //--- ok
      return(true);
     }

   //+------------------------------------------------------------------+
   //| Release ONNX session                                             |
   //+------------------------------------------------------------------+
   void Shutdown(void)
     {
      if(m_handle!=INVALID_HANDLE)
        {
         OnnxRelease(m_handle);
         m_handle=INVALID_HANDLE;
        }
     }

   //+------------------------------------------------------------------+
   //| Check for continue OnTick                                        |
   //+------------------------------------------------------------------+
   virtual bool CheckOnTick(void)
     {
      //--- check new bar
      if(TimeCurrent()<m_next_bar)
         return(false);
      //--- set next bar time
      m_next_bar=TimeCurrent();
      m_next_bar-=m_next_bar%PeriodSeconds(m_period);
      m_next_bar+=PeriodSeconds(m_period);

      //--- work on new day bar
      return(true);
     }

   //+------------------------------------------------------------------+
   //| virtual stub for PredictPrice (regression model)                 |
   //+------------------------------------------------------------------+
   virtual double PredictPrice(void)
     {
      return(DBL_MAX);
     }

   //+------------------------------------------------------------------+
   //| Predict class (regression -> classification)                     |
   //+------------------------------------------------------------------+
   virtual int PredictClass(void)
     {
      double predicted_price=PredictPrice();
      if(predicted_price==DBL_MAX)
         return(-1);

      int    predicted_class=-1;
      double last_close=iClose(m_symbol,m_period,1);
      //--- classify predicted price movement
      double delta=last_close-predicted_price;
      if(fabs(delta)<=m_class_delta)
         predicted_class=PRICE_SAME;
      else
        {
         if(delta<0)
            predicted_class=PRICE_UP;
         else
            predicted_class=PRICE_DOWN;
        }

      //--- return predicted class
      return(predicted_class);
     }
  };
//+------------------------------------------------------------------+

The base class can be used for both regression and classification models. We only need to implement the appropriate method in the descendant class — PredictPrice or PredictClass.

The base class sets the symbol-period the model is to work with (the data the model was trained on). The base class also checks that the EA using the model works on the required symbol-period and creates an ONNX session to execute the model. The base class provides work only at the beginning of a new bar.


3. First model class

Our first model is called model.eurusd.D1.10.class.onnx, which is a classification model trained on EURUSD D1 on a series of 10 OHLC prices.

//+------------------------------------------------------------------+
//|                                        ModelEurusdD1_10Class.mqh |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#include "ModelSymbolPeriod.mqh"

#resource "Python/model.eurusd.D1.10.class.onnx" as uchar model_eurusd_D1_10_class[]

//+------------------------------------------------------------------+
//| ONNX-model wrapper class                                         |
//+------------------------------------------------------------------+
class CModelEurusdD1_10Class : public CModelSymbolPeriod
  {
private:
   int               m_sample_size;

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
   CModelEurusdD1_10Class(void) : CModelSymbolPeriod("EURUSD",PERIOD_D1)
     {
      m_sample_size=10;
     }

   //+------------------------------------------------------------------+
   //| ONNX-model initialization                                        |
   //+------------------------------------------------------------------+
   virtual bool Init(const string symbol, const ENUM_TIMEFRAMES period)
     {
      //--- check symbol, period, create model
      if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_10_class))
        {
         Print("model_eurusd_D1_10_class : initialization error");
         return(false);
        }

      //--- 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_shape[] = {1,m_sample_size,4};
      if(!OnnxSetInputShape(m_handle,0,input_shape))
        {
         Print("model_eurusd_D1_10_class : OnnxSetInputShape error ",GetLastError());
         return(false);
        }
   
      //--- 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_shape[] = {1,3};
      if(!OnnxSetOutputShape(m_handle,0,output_shape))
        {
         Print("model_eurusd_D1_10_class : OnnxSetOutputShape error ",GetLastError());
         return(false);
        }
      //--- ok
      return(true);
     }

   //+------------------------------------------------------------------+
   //| Predict class                                                    |
   //+------------------------------------------------------------------+
   virtual int PredictClass(void)
     {
      static matrixf input_data(m_sample_size,4);    // matrix for prepared input data
      static vectorf output_data(3);                 // vector to get result
      static matrix  mm(m_sample_size,4);            // matrix of horizontal vectors Mean
      static matrix  ms(m_sample_size,4);            // matrix of horizontal vectors Std
      static matrix  x_norm(m_sample_size,4);        // matrix for prices normalize
   
      //--- prepare input data
      matrix rates;
      //--- request last bars
      if(!rates.CopyRates(m_symbol,m_period,COPY_RATES_OHLC,1,m_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<m_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(m_handle,ONNX_NO_CONVERSION,input_data,output_data))
         return(-1);
      //--- evaluate prediction
      return(int(output_data.ArgMax()));
     }
  };
//+------------------------------------------------------------------+

As already mentioned above: "There are three models. Each differs from others in the size and preparation of the input data". We have redefined only two methods — Init and PredictClass. The same methods will be redefined in other two classes for the other two models.

The Init method calls the CheckInit base class method where a session for our ONNX model is created and the sizes of the input and output tensors are explicitly set. There are more comments than code here.

The PredictClass method provides exactly the same input data preparation as when training the model. The input is a matrix of normalized OHLC prices.


4. Let's check how it works

A very compact Expert Advisor was created to test the performance of our class.

//+------------------------------------------------------------------+
//|                                    ONNX.eurusd.D1.Prediction.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright   "Copyright 2023, MetaQuotes Ltd."
#property link        "https://www.mql5.com"
#property version     "1.00"

#include "ModelEurusdD1_10Class.mqh"
#include <Trade\Trade.mqh>

input double InpLots = 1.0;    // Lots amount to open position

CModelEurusdD1_10Class ExtModel;
CTrade                 ExtTrade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(!ExtModel.Init(_Symbol,_Period))
      return(INIT_FAILED);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ExtModel.Shutdown();
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   if(!ExtModel.CheckOnTick())
      return;

//--- predict next price movement
   int predicted_class=ExtModel.PredictClass();
//--- check trading according to prediction
   if(predicted_class>=0)
      if(PositionSelect(_Symbol))
         CheckForClose(predicted_class);
      else
         CheckForOpen(predicted_class);
  }
//+------------------------------------------------------------------+
//| Check for open position conditions                               |
//+------------------------------------------------------------------+
void CheckForOpen(const int predicted_class)
  {
   ENUM_ORDER_TYPE signal=WRONG_VALUE;
//--- check signals

   if(predicted_class==PRICE_DOWN)
      signal=ORDER_TYPE_SELL;    // sell condition
   else
     {
      if(predicted_class==PRICE_UP)
         signal=ORDER_TYPE_BUY;  // buy condition
     }

//--- open position if possible according to signal
   if(signal!=WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
      double price=SymbolInfoDouble(_Symbol,(signal==ORDER_TYPE_SELL) ? SYMBOL_BID : SYMBOL_ASK);
      ExtTrade.PositionOpen(_Symbol,signal,InpLots,price,0,0);
     }
  }
//+------------------------------------------------------------------+
//| Check for close position conditions                              |
//+------------------------------------------------------------------+
void CheckForClose(const int predicted_class)
  {
   bool bsignal=false;
//--- position already selected before
   long type=PositionGetInteger(POSITION_TYPE);
//--- check signals
   if(type==POSITION_TYPE_BUY && predicted_class==PRICE_DOWN)
      bsignal=true;
   if(type==POSITION_TYPE_SELL && predicted_class==PRICE_UP)
      bsignal=true;

//--- close position if possible
   if(bsignal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
      ExtTrade.PositionClose(_Symbol,3);
      //--- open opposite
      CheckForOpen(predicted_class);
     }
  }
//+------------------------------------------------------------------+

Since the model was trained on price data until 2023, let's launch the test from January 1, 2023.

Test settings

The result is displayed below:

Test results

As we can see, the model is fully functional.


5. Second model class

The second model is called model.eurusd.D1.30.class.onnx. The classification model trained on EURUSD D1 on a series of 30 Close prices and two simple moving averages with averaging periods of 21 and 34.

//+------------------------------------------------------------------+
//|                                        ModelEurusdD1_30Class.mqh |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#include "ModelSymbolPeriod.mqh"

#resource "Python/model.eurusd.D1.30.class.onnx" as uchar model_eurusd_D1_30_class[]

//+------------------------------------------------------------------+
//| ONNX-model wrapper class                                         |
//+------------------------------------------------------------------+
class CModelEurusdD1_30Class : public CModelSymbolPeriod
  {
private:
   int               m_sample_size;
   int               m_fast_period;
   int               m_slow_period;
   int               m_sma_fast;
   int               m_sma_slow;

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
   CModelEurusdD1_30Class(void) : CModelSymbolPeriod("EURUSD",PERIOD_D1)
     {
      m_sample_size=30;
      m_fast_period=21;
      m_slow_period=34;
      m_sma_fast=INVALID_HANDLE;
      m_sma_slow=INVALID_HANDLE;
     }

   //+------------------------------------------------------------------+
   //| ONNX-model initialization                                        |
   //+------------------------------------------------------------------+
   virtual bool Init(const string symbol, const ENUM_TIMEFRAMES period)
     {
      //--- check symbol, period, create model
      if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_30_class))
        {
         Print("model_eurusd_D1_30_class : initialization error");
         return(false);
        }

      //--- 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 (Close, MA fast, MA slow)
      const long input_shape[] = {1,m_sample_size,3};
      if(!OnnxSetInputShape(m_handle,0,input_shape))
        {
         Print("model_eurusd_D1_30_class : OnnxSetInputShape error ",GetLastError());
         return(false);
        }
   
      //--- 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_shape[] = {1,3};
      if(!OnnxSetOutputShape(m_handle,0,output_shape))
        {
         Print("model_eurusd_D1_30_class : OnnxSetOutputShape error ",GetLastError());
         return(false);
        }
      //--- indicators
      m_sma_fast=iMA(m_symbol,m_period,m_fast_period,0,MODE_SMA,PRICE_CLOSE);
      m_sma_slow=iMA(m_symbol,m_period,m_slow_period,0,MODE_SMA,PRICE_CLOSE);
      if(m_sma_fast==INVALID_HANDLE || m_sma_slow==INVALID_HANDLE)
        {
         Print("model_eurusd_D1_30_class : cannot create indicator");
         return(false);
        }
      //--- ok
      return(true);
     }

   //+------------------------------------------------------------------+
   //| Predict class                                                    |
   //+------------------------------------------------------------------+
   virtual int PredictClass(void)
     {
      static matrixf input_data(m_sample_size,3);    // matrix for prepared input data
      static vectorf output_data(3);                 // vector to get result
      static matrix  x_norm(m_sample_size,3);        // matrix for prices normalize
      static vector  vtemp(m_sample_size);
      static double  ma_buffer[];
   
      //--- request last bars
      if(!vtemp.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,1,m_sample_size))
         return(-1);
      //--- get series Mean
      double m=vtemp.Mean();
      //--- get series Std
      double s=vtemp.Std();
      //--- normalize
      vtemp-=m;
      vtemp/=s;
      x_norm.Col(vtemp,0);
      //--- fast sma
      if(CopyBuffer(m_sma_fast,0,1,m_sample_size,ma_buffer)!=m_sample_size)
         return(-1);
      vtemp.Assign(ma_buffer);
      m=vtemp.Mean();
      s=vtemp.Std();
      vtemp-=m;
      vtemp/=s;
      x_norm.Col(vtemp,1);
      //--- slow sma
      if(CopyBuffer(m_sma_slow,0,1,m_sample_size,ma_buffer)!=m_sample_size)
         return(-1);
      vtemp.Assign(ma_buffer);
      m=vtemp.Mean();
      s=vtemp.Std();
      vtemp-=m;
      vtemp/=s;
      x_norm.Col(vtemp,2);
   
      //--- run the inference
      input_data.Assign(x_norm);
      if(!OnnxRun(m_handle,ONNX_NO_CONVERSION,input_data,output_data))
         return(-1);
      //--- evaluate prediction
      return(int(output_data.ArgMax()));
     }
  };
//+------------------------------------------------------------------+

As in the previous class, the CheckInit base class method is called in the Init method. In the base class method, a session is created for the ONNX model and the sizes of the input and output tensors are explicitly set.

The PredictClass method provides a series of 30 previous Closes and calculated moving averages. The data are normalized in the same way as in training.

Let's see how this model works. To do this, let's change only two strings of the test EA

#include "ModelEurusdD1_30Class.mqh"
#include <Trade\Trade.mqh>

input double InpLots = 1.0;    // Lots amount to open position

CModelEurusdD1_30Class ExtModel;
CTrade                 ExtTrade;

The test parameters are the same.

Second model testing results

We see that the model works.


6. Third model class

The last model is called model.eurusd.D1.63.class.onnx. The classification model trained on EURUSD D1 on a series of 63 Close prices.

//+------------------------------------------------------------------+
//|                                             ModelEurusdD1_63.mqh |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#include "ModelSymbolPeriod.mqh"

#resource "Python/model.eurusd.D1.63.class.onnx" as uchar model_eurusd_D1_63_class[]

//+------------------------------------------------------------------+
//| ONNX-model wrapper class                                         |
//+------------------------------------------------------------------+
class CModelEurusdD1_63Class : public CModelSymbolPeriod
  {
private:
   int               m_sample_size;

public:
   //+------------------------------------------------------------------+
   //| Constructor                                                      |
   //+------------------------------------------------------------------+
   CModelEurusdD1_63Class(void) : CModelSymbolPeriod("EURUSD",PERIOD_D1,0.0001)
     {
      m_sample_size=63;
     }

   //+------------------------------------------------------------------+
   //| ONNX-model initialization                                        |
   //+------------------------------------------------------------------+
   virtual bool Init(const string symbol, const ENUM_TIMEFRAMES period)
     {
      //--- check symbol, period, create model
      if(!CModelSymbolPeriod::CheckInit(symbol,period,model_eurusd_D1_63_class))
        {
         Print("model_eurusd_D1_63_class : initialization error");
         return(false);
        }

      //--- 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_shape[] = {1,m_sample_size};
      if(!OnnxSetInputShape(m_handle,0,input_shape))
        {
         Print("model_eurusd_D1_63_class : OnnxSetInputShape error ",GetLastError());
         return(false);
        }
   
      //--- 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_shape[] = {1,3};
      if(!OnnxSetOutputShape(m_handle,0,output_shape))
        {
         Print("model_eurusd_D1_63_class : OnnxSetOutputShape error ",GetLastError());
         return(false);
        }
      //--- ok
      return(true);
     }

   //+------------------------------------------------------------------+
   //| Predict class                                                    |
   //+------------------------------------------------------------------+
   virtual int PredictClass(void)
     {
      static vectorf input_data(m_sample_size);  // vector for prepared input data
      static vectorf output_data(3);             // vector to get result
   
      //--- request last bars
      if(!input_data.CopyRates(m_symbol,m_period,COPY_RATES_CLOSE,1,m_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(m_handle,ONNX_NO_CONVERSION,input_data,output_data))
         return(-1);
      //--- evaluate prediction
      return(int(output_data.ArgMax()));
     }
  };
//+------------------------------------------------------------------+

This is the simplest model of the three. This is why the code for the PredictClass method is so compact.

Let's change two strings in the EA again

#include "ModelEurusdD1_63Class.mqh"
#include <Trade\Trade.mqh>

input double InpLots = 1.0;    // Lots amount to open position

CModelEurusdD1_63Class ExtModel;
CTrade                 ExtTrade;

Launch the test with the same settings.

Third model testing results

The model works.



7. Collecting all models in one EA. Hard voting

All three models have shown their working capacity. Now let's try to combine their efforts. Let's arrange a vote of models.

Forward declarations and definitions

#include "ModelEurusdD1_10Class.mqh"
#include "ModelEurusdD1_30Class.mqh"
#include "ModelEurusdD1_63Class.mqh"
#include <Trade\Trade.mqh>

input double  InpLots  = 1.0;    // Lots amount to open position

CModelSymbolPeriod *ExtModels[3];
CTrade              ExtTrade;

OnInit function

int OnInit()
  {
   ExtModels[0]=new CModelEurusdD1_10Class;
   ExtModels[1]=new CModelEurusdD1_30Class;
   ExtModels[2]=new CModelEurusdD1_63Class;

   for(long i=0; i<ExtModels.Size(); i++)
      if(!ExtModels[i].Init(_Symbol,_Period))
         return(INIT_FAILED);
//---
   return(INIT_SUCCEEDED);
  }

OnTick function

void OnTick()
  {
   for(long i=0; i<ExtModels.Size(); i++)
      if(!ExtModels[i].CheckOnTick())
         return;

//--- predict next price movement
   int returned[3]={0,0,0};
//--- collect returned classes
   for(long i=0; i<ExtModels.Size(); i++)
     {
      int pred=ExtModels[i].PredictClass();
      if(pred>=0)
         returned[pred]++;
     }
//--- get one prediction for all models
   int predicted_class=-1;
//--- count votes for predictions
   for(int n=0; n<3; n++)
     {
      if(returned[n]>=2)
        {
         predicted_class=n;
         break;
        }
     }

//--- check trading according to prediction
   if(predicted_class>=0)
      if(PositionSelect(_Symbol))
         CheckForClose(predicted_class);
      else
         CheckForOpen(predicted_class);
  }

The majority of votes is calculated according to the equation <total number of votes>/2 + 1. For a total of 3 votes, the majority is 2 votes. This is a so-called "hard voting".

The test result is still with the same settings.

Hard voting test results

Let's recall the work of all three models separately, namely the number of profitable and unprofitable trades. First model — 11 : 3, second — 6 : 1, third — 16 : 10.

It seems that we have improved the result with the help of hard voting — 16 : 4. But, of course, we need to look at full reports and test charts.


8. Soft voting

Soft voting differs from hard one in that it is not the number of votes that is taken into account, but the sum of the probabilities of all three classes from all three models. The class is chosen by the highest probability.

To ensure soft voting, some changes need to be made.

In the base class:

   //+------------------------------------------------------------------+
   //| Predict class (regression -> classification)                     |
   //+------------------------------------------------------------------+
   virtual int PredictClass(vector& probabilities)
     {
...
      //--- set predicted probability as 1.0
      probabilities.Fill(0);
      if(predicted_class<(int)probabilities.Size())
         probabilities[predicted_class]=1;
      //--- and return predicted class
      return(predicted_class);
     }

In the descendant classes:

   //+------------------------------------------------------------------+
   //| Predict class                                                    |
   //+------------------------------------------------------------------+
   virtual int PredictClass(vector& probabilities)
     {
...
      //--- evaluate prediction
      probabilities.Assign(output_data);
      return(int(output_data.ArgMax()));
     }

In the EA:

#include "ModelEurusdD1_10Class.mqh"
#include "ModelEurusdD1_30Class.mqh"
#include "ModelEurusdD1_63Class.mqh"
#include <Trade\Trade.mqh>

enum EnVotes
  {
   Two=2,    // Two votes
   Three=3,  // Three votes
   Soft=4    // Soft voting
  };

input double  InpLots  = 1.0;    // Lots amount to open position
input EnVotes InpVotes = Two;    // Votes to make trade decision

CModelSymbolPeriod *ExtModels[3];
CTrade              ExtTrade;
void OnTick()
  {
   for(long i=0; i<ExtModels.Size(); i++)
      if(!ExtModels[i].CheckOnTick())
         return;

//--- predict next price movement
   int    returned[3]={0,0,0};
   vector soft=vector::Zeros(3);
//--- collect returned classes
   for(long i=0; i<ExtModels.Size(); i++)
     {
      vector prob(3);
      int    pred=ExtModels[i].PredictClass(prob);
      if(pred>=0)
        {
         returned[pred]++;
         soft+=prob;
        }
     }
//--- get one prediction for all models
   int predicted_class=-1;
//--- soft or hard voting
   if(InpVotes==Soft)
      predicted_class=(int)soft.ArgMax();
   else
     {
      //--- count votes for predictions
      for(int n=0; n<3; n++)
        {
         if(returned[n]>=InpVotes)
           {
            predicted_class=n;
            break;
           }
        }
     }

//--- check trading according to prediction
   if(predicted_class>=0)
      if(PositionSelect(_Symbol))
         CheckForClose(predicted_class);
      else
         CheckForOpen(predicted_class);
  }

The testing settings are the same. In the inputs, select Soft.

Input settings

The result is as follows.

Soft voting test results

Profitable trades — 15, unprofitable trades — 3. In monetary terms, hard voting also turned out to be better than soft voting.


Let's look at the result of an unanimous vote, that is, with a number of votes of 3.

Unanimous vote test results

Very conservative trading. The only unprofitable trade was closed at the end of testing (perhaps, it is not unprofitable).

Unanimous vote test graph


Important note: Please be advised that the models used in the article are presented only to demonstrate how to work with ONNX models using the MQL5 language. The Expert Advisor is not intended for trading on real accounts.


Conclusion

In this article, we showed how object-oriented programming makes it easier to write programs. All the complexities of the models are hidden in their classes (models can be much more complex than the ones we have presented as an example). The rest of the "complexity" fit in 45 strings of the OnTick function.


Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/12484

Attached files |
MQL5.zip (190.3 KB)
Last comments | Go to discussion (3)
Xiaoyu Huang
Xiaoyu Huang | 25 Aug 2023 at 07:17
Very useful article!

Is ONNX version 1.13.0 or 1.14.0 used by MT5?
Too Chee Ng
Too Chee Ng | 16 Nov 2023 at 11:40

This is really interesting, thank you very much.

In the attached ML file ONNX.eurusd.D1.30.class.Training.py to the article, there are the following lines of code (line 48 - 59) in def collect_dataset():

    for i in tqdm(range(n - sample_size)):
        w = df.iloc[i: i + sample_size + 1]
        x = w[['close', 'ma_fast', 'ma_slow']].iloc[:-1].values

        delta = x[0][-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

What is the logic behind the highlighted line above please?

The classification is based on the different between the first sample's 'ma_slow' (x[0][-1]) and the new target's 'close' (w.iloc[-1]['close']). Moreover there would have a time different of 'sample_size-1'.

In addition:

if delta>0:
              y = 1, 0, 0

shouldn't this be y = 0,0,1? I.e. a Sell signal.

Likewise to ONNX.eurusd.D1.10.class.Training.py in def collect_dataset(), line45-47:

        x = w[['open', 'high', 'low', 'close']].iloc[:-1].values

        delta = x[3][-1] - w.iloc[-1]['close']
How? The classification is based on the different between the fourth sample's 'close' (x[3][-1]) and the new target's 'close' (w.iloc[-1]['close']); and there would have a time different of 'sample_size-4'.
Too Chee Ng
Too Chee Ng | 16 Nov 2023 at 11:43
Xiaoyu Huang #:
Very useful article!

Is ONNX version 1.13.0 or 1.14.0 used by MT5?
Can't seem to find the information. Any reason for asking?
Developing a Replay System — Market simulation (Part 05): Adding Previews Developing a Replay System — Market simulation (Part 05): Adding Previews
We have managed to develop a way to implement the market replay system in a realistic and accessible way. Now let's continue our project and add data to improve the replay behavior.
OpenAI's ChatGPT features within the framework of MQL4 and MQL5 development OpenAI's ChatGPT features within the framework of MQL4 and MQL5 development
In this article, we will fiddle around ChatGPT from OpenAI in order to understand its capabilities in terms of reducing the time and labor intensity of developing Expert Advisors, indicators and scripts. I will quickly navigate you through this technology and try to show you how to use it correctly for programming in MQL4 and MQL5.
Monte Carlo Permutation Tests in MetaTrader 5 Monte Carlo Permutation Tests in MetaTrader 5
In this article we take a look at how we can conduct permutation tests based on shuffled tick data on any expert advisor using only Metatrader 5.
Testing different Moving Average types to see how insightful they are Testing different Moving Average types to see how insightful they are
We all know the importance of the Moving Average indicator for a lot of traders. There are other Moving average types that can be useful in trading, we will identify these types in this article and make a simple comparison between each one of them and the most popular simple Moving average type to see which one can show the best results.