Custom indicators are one of the most practical ways to turn a trading idea into a repeatable, visual tool. While MetaTrader 5 ships with dozens of built-in indicators, sooner or later you will want something more specific: a specialized filter, a custom oscillator, a volatility regime detector, a bespoke “signal line,” or simply a cleaner visualization of existing information.
This article explains—thoroughly and practically—how to write custom technical indicators in MQL5, with multiple working examples, performance patterns, and tables you can use as a reference.
1. What Makes an MQL5 Indicator Different From an Expert Advisor?
Both indicators and Expert Advisors are MQL5 programs, but they serve different roles:
| Component | Purpose | Runs on | Core event function |
|---|---|---|---|
| Indicator | Calculate and visualize data | Chart/subwindow | OnCalculate() |
| Expert Advisor | Trade automation and management | Chart | OnTick() |
| Script | One-time task | Chart | OnStart() |
An indicator’s primary job is to compute values per bar (or per tick) and provide those values to the platform for plotting and use by other tools (EAs, other indicators, visual analysis).
2. The Anatomy of a Custom Indicator in MQL5
A typical indicator consists of:
- Properties (
#property) defining how it will be plotted - Inputs (
input) to control parameters - Indicator buffers (arrays) used to store output values
- Initialization in
OnInit()(buffer mapping, styling) - Calculation in
OnCalculate()(the actual algorithm)
Key Indicator Functions
| Function | When called | What you do there |
|---|---|---|
OnInit() | Once when indicator loads | Bind buffers, configure plots, validate inputs |
OnDeinit() | When indicator unloads | Release handles, cleanup |
OnCalculate() | Whenever MT5 needs indicator values | Calculate values efficiently for new bars |
3. Understanding Indicator Buffers (The Most Important Concept)
Indicator buffers are arrays where you store calculated values (one value per bar). MetaTrader reads these arrays and draws them.
Buffer Types You Will Commonly Use
| Buffer purpose | Typical use | Notes |
|---|---|---|
| Data buffer | Line values, histogram values | Plotted on chart |
| Calculation buffer | Intermediate computations | Not plotted |
| Color index buffer | Colored plots | Works with DRAW_COLOR_* plot types |
Series Indexing: “Bar 0 is the latest”
In MQL5, price arrays (close[], high[], etc.) are usually provided as series arrays, where:
close[0]is the latest bar (most recent)close[1]is the previous bar- …and so on
Your indicator buffers should typically be set as series too:
ArraySetAsSeries(BufferMain, true);
However, note that in OnCalculate() the framework already provides price arrays as series. For indicator buffers you create, it’s good practice to set them as series for consistency—especially when you use index [0] for “current bar.”
4. OnCalculate() Explained (Parameters and Efficient Loops)
The signature of OnCalculate() commonly looks like this:
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[]
)

What the Key Parameters Mean
| Parameter | Meaning | Why it matters |
|---|---|---|
rates_total | Total bars available | Upper bound for loops |
prev_calculated | Bars calculated last time | Enables incremental updates |
close[], high[], etc. | Price series arrays | Your data source |
The Performance Rule
Do not recalculate the entire history on every tick. Instead:
- If
prev_calculated == 0, compute everything (first run) - Otherwise compute only from
prev_calculated - 1onward (to handle indicator dependencies)
A standard pattern:
int start = (prev_calculated == 0) ? 0 : prev_calculated - 1;
for(int i = start; i < rates_total; i++)
{
// compute buffers[i]
}
return rates_total;
5. Example #1: “MA Distance Oscillator” (Practical, Simple, Useful)
This indicator measures the distance between price and a moving average and plots it as a histogram in a separate window. It’s useful for mean-reversion work, regime detection, and as a filter for entries.
Indicator Behavior
- Compute SMA of
closewith periodN - Output:
close - SMA(optionally in points)
Full Code (Histogram in Subwindow)
//+------------------------------------------------------------------+
//| MA_Distance_Osc.mq5 |
//+------------------------------------------------------------------+
#property strict
#property indicator_separate_window
#property indicator_plots 1
#property indicator_buffers 1
#property indicator_label1 "MA Distance"
#property indicator_type1 DRAW_HISTOGRAM
#property indicator_style1 STYLE_SOLID
#property indicator_width1 2
input int InpMAPeriod = 50; // SMA period
input bool InpInPoints = false; // Output in points
double BufferOsc[];
//--- helper: simple SMA (for educational clarity)
double SMA(const double &close[], int index, int period)
{
double sum = 0.0;
for(int k = 0; k < period; k++)
sum += close[index - k];
return sum / period;
}
int OnInit()
{
SetIndexBuffer(0, BufferOsc, INDICATOR_DATA);
ArraySetAsSeries(BufferOsc, true);
PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
IndicatorSetString(INDICATOR_SHORTNAME, "MA Distance Osc (" + 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[]
)
{
if(InpMAPeriod < 2)
return 0;
if(rates_total < InpMAPeriod)
return 0;
int start = (prev_calculated == 0) ? (InpMAPeriod - 1) : (prev_calculated - 1);
if(start < InpMAPeriod - 1) start = InpMAPeriod - 1;
for(int i = start; i < rates_total; i++)
{
double ma = SMA(close, i, InpMAPeriod);
double value = close[i] - ma;
if(InpInPoints)
value = value / _Point;
BufferOsc[i] = value;
}
return rates_total;
}
Practical Notes
- This version uses a simple SMA function to make the logic clear.
- For production speed, you would typically compute SMA using running sums or built-in
iMA()handles. That is covered later.
6. Plot Configuration: Types, Styles, and Common Combinations
MQL5 supports multiple plot types. Here is a compact reference:
| Plot type | Property | Typical use |
|---|---|---|
| Line | DRAW_LINE | Trend lines, oscillators |
| Histogram | DRAW_HISTOGRAM | Momentum / distance / volume-like metrics |
| Section line | DRAW_SECTION | Segmented lines with gaps |
| Arrows | DRAW_ARROW | Signals on chart |
| Colored line | DRAW_COLOR_LINE | Trend state via color |
| Colored histogram | DRAW_COLOR_HISTOGRAM | Positive/negative regime coloring |
7. Example #2: Colored Trend Line (Two Buffers: Value + Color Index)
A very common requirement is to draw one line but color it based on trend conditions (e.g., green in uptrend, red in downtrend). In MQL5, colored plots require:
- Data buffer for the line values
- Color index buffer indicating which color to use at each bar
Strategy
- Compute EMA (or SMA)
- If
close > EMA, color index = 0 (e.g., up) - Else color index = 1 (down)
Full Code (Colored Line in Main Chart Window)
//+------------------------------------------------------------------+
//| Colored_Trend_EMA.mq5 |
//+------------------------------------------------------------------+
#property strict
#property indicator_chart_window
#property indicator_plots 1
#property indicator_buffers 2
#property indicator_label1 "Trend EMA"
#property indicator_type1 DRAW_COLOR_LINE
#property indicator_width1 2
// Define plot colors via palette indices
#property indicator_color1 clrLime, clrTomato
input int InpEMAPeriod = 50;
double BufferEma[];
double BufferColorIdx[];
//--- EMA calculation (simple iterative)
double CalcEMA(double prevEma, double price, double alpha)
{
return alpha * price + (1.0 - alpha) * prevEma;
}
int OnInit()
{
SetIndexBuffer(0, BufferEma, INDICATOR_DATA);
SetIndexBuffer(1, BufferColorIdx, INDICATOR_COLOR_INDEX);
ArraySetAsSeries(BufferEma, true);
ArraySetAsSeries(BufferColorIdx, true);
IndicatorSetString(INDICATOR_SHORTNAME, "Colored Trend EMA (" + IntegerToString(InpEMAPeriod) + ")");
PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
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(InpEMAPeriod < 2) return 0;
if(rates_total < InpEMAPeriod) return 0;
double alpha = 2.0 / (InpEMAPeriod + 1.0);
int start;
if(prev_calculated == 0)
{
// Seed EMA at the first calculable point
int seed = rates_total - 1; // oldest bar index in series arrays
// But with series arrays, oldest is rates_total-1
BufferEma[seed] = close[seed];
BufferColorIdx[seed] = 0.0;
start = seed - 1;
// We will run backwards (from older to newer) because EMA depends on previous EMA
// In series arrays: decreasing index means moving forward in time (toward latest)
for(int i = seed - 1; i >= 0; i--)
{
BufferEma[i] = CalcEMA(BufferEma[i + 1], close[i], alpha);
BufferColorIdx[i] = (close[i] >= BufferEma[i]) ? 0.0 : 1.0;
}
return rates_total;
}
// Incremental update: recalc last few bars
// Recompute from bar 1 down to 0 using EMA dependency
int i = 1;
if(i < rates_total)
{
BufferEma[i] = CalcEMA(BufferEma[i + 1], close[i], alpha);
BufferColorIdx[i] = (close[i] >= BufferEma[i]) ? 0.0 : 1.0;
}
BufferEma[0] = CalcEMA(BufferEma[1], close[0], alpha);
BufferColorIdx[0] = (close[0] >= BufferEma[0]) ? 0.0 : 1.0;
return rates_total;
}
Why the Loop Looks “Backward”
EMA depends on the previous EMA value. With series arrays:
- Index increases = older bars
- Index decreases = newer bars
So if you seed at rates_total - 1 (oldest bar) you can compute toward the present by decrementing the index.
8. Example #3: ATR Bands Indicator (Two Lines + Middle Line + Optional Fill)
Volatility bands are extremely useful for stops, breakouts, regime filtering, and position sizing. This example builds bands around a moving average using ATR.
Band Formula
- Middle line:
MA(close, periodMA) - ATR: average true range over
periodATR - Upper:
Middle + Multiplier * ATR - Lower:
Middle - Multiplier * ATR
True Range Reference Table
| Component | Formula |
|---|---|
| TR | max(high-low, abs(high-prevClose), abs(low-prevClose)) |
| ATR (simple) | SMA of TR over N |
Full Code (3 Lines on Chart)
//+------------------------------------------------------------------+
//| ATR_Bands.mq5 |
//+------------------------------------------------------------------+
#property strict
#property indicator_chart_window
#property indicator_plots 3
#property indicator_buffers 3
#property indicator_label1 "Upper"
#property indicator_type1 DRAW_LINE
#property indicator_width1 1
#property indicator_label2 "Middle"
#property indicator_type2 DRAW_LINE
#property indicator_width2 2
#property indicator_label3 "Lower"
#property indicator_type3 DRAW_LINE
#property indicator_width3 1
input int InpMAPeriod = 50;
input int InpATRPeriod = 14;
input double InpATRMult = 2.0;
double BufUpper[];
double BufMiddle[];
double BufLower[];
//--- simple SMA for MA
double SMA_Close(const double &close[], int index, int period)
{
double sum = 0.0;
for(int k = 0; k < period; k++)
sum += close[index - k];
return sum / period;
}
//--- true range at bar index
double TrueRange(const double &high[], const double &low[], const double &close[], int index)
{
if(index + 1 >= ArraySize(close)) // for safety; series indexing
return high[index] - low[index];
double prevClose = close[index + 1];
double a = high[index] - low[index];
double b = MathAbs(high[index] - prevClose);
double c = MathAbs(low[index] - prevClose);
return MathMax(a, MathMax(b, c));
}
//--- simple ATR as SMA of TR
double ATR_Simple(const double &high[], const double &low[], const double &close[], int index, int period)
{
double sum = 0.0;
for(int k = 0; k < period; k++)
sum += TrueRange(high, low, close, index - k);
return sum / period;
}
int OnInit()
{
SetIndexBuffer(0, BufUpper, INDICATOR_DATA);
SetIndexBuffer(1, BufMiddle, INDICATOR_DATA);
SetIndexBuffer(2, BufLower, INDICATOR_DATA);
ArraySetAsSeries(BufUpper, true);
ArraySetAsSeries(BufMiddle, true);
ArraySetAsSeries(BufLower, true);
IndicatorSetString(INDICATOR_SHORTNAME,
"ATR Bands (MA=" + IntegerToString(InpMAPeriod) +
", ATR=" + IntegerToString(InpATRPeriod) +
", x" + DoubleToString(InpATRMult, 2) + ")");
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(InpMAPeriod < 2 || InpATRPeriod < 2) return 0;
int minBars = MathMax(InpMAPeriod, InpATRPeriod) + 2;
if(rates_total < minBars) return 0;
int start = (prev_calculated == 0) ? (minBars - 1) : (prev_calculated - 1);
if(start < minBars - 1) start = minBars - 1;
for(int i = start; i < rates_total; i++)
{
double mid = SMA_Close(close, i, InpMAPeriod);
double atr = ATR_Simple(high, low, close, i, InpATRPeriod);
BufMiddle[i] = mid;
BufUpper[i] = mid + InpATRMult * atr;
BufLower[i] = mid - InpATRMult * atr;
}
return rates_total;
}
Practical Notes
- This computes ATR internally for clarity. For performance, you may switch to a running ATR or use the built-in
iATR()handle andCopyBuffer(). - Band indicators are excellent examples of multi-buffer outputs (upper/middle/lower).
9. Using Built-In Indicators Inside Your Custom Indicator (Handles + CopyBuffer)
Sometimes you want your indicator to reuse a built-in indicator rather than re-implement it. MQL5 supports this with indicator handles, the same way EAs do.
Pattern
- Create handle in
OnInit()(e.g.,iRSI,iMA,iATR) - In
OnCalculate()callCopyBuffer(handle, ...) - Release handle in
OnDeinit()

Example: Smoothed RSI (RSI + EMA Smoothing)
We’ll compute RSI using iRSI() and then smooth it with a simple EMA in our indicator.
//+------------------------------------------------------------------+
//| Smoothed_RSI.mq5 |
//+------------------------------------------------------------------+
#property strict
#property indicator_separate_window
#property indicator_plots 2
#property indicator_buffers 2
#property indicator_label1 "RSI"
#property indicator_type1 DRAW_LINE
#property indicator_width1 1
#property indicator_label2 "Smoothed RSI"
#property indicator_type2 DRAW_LINE
#property indicator_width2 2
input int InpRSIPeriod = 14;
input int InpSmoothPeriod = 10;
int hRSI = INVALID_HANDLE;
double BufRSI[];
double BufSmooth[];
double CalcEMA(double prev, double value, double alpha)
{
return alpha * value + (1.0 - alpha) * prev;
}
int OnInit()
{
SetIndexBuffer(0, BufRSI, INDICATOR_DATA);
SetIndexBuffer(1, BufSmooth, INDICATOR_DATA);
ArraySetAsSeries(BufRSI, true);
ArraySetAsSeries(BufSmooth, true);
hRSI = iRSI(_Symbol, PERIOD_CURRENT, InpRSIPeriod, PRICE_CLOSE);
if(hRSI == INVALID_HANDLE)
{
Print("Failed to create RSI handle");
return INIT_FAILED;
}
IndicatorSetString(INDICATOR_SHORTNAME,
"Smoothed RSI (RSI=" + IntegerToString(InpRSIPeriod) +
", Smooth=" + IntegerToString(InpSmoothPeriod) + ")");
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(hRSI != INVALID_HANDLE)
IndicatorRelease(hRSI);
}
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(InpRSIPeriod < 2 || InpSmoothPeriod < 2) return 0;
if(rates_total < InpRSIPeriod + InpSmoothPeriod + 2) return 0;
// Pull RSI values for the required range
// We'll request enough bars to cover calculation; simplest: request rates_total
if(CopyBuffer(hRSI, 0, 0, rates_total, BufRSI) <= 0)
{
Print("CopyBuffer failed for RSI");
return prev_calculated;
}
double alpha = 2.0 / (InpSmoothPeriod + 1.0);
// Seed smoothing on oldest bar, then move toward latest
// Oldest in series array is index rates_total-1
int seed = rates_total - 1;
BufSmooth[seed] = BufRSI[seed];
for(int i = seed - 1; i >= 0; i--)
BufSmooth[i] = CalcEMA(BufSmooth[i + 1], BufRSI[i], alpha);
return rates_total;
}
This is a real-world pattern: use robust built-in indicator math, then add your custom logic/visualization.
10. Practical Tables: Buffer Mapping and Plot Types
Buffer Mapping (What to Use Where)
SetIndexBuffer() buffer type | When to use |
|---|---|
INDICATOR_DATA | Values that should be plotted |
INDICATOR_CALCULATIONS | Internal arrays used only for intermediate computations |
INDICATOR_COLOR_INDEX | Per-bar color selection for DRAW_COLOR_* plots |
Common Plot + Buffer Combinations
| Plot type | Buffers required | Buffer roles |
|---|---|---|
DRAW_LINE | 1 | data |
DRAW_HISTOGRAM | 1 | data |
DRAW_COLOR_LINE | 2 | data + color index |
DRAW_COLOR_HISTOGRAM | 2 | data + color index |
Arrows (DRAW_ARROW) | 1 (often) | arrow “price level” values; use EMPTY_VALUE to hide |
11. Avoiding Common Pitfalls (With Fixes)
| Problem | Symptom | Typical cause | Fix |
|---|---|---|---|
| Indicator draws nothing | Empty subwindow/chart | Buffers not bound or filled | Ensure SetIndexBuffer() and write buffer values |
| Random spikes | Strange values on early bars | Not enough bars / uninitialized values | Check rates_total, set EMPTY_VALUE, start from safe index |
| Very slow performance | Terminal lag | Recalculating entire history every tick | Use prev_calculated properly |
| Wrong direction / inverted logic | Trend states reversed | Misunderstanding series indexing | Confirm which index is “current”; keep arrays series-consistent |
| Color plot not working | Line stays one color | Missing color index buffer | Use INDICATOR_COLOR_INDEX and DRAW_COLOR_* plot type |
12. Best Practices for Professional-Grade Indicators
12.1. Validate Inputs Early
If periods are invalid, return gracefully:
if(InpPeriod < 2) return 0;
12.2. Use EMPTY_VALUE to Create Gaps
To hide data on bars where you cannot compute:
Buffer[i] = EMPTY_VALUE;
12.3. Incremental Computation
Always use prev_calculated:
- Recalculate from
prev_calculated - 1 - Recompute just enough to maintain dependencies
12.4. Keep Heavy Work Out of Tight Loops
If you use handles (iMA, iATR, etc.), prefer CopyBuffer() once per call rather than calling indicator functions inside a loop.
13. Extending Your Indicators: Alerts and Signal Outputs
Indicators often need “signal logic” (crosses, thresholds, divergence markers). A practical approach:
- Detect signals in
OnCalculate() - Only trigger alerts on bar close (to avoid spamming on every tick)
- Maintain internal state to avoid duplicate alerts
A simple technique is to detect when time[0] changes (new bar) and then evaluate signal on bar 1.
14. Summary
Writing custom indicators in MQL5 is a highly leverageable skill: it lets you turn a strategy concept into a tool you can see, test, refine, and ultimately automate.
In this article you learned:
- How indicator structure works (
OnInit,OnCalculate, buffers) - How to plot histograms, lines, and colored plots
- How to compute indicators directly (SMA, EMA, ATR)
- How to reuse built-in indicators via handles and
CopyBuffer() - How to write faster, cleaner indicator code using
prev_calculated
From here, the most productive next step is to pick one idea you currently use manually (a filter, condition, or visual aid) and implement it as a custom indicator—starting from the patterns shown above.
If you want, I can write a follow-up deep dive article with advanced patterns such as: multi-timeframe indicators, drawing arrows and zones, caching, precision handling for different symbol digits, and exporting indicator signals for EAs.
