preview
Build Self Optimizing Expert Advisors in MQL5 (Part 8): Multiple Strategy Analysis

Build Self Optimizing Expert Advisors in MQL5 (Part 8): Multiple Strategy Analysis

MetaTrader 5Examples |
3 037 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

There are many challenges we face as algorithmic traders, which we have already discussed in this series. For example, we have noticed that our statistical models find it easier to predict future technical indicator readings than to predict future price levels.

We also looked at the benefits of a trading system that models the relationship between the strategy it follows and the market where it applies that strategy.

Our models consistently performed better when we replaced the classical task of direct price prediction with these alternative tasks. Direct price prediction is difficult, but by changing how we frame the problem, we can outperform models stuck on the classical task while still using the same statistical tools.

Today, we will explore a new potential strategy that builds on our previous findings. What if we create an application that knows three different trading strategies? Can this application learn to choose one strategy at a time, periodically switching to the most profitable one instead of following all three simultaneously? If the application can change strategies periodically, can it profitably select the best one from the three it knows?

Such an application could be more useful than a fixed trading algorithm that follows all three strategies or a combination of them.

To measure the value of our statistical model, we first need a baseline performance level that our model should beat.

We will combine three independent trading strategies: a Moving Average Crossover Continuation Strategy, a Relative Strength Index Momentum Strategy, and a Williams Percent Range Trend Breakout Strategy. Each will be explained in detail.

This article will introduce some powerful tools in the MetaTrader 5 Terminal, focusing on Walk Forward Testing. Walk Forward Testing is different from Back Testing, and we will explain these differences later.

Walk Forward Testing gives us more insights than simple back testing, especially when combined with an optimizer that generates new strategy parameters to test. This lets us robustly find profitable settings for our trading strategy. MetaTrader 5 includes this advanced functionality with its Fast and Slow Genetic Optimizers.

By combining these powerful strategy testing tools with the reliable Object-Oriented Design Principles we cover in this series, we will design, test, and validate a strong competitor for our statistical models to beat.


Getting Started in MQL5

This discussion addresses the problem of how to best combine different strategies into one that works. Hardcoded solutions are rare, especially when using multiple strategies at once.

Combining strategies is exciting because it requires creativity. But it also means we must minimize unexpected side effects.

Traders often use different strategies together. For example, one strategy might open positions while another decides when to close them. Each strategy focuses on a part of the problem. We want to mimic this human approach while showing how to use the MetaTrader 5 strategy tester to find good strategy settings.

To combine strategies reliably, we will encapsulate each strategy in a class. Each class must be tested to prove it works. A single parent class, called "Parent," will be the base for all our strategy variations. This class will include common functions like updating parameters and checking for buy or sell signals.

Each class that inherits from the parent will redefine what counts as a buy or sell signal. This is done by making shared methods virtual, allowing each strategy to safely override them.

We have covered enough to start building the first class: the parent strategy class.

In MQL5, each class starts with the keyword class followed by the class name. It is standard practice to name the file the same as the class.

Some class members will be marked virtual to tell the compiler that these functions can be overridden by subclasses. This lets each strategy define its own way of handling these methods safely.

//+------------------------------------------------------------------+
//|                                                     Strategy.mqh |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

class Strategy
  {
private:
                     int  buffer_size;
                     bool buy;
                     bool sell;
public:
                     //--- Class constructors and destructors
                     Strategy(void);
                    ~Strategy(void);
                    
                    //--- Check if we have any valid trading signals from our strategy
                    virtual bool BuySignal(void);
                    virtual bool SellSignal(void);
                    
                    //--- Update the technical indicators in our strategy
                    virtual bool Update(void);
                    
                    //--- Get the size of the technical indicator buffers
                            int  GetIndicatorBufferSize(void);
  };

Our default constructor sets all the default values shared by all instances of a strategy.

//+------------------------------------------------------------------+
//| The only way to create an object of the class                    |
//+------------------------------------------------------------------+
Strategy::Strategy(void)
  {
      //--- Upon initialization, both flags should be false 
      buy = false;
      sell = false;
      buffer_size = 10;
  }

We will need several utility methods, commonly referred to as getters and setters. To get started, we will define a method that returns the current buffer size we have selected for the indicators employed in our strategy.

//+------------------------------------------------------------------+
//| The size of our indicator buffer                                 |
//+------------------------------------------------------------------+
int Strategy::GetIndicatorBufferSize(void)
   {
      int res = buffer_size;
      return(res);
   }

We will also require each strategy to have 2 methods that each inform us if we have any buy or sell signals respectively. Each class that inherits from the base class, should implement the rules that define its entries. Otherwise, the parent class will always return false and instruct the practitioner to overwrite this method in the child class.

//+------------------------------------------------------------------+
//| Check if our strategy is giving us any buy signals               |
//+------------------------------------------------------------------+
bool Strategy::BuySignal(void)
   {
      //--- The user is intended to overwrite the function in the child class
      //--- Otherwise, failing to do so will always return false as a safety feature
      Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class");
      return(false);
   }
   
//+------------------------------------------------------------------+
//| Check if our strategy is giving us any sell signals              |
//+------------------------------------------------------------------+
bool Strategy::SellSignal(void)
   {
      //--- The user is intended to overwrite the function in the child class
      //--- Otherwise, failing to do so will always return false as a safety feature
      Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class");
      return(false);
   }

Updating the strategy object entails updating any strategy parameters being used to trade.

//+------------------------------------------------------------------+
//| Update our strategy parameters                                   |
//+------------------------------------------------------------------+
bool Strategy::Update(void)
   {
      //--- The user is intended to overwrite the function in the child class
      Print("[WARNING] This function has only been implemented in the parent: ",__FUNCSIG__,"\nKindly make the necessary corrections to the child class");
      return(false);
   }

For now, our class destructor is empty.

//+------------------------------------------------------------------+
//| The class destructor is currently empty                          |
//+------------------------------------------------------------------+
Strategy::~Strategy(void)
  {
  }
//+------------------------------------------------------------------+

We will start by defining the body of our class. The class is titled "OpenCloseMACrossover". It is a strategy that relies on 2 moving average indicators with identical periods, applied to the Open and Close price feeds respectively. Notice that the methods that were virtual in our parent class, are yet again virtual in the child.

Sell signals are generated when the Open moving average is above the Close. The opposite, registers as a buy signal. The reasoning is that, if the average Close price is greater than the average Open price, then price action can be seen as bullish, and a strong trend in the direction may persist.

//+------------------------------------------------------------------+
//|                                         OpenCloseMACrossover.mqh |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

#include<VolatilityDoctor\Strategies\Parent\Strategy.mqh>
#include<VolatilityDoctor\Indicators\MA.mqh>

class OpenCloseMACrossover : public Strategy
  {
private:
                     //--- Create 2 moving average instances
                     MA *ma_array[2];

public:
                     //---- Class constructors and destructor
                     OpenCloseMACrossover(string symbol,ENUM_TIMEFRAMES time_frame,int period,int shift,ENUM_MA_METHOD ma_mode);
                     ~OpenCloseMACrossover();
                     
                     //--- Class methods
                     virtual bool Update(void);
                     virtual bool BuySignal(void);
                     virtual bool SellSignal(void);
                    
  };

We have already discussed the rules of the trading strategy, so implementing the methods that check for these conditions is a straightforward task for us to complete.

//+------------------------------------------------------------------+
//| Check For a Buy Signal                                           |  
//+------------------------------------------------------------------+
bool OpenCloseMACrossover::BuySignal(void)
   {
      //--- Our buy signal is generated if the close moving average is above the open.
      return(ma_array[0].GetCurrentReading()>ma_array[1].GetCurrentReading());
   }

//+------------------------------------------------------------------+
//| Check For a Sell Signal                                          |  
//+------------------------------------------------------------------+
bool OpenCloseMACrossover::SellSignal(void)
   {
      //--- Our sell signal is generated if the open moving average is above the close.
      return(ma_array[0].GetCurrentReading()<ma_array[1].GetCurrentReading());
   }

Our update method calls the Indicator update functions we built for our SingleBufferIndicator class. This method requires the buffer size to be passed as a parameter. We created a method in our parent class to return the buffer size to us. We reference the parent class by using the double colon "::" syntax in our call "Strategy::GetIndicatorBufferSize()". The update method will lastly check that the updated values are not 0 before returning control back over to the context from which it was called.

//+------------------------------------------------------------------+
//| Our update method                                                |  
//+------------------------------------------------------------------+
bool OpenCloseMACrossover::Update(void)
   {
      //--- Copy indicator readings 
      //--- We will always get the buffer size from the parent class
      ma_array[0].SetIndicatorValues(Strategy::GetIndicatorBufferSize(),true);
      ma_array[1].SetIndicatorValues(Strategy::GetIndicatorBufferSize(),true);
      
      //--- Make sure neither of the indicator values equal 0
      if((ma_array[0].GetCurrentReading() * ma_array[1].GetCurrentReading()) != 0) return(true);
      
      //--- If one/both indicator values equal 0, something went wrong.
      return(false);
   }

The class constructor dynamically creates 2 new instances of our moving average indicator objects, and stores their pointers in an array of the same type of the pointer, that is our custom defined MA type.

//+------------------------------------------------------------------+
//| Our class constructor                                            |
//+------------------------------------------------------------------+
OpenCloseMACrossover::OpenCloseMACrossover(string symbol,ENUM_TIMEFRAMES time_frame,int period,int shift,ENUM_MA_METHOD ma_mode)
  {
      //--- Create two instances of our moving average indiator objects
      ma_array[0] = new MA(symbol,time_frame,period,shift,ma_mode,PRICE_CLOSE);
      ma_array[1] = new MA(symbol,time_frame,period,shift,ma_mode,PRICE_OPEN);
      
      //--- Give feedback
      Print("Strategy class loaded correctly");
  }

The class destructor deletes the dynamic objects we created and help us manage the memory we are consuming.

//+------------------------------------------------------------------+
//| Our class destructor                                             |
//+------------------------------------------------------------------+
OpenCloseMACrossover::~OpenCloseMACrossover()
  {
   //--- Delete the custom objects we made
   delete ma_array[0];
   delete ma_array[1];
   
   //--- Give feedback
   Print("Strategy deinitialized correctly. Goodbye");
  }
//+------------------------------------------------------------------+

We will now proceed to test our first strategy class. Remember that our final application will run three classes, three different strategies. Therefore, as good developers, we must test each class individually, against a hardcoded version of an identical strategy. The test is passed if both strategies return the same test results, when back tested over the same period. This may save us hours of bug-hunting in the future.

We will first define system constants that we will maintain across both tests. If both strategies are equivalent, they should be triggered at the same times, open the same number of positions and in the same buy:sell ratio. Repeating these system constants is a deliberate part of our test because these constants control the strategy parameters.

//+------------------------------------------------------------------+
//|                                                   MSA Test 1.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Define system constants                                          |
//+------------------------------------------------------------------+
#define MA_TYPE        MODE_EMA
#define MA_PERIOD      10
#define MA_TIME_FRAME  PERIOD_D1
#define MA_SHIFT       0
#define HOLDING_PERIOD 5

Next, we will load our dependencies.

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Time\Time.mqh>

Then we need a few global variables to control our technical indicators and keep count of how long our position has been open.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+

//--- Custom Types
CTrade    Trade;
TradeInfo *TradeInformation;
Time      *TradeTime;

//--- System Types
double ma_open[],ma_close[];
int    ma_open_handler,ma_close_handler;
intn    position_timer; 

When our system is initialized, we will load our technical indicators and validate they are sound.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Our technical indicators
   ma_close_handler = iMA(Symbol(),MA_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE,PRICE_CLOSE);
   ma_open_handler = iMA(Symbol(),MA_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE,PRICE_OPEN);

//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),MA_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),MA_TIME_FRAME);


//--- Safety checks
   if(ma_close_handler == INVALID_HANDLE)
      return(false);
   if(ma_open_handler == INVALID_HANDLE)
      return(false);

//--- Everything was fine
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope

If our system is no longer in use, we will release the technical indicators we loaded, and delete the dynamic objects we created during our setup procedure.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the indicators and dynamic objects
   IndicatorRelease(ma_close_handler);
   IndicatorRelease(ma_open_handler);

   delete TradeTime;
   delete TradeInformation;

  }
//--- End of Deinit Scope

If our Terminal receives new price levels, we will first check if a new candle has formed, when this is the case, we will always update our technical indicators, and then check if we have any positions open so that we either check for a trading signal if none are open or wait until maturity before closing our position.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new daily candle has formed
   if(TradeTime.NewCandle())
     {

      //--- Update our technical indicators
      Update();

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Reset the position timer
         position_timer = 0;

         //--- Check for a trading signal
         CheckSignal();
        }

      //--- Otherwise
      else
        {
         //--- The position has reached maturity
         if(position_timer == HOLDING_PERIOD)
            Trade.PositionClose(Symbol());

         //--- Otherwise keep holding
         else
            position_timer++;
        }
     }
  }
//--- End of OnTick Scope

Our update function writes the current indicator readings in the indicator buffer, into the arrays we have created for them.

//+------------------------------------------------------------------+
//| Update our technical indicators                                  |
//+------------------------------------------------------------------+
void Update(void)
  {
//--- Call the CopyBuffer method to get updated indicator values
   CopyBuffer(ma_close_handler,0,0,1,ma_close);
   CopyBuffer(ma_open_handler,0,0,1,ma_open);
  }
//--- End of Update Scope

The check signal function looks for the trading conditions we defined earlier, the Close moving average should be above the Open moving average for us to consider price action to be bullish.

//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {
//--- Long positions when the close moving average is above the open
   if(ma_close[0] > ma_open[0])
     {
      Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
      return;
     }

//--- Otherwise short
   else
      if(ma_close[0] < ma_open[0])
        {
         Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
         return;
        }

  }
//--- End of CheckSignal Scope

Lastly, always undefine the system variables you created at the end of your program.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef MA_PERIOD
#undef MA_SHIFT
#undef MA_TIME_FRAME
#undef MA_TYPE
#undef HOLDING_PERIOD
//+------------------------------------------------------------------+

Let us now establish a baseline level of performance for our class. 

Fig. 1: Our initial test settings for our baseline level

The next step, is to define the start and end points of our back test. We will select the Daily Time Frame if you wish to follow along.

Fig. 2: The test dates we will use for the baseline level

The equity curve, produced by the hardcoded version of the strategy, must be recovered from the class we implemented, provided we set both strategies to run with the same parameters. Let us now inspect if we have indeed implemented the class, without any errors. 

Fig. 3: Visualizing the equity curve established by our baseline strategy

The precise statistical details of both tests will not be perfectly aligned. Recall that our back test simulates random latency and systematic noise. Therefore, we seek that both results should be very close to each other, not identical.

Fig. 4: Detailed results of the back test we performed using our hardcoded version of the moving average crossover strategy

The system constants we defined will be used as is in this version of our Class Test for consistency.

//+------------------------------------------------------------------+
//|                                                   MSA Test 1.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Define system constants                                          |
//+------------------------------------------------------------------+
#define MA_TYPE        MODE_EMA
#define MA_PERIOD      10
#define MA_TIME_FRAME  PERIOD_D1
#define MA_SHIFT       0
#define HOLDING_PERIOD 5

Next, we will load our dependencies.

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>
#include <VolatilityDoctor\Strategies\OpenCloseMACrossover.mqh>

We will need to create a new global variable for our strategy instance.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+

//--- Custom Types
CTrade               Trade;
TradeInfo            *TradeInformation;
Time                 *TradeTime;
OpenCloseMACrossover *MACross;

//--- System Types
int    position_timer;

For the most part, the majority of the application remain unchanged. We are isolating the effects of the signals being produced by our class. Therefore, most of this code should feel familiar to the reader from the first test we implemented.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Create dynamic instances of our custom types
   TradeTime        = new Time(Symbol(),MA_TIME_FRAME);
   TradeInformation = new TradeInfo(Symbol(),MA_TIME_FRAME);
   MACross          = new OpenCloseMACrossover(Symbol(),MA_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_TYPE);

//--- Everything was fine
   return(INIT_SUCCEEDED);
  }
//--- End of OnInit Scope

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete the dynamic objects
   delete TradeTime;
   delete TradeInformation;
   delete MACross;
  }
//--- End of Deinit Scope

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Check if a new daily candle has formed
   if(TradeTime.NewCandle())
     {
      //--- Update strategy
      Update();

      //--- If we have no open positions
      if(PositionsTotal() == 0)
        {
         //--- Reset the position timer
         position_timer = 0;

         //--- Check for a trading signal
         CheckSignal();
        }

      //--- Otherwise
      else
        {
         //--- The position has reached maturity
         if(position_timer == HOLDING_PERIOD)
            Trade.PositionClose(Symbol());

         //--- Otherwise keep holding
         else
            position_timer++;
        }
     }
  }
//--- End of OnTick Scope

The methods we defined to update our strategy parameters and detect trading signals, only changed in the sense that they now call upon the class we built and, no longer, implement the results from scratch.

//+------------------------------------------------------------------+
//| Update our technical indicators                                  |
//+------------------------------------------------------------------+
void Update(void)
  {
//--- Update the strategy
   MACross.Update();
  }
//--- End of Update Scope

//+------------------------------------------------------------------+
//| Check for a trading signal using our cross-over strategy         |
//+------------------------------------------------------------------+
void CheckSignal(void)
  {
//--- Long positions when the close moving average is above the open
   if(MACross.BuySignal())
     {
      Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),0,0,"");
      return;
     }

//--- Otherwise short
   else
      if(MACross.SellSignal())
        {
         Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),0,0,"");
         return;
        }
  }
//--- End of CheckSignal Scope

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef MA_PERIOD
#undef MA_SHIFT
#undef MA_TIME_FRAME
#undef MA_TYPE
#undef HOLDING_PERIOD
//+------------------------------------------------------------------+

We are now ready to start testing our MQL5 MA Crossover Strategy Class. We will first load the Expert Advisor running the class, and setup the same back test dates we used for the first test.

Fig. 5: The initial dates we will use when evaluating our strategy, these dates will be affected by our future forward test

Ensure that the settings you have selected match the first settings we used.

Fig. 6: Select "Real ticks" and "Random delay" for robust back tests

The detailed test results from the class and hardcoded strategies are almost identical, both strategies placed 127 Trades, with the same buy sell ratio and obtained Sharpe ratios close to each other.

Fig. 7: The detailed statistics produced by our Strategy class match our results obtained from the hardcoded trading strategy

The equity curve, produced by the class, closely resembles Fig 3. Therefore, the 2 strategies are matching each other as expected.

Fig. 8: The equity curve produced by the class matches the hardcoded strategy

We can now start looking for optimal strategy parameters, since we have verified the class has been implemented correctly. 

First we will need to change most of the system constants, to user inputs. This allows our genetic optimizer to tune the strategy for us. Hence, we only have a single system definition.

//+------------------------------------------------------------------+
//|                                                   MSA Test 1.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define MA_SHIFT 0

//+------------------------------------------------------------------+
//| User Inputs                                                      |
//+------------------------------------------------------------------+
input   group          "Strategy Parameters"
input   int             MA_PERIOD      =        10;//Moving Average Period
input   int             HOLDING_PERIOD =         5;//Position Holding Period
input   ENUM_TIMEFRAMES MA_TIME_FRAME  = PERIOD_D1;//Moving Average Time Frame
input   ENUM_MA_METHOD  MA_TYPE        =  MODE_EMA;//Moving Average Type

The rest of our system remains mostly unchanged, so let us now start to discuss the difference between a back test and forward test in MetaTrader 5.

A back test, simply, runs a trading strategy over historical data. We can make more use of our data beyond simple historical testing. By partitioning the data, we can use one fraction of the data to search for strategy parameters, and then validate the parameters we've found, with the remaining fraction of data.

This is the virtue of forward testing. We are not simply looking for good settings, but we are also trying to learn how stable these settings are.

Therefore, set the "Forward" parameter to "1/2" to use 50% of your data for training, and the latter for testing.

Fig. 9: Set the "Forward" field to 1/2 to use half of the data for training and the other half for testing

There are different optimization strategies we have offered to us in our MetaTrader 5 terminal. We will select "Fast genetic-based algorithm" because it is not too demanding on our system, but it still provides reliable results.

Fig. 10: We need an optimizer to generate new strategy parameters after each test

Once the test starts, you will observe a scatter plot of results, similar to Fig. 11 below. The plot helps us visualize how well we performed on each training iteration. The optimizer only sees the results obtained in the first half of the data, and uses that to learn better parameters to try in the next iteration.

Fig. 11: We set the Fast genetic Optimizer to improve the Sharpe Ratio of our strategy. Each point represents the results of the test iteration results

We can right-click on this scatter plot of results and manipulate it in many ways. We can change the axis being plotted, or even make the chart a 3D representation of our results.

Fig. 12: The context menu allows us to change the axis of the plot and explore different relationships

Visualizing the data, in alternative forms, can help us observe phenomena happening between our strategy and the market we have chosen. For example, it appears that as we pick higher timeframes, our strategy grows more profitable.

Fig. 13: We can observe that higher timeframes, help us obtain better Sharpe Ratios by making a 3D Bar Graph

The strategy tester will also provide you with detailed results obtained through each combination of inputs it tested. Notice there is a panel at the bottom of the table that separates "Back test" and "Forward". 

Fig. 14:  MetaTrader 5 also provides us with a detailed analysis of all the strategy parameters it tried

Right-clicking this table with your mouse will load a context menu that allows us to perform many useful tasks, such as deciding which columns should be included in the table, or even exporting the Forward results to a file.

Fig. 15: Loading the context menu on the table of results, shows us that we can export our results to XML format for further studies

We can take a detailed look at the back test and forward results separately. The back test results are displayed below in Fig. 16. Recall that we are more interested in the forward results, and how far away they drifted from the back test.

Fig. 16: Detailed statistical results of the back test, our genetic optimizer obtained

The forward results closely resemble the back test. This is a good indicator of potential stability. Otherwise, if your best forward results were inconsistent with your back tests, then the strategy is demonstrating unstable qualities.

Fig. 17: These are the results we are particularly interested in, the forward results

We are also provided with the equity curve produced by both tests. The long vertical line in the middle marks the separation between the back and forward tests.

Fig. 18: Notice that both the equity curve obtained during training and in the forward test have positive trends

Lastly, these are the optimal strategy settings our genetic optimizer helped us find today.

Fig. 19: The best settings obtained by our genetic algorithm search


Conclusion

This article has demonstrated the value of the MetaTrader 5 strategy tester. Readers who do not plan to design classes can still gain practical use cases for integrating more Object-Oriented Programming (OOP) principles embedded in MQL5 into their development process. This article also encourages readers to adopt good development practices for creating and testing reliable classes.

Finally, by using the OOP design principles we have highlighted, readers can build their strategies reliably and easily test them for optimal inputs across different symbols and timeframes. Join us in our next discussion, where we will join our RSI and moving average strategies. 

File Name File Description
MSA Test 1 Baseline.mq5 The hardcoded implementation of our crossover strategy that we used as a test result our class should emulate.
MSA Test 1 Class.mq5 The file testing our moving average strategy class.
MSA Test 1.mq5 We used this expert advisor to search for good strategy parameters using the MetaTrader 5 strategy tester.
OpenCloseMACrossover.mqh The class implementing our moving average strategy.
Strategy.mqh The base class for all our strategies.
MSA Test 1.ex5 A compiled version of our Expert Advisor.
Attached files |
MSA_Test_1.mq5 (5.28 KB)
Strategy.mqh (3.98 KB)
MSA_Test_1.ex5 (44.16 KB)
Developing a Replay System (Part 72): An Unusual Communication (I) Developing a Replay System (Part 72): An Unusual Communication (I)
What we create today will be difficult to understand. Therefore, in this article I will only talk about the initial stage. Please read this article carefully, it is an important prerequisite before we proceed to the next step. The purpose of this material is purely didactic as we will only study and master the presented concepts, without practical application.
Creating a Trading Administrator Panel in MQL5 (Part XII): Integration of a Forex Values Calculator Creating a Trading Administrator Panel in MQL5 (Part XII): Integration of a Forex Values Calculator
Accurate calculation of key trading values is an indispensable part of every trader’s workflow. In this article, we will discuss, the integration of a powerful utility—the Forex Calculator—into the Trade Management Panel, further extending the functionality of our multi-panel Trading Administrator system. Efficiently determining risk, position size, and potential profit is essential when placing trades, and this new feature is designed to make that process faster and more intuitive within the panel. Join us as we explore the practical application of MQL5 in building advanced, trading panels.
From Basic to Intermediate: Array (IV) From Basic to Intermediate: Array (IV)
In this article, we'll look at how you can do something very similar to what's implemented in languages like C, C++, and Java. I am talking about passing a virtually infinite number of parameters inside a function or procedure. While this may seem like a fairly advanced topic, in my opinion, what will be shown here can be easily implemented by anyone who has understood the previous concepts. Provided that they were really properly understood.
Neural Networks in Trading: Node-Adaptive Graph Representation with NAFS Neural Networks in Trading: Node-Adaptive Graph Representation with NAFS
We invite you to get acquainted with the NAFS (Node-Adaptive Feature Smoothing) method, which is a non-parametric approach to creating node representations that does not require parameter training. NAFS extracts features of each node given its neighbors and then adaptively combines these features to form a final representation.