MQL5: Create Your Own Indicator

MetaQuotes | 6 January, 2010

Introduction

What is an indicator? It is a set of calculated values that we want to be displayed on the screen in a convenient way. Sets of values are represented in programs as arrays. Thus, creation of an indicator means writing an algorithm that handles some arrays (price arrays) and records results of handling to other arrays (indicator values).

Despite the fact that there are a lot of ready indicators, which have become classics already, the necessity to create one's own indicators will always exist. Such indicators that we create using our own algorithms are called custom indicators. In this article we will discuss how to create a simple custom indicator.

Indicators Are Different

An indicator can be presented as colored lines or areas, or it can be displayed as special labels pointing at favorable moments for position entering. Also these types can be combined, which gives even more indicator types. We'll consider creation of an indicator on the example of the well-known True Strength Index developed by William Blau.

True Strength Index

The TSI indicator is based on the double-smoothed momentum to identify trends, as well as oversold/overbought areas. Mathematical explanation of it can be found in Momentum, Direction, and Divergence by William Blau. Here we include only its calculation formula.

TSI(CLOSE,r,s) =100*EMA(EMA(mtm,r),s) / EMA(EMA(|mtm|,r),s)

where:

From this formula, we can extract three parameters that influence the indicator calculation. These are periods r and s, as well as the type of prices used for calculations. In our case we use CLOSE price.

MQL5 Wizard

Let's display TSI as a blue line - here we need to start MQL5 Wizard. At the first stage we should indicated the type of a program we want to create - custom indicator. At the second stage, let's set the program name, r and s parameters and their values.

MQL5 Wizard: Setting indicator name and parameter

After that let's define that the indicator shall be displayed in a separate window as a blue line and set the TSI label for this line.

MQL5 Wizard: setting indicator type

All initial data have been entered, so we press Done and get a draft of our indicator. 

//+------------------------------------------------------------------+
//|                                          True Strength Index.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
//---- plot TSI
#property indicator_label1  "TSI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Blue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      r=25;
input int      s=13;
//--- indicator buffers
double         TSIBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

MQL5 Wizard creates the indicator header, in which it writes indicator properties, namely:

All preparations are ready, now we can refine and improve our code.

OnCalculate()

The OnCalculate() function is the handler of the Calculate event, that appears when it's necessary to recalculate indicator values and redraw it on the chart. This is the event of a new tick receiving, symbol history update, etc. That's why the main code for all calculations of indicator values must be located exactly in this function.

Of course, auxiliary calculations can be implemented in other separate functions, but those functions must be used in the OnCalculate handler.

By default, MQL5 Wizard creates the second form of OnCalculate(), which provides access to all types of timeseries:

But in our case we need only one data array, that's why let's change OnCalculate() the first form of calling.

int OnCalculate (const int rates_total,      // size of the price[] array
                 const int prev_calculated,  // number of available bars at the previous call
                 const int begin,            // from what index in price[] authentic data start
                 const double& price[])      // array, on which the indicator will be calculated
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }  

This will enable us to further apply the indicator not only to price data, but also create the indicator based on values of other indicators.

Specifying type of data for the calculation of the custom indicator

If we select Close in the Parameters tab (it is offered by default), then price[] passed to OnCalculate() will contain close prices. If we select, for example, Typical Price, price[] will contain prices of (High+Low+Close)/3 for each period.

The rates_total parameter denotes the size of the price[] array; it will be useful for organizing calculations in a cycle. Indexing of elements in price[] starts from zero and is directed from past to future. I.e. the price[0] element contains the oldest value, while price[rates_total-1] contains the latest array element.

Organizing Auxiliary Indicator Buffers

Only one line will be shown in a chart, i.e. data of one indicator array. But before that we need to organize intermediate calculations. Intermediate data are stored in indicator arrays that are marked by the INDICATOR_CALCULATIONS attribute. From the formula we see that we need additional arrays:

  1. for values mtm - array MTMBuffer[];
  2. for values |mtm| - array AbsMTMBuffer[];
  3. for EMA(mtm,r) - array EMA_MTMBuffer[];
  4. for EMA(EMA(mtm,r),s) - array EMA2_MTMBuffer[];
  5. for EMA(|mtm|,r) - array EMA_AbsMTMBuffer[];
  6. for EMA(EMA(|mtm|,r),s) - array EMA2_AbsMTMBuffer[].

Totally we need to add 6 more arrays of double type at global level and bind these arrays with the indicator buffers on the OnInit() function. Don't forget to indicate the new number of indicator buffers; the indicator_buffers property must be equal to 7 (there was 1, and 6 buffers more were added).

#property indicator_buffers 7

Now the indicator code looks like this:

#property indicator_separate_window
#property indicator_buffers 7
#property indicator_plots   1
//---- plot TSI
#property indicator_label1  "TSI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Blue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      r=25;
input int      s=13;
//--- indicator buffers
double         TSIBuffer[];
double         MTMBuffer[];
double         AbsMTMBuffer[];
double         EMA_MTMBuffer[];
double         EMA2_MTMBuffer[];
double         EMA_AbsMTMBuffer[];
double         EMA2_AbsMTMBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate (const int rates_total,    // size of the price[] array;
                 const int prev_calculated,// number of available bars;
                                           // during the previous call;
                 const int begin,          // from what index in  
                                           // price[] authentic data start;
                 const double& price[])    // array, on which the indicator will be calculated;
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }

Intermediate Calculations

It is very easy to organize calculation of values for buffers MTMBuffer[] and AbsMTMBuffer[]. In the loop, one by one go through values from price[1] to price[rates_total-1] and write difference into one array, and the absolute value of difference into the second one.

//--- calculate values of mtm and |mtm|
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

The next stage is the calculation of the exponential average of these arrays. There are two ways to do it. In the first one we write the whole algorithm trying to make no mistakes. In the second case we use ready functions that are already debugged and intended exactly for these purposes.

In MQL5 there are no built-in functions for calculating moving averages by array values, but there is a ready library of functions MovingAverages.mqh, the complete path to which is terminal_directory/MQL5/Include/MovingAverages.mqh, where the terminal_directory is a catalog where the MetaTrader 5 terminal is installed. The library is an include file; it contains functions for calculating moving averages on arrays using one of the four classical methods:

In order to use these functions, in any MQL5 program add the following in the code heading:

#include <MovingAverages.mqh>

We need the function ExponentialMAOnBuffer(), which calculates exponential moving average on the array of values and records values of the average into another array.

The Function of Smoothing of an Array

Totally, the include file MovingAverages.mqh contains eight functions that can be divided into two groups of functions of the same type, each containing 4 of them. The first group contains functions that receive an array and simply return a value of a moving average at a specified position:

These functions are intended for obtaining the value of an average once for an array, and are not optimized for multiple calls. If you need to use a function from this group in a loop (to calculate values of an average and further write each calculated value into an array), you'll have to organize an optimal algorithm.

The second group of functions is intended for filling out the recipient array by values of a moving average based on the array of initial values:

All the specified functions, except for arrays buffer[], price[] and the period averaging period, get 3 more parameters, the purpose of which is analogous to parameters of the OnCalculate() function - rates_total, prev_calculated and begin. Functions of this group correctly process passed arrays of price[] and buffer[], taking into account the direction of indexing (AS_SERIES flag).

The begin parameter indicates the index of a source array, from which meaningful data start, i.e. data that need to be handled. For the MTMBuffer[] array real data start with the index 1, because MTMBuffer[1]=price[1]-price[0]. The value of MTMBuffer[0] is undefined, that's why begin=1.

//--- calculate the first moving
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // index, starting from which data for smoothing are available 
                         r,  // period of the exponential average
                         MTMBuffer,       // buffer to calculate average
                         EMA_MTMBuffer);  // into this buffer locate value of the average
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

When averaging, the period value should be taken into account, because in the output array the calculated values are filled out with a small delay, which is larger at larger averaging periods. For example, if period=10, values in the resulting array will start with begin+period-1=begin+10-1. At further calls of buffer[] it should be taken into account, and handling should be started with the index begin+period-1.

Thus we can easily obtain the second exponential average from the arrays of MTMBuffer[] and AbsMTMBuffer:

//--- calculate the second moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);

The value of begin is now equal to r, because begin=1+r-1 (r is the period of the primary exponential averaging, handling starts with the index 1). In output arrays of EMA2_MTMBuffer[] and EMA2_AbsMTMBuffer[], calculated values start with the index r+s-1, because we started to handle input arrays with the index r, and the period for the second exponential averaging is equal to s.

All the pre-calculations are ready, now we can calculate values of the indicator buffer TSIBuffer[], which will be plotted in the chart.

//--- now calculate values of the indicator
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
Compile the code by pressing the F5 key and start it in the MetaTrader 5 terminal. It works!

The first version of True Strength Index

Still there are some questions left.

Optimizing Calculations

Actually, it's not enough just to write a working indicator. If we carefully look at the current implementation of OnCalculate(), we'll see that it's not optimal.

int OnCalculate (const int rates_total,    // size of the price[] array;
                 const int prev_calculated,// number of available bars;
                 // at the previous call;
                 const int begin,// from what index of the 
                 // price[] array true data start;
                 const double &price[]) // array, at which the indicator will be calculated;
  {
//--- calculate values of mtm and |mtm|
   MTMBuffer[0]=0.0;
   AbsMTMBuffer[0]=0.0;
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }
//--- calculate the first moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // index, starting from which data for smoothing are available 
                         r,  // period of the exponential average
                         MTMBuffer,       // buffer to calculate average
                         EMA_MTMBuffer);  // into this buffer locate value of the average
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

//--- calculate the second moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);
//--- now calculate values of the indicator
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }

At each function start we calculate values in arrays of MTMBuffer[] and AbsMTMBuffer[]. In this case, if the size of price[] equals to hundreds of thousands or even millions, unnecessary repeated calculations can take all CPU resources, no matter how powerful it is.

For organizing optimal calculations, we use the prev_calculated input parameter, which is equal to the value returned by OnCalculate() at the previous call. In the first call of the function, the value of prev_calculated is always equal to 0. In this case we calculate all values in the indicator buffer. During the next call, we won't have to calculate the whole buffer - only the last value will be calculated. Let's write it down like this:

//--- if it is the first call 
   if(prev_calculated==0)
     {
      //--- set zero values to zero indexes
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }
//--- calculate values of mtm and |mtm|
   int start;
   if(prev_calculated==0) start=1;  // start filling out MTMBuffer[] and AbsMTMBuffer[] from the 1st index 
   else start=prev_calculated-1;    // set start equal to the last index in the arrays 
   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

Calculation blocks of EMA_MTMBuffer[], EMA_AbsMTMBuffer[], EMA2_MTMBuffer[] and EMA2_AbsMTMBuffer[] do not require optimization of calculations, because ExponentialMAOnBuffer() is already written in the optimal way. We need to optimize only calculation of values for the TSIBuffer[] array. We use the same method as the one, used for MTMBuffer[].

//--- now calculate the indicator values
   if(prev_calculated==0) start=r+s-1; // set the starting index for input arrays
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//--- return value of prev_calculated for next call
   return(rates_total);

The last remark for the optimization procedure: OnCalculate() returns the value of rates_total. This means the number of elements in the price[] input array, which is used for indicator calculations.

Value returned by OnCalculate() is saved in the terminal memory, and at the next call of OnCalculate() it is passed to the function as the value of the input parameter prev_calculated.

This allows to always know the size of the input array at the previous call of OnCalculate() and start the calculation of indicator buffers from a correct index without unnecessary recalculations.

Checking Input Data

There is one more thing we need to do for OnCalculate() to operate perfectly. Let's add checking of the price[] array, on which indicator values are calculated. If the size of the array (rates_total) is too small, no calculations are required - we need to wait till the next call of OnCalculate(), when data are enough.

//--- if the size of price[] is too small
  if(rates_total<r+s) return(0); // do not calculate or draw anything
//--- if it's the first call 
   if(prev_calculated==0)
     {
      //--- set zero values for zero indexes
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }

Since the exponential smoothing is used twice sequentially to calculate True Strength Index, the size of price[] must be at least equal to or larger than the sum of r and s periods; otherwise the execution is terminated, and OnCalculate() returns 0. The returned zero value means that the indicator will not be plotted in the chart, because its values aren't calculated.

Setting up Representation

As for the correctness of calculations, the indicator is ready to use. But if we call it from another mql5-program, it will be built by Close prices on default. We can specify another default price type - specify a value from the ENUM_APPLIED_PRICE enumeration in the indicator_applied_price property of the indicator. 

For example, in order to set a typical price ( (high+low+close)/3) for a price, let's write the following:

#property indicator_applied_price PRICE_TYPICAL


If we are planning to use only its values using iCustom() or IndicatorCreate() functions, no further refinements are required. But if used directly, i.e. plotted in the chart, additional settings are recommended:

These settings can be tuned in the OnInit() handler, using functions from the Custom Indicators group. Add new lines and save the indicator as True_Strength_Index_ver2.mq5.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
//--- bar, starting from which the indicator is drawn
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,r+s-1);
   string shortname;
   StringConcatenate(shortname,"TSI(",r,",",s,")");
//--- set a label do display in DataWindow
   PlotIndexSetString(0,PLOT_LABEL,shortname);   
//--- set a name to show in a separate sub-window or a pop-up help
   IndicatorSetString(INDICATOR_SHORTNAME,shortname);
//--- set accuracy of displaying the indicator values
   IndicatorSetInteger(INDICATOR_DIGITS,2);
//---
   return(0);
  }

If we start both versions of the indicator and scroll the chart to the beginning, we'll see all the differences.


The second version of True Strength Index indicator looks better

Conclusion

Based on the example of creating the True Strength Index indicator, we can outline the basic moments in the process of writing of any indicator in MQL5: