How to Write a Custom MQL5 Indicator with Consistent Buffer Indexing

Technical revision: 1 October 2026 · Educational material

A useful first custom indicator is the distance between a closing price and its simple moving average. It exposes the central implementation problem: every input array, output buffer and loop must agree on the direction of time.

Define the output and its units

For each bar, calculate the average of that bar’s close and the preceding N-1 closes. Output close - average. A positive value means the close is above its average; it is not, by itself, a buy instruction. The source can display the difference in raw price units or divide it by _Point to show points.

Points are not universally interchangeable with pips. A value comparable on one symbol can mean a different economic exposure on another. This indicator does not normalize volatility or currency risk.

Choose one array convention

The example uses chronological arrays: index 0 is the oldest available bar, and increasing indices move toward the present. ArraySetAsSeries(close,false) explicitly sets the input convention. After binding the output with SetIndexBuffer, its convention is set explicitly too.

With this convention, close[i-k] moves backward from bar i. Mixing a series-oriented input with a chronological loop would calculate against the wrong bars and can read outside the array.

Complete indicator source

#property strict
#property version "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots 1
#property indicator_label1 "SMA distance"
#property indicator_type1 DRAW_HISTOGRAM
#property indicator_color1 clrDodgerBlue
#property indicator_width1 2
input int InpMAPeriod=20;
input bool InpInPoints=true;
double DistanceBuffer[];
int OnInit()
{
   if(InpMAPeriod<2) return INIT_PARAMETERS_INCORRECT;
   if(!SetIndexBuffer(0,DistanceBuffer,INDICATOR_DATA)) return INIT_FAILED;
   ArraySetAsSeries(DistanceBuffer,false); // Oldest bar at index 0.
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,InpMAPeriod-1);
   PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,EMPTY_VALUE);
   IndicatorSetString(INDICATOR_SHORTNAME,
      "SMA distance ("+IntegerToString(InpMAPeriod)+")");
   return INIT_SUCCEEDED;
}
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[])
{
   ArraySetAsSeries(close,false); // Match the output buffer explicitly.
   if(rates_total<InpMAPeriod || (InpInPoints && _Point<=0.0))
   {
      ArrayInitialize(DistanceBuffer,EMPTY_VALUE);
      return 0;
   }
   int start;
   if(prev_calculated<=0 || prev_calculated>rates_total)
   {
      ArrayInitialize(DistanceBuffer,EMPTY_VALUE);
      start=InpMAPeriod-1;
   }
   else start=MathMax(InpMAPeriod-1,prev_calculated-1);
   for(int i=start; i<rates_total; i++)
   {
      double sum=0.0;
      for(int k=0; k<InpMAPeriod; k++) sum+=close[i-k];
      double distance=close[i]-sum/InpMAPeriod;
      DistanceBuffer[i]=(InpInPoints ? distance/_Point : distance);
   }
   return rates_total;
}

Warm-up, updates and history changes

The first N-1 outputs are EMPTY_VALUE, because they do not have enough earlier observations. The first valid index is N-1. On subsequent calls the calculation resumes at the previous count minus one, so the last previously calculated candle is refreshed.

A reset of prev_calculated causes a full recalculation. This matters when the terminal downloads deeper history or fills a history gap. The implementation intentionally uses a straightforward summation loop. It is easy to audit; very large periods may justify a separately tested rolling-sum optimization.

Verify the numbers before interpreting the shape

  1. Set period to 3 and manually check a few successive windows of three closes.
  2. For closes 10, 12 and 14, the first valid average is 12 and the distance is 2.
  3. Verify that earlier warm-up bars are blank, rather than plausible-looking zeros.
  4. Compare a fresh attachment with a chart that has received incremental updates.
  5. When an EA reads this indicator, use shift 1 for a closed output. The current bar’s value can still change.

Install the file under MQL5/Indicators/FTS-Learning and compile in MetaEditor. The learning route connects this lesson with closed-bar reads and validation.

References: SetIndexBuffer, ArraySetAsSeries, OnCalculate.

By Forex Real Trader

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like