Developing an Expert Advisor template using MQL5

To effectively assess the performance of our model in the strategy tester, we need to encapsulate it in a trading robot. Hence, in this section, I decided to present a small template of an Expert Advisor utilizing a neural network as the primary and sole decision-making block. I must clarify that this is just a template, aimed at demonstrating implementation principles and approaches. Its code is considerably simplified and is not intended for use on real accounts. Nevertheless, it is fully functional and can serve as a foundation for constructing a working Expert Advisor. Additionally, I want to caution you that financial market trading implies high-risk investments. You perform all your operations at your own risk and under your full responsibility, including if you use Expert Advisors on your accounts. Of course, unless the creators of such trading robots offer explicit guarantees, subject to your individual agreements.

Regarding Expert Advisors, before installing them on your real trading accounts and entrusting them with your funds, carefully study their parameters and configuration options. Also, validate their performance across various modes in the strategy tester and on demo accounts.

I hope this clarification is comprehensible to everyone. Now, let's proceed to the implementation of the template. Primarily, as I mentioned earlier, the presented template is significantly simplified, omitting several essential functions required for Expert Advisors that are not related to the operation of our model. In particular, the Expert Advisor completely lacks a money management block. For simplicity, we use a fixed Lot. We also use a fixed StopLoss and set the range for take profit between MinTarget and MaxTP. This approach to setting the take profit stems from the fact that in the models we are testing, the second target variable precisely represented the distance to the nearest future extreme point.

sinput string          Model = "our_model.net";     
sinput int             BarsToPattern = 40;      
sinput bool            Common = true;
input ENUM_TIMEFRAMES  TimeFrame = PERIOD_M5;
input double           TradeLevel=0.9;
input double           Lot = 0.01;
input int              MaxTP500;
input double           ProfitMultiply = 0.8;
input int              MinTarget=100;
input int              StopLoss=300;

Additionally, I have opted for a simplified approach to model usage. Rather than creating and training the model within the Expert Advisor, I approached it from a different angle. In all the scripts we created to test the architectural solutions of neural layers, we saved the trained models. So why not simply load one of the trained models? You can create and train your own model, and then just specify the file name of the trained model in the external Model parameter and use it. All that remains is to specify the storage location of the Common file, the number of bars describing one pattern BarsToPattern, and the TimeFrame used. Also, to make a decision, we will indicate the minimum predicted probability of profit TradeLevel.

To increase the probability of closing a trade at the take profit level, we add the ProfitMultiply parameter in which we indicate the coefficient of confidence in the predicted movement strength. In other words, when specifying the take profit level for an open position, we will adjust the size of the expected movement by this coefficient.

Using the Common parameter to specify the location of the trained model file is quite important, as strange as it may seem. The reason is that access to files in MetaTrader 5 is restricted within its sandbox. Each terminal installed on the computer has its own sandbox. So, each of the two terminals installed on the same computer works in its own sandbox and does not interfere with the second. For cases where data exchange is needed between terminals on the same computer, a separate common folder is used. So, the true value of the Common parameter indicates the use of this common folder.

When using the strategy tester optimization mode, each testing agent works in its own separate sandbox, even within the same trading terminal. Therefore, to provide equal access to the trained model for all testing agents, you need to place it in the common terminal folder and specify the corresponding flag value.

After declaring the external parameters of our Expert Advisor, we include our library for working with neural network models neuronnet.mqh and the standard library for trading operations Trade\Trade.mqh in the global space.

#include "..\..\Include\NeuroNetworksBook\realization\neuronnet.mqh"
#include <Trade\Trade.mqh>
 
CNet *net;
CTrade *trade;
datetime lastbar = 0;
int h_RSI;
int h_MACD;

Next, we declare global variables:

  • net — pointer to the model object
  • trade — pointer to the object of trade operations
  • lastbar — time of the last analyzed bar, used to check the new candlestick opening event
  • h_RSI — handle of the RSI indicator
  • h_MACD — handle of the MACD indicator

Our template will contain a minimum set of functions. But this does not mean that your Expert Advisor should contain exactly the same number of them.

In the OnInit function, we initialize the Expert Advisor. At the beginning of the function, we create a new instance of a neural network object and immediately check the result of the operation. If the creation of a new object is successful, we load the model from the specified file. Of course, we verify the result of these operations.

int OnInit()
  {
//---
   if(!(net = new CNet()))
     {
      PrintFormat("Error creating Net: %d"GetLastError());
      return INIT_FAILED;
     }
   if(!net.Load(ModelCommon))
     {
      PrintFormat("Error loading mode %s: %d"ModelGetLastError());
      return INIT_FAILED;
     }
   net.UseOpenCL(UseOpenCL);

After loading the model, we load the required indicators. Within the framework of this book, we trained models on historical datasets from two indicators: RSI and MACD. As always, we check the result of the operation.

   h_RSI = iRSI(_SymbolTimeFrame12PRICE_TYPICAL);
   if(h_RSI == INVALID_HANDLE)
     {
      PrintFormat("Error loading indicator %s""RSI");
      return INIT_FAILED;
     }
   h_MACD = iMACD(_SymbolTimeFrame124812PRICE_TYPICAL);
   if(h_MACD == INVALID_HANDLE)
     {
      PrintFormat("Error loading indicator %s""MACD");
      return INIT_FAILED;
     }

The next step is to create an instance of an object to perform trading operations. Again, we check the object creation result and set the order execution type.

void OnDeinit(const int reason)
  {
   if(!!net)
      delete net;
   if(!!trade)
      delete trade;
   IndicatorRelease(h_RSI);
   IndicatorRelease(h_MACD);
  }

At the end of the function, we set the initial value for the time of the last bar and exit the function.

Immediately after the initialization function, we create the OnDeinit deinitialization function, in which we delete the objects created in the program. We also close the indicators.

void OnDeinit(const int reason)
  {
   if(CheckPointer(net) == POINTER_DYNAMIC)
      delete net;
   if(CheckPointer(trade) == POINTER_DYNAMIC)
      delete trade;
   IndicatorRelease(h_RSI);
   IndicatorRelease(h_MACD);
  }

We write the entire algorithm of the Expert Advisor in the OnTick function. The terminal calls this function when a new tick event occurs on the chart with the program running. At the beginning of the function, we check if a new bar has opened. If the candlestick has already been processed, we exit the function and wait for a new tick. The essence of this action is simple: we feed our model with information only from closed candlesticks, and to ensure the information is as up-to-date as possible, we do this at the opening of a new candlestick.

void OnTick()
  {
   if(lastbar >= iTime(_SymbolTimeFrame0))
      return;
   lastbar = iTime(_SymbolTimeFrame0);

There are no functions in our template that process every tick, so we will only perform actions at the opening of a new candlestick. If you include functions in your program that need to process every tick, such as trailing stops, moving orders to breakeven, or anything else, you will need to call these functions before checking for the new candlestick event.

When a new candlestick event occurs, we load information from our indicators into local dynamic arrays. Here we need to be sure to check the result of the operations.

   double macd_main[], macd_signal[], rsi[];
   if(h_RSI == INVALID_HANDLE || CopyBuffer(h_RSI01BarsToPatternrsi) <= 0)
     {
      PrintFormat("Error loading indicator %s data""RSI");
      return;
     }
   if(h_MACD == INVALID_HANDLE || CopyBuffer(h_MACDMAIN_LINE1BarsToPatternmacd_main) <= 0 ||
      CopyBuffer(h_MACDSIGNAL_LINE1BarsToPatternmacd_signal) <= 0)
     {
      PrintFormat("Error loading indicator %s data""MACD");
      return;
     }

Once the indicator data is loaded, we create an instance of a data buffer object to collect the current state. Also, we run a loop to fill the data buffer with the current state of the indicators. Here we should organize exactly the same sequence of values describing the current state, as we filled in the training dataset file. Otherwise, the result of the model will be unpredictable.

   CBufferType *input_data = new CBufferType();
   if(!input_data)
     {
      PrintFormat("Error creating Input data array: %d"GetLastError());
      return;
     }
   if(!input_data.BufferInit(BarsToPattern40))
      return;

   for(int i = 0i < BarsToPatterni++)
     {
      if(!input_data.Update(i0, (TYPE)rsi[i]))
        {
         PrintFormat("Error adding Input data to array: %d"GetLastError());
         delete input_data;
         return;
        }

      if(!input_data.Update(i1, (TYPE)macd_main[i]))
        {
         PrintFormat("Error adding Input data to array: %d"GetLastError());
         delete input_data;
         return;
        }

      if(!input_data.Update(i2, (TYPE)macd_signal[i]))
        {
         PrintFormat("Error adding Input data to array: %d"GetLastError());
         delete input_data;
         return;
        }

      if(!input_data.Update(i3, (TYPE)(macd_main[i] - macd_signal[i])))
        {
         PrintFormat("Error adding Input data to array: %d"GetLastError());
         delete input_data;
         return;
        }
     }
   if(!input_data.Reshape(1,input_data.Total())
     return;

When we have fully gathered the description of the current state in the data buffer, we proceed to work on our model. First, we validate the model pointer and then call the feed-forward method. After a successful completion of the feed-forward method, we obtain its results in a local buffer. We do not create a new instance of an object for the results buffer; instead, we use the input data buffer.

   if(!net)
     {
      delete input_data;
      return;
     }
   if(!net.FeedForward(input_data))
     {
      PrintFormat("Error of Feed Forward: %d"GetLastError());
      delete input_data;
      return;
     }

Next comes the decision-making block based on signals from our model. As a result of the feed-forward pass, the model returns two numbers. The first number is trained to determine the direction of the upcoming movement, while the second one determines the distance to the nearest extreme point. Thus, to execute operations, we will rely on both signals, which should be aligned.

First, we check the buy signal. The parameter responsible for the direction of movement must be positive. We also immediately check for open positions. If there are open long positions, we refrain from opening a new position and exit the function until the next tick.

Please note that we do not check for the presence of an open sell position. In our simplified version of the EA, we trust the forecasts of our model and expect all open positions to be closed by the take profit or stop loss. Consequently, we excluded the position management block from our Expert Advisor. As a result, we expect the possibility of simultaneously holding two opposite positions, which is only possible with position hedging. Therefore, testing such an Expert Advisor is possible only on the corresponding accounts.

This approach allows us to assess the effectiveness of forecasts made by our model. But when building Expert Advisors for real market usage, I would recommend considering and adding a position management block to the Expert Advisor.

   if(!net.GetResults(input_data))
     {
      PrintFormat("Error of Get Result: %d"GetLastError());
      delete input_data;
      return;
     }
   if(input_data.At(0) > 0.0)
     {
      bool opened = false;
      for(int i = 0i < PositionsTotal(); i++)
        {
         if(PositionGetSymbol(i) != _Symbol)
            continue;
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
            opened = true;
        }

      if(opened)
        {
         delete input_data;
         return;
        }

If there are no open long positions, we check the strength of the signal (probability of movement in the desired direction) and the expected movement to the upcoming extreme point. If at least one of the parameters does not meet the requirements, we exit the function until the next tick.

      if(input_data.At(0) < TradeLevel ||
         input_data.At(1) < (MinTarget * SymbolInfoDouble(_SymbolSYMBOL_POINT)))
        {
         delete input_data;
         return;
        }

If, however, a decision is made to open a position, we determine the stop loss and take profit levels and send a buy order.

      double tp = SymbolInfoDouble(_SymbolSYMBOL_BID) + MathMin(input_data.At(1) * 
                    ProfitMultiplyMaxTP * SymbolInfoDouble(_SymbolSYMBOL_POINT));
      double sl = SymbolInfoDouble(_SymbolSYMBOL_BID) - 
                  StopLoss * SymbolInfoDouble(_SymbolSYMBOL_POINT);
      trade.Buy(Lot_Symbol0sltp);
     }

The algorithm for making a sell decision is organized in a similar way.

   if(input_data.At(0) < 0)
     {
      bool opened = false;
      for(int i = 0i < PositionsTotal(); i++)
        {
         if(PositionGetSymbol(i) != _Symbol)
            continue;
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
            opened = true;
        }

      if(opened)
        {
         delete input_data;
         return;
        }

      if(input_data.At(0) > -TradeLevel ||
         input_data.At(1) > -(MinTarget * SymbolInfoDouble(_SymbolSYMBOL_POINT)))
        {
         delete input_data;
         return;
        }

      double tp = SymbolInfoDouble(_SymbolSYMBOL_BID) + MathMax(input_data.At(1) * 
                   ProfitMultiply, -MaxTP * SymbolInfoDouble(_SymbolSYMBOL_POINT));
      double sl = SymbolInfoDouble(_SymbolSYMBOL_BID) + 
                  StopLoss * SymbolInfoDouble(_SymbolSYMBOL_POINT);
      trade.Sell(Lot_Symbol0sltp);
     }
   delete input_data;
  }

After performing all the operations according to the described algorithm, we delete the buffer of the current state and exit the function.

The Expert Advisor has been made very simplified, but it will also allow you to test the operation of our model in the MetaTrader 5 strategy tester.