← All articles
·8 min read·TradersMarket.io

Coding Custom Indicators in MQL5: A Practical Guide for Algo Traders

If you’ve been trading algorithmically on MetaTrader 5, you’ve probably reached that point where the built-in indicators just don’t cut it anymore. Maybe y…

Medium

If you’ve been trading algorithmically on MetaTrader 5, you’ve probably reached that point where the built-in indicators just don’t cut it anymore. Maybe you want to tweak a calculation, combine multiple signals into one visual, or implement some obscure academic paper you stumbled upon. Whatever the reason, learning to code custom indicators in MQL5 is a skill that pays dividends.

This post walks you through building a Simple Moving Average indicator from scratch. Yes, MT5 already has one built-in. That’s precisely the point. By recreating something familiar, we can focus entirely on the mechanics of indicator development without getting lost in complex math.

The Anatomy of an MQL5 Indicator

Before we write any code, let’s understand what we’re building. An MQL5 indicator is fundamentally different from an Expert Advisor. EAs execute trades. Indicators calculate and display values on charts. They don’t trade, they inform.

Every indicator in MQL5 has a predictable structure:

#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots 1

double Buffer[];
int OnInit()
{
// Setup code runs once when 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 logic runs on every tick
return(rates_total);
}

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

Understanding Indicator Properties

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

#property indicator_chart_window

This places your indicator directly on the price chart. If you were building an oscillator like RSI, you’d use indicator_separate_window instead to create a separate pane below the chart.

#property indicator_buffers 1
#property indicator_plots 1

These two are related but distinct. Buffers are arrays that store calculated values. Plots are visual representations drawn on the chart. You might have more buffers than plots if some buffers are used for intermediate calculations that you don’t want to display.

Building the Simple Moving Average

Let’s build a complete, working SMA indicator. I’ll show you the full code first, then break down the important parts.

#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()
{
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 Critical Concepts

Input Parameters

input int InpPeriod = 14; // SMA Period

The input keyword creates a parameter that users can modify in the indicator settings dialog. The comment after the declaration becomes the label users see. Always provide sensible defaults, and always validate them in your code.

Buffer Registration

SetIndexBuffer(0, SmaBuffer, INDICATOR_DATA);

This line connects your array to MetaTrader’s indicator system. The first argument is the buffer index (zero-based), the second is your array, and the third specifies the buffer type.

INDICATOR_DATA means this buffer contains values to be drawn. You might also use INDICATOR_CALCULATIONS for intermediate values you don't want displayed, or INDICATOR_COLOR_INDEX for multi-colored indicators.

Plot Configuration

PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, InpPeriod - 1);

This tells MT5 to start drawing from bar InpPeriod - 1. For a 14-period SMA, the first 13 bars don't have enough data for a valid calculation, so we skip them. Without this, you'd see misleading values or zeros at the start of your chart.

The OnCalculate Parameters

This function signature looks intimidating, but each parameter has a purpose:

  • rates_total: Total number of bars available
  • prev_calculated: What your indicator returned last time it ran
  • time[], open[], high[], low[], close[]: Price data arrays
  • tick_volume[], volume[]: Volume data
  • spread[]: Spread values

The most important pair is rates_total and prev_calculated. These enable efficient calculation.

The Optimization Logic

Here’s 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. You need to calculate everything from the beginning. But on subsequent ticks, prev_calculated tells you how many bars you've already processed. You only need to update from that point forward.

Why prev_calculated - 1 instead of just prev_calculated? Because the last bar (the current, incomplete bar) changes with every tick. You need to recalculate it each time. The completed historical bars don't change, so calculating them once is enough.

This optimization matters. Without it, a 14-period SMA on a chart with 10,000 bars would perform 140,000 additions on every single tick. With it, you’re doing just 14 additions per tick after the initial load.

Array Indexing in MQL5

Here’s something that trips up programmers coming from other languages. MQL5 indicator arrays are indexed with zero being the oldest bar by default. This is opposite to how you might think about it.

Index:  0    1    2    3    ...  rates_total-1
Bars: Old ... ... ... ... Current

So close[rates_total - 1] is the current bar's close price, 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;
}

We iterate forward through time (i increases). For each bar, we look back j bars to calculate the average. This is the standard approach for most indicators.

Return Value

return(rates_total);

Whatever you return becomes prev_calculated on the next call. Returning rates_total tells MT5 you've processed all available bars. If something goes wrong and you can't calculate, return 0 to force a full recalculation next time.

Common Pitfalls

Forgetting Edge Cases

Always check if you have enough data:

if(rates_total < InpPeriod)
return(0);

Without this, your indicator crashes or produces garbage when there aren’t enough bars to calculate.

Ignoring Input Validation

What happens if someone sets the period to zero or negative? Add validation:

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

Expensive Calculations in Loops

Our SMA implementation recalculates the sum for each bar. For a simple demonstration, that’s fine. For production code with large periods, consider a running sum approach:

// More efficient for large periods
double sum = 0.0;

// Initial sum for first valid bar
for(int j = 0; j < InpPeriod; j++)
sum += close[start - j];
SmaBuffer[start] = sum / InpPeriod;
// Subsequent bars: add new value, subtract old value
for(int i = start + 1; i < rates_total; i++)
{
sum = sum + close[i] - close[i - InpPeriod];
SmaBuffer[i] = sum / InpPeriod;
}

This reduces complexity from O(n × period) to O(n).

Making It Your Own

Once you understand the structure, modifications become straightforward. Want an EMA instead? Change the calculation:

double multiplier = 2.0 / (InpPeriod + 1);

// First value is SMA
double sum = 0.0;
for(int j = 0; j < InpPeriod; j++)
sum += close[start - j];
SmaBuffer[start] = sum / InpPeriod;
// Subsequent values use EMA formula
for(int i = start + 1; i < rates_total; i++)
{
SmaBuffer[i] = (close[i] - SmaBuffer[i-1]) * multiplier + SmaBuffer[i-1];
}

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

Want multiple MAs on one indicator? Add more buffers, more plots, more input parameters.

Debugging Tips

MQL5’s Print() function sends output to the Experts tab in the terminal:

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

Use it liberally during development. Just remember to remove or comment out debug prints before deploying, as they slow things down.

For visual debugging, you can temporarily set buffer values to specific numbers to see if they’re being calculated at all:

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

If you see the line, your buffer is connected correctly. If not, check your SetIndexBuffer() call.

Final Thoughts

Custom indicators in MQL5 follow a pattern. Once you internalize the structure — properties, buffers, initialization, and the calculate loop — you can implement almost anything. The math might get more complex for advanced indicators, but the framework remains the same.

Start simple. Get a basic indicator working. Then iterate. Add features, optimize performance, handle edge cases. That’s how production-quality indicators get built.

The SMA we built is trivial mathematically, but the infrastructure around it — buffer management, efficient updates, proper initialization — applies to indicators of any complexity. Master these fundamentals, and you’ll be coding custom signals, oscillators, and pattern detectors in no time.

Let’s connect!

Thanks for reading! If you want to develop software solutions in finance space, feel free to contact me at lukasavic18@gmail.com. I’m open to consultations, collaborations, or interesting technical challenges.

Disclaimer:

This article is for educational purposes only and does not constitute financial advice. Trading in financial markets involves significant risk and may result in the loss of your capital. Past performance is not indicative of future results. You are solely responsible for your trading decisions. Always conduct your own research and consult with a qualified financial advisor before making any trading decisions.

Keep reading