Test Driving MQL - Part 2

The importance of negative test cases

Risk Disclaimer

Please read the Risk Disclaimer before reading this post.

Introduction

In a recent post about TDD in MQL I mentioned negative test cases and how I believe that they are as important to implement as positive test cases, especially when it comes to trading code.

Whilst adding a Pin Bar detector to my “Ultimate Indicator” this evening I found a perfect example to demonstrate why this is the case.

I initially added two tests to my Pin Bar detector:

  1. A basic bullish case
  2. A basic bearish case

The test cases I found on the charts are clean and easy to spot, and the detector found them easily. The image below shows a merger of the two basic test cases.

Basic Bearish and Bullish Pin Bars

The Current Test Cases

//+------------------------------------------------------------------+
//|                                         Test Pinbar Detector.mqh |
//|                                    Copyright 2019, Scott Edwards |
//|                                                scottedwards.tech |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, Scott Edwards"
#property link      "scottedwards.tech"

// SUT
#include <PinBar\PinBarDetector.mqh>

#include <PinBar\PinBar.mqh>
#include <BullishOrBearish.mqh>
#include <Testing\Helpers\Price Data Scraper.mqh>

bool RunAllPinBarDetectorTests()
{
   bool allTestsPassed = true;

   allTestsPassed = _PBD_DetectABullishPinBar() && allTestsPassed;
   allTestsPassed = _PBD_DetectABearishPinBar() && allTestsPassed;

   return allTestsPassed;
}


bool _PBD_DetectABullishPinBar()
{
   // Test data period
   string symbol = "USDJPY";
   ENUM_TIMEFRAMES timeframe = PERIOD_D1;
   datetime pinBarDate = D'2019-05-15 00:00:00';

   // Get scraped price data
   MqlRates rates[];
   GetScrapedPriceData(symbol, timeframe, pinBarDate, pinBarDate, rates);

   PinBarDetector pinBarDetector;
   PinBar pinBars[];
   pinBarDetector.DetectPinBars(rates, pinBars, symbol);

   int arraySize = ArraySize(pinBars);

   if(arraySize != 1)
   {
      Alert("Pin Bar Detector: bullish pin bar array size incorrect");
      return false;
   }

   if(pinBars[0].PinBarResult != BullishOrBearish_Bullish)
   {
      Alert("Pin Bar Detector: bullish pin bar not detected");
      return false;
   }

   if(pinBars[0].Rates.time != pinBarDate)
   {
      Alert("Pin Bar Detector: bullish pin bar date incorrect");
      return false;
   }

   return true;
}

bool _PBD_DetectABearishPinBar()
{
   // Test data period
   string symbol = "EURUSD";
   ENUM_TIMEFRAMES timeframe = PERIOD_D1;
   datetime pinBarDate = D'2018-11-26 00:00:00';

   // Get scraped price data
   MqlRates rates[];
   GetScrapedPriceData(symbol, timeframe, pinBarDate, pinBarDate, rates);

   PinBarDetector pinBarDetector;
   PinBar pinBars[];
   pinBarDetector.DetectPinBars(rates, pinBars, symbol);

   int arraySize = ArraySize(pinBars);

   if(arraySize != 1)
   {
      Alert("Pin Bar Detector: bearish pin bar array size incorrect");
      return false;
   }

   if(pinBars[0].PinBarResult != BullishOrBearish_Bearish)
   {
      Alert("Pin Bar Detector: bearish pin bar not detected");
      return false;
   }

   if(pinBars[0].Rates.time != pinBarDate)
   {
      Alert("Pin Bar Detector: bearish pin bar date incorrect");
      return false;
   }

   return true;
}

There is quite a lot going on in this test suite, but the summary is:

  • It runs two tests: one for a valid bullish Pin Bar case and one for a valid bearish Pin Bar case.
  • In both cases, another library that I wrote (the Price Data Scraper) scrapes real data from my broker’s data feed and uses this for the test cases. More on this library at the end of this post.
  • Three things are asserted per test case:
    • Was exactly one Pin Bar detected?
    • Was the BullishOrBearish result for this Pin Bar correct?
    • Was the Rates.time property of the Pin Bar object correctly set?

Identifying Negative Test Cases

When I ran my indicator over a larger selection of price data and inspected the results on the charts I spotted several cases where non-Pin Bar cases were detected as Pin Bars.

In the example below (the USDJPY daily chart from 4 September 2019 to 29 October 2019), we can see that several valid Pin Bars were not detected and that several of the detected Pin Bars are invalid.

Valid and Invalid Pin Bars

The current version of the Pin Bar detector algorithm uses one simple rule for Pin Bar detection: a minimum ratio of 1:3 for candle body length:candle tail wick length.

What is clear from the image above is that the algorithm needs some work.

Adding the Negative Test Cases

I used the Price Data Scraper library to include a golden master test on existing data to make sure that the negative cases are no longer detected.

Once this test was implemented and failing as expected, I added extra individual test cases using double-loop TDD until my golden master test passed. In this case, only one negative test case was needed to get the golden master test passing.

The final test suite is shown below.

//+------------------------------------------------------------------+
//|                                         Test Pinbar Detector.mqh |
//|                                    Copyright 2019, Scott Edwards |
//|                                                scottedwards.tech |
//+------------------------------------------------------------------+
#property copyright "Copyright 2019, Scott Edwards"
#property link      "scottedwards.tech"

// SUT
#include <PinBar\PinBarDetector.mqh>

#include <PinBar\PinBar.mqh>
#include <BullishOrBearish.mqh>
#include <Testing\Helpers\Price Data Scraper.mqh>

bool RunAllPinBarDetectorTests()
{
   bool allTestsPassed = true;

   allTestsPassed = _PBD_DetectABullishPinBar() && allTestsPassed;
   allTestsPassed = _PBD_DetectABearishPinBar() && allTestsPassed;
   allTestsPassed = _PBD_NotTreatADojiAsAPinBar() && allTestsPassed;
   allTestsPassed = _PBD_DetectPinBarGoldenMasterCases() && allTestsPassed;

   return allTestsPassed;
}


bool _PBD_DetectABullishPinBar()
{
   // Test data period
   string symbol = "EURUSD";
   ENUM_TIMEFRAMES timeframe = PERIOD_D1;
   datetime pinBarDate = D'2019-09-03 00:00:00';

   // Get scraped price data
   MqlRates rates[];
   GetScrapedPriceData(symbol, timeframe, pinBarDate, pinBarDate, rates);

   PinBarDetector pinBarDetector;
   PinBar pinBars[];
   pinBarDetector.DetectPinBars(rates, pinBars, symbol);

   int arraySize = ArraySize(pinBars);

   if(arraySize != 1)
   {
      Alert("Pin Bar Detector: bullish pin bar array size incorrect");
      return false;
   }

   if(pinBars[0].PinBarResult != BullishOrBearish_Bullish)
   {
      Alert("Pin Bar Detector: bullish pin bar not detected");
      return false;
   }

   if(pinBars[0].Rates.time != pinBarDate)
   {
      Alert("Pin Bar Detector: bullish pin bar date incorrect");
      return false;
   }

   return true;
}

bool _PBD_DetectABearishPinBar()
{
   // Test data period
   string symbol = "EURUSD";
   ENUM_TIMEFRAMES timeframe = PERIOD_D1;
   datetime pinBarDate = D'2018-11-26 00:00:00';

   // Get scraped price data
   MqlRates rates[];
   GetScrapedPriceData(symbol, timeframe, pinBarDate, pinBarDate, rates);

   PinBarDetector pinBarDetector;
   PinBar pinBars[];
   pinBarDetector.DetectPinBars(rates, pinBars, symbol);

   int arraySize = ArraySize(pinBars);

   if(arraySize != 1)
   {
      Alert("Pin Bar Detector: bearish pin bar array size incorrect");
      return false;
   }

   if(pinBars[0].PinBarResult != BullishOrBearish_Bearish)
   {
      Alert("Pin Bar Detector: bearish pin bar not detected");
      return false;
   }

   if(pinBars[0].Rates.time != pinBarDate)
   {
      Alert("Pin Bar Detector: bearish pin bar date incorrect");
      return false;
   }

   return true;
}

bool _PBD_NotTreatADojiAsAPinBar()
{
   // Test data period
   string symbol = "EURUSD";
   ENUM_TIMEFRAMES timeframe = PERIOD_D1;
   datetime dojiDate = D'2019-10-04 00:00:00';

   // Get scraped price data
   MqlRates rates[];
   GetScrapedPriceData(symbol, timeframe, dojiDate, dojiDate, rates);

   PinBarDetector pinBarDetector;
   PinBar pinBars[];
   pinBarDetector.DetectPinBars(rates, pinBars, symbol);

   int arraySize = ArraySize(pinBars);

   if(arraySize != 0)
   {
      Alert("Pin Bar Detector: doji detected as a pin bar");
      return false;
   }

   return true;
}

bool _PBD_DetectPinBarGoldenMasterCases()
{
   // Test data period
   string symbol = "USDJPY";
   ENUM_TIMEFRAMES timeframe = PERIOD_D1;
   datetime fromDate = D'2019-09-04 00:00:00';
   datetime toDate = D'2019-10-29 00:00:00';

   // Get scraped price data
   MqlRates rates[];
   GetScrapedPriceData(symbol, timeframe, fromDate, toDate, rates);

   PinBarDetector pinBarDetector;
   PinBar pinBars[];
   pinBarDetector.DetectPinBars(rates, pinBars, symbol);

   int arraySize = ArraySize(pinBars);

   if(arraySize != 2)
   {
      Alert("Pin Bar Detector golden master: array size incorrect");
      return false;
   }

   if(pinBars[0].PinBarResult != BullishOrBearish_Bullish)
   {
      Alert("Pin Bar Detector golden master: bullish pin bar not detected at index 0");
      return false;
   }

   if(pinBars[0].Rates.time != D'2019-10-14 00:00:00')
   {
      Alert("Pin Bar Detector golden master: bullish pin bar date incorrect at index 0");
      return false;
   }

   if(pinBars[1].PinBarResult != BullishOrBearish_Bullish)
   {
      Alert("Pin Bar Detector golden master: bullish pin bar not detected at index 1");
      return false;
   }

   if(pinBars[1].Rates.time != D'2019-09-30 00:00:00')
   {
      Alert("Pin Bar Detector golden master: bullish pin bar date incorrect at index 1");
      return false;
   }

   return true;
}

Improving the code

As always, there are several ways that this code could be improved.

  • Most importantly, I could include some bearish Pin Bar results in the golden master test.
  • I could also include a broader range of price data in the golden master, for example a year’s worth of daily price data.

Constantly Updating and Adding Test Cases

If I notice any other issues with Pin Bar detection in the future, I will use the same methods described in this post to set up extra test cases to make sure that the errors are corrected without breaking existing, working test cases.

This ties in well with the point I made in my first post about TDD in MQL. TDD provides us with an ever-growing suite of automated tests that evolves over time and gives us confidence that new changes have not broken existing behaviour.

The “Price Data Scraper” library

As a final note, this is a library I wrote that scrapes price data from my broker to my local files system.

If you request data from this library it first checks whether the data exists locally. If not, it scrapes the data and stores it.

This means that I can quickly pull real price data into my tests and, going forward, can get hold of this data without relying on a specific broker’s data feed, or even a live Internet connection.

Feedback

As always, I’d appreciate any feedback or suggestions on this approach. It is constantly evolving and any new ideas are welcome!

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.

Related