All posts · MQL5 · Indicators

Coding Custom Indicators in MQL5: A Practical Guide

Build a custom MQL5 indicator from scratch. The #property setup, indicator buffers, OnCalculate, prev_calculated and the mistakes that slow indicators down.

· 5 min read · Updated

If you trade algorithmically on MetaTrader 5, you've probably reached the point where the built-in indicators don't cut it anymore. Maybe you want to tweak a calculation, combine several signals into one line, or implement an idea from a paper you came across. Whatever the reason, coding custom indicators in MQL5 is a skill that pays off.

This guide builds a Simple Moving Average indicator from scratch. Yes, MT5 already has one. That's the point: by recreating something familiar, we can focus on how indicators work without getting lost in the math.

The anatomy of an MQL5 indicator

An indicator is a different kind of program from an Expert Advisor. EAs trade. Indicators calculate values and draw them on the chart; they inform, they don't trade.

Every MQL5 indicator has the same basic structure:

MQL5
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1

double Buffer[];

int OnInit()
{
   // Setup: runs once when the indicator loads
   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[])
{
   // Calculation: runs on every new tick
   return(rates_total);
}

That's the skeleton. Two functions: OnInit() for setup and OnCalculate() for the work. Everything else is configuration and logic.

Indicator properties

The #property lines at the top aren't decoration. They tell MetaTrader how to handle your indicator.

MQL5
#property indicator_chart_window

This draws the indicator on the price chart itself. For an oscillator like RSI you'd use indicator_separate_window, which opens its own pane below the chart.

MQL5
#property indicator_buffers 1
#property indicator_plots   1

These two are related but different. Buffers are arrays that hold calculated values. Plots are what gets drawn. You can have more buffers than plots when some buffers hold intermediate results you don't want on the chart.

Building the Simple Moving Average

Here's the complete, working SMA indicator first. We'll go through the important parts after.

MQL5
#property copyright "Your Name"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1

#property indicator_label1  "SMA"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  2

input int InpPeriod = 14; // SMA Period

double SmaBuffer[];

int OnInit()
{
   if(InpPeriod < 1)
   {
      Print("Error: Period must be at least 1");
      return(INIT_PARAMETERS_INCORRECT);
   }

   SetIndexBuffer(0, SmaBuffer, INDICATOR_DATA);
   PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, InpPeriod - 1);
   PlotIndexSetString(0, PLOT_LABEL, "SMA(" + IntegerToString(InpPeriod) + ")");

   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
   IndicatorSetString(INDICATOR_SHORTNAME, "Custom SMA(" + IntegerToString(InpPeriod) + ")");

   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[])
{
   if(rates_total < InpPeriod)
      return(0);

   int start;
   if(prev_calculated == 0)
      start = InpPeriod - 1;
   else
      start = prev_calculated - 1;

   for(int i = start; i < rates_total; i++)
   {
      double sum = 0.0;
      for(int j = 0; j < InpPeriod; j++)
         sum += close[i - j];
      SmaBuffer[i] = sum / InpPeriod;
   }

   return(rates_total);
}

The concepts that matter

Input parameters

MQL5
input int InpPeriod = 14; // SMA Period

The input keyword creates a setting users can change in the indicator's dialog. The comment after it becomes the label they see. Give every input a sensible default, and validate it: the OnInit() above refuses to load with a period below 1 instead of dividing by zero later.

Registering the buffer

MQL5
SetIndexBuffer(0, SmaBuffer, INDICATOR_DATA);

This connects your array to MetaTrader's indicator system. The arguments are the buffer index (starting at 0), your array, and the buffer type.

INDICATOR_DATA means the buffer holds values to draw. There's also INDICATOR_CALCULATIONS for intermediate values you don't want displayed, and INDICATOR_COLOR_INDEX for lines that change color.

Where drawing starts

MQL5
PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, InpPeriod - 1);

This tells MT5 to start drawing from bar InpPeriod - 1. A 14-period SMA has no valid value for the first 13 bars, so we skip them. Without this you'd see zeros or misleading values at the left edge of the chart.

OnCalculate's parameters

The signature looks intimidating, but each parameter has a job:

  • rates_total: the number of bars available
  • prev_calculated: what your indicator returned last time it ran
  • time[], open[], high[], low[], close[]: price data
  • tick_volume[], volume[]: volume data
  • spread[]: spread per bar

The pair that matters most is rates_total and prev_calculated. They're what make efficient indicators possible.

Only recalculate what changed

This is where most beginners go wrong:

MQL5
int start;
if(prev_calculated == 0)
   start = InpPeriod - 1;
else
   start = prev_calculated - 1;

When the indicator first loads, prev_calculated is zero and you calculate every bar. After that, prev_calculated tells you how many bars you've already processed, so you only update from there.

Why prev_calculated - 1 and not prev_calculated? Because the last bar is still forming and changes with every tick, so it has to be recalculated each time. Finished bars don't change, so calculating them once is enough.

It makes a real difference. Without it, a 14-period SMA on a chart with 10,000 bars does 140,000 additions on every tick. With it, a tick costs a handful of additions after the first load.

Array indexing

This one trips up programmers coming from MQL4 or other platforms. The price arrays in OnCalculate() are indexed with 0 as the oldest bar by default:

text
Index:  0     1     2     3    ...   rates_total-1
Bars:   oldest                 ...   current

So close[rates_total - 1] is the current bar's close, and close[0] is the oldest bar on the chart.

In our loop:

MQL5
for(int i = start; i < rates_total; i++)
{
   double sum = 0.0;
   for(int j = 0; j < InpPeriod; j++)
      sum += close[i - j];
   SmaBuffer[i] = sum / InpPeriod;
}

i moves forward through time, and for each bar we look back j bars to build the average. Most indicators follow this shape.

The return value

MQL5
return(rates_total);

Whatever you return becomes prev_calculated on the next call. Returning rates_total says "all bars are done". If something goes wrong and you can't calculate, return 0 to force a full recalculation next time.

Common pitfalls

Not checking for enough data

MQL5
if(rates_total < InpPeriod)
   return(0);

Without this, the loop reads before the start of the array when there aren't enough bars, and the indicator stops with an "array out of range" error.

Not validating inputs

What if someone sets the period to zero or a negative number? That's why the indicator's OnInit() returns INIT_PARAMETERS_INCORRECT for a period below 1.

Expensive loops

Our SMA re-adds the whole window for every bar. That's fine for a demonstration. For large periods, keep a running sum instead:

MQL5
// Sum for the first bar to (re)calculate
double sum = 0.0;
for(int j = 0; j < InpPeriod; j++)
   sum += close[start - j];
SmaBuffer[start] = sum / InpPeriod;

// Every later bar: add the new close, drop the oldest one
for(int i = start + 1; i < rates_total; i++)
{
   sum += close[i] - close[i - InpPeriod];
   SmaBuffer[i] = sum / InpPeriod;
}

That takes the work from O(n × period) down to O(n).

Making it your own

Once the structure clicks, changes are straightforward. Want an EMA instead? Seed it with an SMA on the first run, then apply the EMA formula from there on:

MQL5
double k = 2.0 / (InpPeriod + 1);

if(prev_calculated == 0)
{
   int first = InpPeriod - 1;
   double sum = 0.0;
   for(int j = 0; j < InpPeriod; j++)
      sum += close[first - j];
   SmaBuffer[first] = sum / InpPeriod; // the first value is an SMA
   start = first + 1;
}

for(int i = start; i < rates_total; i++)
   SmaBuffer[i] = (close[i] - SmaBuffer[i - 1]) * k + SmaBuffer[i - 1];

Seeding only on the first run matters: re-seeding on every tick would reset the EMA at the last bar each time.

Want it on the high-low midpoint instead of the close? Replace close[i] with (high[i] + low[i]) / 2.0.

Want several moving averages in one indicator? Add more buffers, more plots and more inputs.

Debugging tips

Print() writes to the Experts tab of the terminal:

MQL5
Print("rates_total: ", rates_total, " prev_calculated: ", prev_calculated);

Use it freely while developing, then remove or comment out the prints: printing on every tick slows the terminal down and floods the log.

For a quick visual check, set the buffer to a fixed number to see whether it's drawn at all:

MQL5
SmaBuffer[i] = 1.0; // should draw a flat line at 1.0

If the line shows up, the buffer is wired correctly. If not, check your SetIndexBuffer() call and the #property lines.

Final thoughts

Custom indicators in MQL5 follow a pattern. Once you know the pieces (properties, buffers, initialization and the calculation loop) you can build almost anything. The math gets harder for advanced indicators, but the framework stays the same.

Start simple, get a basic indicator working, then iterate: add features, speed it up, handle the edge cases. The SMA here is trivial math, but the plumbing around it (buffer management, incremental updates, proper initialization) is the same for indicators of any complexity.

To read your indicator from an Expert Advisor, load it with iCustom() and read its values with CopyBuffer(), the same handle-and-buffer model explained in MQL4 vs MQL5: the differences that matter.

Build a bot from your idea

Describe your trading idea and the AI writes MT4 or MT5 code for you to review and test, with a plain-English explanation and setup steps. Your first bot is free.

More from the blog