Test Driving MQL - Part 1

Ideas and techniques for test driving MetaTrader code

Risk Disclaimer

Please read the Risk Disclaimer before reading this post.

Introduction

Something that I noticed very early on in my trading adventure is that I am quite prone to making small errors when executing repetitive tasks in my workflow. This includes important things like setting risk limits and calculating lot sizes, and so these “small” errors can cause big, expensive problems down the line.

Luckily, the MetaTrader platform has good built-in support for writing scripts, indicators and expert advisors to automate these repetitive tasks.

In this post we will run through my approach to building these tools using Test Driven Development (TDD) in MetaTrader’s built-in MQL language.

Why TDD?

TDD is an approach to building software where the main focus is on writing tests to define the expected behaviour before writing any implementation code.

Some of the benefits of this approach are:

  1. Quick feedback as to whether the implementation is working or not.
  2. The creation of an ever-growing suite of automated tests to give us confidence that new changes haven’t broken existing behaviour.
  3. The test suite effectively documents the behaviour of the code under test.
  4. The focus on behaviour before implementation saves a lot of wasted coding time (as long as we are disciplined in only implementing enough code to get the tests passing).

A full rundown on the TDD approach is outside of the scope of this post, but if you’d like a basic tutorial I’d recommend reading through this kata on the excellent Katalyst website.

In this post we will use my MQL-TDD approach to build a simple Inside Bar detector.

What is an Inside Bar?

An Inside Bar is a chart pattern where the entire range (from high to low) of a bar is fully contained within the range of the previous bar.

In the example below, bar 2 is fully contained (including its wicks) within the range of bar 1. Thus, it is an Inside Bar.

Inside Bar example

Building the Inside Bar detector

Code Layout

The first thing to understand is the way the codebase is laid out.

MetaEditor layout

  • We use classes to encapsulate behaviour. For example, all of the logic for detecting Inside Bars is held in the class InsideBarDetector.mqh.
  • Each logical grouping of behaviours gets its own suite of tests. For example, the Inside Bar detector’s tests are held in the file Inside Bar Detector Should.mqh.
  • The script Run All Tests.mq5 is used to run all of the tests in the codebase.

The script used to run tests

The script Run All Tests.mq5 runs every test across the entire codebase. This means that it helps to drive out new behaviour, as well as to ensure that old behaviour is not broken by new changes.

We only have Inside Bar tests for now, and so the script is fairly simple.

//+------------------------------------------------------------------+
//|                                                Run All Tests.mq5 |
//|                                    Copyright 2019, Scott Edwards |
//|                                       https://scottedwards.tech/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, Scott Edwards"
#property link      "https://scottedwards.tech/"
#property version   "1.00"

#include <Testing\Inside Bar Detector Should.mqh>

void OnStart()
{
   Alert("");
   Alert("New test run.");
   
   bool allTestsPassed = true;
   
   allTestsPassed = RunAllInsideBarTests() && allTestsPassed;
   
   if(allTestsPassed == true)
   {
      Alert("Overall result: All tests passed.");
   }
   else
   {
      Alert("Overall result: One or more test(s) failed.");
   }
}

We use MT5’s built-in alerting system to tell us whether the test run has passed, or whether some of tests have failed.

Each test run is split with an empty line, and the latest results are shown on top.

Test runner alerts

Building the Inside Bar Detector

I spent some time looking for a nice, clear example of an Inside Bar on the EURUSD daily chart. I always prefer to use real chart examples to ensure that I am using realistic data in my tests.

The Inside Bar I decided to use is the same one that was shown in the earlier example.

Inside Bar example

Let’s use the high and low prices of these two bars to set up the data for our first test.

//+------------------------------------------------------------------+
//|                                   Inside Bar Detector Should.mqh |
//|                                    Copyright 2019, Scott Edwards |
//|                                       https://scottedwards.tech/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, Scott Edwards"
#property link      "https://scottedwards.tech/"

#include <ChartPatterns\InsideBar\InsideBarDetector.mqh>

bool RunAllInsideBarTests()
{
   bool allTestsPassed = true;
   
   allTestsPassed = DetectAnInsideBar() && allTestsPassed;
   
   return allTestsPassed;
}

bool DetectAnInsideBar()
{
   double previousBarHigh = 1.18280;
   double previousBarLow = 1.17307;
   
   double currentBarHigh = 1.18243;
   double currentBarLow = 1.17449;
   
   InsideBarDetector insideBarDetector;
   
   bool isInsideBar = insideBarDetector.IsInsideBar(previousBarHigh, previousBarLow, currentBarHigh, currentBarLow);
   
   if(!isInsideBar)
   {
      Alert("Inside Bar detection failed.");
      return false;
   }
   
   return true;
}

As expected, this test initially fails as the Inside Bar detector’s logic is not yet implemented.

Test failed

A simple implementation in the class InsideBarDetector.mqh causes this test to pass.

//+------------------------------------------------------------------+
//|                                            InsideBarDetector.mqh |
//|                                    Copyright 2019, Scott Edwards |
//|                                       https://scottedwards.tech/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, Scott Edwards"
#property link      "https://scottedwards.tech/"
#property version   "1.00"

class InsideBarDetector
{
private:

public:
                  InsideBarDetector();
                  ~InsideBarDetector();
                  
                  bool IsInsideBar(double previousBarHigh, double previousBarLow, double currentBarHigh, double currentBarLow)
                  {
                     return previousBarLow < currentBarLow && previousBarHigh > currentBarHigh;
                  }
};
InsideBarDetector::InsideBarDetector()
{

}
InsideBarDetector::~InsideBarDetector()
{

}

Test passed

Adding a negative test case

I believe that negative test cases are as important as positive test cases, especially when writing trading code. A false positive could lead to a bad trading decision, which could be expensive.

We’re happy that we are successfully detecting an Inside Bar, but we should also make sure that we don’t detect one if it does not exist.

I found a clear example of a non-Inside Bar, shown below. Let’s add a test to make sure that an Inside Bar was not detected in this case.

Non-Inside Bar example

//+------------------------------------------------------------------+
//|                                   Inside Bar Detector Should.mqh |
//|                                    Copyright 2019, Scott Edwards |
//|                                       https://scottedwards.tech/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, Scott Edwards"
#property link      "https://scottedwards.tech/"

#include <ChartPatterns\InsideBar\InsideBarDetector.mqh>

bool RunAllInsideBarTests()
{
   bool allTestsPassed = true;
   
   allTestsPassed = DetectAnInsideBar() && allTestsPassed;
   allTestsPassed = DetectANonInsideBar() && allTestsPassed;
   
   return allTestsPassed;
}

bool DetectAnInsideBar()
{
   double previousBarHigh = 1.18280;
   double previousBarLow = 1.17307;
   
   double currentBarHigh = 1.18243;
   double currentBarLow = 1.17449;
   
   InsideBarDetector insideBarDetector;
   
   bool isInsideBar = insideBarDetector.IsInsideBar(previousBarHigh, previousBarLow, currentBarHigh, currentBarLow);
   
   if(!isInsideBar)
   {
      Alert("Inside Bar detection failed.");
      return false;
   }
   
   return true;
}

bool DetectANonInsideBar()
{
   double previousBarHigh = 1.19222;
   double previousBarLow = 1.18373;
   
   double currentBarHigh = 1.19869;
   double currentBarLow = 1.19007;
   
   InsideBarDetector insideBarDetector;
   
   bool isInsideBar = insideBarDetector.IsInsideBar(previousBarHigh, previousBarLow, currentBarHigh, currentBarLow);
   
   if(isInsideBar)
   {
      Alert("Inside Bar detected where one does not exist.");
      return false;
   }
   
   return true;
}

The tests were run again and reported back that they all passed successfully.

Now that we have both a positive test case and negative test case written and passing, we can conclude that this (admittedly simple) class is complete and behaving as expected.

There are a few things that I think could be done to improve the code and increase our confidence in the correctness of its results.

Improving the code

Naming

We should think of a better name for the test DetectANonInsideBar. I feel that this name doesn’t immediately tell the reader of the code what the test does.

Golden master and acceptance testing

We could write a golden master test that takes in a large set of price action data and detect Inside Bars across it.

This is something that I typically do in my much larger, private MQL codebase.

I often use a golden master test as an acceptance test, and then use double-loop TDD to get individual test cases passing until my acceptance test passes.

Golden master testing is typically used to help with getting legacy code under test, but I find it particularly helpful in test driving MQL code.

Refactoring as we go

While I was writing this post I realised that passing built-in MqlRates objects into the method InsideBarDetector.IsInsideBar was not a great approach. I then changed the code to just take in each bar’s high and low price.

To avoid Primitive Obsession, I could improve this further by creating a simple DTO that is used to hold the high and low price of each bar. I could then pass these objects into the method InsideBarDetector.IsInsideBar instead of using double-type parameters.

Final thoughts

I have purposely used a simple example to illustrate my approach to TDD in MQL. In later posts I will do something a bit more interesting to show how this suite of tests grows over time.

You can find the code used in this posts in my MQL5-Public GitHub repository.

I use MT 5 as my trading platform, and so the code here is written in MQL 5. This approach is also completely feasible in MT 4 and MQL 4. I have spent some time porting indicators back to MT 4 for the trading group I’m part of, and it is usually quite straightforward to get this done.

Any feedback, comments etc. are appreciated, so let me know what you think of my approach!

Avatar
Scott Edwards
Engineering Manager / Lead Software Engineer

Lead software engineer, engineering manager, and beginner guitarist. I also have a strong interest in long-term investing.