
Build Self Optimizing Expert Advisors in MQL5 (Part 8): Multiple Strategy Analysis
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. |





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use