preview
Modular Indicator Architecture in MQL5 (Part 1): Stop Copy-Pasting and Start Writing Scalable, Reusable Code

Modular Indicator Architecture in MQL5 (Part 1): Stop Copy-Pasting and Start Writing Scalable, Reusable Code

MetaTrader 5Examples |
236 0
Vladislav Boyko
Vladislav Boyko

Introduction

Some time ago, I decided to write an article about developing a custom indicator. I then realized it would be misleading to use my framework without introducing it first. That is why you are reading this introductory article. The material turned out to be extensive, so it will be split into two parts.

In this first article, we will develop a framework for portable, reusable, and maintainable indicators. I could have presented the final solution and explained it afterwards, but that would not provide a solid understanding of the mechanics. Instead, we will start with a primitive implementation of a very simple indicator and gradually refactor it, evolving the codebase step by step into a full-featured template. By building it from the ground up, you'll see exactly what problems we are solving at each stage and why this specific architecture is necessary.

In the second article, we will use this framework to create new indicators. We will expand the module set and demonstrate how to combine ready-made modules into more complex indicators. I will also create an AlgoForge template that will allow you to quickly and conveniently set up new projects, ensuring you have everything you need right at your fingertips without any unnecessary clutter getting in the way.

A Note on Following the Code

Throughout this article, every code snippet is accompanied by a caption with a link. As shown in the image below, these links are the commit messages themselves, and they lead directly to the corresponding commit on AlgoForge.

Code snippet caption with a link to the corresponding Git commit

I strongly encourage you to click on them! When refactoring code, looking at isolated, static snippets is rarely enough. By clicking these links, you will see the exact line-by-line diffs for each step—what was added (in green) and what was removed (in red). Following these commit diffs is the absolute best way to grasp the logic behind the changes and clearly see how the project evolves.



1. The Starting Point: A Primitive Implementation

A very simple task: the user selects a price from the ENUM_APPLIED_PRICE enumeration in the indicator settings, and the indicator draws a line based on this price. We will need an array for the indicator buffer, a function that calculates the applied price, and a single input parameter.

input(name="Price") ENUM_APPLIED_PRICE inpAppliedPrice = PRICE_CLOSE;

double buffer[];

double calculate(int barIdx, ENUM_APPLIED_PRICE a_mode, const double &open[], const double &high[], const double &low[], const double &close[])
  {
   switch(a_mode)
     {
      case PRICE_OPEN:     return open[barIdx];
      case PRICE_HIGH:     return high[barIdx];
      case PRICE_LOW:      return low[barIdx];
      case PRICE_MEDIAN:   return (high[barIdx] + low[barIdx]) / 2.0;
      case PRICE_TYPICAL:  return (high[barIdx] + low[barIdx] + close[barIdx]) / 3.0;
      case PRICE_WEIGHTED: return (high[barIdx] + low[barIdx] + close[barIdx] + close[barIdx]) / 4.0;
      default:             return close[barIdx];
     }
  }

Code #1 | Evolution.mq5 | Initial commit

Almost done. All that’s left is to assign each element of the buffer array the value returned by our function. But to avoid recalculating the entire indicator buffer on every Calculate event, we need to figure out where to start the recalculation from. You’ve probably seen a lot of code like this on the Forum or in the CodeBase:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   int limit, counted_bars = prev_calculated;
   if(counted_bars < 0) // checking for possible errors
      return(-1);
   if(counted_bars > 0) 
      counted_bars--; // the last calculated bar must be recalculated
   limit = rates_total - counted_bars - 1;
   for(int bar = limit; bar >= 0; bar--)
     {
      //...
     }
   return(rates_total);
  }

I still remember my early days, spending hours sketching bars in a notebook, trying to wrap my head around how this kind of logic actually worked. But it’s actually quite simple—there are only three possible scenarios:

  1. Only the current bar changed: we recalculate just the current bar.
  2. A new bar appeared: we recalculate the previous and the current bars.
  3. In all other cases: we recalculate the entire indicator buffer.

Since scenario #3 is essentially «everything else,» we only need to detect the first two cases: «only the current bar changed» and «a new bar appeared.» To do this, we calculate the difference between rates_total and prev_calculated:

  • Only the current bar changed → rates_total - prev_calculated == 0
  • A new bar appeared → rates_total - prev_calculated == 1

We’ve now fully formalized the logic, and we can visualize it as a flowchart:

Flowchart illustrating the indicator buffer recalculation logic

The code below is a direct translation of the flowchart into MQL5:

int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
   int diff = rates_total - prev_calculated;
   //--- Only the current bar changed
   if(diff == 0)
     {
      //--- Recalculate current bar
      int curBarIdx = rates_total - 1;
      buffer[curBarIdx] = calculate(curBarIdx, inpAppliedPrice, open, high, low, close);
     }
   //--- A new bar appeared
   else if(diff == 1)
     {
      //--- Recalculate previous bar
      int prevBarIdx = rates_total - 2;
      buffer[prevBarIdx] = calculate(prevBarIdx, inpAppliedPrice, open, high, low, close);
      //--- Recalculate current bar
      int curBarIdx = rates_total - 1;
      buffer[curBarIdx] = calculate(curBarIdx, inpAppliedPrice, open, high, low, close);
     }
   //--- In all other cases
   else
     {
      //--- Recalculate entire buffer
      for(int i = 0; i < rates_total; i++)
         buffer[i] = calculate(i, inpAppliedPrice, open, high, low, close);
     }
   return rates_total;
  }

Code #2 | Evolution.mq5 | Initial commit

Of course, the code that represents a direct translation of the flowchart into MQL5 isn't particularly good from a programming standpoint. Below is the OnCalculate function after some refactoring. I simply moved the buffer recalculation out of the branch statement.

int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
   int barIdxToRecalcFrom;
   switch(rates_total - prev_calculated)
     {
      case 0:
         barIdxToRecalcFrom = rates_total - 1;
         break;
      case 1:
         barIdxToRecalcFrom = rates_total - 2;
         break;
      default:
         barIdxToRecalcFrom = 0;
         break;
     }
   for(int i = barIdxToRecalcFrom; i < rates_total; i++)
      buffer[i] = calculate(i, inpAppliedPrice, open, high, low, close);
   return rates_total;
  }

Code #3 | Evolution.mq5 | refactor: unify buffer recalculation loop in OnCalculate

We can simplify the switch statement even further:

int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
   int barIdxToRecalcFrom;
   switch(rates_total - prev_calculated)
     {
      case 0:
      case 1:
         barIdxToRecalcFrom = prev_calculated - 1;
         break;
      default:
         barIdxToRecalcFrom = 0;
         break;
     }
   for(int i = barIdxToRecalcFrom; i < rates_total; i++)
      buffer[i] = calculate(i, inpAppliedPrice, open, high, low, close);
   return rates_total;
  }

Code #4 | Evolution.mq5 | refactor: collapse switch cases in OnCalculate

It should be noted that this logic is slightly simplified compared to the traditional approach. It’s more robust, less verbose, and easier to read. Also, simplifying the recalculation logic opens the door to performance improvements—for example, by getting rid of auxiliary indicator buffers, like in Tillson T3 (we’ll get back to that in the next parts).

However, there is one scenario we still haven’t accounted for. In the Strategy Tester with visual mode disabled, an EA may request indicator values less frequently than once per bar—meaning multiple new bars can appear between CopyBuffer() calls. To avoid falling back to a full buffer recalculation in this case, simply add the tester_everytick_calculate property directive (see also The Calculation of Indicators During Testing):

#property tester_everytick_calculate

Code #5 | Evolution.mq5 | fix: enable tester_everytick_calculate

In rare cases, OnCalculate() may be called with rates_total = 1 and prev_calculated = 0. This can happen if the user switches to a previously unused server while the indicator is running. As a result, barIdxToRecalcFrom would be set to -1, which triggers an «array out of range» runtime error and causes the indicator to crash. The simple check below is enough to prevent that. I don’t actually expect rates_total to ever be less than prev_calculated, but if it is—we're covered.

if(rates_total < 2 || rates_total < prev_calculated)
  {
   PrintFormat(__FUNCTION__" Something went wrong. rates_total %i, prev_calculated %i", rates_total, prev_calculated);
   return 0;
  }

Code #6 | Evolution.mq5 | fix: add sanity checks for rates_total and prev_calculated

This completes the primitive indicator, implemented in a non-portable way. The full source code is available on AlgoForge.



2. Key Ingredients: Header Files and Object-Oriented Programming

To reuse this code in another project, you would have to copy-paste it. Since the code we just developed is very short and primitive, you probably won't have any trouble doing so.

Now imagine that in a new project, you want to reuse a large chunk of long spaghetti code—code where the data and functions you need are hopelessly entangled with other parts of the system. This happens because when you wrote that code, you didn’t bother to think about making it easily reusable later. On top of that, the new project isn’t empty either—it already contains a significant amount of spaghetti itself. Reusing code in such conditions will not be a pleasant experience. You'll likely spend hours—if not days—with the debugger. When I was a beginner, it sometimes happened that I spent three days debugging code that took only two hours to write.

Header Files

If you want your code to be reusable, you cannot do without .mqh files. Throughout this article, I will refer to .mqh files as header files. Header files aren't just for reusability—they greatly simplify development and reduce the amount of time you spend coding.

  • Imagine a library without shelves, where all the books are simply thrown together into a huge pile.
  • Imagine a supermarket spanning several thousand square meters, selling an enormous variety of goods—but without any shelves, sections, or any form of organization. Everything is just scattered chaotically on the floor. How long would it take you to find the coffee and cat food you came for?

This is exactly what a project looks like when it consists of a single file containing thousands—or even tens of thousands—of lines of code. I, too, once wrote large projects packed into a single file. The function list spanned multiple screens, so it was easier for me to find functions using <Ctrl+F>. But sometimes I couldn’t even remember the name of the function I was looking for. Worse yet, sometimes I forgot that the function already existed—and wrote another one doing the same thing. Obviously, such code was very hard to maintain and nearly impossible to scale.

Every header file must compile without errors and warnings.

To me, this seems obvious. In most cases, you compile code not because you are ready to run your program, but because you want the compiler to check your code for errors. By the way, compiling the entire solution can take seconds or even dozens of seconds if your project is large. But if your project is split into small header files, after making changes to one of them, you only need to compile that particular file to check for errors and save the changes. Unlike compiling the whole solution, compiling a single header file is almost instantaneous (with rare exceptions).

Object-Oriented Programming

Have you ever tried splitting code into header files without using OOP? Back when I was skeptical about OOP—and honestly too lazy to learn it—I tried every idea I could think of to organize code into header files without it. Nothing worked.

Non-object-oriented code basically consists of two main parts: functions and data in the global scope. Typical examples of global-scope data include:

  • Configuration settings.
  • Arrays of indicator buffers.
  • Variables that store values between OnCalculate() calls (a simple example would be the names of created chart objects).
  • Variables created to reduce the number of parameters passed to functions. We’ve all done it—creating a global variable just so we could access it from different functions.

Sure, you can move the declarations of global data into a header file. But that’s just the declaration! It’s not enough to declare data—you must also correctly call the functions that operate on this data, in the right place and in the right order. Otherwise, you risk creating bugs without even writing new code—just by accidentally breaking the function call sequence or unintentionally modifying shared data.

Before I started programming in the object-oriented paradigm, I constantly had to choose between spaghetti code and bloated function signatures. The more variables I made global, the more tangled the code became. Trying to avoid spaghetti, I moved variables inside functions—which led to massive function signatures.

Without OOP, your identifiers (function and variable names) become ridiculously long and awkward. Sometimes, I wanted to break a long function into several smaller ones, but I abandoned the idea because it would require coming up with several long and clumsy names.

All the problems described above are easily solved with encapsulation—a mechanism in OOP that bundles together data and the functions that operate on that data. Encapsulation is not just the key to splitting code into header files effectively—it’s the key to object-oriented programming in general.



3. Encapsulating Logic: The First Module

Now let’s make the code we just wrote a bit more portable (without changing its behavior). I wrapped the applied price code into a class called CAppliedPrice and moved that class into a separate file: src/AppliedPrice.mqh. The full code is shown below.

class CAppliedPrice
  {
public:
   static double calculate(int barIdx, ENUM_APPLIED_PRICE mode, const double &open[], const double &high[], const double &low[],
                           const double &close[])
     {
      switch(mode)
        {
         case PRICE_OPEN:     return open[barIdx];
         case PRICE_HIGH:     return high[barIdx];
         case PRICE_LOW:      return low[barIdx];
         case PRICE_MEDIAN:   return (high[barIdx] + low[barIdx]) / 2.0;
         case PRICE_TYPICAL:  return (high[barIdx] + low[barIdx] + close[barIdx]) / 3.0;
         case PRICE_WEIGHTED: return (high[barIdx] + low[barIdx] + close[barIdx] + close[barIdx]) / 4.0;
         default:             return close[barIdx];
        }
     }

   int onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[],
                   const double &close[])
     {
      if(rates_total < 2 || rates_total < prev_calculated)
        {
         PrintFormat(__FUNCTION__" Something went wrong. rates_total %i, prev_calculated %i", rates_total, prev_calculated);
         return 0;
        }
      int barIdxToRecalcFrom;
      switch(rates_total - prev_calculated)
        {
         case 0:
         case 1:
            barIdxToRecalcFrom = prev_calculated - 1;
            break;
         default:
            barIdxToRecalcFrom = 0;
            break;
        }
      for(int i = barIdxToRecalcFrom; i < rates_total; i++)
         buffer[i] = calculate(i, mode, open, high, low, close);
      return rates_total;
     }

private:
   const ENUM_APPLIED_PRICE mode;
public:
   double                   buffer[];
public:
   CAppliedPrice(ENUM_APPLIED_PRICE a_mode) : mode(a_mode) {}
  };

Code #7 | AppliedPrice.mqh | refactor: extract applied price into CAppliedPrice

The calculate() function was moved without any changes. I made CAppliedPrice::calculate() public and static in case I ever need that method but want to avoid storing the applied price in a buffer. The body of OnCalculate() became the body of CAppliedPrice::onCalculate().

Here’s all that remained in Evolution.mq5 after extracting CAppliedPrice (I’m not quoting the #property directives):

#include "src\AppliedPrice.mqh"

input(name="Price") ENUM_APPLIED_PRICE inpAppliedPrice = PRICE_CLOSE;

CAppliedPrice* appliedPrice;

int OnInit()
  {
   appliedPrice = new CAppliedPrice(inpAppliedPrice);
   SetIndexBuffer(0, appliedPrice.buffer, INDICATOR_DATA);
   return INIT_SUCCEEDED;
  }

int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
   return appliedPrice.onCalculate(rates_total, prev_calculated, open, high, low, close);
  }

void OnDeinit(const int reason)
  {
   delete appliedPrice;
  }

Code #8 | Evolution.mq5 | refactor: extract applied price into CAppliedPrice

In OnInit(), we create an instance of CAppliedPrice, store the pointer in the global variable appliedPrice, and bind appliedPrice.buffer to the indicator buffer. In OnDeinit(), the instance is deleted, and OnCalculate() contains just a single method call.

Now CAppliedPrice is a portable module. Its code lives in a separate file, which removes the risk of accidentally modifying it unintentionally. Also, the applied price code is no longer «underfoot» when you’re working on other parts of the indicator.

Let’s agree to call modules like CAppliedPrice «sub-indicators» from now on. In other words, a sub-indicator is a class that contains one or more indicator-buffer arrays and the code to compute them. Another distinguishing feature of sub-indicators is a public onCalculate() method—which makes sense, because indicator-buffer arrays must be updated whenever the Calculate event occurs.



4. Eliminating Global State: Introducing CIndicator

As I already mentioned, I started learning OOP back then because I needed encapsulation to split the source code into portable module files.

I remember a period when I was still in the middle of learning OOP. My existing codebase was in a transitional state: part of the code had already been encapsulated into classes, while the rest was procedural spaghetti with lots of global state. While I was wrapping the existing procedural code into classes, a question came up that seriously bothered me: what if a class member name collides with an identifier that’s already taken in the global scope? I couldn’t answer it because I still wasn’t familiar with a significant portion of OOP syntax—but the problem got stuck in my head.

A few days later, I was thinking about it again before going to sleep. I dreamed about code all night, and in the morning I woke up with a sudden idea: «Global state must be eliminated; nothing should be left in the global scope.» I sat down at the computer, wrapped the entire program into one big class, CAdvisor (I was working on an Expert Advisor back then), and made a pact with myself that the global scope would contain nothing except a pointer to the CAdvisor instance.

A fair amount of time has passed since then, but that pact is still in effect. So let’s create CIndicator in a separate file, src/Indicator.mqh, right away and work exclusively inside it.

UML class diagram showing CIndicator composed of CAppliedPrice

#include "AppliedPrice.mqh"

class CIndicator
  {
public:
   int onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[],
                   const double &close[], const datetime &time[])
     {
      return appliedPrice.onCalculate(rates_total, prev_calculated, open, high, low, close);
     }

private:
   CAppliedPrice appliedPrice;
public:
   CIndicator(ENUM_APPLIED_PRICE mode)
      : appliedPrice(mode)
     {
      SetIndexBuffer(0, appliedPrice.buffer, INDICATOR_DATA);
     }
  };

Code #9 | Indicator.mqh | refactor: introduce CIndicator as root object

#include "src\Indicator.mqh"

input(name="Price") ENUM_APPLIED_PRICE inpAppliedPrice = PRICE_CLOSE;

CIndicator* Indicator;

int OnInit()
  {
   Indicator = new CIndicator(inpAppliedPrice);
   return INIT_SUCCEEDED;
  }

int OnCalculate(const int32_t rates_total,
                const int32_t prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int32_t &spread[])
  {
   return Indicator.onCalculate(rates_total, prev_calculated, open, high, low, close, time);
  }

void OnDeinit(const int reason)
  {
   delete Indicator;
  }

Code #10 | Evolution.mq5 | refactor: introduce CIndicator as root object



5. Isolating Input Parameters

Passing input parameters to the CIndicator constructor becomes more cumbersome as the number of input parameters grows, because every input requires a corresponding constructor parameter. What I find especially inconvenient when adding or removing inputs is updating the argument list at the call site. Quite often, the arguments that need to be added or removed are scattered across different parts of the list, which forces me to go through the entire argument list again and check it against the constructor signature.

Wrapping the parameters into a class seems like a good idea. Below is an example of what that might look like:

#include "src\Indicator.mqh"

input(name="Price") ENUM_APPLIED_PRICE inpAppliedPrice = PRICE_CLOSE;

class CIndiParams
  {
public:
   ENUM_APPLIED_PRICE appliedPrice;
  
   CIndiParams()
     {
      appliedPrice = inpAppliedPrice;
     }
  };

CIndicator* Indicator;

int OnInit()
  {
   CIndiParams params;
   Indicator = new CIndicator(params);
   return INIT_SUCCEEDED;
  }

However, this code will not compile because CIndicator is defined in src/Indicator.mqh and cannot see the definition of CIndiParams in Evolution.mq5.

If CIndiParams is moved into a separate file, it will no longer have access to the input variables from Evolution.mq5.

Of course, we could move both CIndiParams and the input variables into a shared header file. In that case, however, the input variables would also become visible from src/Indicator.mqh. Objectively speaking, that is not a critical problem, but I would prefer a higher degree of isolation.

So the requirements are roughly as follows:

  • Input parameters should be passed to the CIndicator constructor wrapped in a CIndiParams class located in a separate header file;
  • Input variables should be accessible to CIndiParams;
  • CIndicator should have access to CIndiParams, but not to the input variables themselves.

Taken literally, this means that CIndiParams somehow has to both see and not see the input variables at the same time. At first glance, that sounds absurd—but it can be achieved with inheritance.

UML class diagram demonstrating input parameter isolation through CIndiParamsBase

CIndiParamsBase knows nothing about the input variables and is located in a separate file:

class CIndiParamsBase
  {
public:
   ENUM_APPLIED_PRICE appliedPrice;
  };

Code #11 | IndiParamsBase.mqh | refactor: replace CIndicator ctor args with params object

CIndicator then takes a reference to CIndiParamsBase in its constructor:

#include "IndiParamsBase.mqh"
#include "AppliedPrice.mqh"

class CIndicator
  {
private:
   CAppliedPrice appliedPrice;
public:
   CIndicator(const CIndiParamsBase &params)
      : appliedPrice(params.appliedPrice)
     {
      SetIndexBuffer(0, appliedPrice.buffer, INDICATOR_DATA);
     }
  };

Code #12 | Indicator.mqh | refactor: replace CIndicator ctor args with params object

The file src/InputsAndRelated.mqh contains the input variables and the derived class CIndiParams, which assigns the values of those input variables to the inherited fields of the base class:

#include "IndiParamsBase.mqh"

input(name="Price") ENUM_APPLIED_PRICE inpAppliedPrice = PRICE_CLOSE;

class CIndiParams : public CIndiParamsBase
  {
public:
   CIndiParams()
     {
      appliedPrice = inpAppliedPrice;
     }
  };

Code #13 | InputsAndRelated.mqh | refactor: replace CIndicator ctor args with params object

As a result, Evolution.mq5 no longer declares input variables directly; it includes the header file that defines them:

#include "src\Indicator.mqh"
#include "src\InputsAndRelated.mqh"

CIndicator* Indicator;

int OnInit()
  {
   CIndiParams params;
   Indicator = new CIndicator(params);
   return INIT_SUCCEEDED;
  }

Code #14 | Evolution.mq5 | refactor: replace CIndicator ctor args with params object

The inclusion diagram below makes it easy to see that the input variables are visible only to the .mq5 file.

File inclusion diagram showing the isolation of input variables



6. Extracting Common Traits: CSubIndiBase

We already have the applied price; next, I suggest we build a simple moving average (SMA) on top of it. But before we get to that, let's return to CAppliedPrice one more time.

class CAppliedPrice
  {
public:
   int onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[],
                   const double &close[])
     {
      if(!checkRatesTotal(rates_total, prev_calculated, 1))
         return 0;
      int barIdxToRecalcFrom;
      switch(rates_total - prev_calculated)
        {
         case 0:
         case 1:
            barIdxToRecalcFrom = prev_calculated - 1;
            break;
         default:
            barIdxToRecalcFrom = 0;
            break;
        }
      for(int i = barIdxToRecalcFrom; i < rates_total; i++)
         buffer[i] = calculate(i, mode, open, high, low, close);
      return rates_total;
     }

private:
   static bool checkRatesTotal(const int rates_total, const int prev_calculated, int barsRequired)
     {
      if(barsRequired < 2)
         barsRequired = 2;
      if(rates_total < barsRequired)
        {
         PrintFormat("Not enough bars on the chart to calculate the indicator. Required: %i, available: %i.", barsRequired, rates_total);
         return false;
        }
      if(rates_total < prev_calculated)
        {
         PrintFormat(__FUNCTION__" Something went wrong. rates_total: %i, prev_calculated: %i", rates_total, prev_calculated);
         return false;
        }
      return true;
     }
  };

Code #15 | AppliedPrice.mqh | refactor: extract rates_total checks into checkRatesTotal

I extracted the code that checks rates_total into a separate checkRatesTotal() method and made it a bit clearer. As we established earlier, an indicator always requires at least 2 bars on the chart. But strictly speaking, it is not CAppliedPrice itself that requires two bars. From a technical point of view, CAppliedPrice can be calculated even if there is only one bar on the chart. That is why 1 is passed to the barsRequired parameter of checkRatesTotal(), while the strict two-bar minimum is enforced internally within the method's logic.

For the SMA we are about to implement, the number of bars on the chart must be at least equal to the SMA period. The newly extracted checkRatesTotal() method is flexible enough that we can reuse it for the SMA by simply passing the indicator's period as the barsRequired argument. Besides, it is characteristic of any sub-indicator to require some minimum number of bars on the chart. Therefore, I will create a CSubIndiBase class, and from now on, all sub-indicators must be derived from it.

UML class diagram introducing CSubIndiBase as the base class for sub-indicators

class CSubIndiBase
  {
private:
   static int correctBarsRequired(int barsRequired)
     {
      if(barsRequired >= 1)
         return barsRequired;
      const int prevValue = barsRequired;
      barsRequired = 1;
      PrintFormat(__FUNCTION__": invalid barsRequired value (%i); corrected to %i", prevValue, barsRequired);
      return barsRequired;
     }
   
protected:
   static bool checkRatesTotal(const int rates_total, const int prev_calculated, int barsRequired)
     {
      if(barsRequired < 2)
         barsRequired = 2;
      if(rates_total < barsRequired)
        {
         PrintFormat("Not enough bars on the chart to calculate the indicator. Required: %i, available: %i.", barsRequired, rates_total);
         return false;
        }
      if(rates_total < prev_calculated)
        {
         PrintFormat(__FUNCTION__" Something went wrong. rates_total: %i, prev_calculated: %i", rates_total, prev_calculated);
         return false;
        }
      return true;
     }
   
public:
   const int    barsRequired;
   const int    drawBegin;
   const double emptyValue;
protected:
   CSubIndiBase(int a_barsRequired, double a_emptyValue)
      : barsRequired(correctBarsRequired(a_barsRequired)),
        drawBegin(barsRequired - 1),
        emptyValue(a_emptyValue) {}
  };

Code #16 | SubIndiBase.mqh | refactor: introduce CSubIndiBase for sub-indicators

#include "SubIndiBase.mqh"

class CAppliedPrice : public CSubIndiBase
  {
public:
   int onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[],
                   const double &close[])
     {
      if(!checkRatesTotal(rates_total, prev_calculated, barsRequired))
         return 0;
      int barIdxToRecalcFrom;
      switch(rates_total - prev_calculated)
        {
         case 0:
         case 1:
            barIdxToRecalcFrom = prev_calculated - 1;
            break;
         default:
            barIdxToRecalcFrom = 0;
            break;
        }
      for(int i = barIdxToRecalcFrom; i < rates_total; i++)
         buffer[i] = calculate(i, mode, open, high, low, close);
      return rates_total;
     }

private:
   const ENUM_APPLIED_PRICE mode;
public:
   double                   buffer[];
public:
   CAppliedPrice(ENUM_APPLIED_PRICE a_mode) : CSubIndiBase(1, EMPTY_VALUE), mode(a_mode) {}
  };

Code #17 | AppliedPrice.mqh | refactor: introduce CSubIndiBase for sub-indicators

Beyond defining the checkRatesTotal() method, CSubIndiBase introduces three essential fields:

  • By drawBegin, I mean the index of the bar where the first non-empty value of the indicator can be expected. For example, an SMA with a period of 5 requires 5 bars on the chart (barsRequired = 5), and the first non-empty value of the SMA will be on the bar with index 4.
  • barsRequired is primarily used only to make sure that there are enough bars on the chart to calculate the sub-indicator.
  • emptyValue is, roughly speaking, the value you normally pass to the PLOT_EMPTY_VALUE property of an indicator plot.

Visual representation of barsRequired and drawBegin concepts for a single sub-indicator

With CAppliedPrice now inheriting from CSubIndiBase, the call to checkRatesTotal() supplies the barsRequired field rather than the integer literal 1.



7. Centralizing Requirements: CSubIndiRegistry

A shared checkRatesTotal() method is better than having a personal copy for each sub-indicator, but it's still somewhat clumsy. It forces us to insert this exact same boilerplate code at the beginning of the onCalculate() method for every sub-indicator:

if(!checkRatesTotal(rates_total, prev_calculated, barsRequired))
   return 0;

Furthermore, this is the only thing that forces sub-indicators to return a value from the onCalculate() method, which is not very convenient from the perspective of CIndicator::onCalculate().

Imagine an indicator consisting of 3 sub-indicators with barsRequired values of 12, 28, and 5. We could simply call checkRatesTotal() once, passing the value 28—the largest barsRequired among all the sub-indicators. To achieve this, we need some entity that keeps track of the maximum barsRequired. Let's create a class named CSubIndiRegistry for this purpose. I made it entirely static because I definitely won't need more than one instance. The advantage of a purely static class is that I don't have to declare an instance and pass references to it around, which simplifies the code.

#include "SubIndiBase.mqh"

class CSubIndiBase;

class CSubIndiRegistry
  {
public:
   static void clear()
     {
      // Even with drawBegin = 0, at least 2 bars on the chart are required
      barsRequired = 2;
     }
   
   static void register(CSubIndiBase& instance)
     {
      if(instance.barsRequired > barsRequired)
         barsRequired = instance.barsRequired;
     }
   
   static bool checkRatesTotal(const int rates_total, const int prev_calculated)
     {
      if(rates_total < barsRequired)
        {
         PrintFormat("Not enough bars on the chart to calculate the indicator. Required: %i, available: %i.", barsRequired, rates_total);
         return false;
        }
      if(rates_total < prev_calculated)
        {
         PrintFormat(__FUNCTION__" Something went wrong. rates_total: %i, prev_calculated: %i", rates_total, prev_calculated);
         return false;
        }
      return true;
     }
   
private:
   static int barsRequired;
  };

int CSubIndiRegistry::barsRequired;

Code #18 | SubIndiRegistry.mqh | refactor: centralize rates_total checks in CSubIndiRegistry

The checkRatesTotal() method has been moved from CSubIndiBase to the new CSubIndiRegistry class. The general requirement of having at least two bars on the chart is now enforced in the clear() method. The register() method is how the registry learns about all the sub-indicators.

I simply added a call to CSubIndiRegistry::register() in the CSubIndiBase constructor. This way, whenever a sub-indicator instance is created, CSubIndiRegistry learns about it «automatically.»

#include "SubIndiRegistry.mqh"

class CSubIndiBase
  {
protected:
   CSubIndiBase(int a_barsRequired, double a_emptyValue)
      : barsRequired(correctBarsRequired(a_barsRequired)),
        drawBegin(barsRequired - 1),
        emptyValue(a_emptyValue)
     {
      CSubIndiRegistry::register(this);
     }
  };

Code #19 | SubIndiBase.mqh | refactor: centralize rates_total checks in CSubIndiRegistry

Call CSubIndiRegistry::clear() during initialization, before constructing CIndicator. Sub-indicators are created in CIndicator’s initializer list, which invokes the CSubIndiBase constructor. This is why CSubIndiRegistry::clear() is called directly from OnInit().

int OnInit()
  {
   CSubIndiRegistry::clear();
   CIndiParams params;
   Indicator = new CIndicator(params);
   return INIT_SUCCEEDED;
  }

Code #20 | Evolution.mq5 | refactor: centralize rates_total checks in CSubIndiRegistry

CSubIndiRegistry::checkRatesTotal() is now called from within CIndicator::onCalculate(). This allowed us to change the return type of the onCalculate() method in the CAppliedPrice class (and all future sub-indicators) from int to void. For the changes described in this paragraph, please refer to the files on AlgoForge: AppliedPrice.mqh, Indicator.mqh.



8. Hiding Infrastructure Boilerplate: CIndicatorBase

Indicator.mqh is one of the most frequently edited files, as it is exactly where you wire everything together. After a while, I got tired of constantly seeing the CSubIndiRegistry::checkRatesTotal() call. So, I decided to abstract it away into a base class to free up screen space from code that never changes and requires no attention. Once I introduced CIndicatorBase, I realized that its constructor was the perfect place to call CSubIndiRegistry::clear().

#include "SubIndiRegistry.mqh"

class CIndicatorBase
  {
public:
   int onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[],
                   const double &close[], const datetime &time[])
     {
      if(!CSubIndiRegistry::checkRatesTotal(rates_total, prev_calculated))
         return 0;
      onCalculateImpl(rates_total, prev_calculated, open, high, low, close, time);
      return rates_total;
     }
   
private:
   virtual void onCalculateImpl(const int rates_total, const int prev_calculated, const double &open[], const double &high[],
                                const double &low[], const double &close[], const datetime &time[]) = 0;

protected:
   CIndicatorBase() { CSubIndiRegistry::clear(); }
  };

Code #21 | IndicatorBase.mqh | refactor: extract infrastructure logic into CIndicatorBase

#include "IndicatorBase.mqh"

class CIndicator : public CIndicatorBase
  {
public:
   void onCalculateImpl(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[],
                        const double &close[], const datetime &time[]) override
     {
      appliedPrice.onCalculate(rates_total, prev_calculated, open, high, low, close);
     }
  };

Code #22 | Indicator.mqh | refactor: extract infrastructure logic into CIndicatorBase



9. Building the Second Module: SMA

Let's return to our goal of building a Simple Moving Average (SMA) on top of the CAppliedPrice module we developed earlier. First, let's prepare the indicator parameters:

  1. I renamed the only existing parameter so it looks natural next to the SMA period. See refactor: rename applied price parameter to 'Apply to'.
  2. I added a second parameter for the SMA period. See feat: add SMA period input parameter.

Here is what the indicator settings look like now:

MetaTrader 5 indicator inputs tab showing SMA and Applied Price settings

To create the new module (sub-indicator), I drafted a CSma class (in its own file, of course) that inherits from CSubIndiBase. I've left the onCalculate() method empty for now; we'll come back to it shortly. As for the class fields, we’ll need a variable for the SMA period and an array for the indicator buffer (period and buffer, respectively)...

#include "SubIndiBase.mqh"

class CSma : public CSubIndiBase
  {
private:
   static int correctPeriod(int p, bool mute = false)
     {
      if(p >= 2)
         return p;
      const int prevValue = p;
      p = 2;
      if(!mute)
         PrintFormat(__FUNCTION__": invalid period value (%i); corrected to %i", prevValue, p);
      return p;
     }

public:
   void onCalculate(const int rates_total, const int prev_calculated, const double &source[])
     {
      Print(__FUNCTION__" not implemented");
     }

private:
   const int period;
public:
   double    buffer[];
public:
   CSma(int a_period, int sourceBarsRequired)
      : CSubIndiBase(sourceBarsRequired + correctPeriod(a_period) - 1, EMPTY_VALUE),
        period(correctPeriod(a_period, true))
     {}
  };

Code #23 | Sma.mqh | feat: outline CSma sub-indicator

Notice that along with emptyValue, the CSubIndiBase constructor expects a value for barsRequired. At first glance, passing the SMA period directly might seem like the obvious choice. However, it's better to think ahead: the data source itself might require more than one bar to be present on the chart. To account for this, I introduced the sourceBarsRequired parameter. To calculate the actual barsRequired, we just add the SMA's own requirement (period) to the source's requirement (sourceBarsRequired) and subtract 1. To visualize this, let's look at an example where the SMA period is 3, and the source requires 5 bars:

Visual representation of barsRequired and drawBegin calculation for chained sub-indicators

Now, let's wire up our new sub-indicator:

  1. Add an instance of CSma to the CIndicator class. See feat: integrate CSma into CIndicator.
  2. Bind the indicator buffers. See feat: reconfigure plots and buffers.

A Quick Detour

While writing this section, I was looking over AppliedPrice.mqh and suddenly felt the urge to improve one of the variable names. I didn't hold back and went ahead with it in a small, separate commit:

refactor: rename barIdxToRecalcFrom to recalcStartIdx

I also realized that when making CAppliedPrice inherit from CSubIndiBase, I forgot to replace the hardcoded 0 with drawBegin. So, I took care of that with another quick micro-refactoring:

refactor: replace 0 with drawBegin in CAppliedPrice



10. Calculating the Simple Moving Average

All that’s left is to implement the onCalculate() method. I'll take the ready-made logic from the SimpleMAOnBuffer() function—found in the MQL5/Include/MovingAverages.mqh file—and simply adapt it to my architecture. At first glance, the function might look a bit confusing, but the underlying logic is actually quite simple. Here’s what we need to do:

  1. Fill the bars to the left of drawBegin with empty values.
  2. Calculate the value for the bar at the drawBegin index using the base formula.
  3. Calculate the values for all remaining bars (to the right of drawBegin) using the recurrence formula.

Points 1 and 2 sound like great candidates for separate methods (it just makes the code more convenient and readable):

class CSma : public CSubIndiBase
  {
private:
   void clearLeftFromDrawBegin()
     {
      for(int i = 0; i < drawBegin; i++)
         buffer[i] = emptyValue;
     }

   void computeValueAtDrawBegin(const double &source[])
     {
      double result = 0.0;
      for(int i = drawBegin - (period - 1); i <= drawBegin; i++)
         result += source[i];
      buffer[drawBegin] = result / period;
     }
  };

Code #24 | Sma.mqh | feat: implement CSma::onCalculate

With the clearLeftFromDrawBegin() and computeValueAtDrawBegin() methods ready to go, implementing onCalculate() is a breeze. Especially since we already have the exact code we need to calculate recalcStartIdx over in CAppliedPrice.

class CSma : public CSubIndiBase
  {
public:
   void onCalculate(const int rates_total, const int prev_calculated, const double &source[])
     {
      int recalcStartIdx;
      switch(rates_total - prev_calculated)
        {
         case 0:
         case 1:
            recalcStartIdx = prev_calculated - 1;
            break;
         default:
            recalcStartIdx = drawBegin;
            clearLeftFromDrawBegin();
            break;
        }
      if(rates_total - prev_calculated > 1)
        {
         computeValueAtDrawBegin(source);
         recalcStartIdx++;
        }
      for(int i = recalcStartIdx; i < rates_total; i++)
         buffer[i] = buffer[i - 1] + (source[i] - source[i - period]) / period;
     }
  };

Code #25 | Sma.mqh | feat: implement CSma::onCalculate

I simply copy-pasted the entire switch statement that calculates recalcStartIdx from CAppliedPrice::onCalculate(). But with one important difference—I added a call to clearLeftFromDrawBegin() inside it.

Done! The indicator works, and its values perfectly match those of the SMA built into MetaTrader 5.



11. DRYing Sub-Indicators: Pulling Logic Up

In the previous section, I copy-pasted the switch statement from CAppliedPrice into CSma. However, given that this article is literally titled «Stop Copy-Pasting,» it's about time I practiced what I preach. We can wrap that switch statement in a separate method—let's call it prepareBeforeCalculating()—and pull it up into the CSubIndiBase base class. Looking ahead, this method will end up being used in almost all of our sub-indicators. The only thing preventing me from extracting it right now is just a single line of code:

class CSma : public CSubIndiBase
  {
private:
   void clearLeftFromDrawBegin()
     {
      for(int i = 0; i < drawBegin; i++)
         buffer[i] = emptyValue;
     }
  };

The problem is that the base class has absolutely no idea which indicator buffers are being used in the derived classes, nor how many there are. The solution is straightforward: we'll declare a pure virtual method called clearBuffersAt() in the base class. This forces every sub-indicator to implement its own specific buffer-clearing logic:

class CSubIndiBase
  {
private:
   virtual void clearBuffersAt(int barIdx) = 0;
  };

Code #26 | SubIndiBase.mqh | refactor: introduce clearBuffersAt hook for subclasses

class CAppliedPrice : public CSubIndiBase
  {
private:
   void clearBuffersAt(int barIdx) override { buffer[barIdx] = emptyValue; }
  };

Code #27 | AppliedPrice.mqh | refactor: introduce clearBuffersAt hook for subclasses

class CSma : public CSubIndiBase
  {
private:
   void clearBuffersAt(int barIdx) override { buffer[barIdx] = emptyValue; }

   void clearLeftFromDrawBegin()
     {
      for(int i = 0; i < drawBegin; i++)
         clearBuffersAt(i);
     }
  };

Code #28 | Sma.mqh | refactor: introduce clearBuffersAt hook for subclasses

With that hurdle cleared, we can safely pull both prepareBeforeCalculating() and clearLeftFromDrawBegin() up into the base class. Here is the class diagram:

UML class diagram showing CIndicator composed of CAppliedPrice and CSma, along with their base classes

The actual code changes are shown below. Notice how much cleaner and more readable the onCalculate() methods inside the sub-indicators have become.

class CSubIndiBase
  {
private:
   virtual void clearBuffersAt(int barIdx) = 0;

protected:
   int prepareBeforeCalculating(const int rates_total, const int prev_calculated)
     {
      int recalcStartIdx;
      switch(rates_total - prev_calculated)
        {
         case 0:
         case 1:
            recalcStartIdx = prev_calculated - 1;
            break;
         default:
            recalcStartIdx = drawBegin;
            clearLeftFromDrawBegin();
            break;
        }
      return recalcStartIdx;
     }

   void clearLeftFromDrawBegin()
     {
      for(int i = 0; i < drawBegin; i++)
         clearBuffersAt(i);
     }
  };

Code #29 | SubIndiBase.mqh | refactor: pull up prepareBeforeCalculating to base class

class CAppliedPrice : public CSubIndiBase
  {
public:
   void onCalculate(const int rates_total, const int prev_calculated, const double &open[], const double &high[], const double &low[],
                    const double &close[])
     {
      for(int i = prepareBeforeCalculating(rates_total, prev_calculated); i < rates_total; i++)
         buffer[i] = calculate(i, mode, open, high, low, close);
     }
  };

Code #30 | AppliedPrice.mqh | refactor: pull up prepareBeforeCalculating to base class

class CSma : public CSubIndiBase
  {
public:
   void onCalculate(const int rates_total, const int prev_calculated, const double &source[])
     {
      int recalcStartIdx = prepareBeforeCalculating(rates_total, prev_calculated);
      if(rates_total - prev_calculated > 1)
        {
         computeValueAtDrawBegin(source);
         recalcStartIdx++;
        }
      for(int i = recalcStartIdx; i < rates_total; i++)
         buffer[i] = buffer[i - 1] + (source[i] - source[i - period]) / period;
     }
  };

Code #31 | Sma.mqh | refactor: pull up prepareBeforeCalculating to base class



Conclusion

We have come a long way from the primitive, procedural implementation we started with. By taking a deliberate, step-by-step approach to refactoring, we successfully transformed a simple task into a robust, object-oriented framework.

Rather than just throwing code together and hoping it works, we focused on architectural integrity. We eliminated the fragile global state by encapsulating data and logic within dedicated classes. We solved the problem of passing parameters cleanly through inheritance with CIndiParamsBase. Furthermore, by introducing CSubIndiBase, CIndicatorBase, and the CSubIndiRegistry, we abstracted away repetitive boilerplate code. Now, crucial requirements—like ensuring enough bars are on the chart and calculating starting indices—are handled automatically under the hood.

The result is a clean, modular architecture where sub-indicators like CAppliedPrice and CSma are entirely self-contained and highly reusable. Their onCalculate() methods are no longer cluttered with infrastructure logic; they contain only the math that actually matters.

We have laid a solid foundation. In the next article, it's time to put this framework to work. We will stop worrying about the underlying mechanics and focus entirely on creating new indicators, expanding our library of modules, and demonstrating just how easily they can be combined to build complex analytical tools.



Files Attached to the Article

File Name Description
Evolution.mq5 The main indicator file acting as the entry point and containing the MQL5 event handlers.
Indicator.mqh The root object that wires all sub-indicators together, eliminating the need for global state.
IndicatorBase.mqh Abstracts away infrastructure boilerplate (such as rates_total checks) from the main indicator logic.
InputsAndRelated.mqh Defines the global input variables and the derived class mapping them to the parameter object.
IndiParamsBase.mqh Defines the base structure for indicator parameters, decoupling the core logic from global input variables.
SubIndiRegistry.mqh A static registry that centrally manages the barsRequired requirement across all modules.
SubIndiBase.mqh The base class for all sub-indicators that extracts common traits and handles routine boilerplate.
AppliedPrice.mqh A sub-indicator that calculates the base applied price.
Sma.mqh A sub-indicator that calculates the Simple Moving Average.
Attached files |
MQL5.zip (6.52 KB)
MQL5 Bootstrap (I): Reusable Functions for Working with Positions and Orders MQL5 Bootstrap (I): Reusable Functions for Working with Positions and Orders
This article presents a compact MQL5 utility layer for routine trade operations. It includes position existence checkers, position counters, bulk close helpers, and functions to retrieve the most recent or oldest position by symbol, magic, or type. A simple SMA crossover Expert Advisor demonstrates integration. The result is cleaner EAs, fewer inconsistencies across projects, and faster maintenance.
Encoding Candlestick Patterns (Part 2): Modeling Price Action as an Ordered Sequence Encoding Candlestick Patterns (Part 2): Modeling Price Action as an Ordered Sequence
Developing permutation-based tools in MQL5 provides a systematic way to analyze candlestick pattern combinations for trading strategies. This article introduces a permutation calculator and generator designed to compute and enumerate all possible ordered candlestick sequences from bullish and bearish sets, with or without repetition. By generating exhaustive pattern combinations, traders can perform data-driven analysis to identify high-probability market patterns and improve decision-making in automated trading systems.
MQL5 Wizard Techniques you should know (Part 92): Using B-Tree Indexing and a Bayesian NN in a Custom Signal Class MQL5 Wizard Techniques you should know (Part 92): Using B-Tree Indexing and a Bayesian NN in a Custom Signal Class
In this article we present yet another custom MQL5 Signal Class that we are labelling ‘CSignalBTreeBayesian’. We are marrying the algorithm of a balanced tree with a neural network that is built on Bayesian principles to formulate yet another custom signal testable independently or with other signals thanks to the MQL5 Wizard.
Beyond GARCH (Part IV): Partition Analysis in MQL5 Beyond GARCH (Part IV): Partition Analysis in MQL5
In this article, we shift from Python research to native MQL5 engineering. We build the first module of the MMAR library: a shared constants header, an SVD-based OLS regression class, a Generalized Hurst Exponent estimator, and the partition analysis engine that computes the partition function, extracts tau(q), estimates H via zero-crossing interpolation, and scores multifractality through three diagnostic tests. Tested on 500,000 bars of EURUSD M10, the engine correctly classifies the data as multifractal in under four seconds. Part 4 of an eight-part series. Part 5 fits the tau(q) curve to four candidate distributions via the Legendre transform.