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:
#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.
#property indicator_chart_windowThis 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.
#property indicator_buffers 1
#property indicator_plots 1These 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.
#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
input int InpPeriod = 14; // SMA PeriodThe 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
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
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 availableprev_calculated: what your indicator returned last time it rantime[],open[],high[],low[],close[]: price datatick_volume[],volume[]: volume dataspread[]: 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:
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:
Index: 0 1 2 3 ... rates_total-1
Bars: oldest ... currentSo close[rates_total - 1] is the current bar's close, and close[0] is the oldest bar on the chart.
In our loop:
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
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
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:
// 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:
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:
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:
SmaBuffer[i] = 1.0; // should draw a flat line at 1.0If 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.