Skip to content

Indicator Buffers & Drawing Styles

Master indicator buffers, drawing modes (line, histogram, arrow, candles), and color customization.

💡
Before you start

A C++ compiler and a terminal. No MetaTrader, no broker, no account and no money. Most Linux machines already have g++; on macOS run xcode-select --install once; on Windows use the Windows Subsystem for Linux or MSYS2.

If you have done an earlier MQL5 page you already have the two setup files — reuse them and start at step 3.

Every bug on this page is SILENT. None of them produces an error message, a warning or a failed compile; they produce a slow terminal, values that drift, and a chart that looks squashed. That is exactly why they are worth measuring rather than reading about.

Understanding Buffers and Plots

In MQL5, indicator buffers and plots are separate concepts:

  • Buffers are arrays that store calculated values. Some are drawn on the chart, others are used internally for calculations.
  • Plots are the visual representations. Each plot uses one or more buffers depending on the drawing style.

The number of buffers is always >= the number of plots. For example, a candlestick plot requires 4 buffers (open, high, low, close) but is only 1 plot.

Drawing Styles Reference

MQL5 offers 18 drawing styles. Here are the most commonly used:

DRAW_LINE — A continuous line connecting values. Uses 1 buffer. The most common style for moving averages, channels, and signal lines.

#property indicator_type1 DRAW_LINE
#property indicator_color1 clrDodgerBlue
#property indicator_width1 2
#property indicator_style1 STYLE_SOLID  // SOLID, DASH, DOT, DASHDOT

DRAW_HISTOGRAM — Vertical bars from zero line. Uses 1 buffer. Perfect for oscillator values, volume displays, and momentum bars.

#property indicator_type1 DRAW_HISTOGRAM
#property indicator_color1 clrGreen
#property indicator_width1 3

DRAW_HISTOGRAM2 — Vertical bars between two values. Uses 2 buffers. Great for showing ranges and fill areas.

DRAW_ARROW — Draws symbols at specific points. Uses 1 buffer. Ideal for buy/sell signals, entry/exit markers.

#property indicator_type1 DRAW_ARROW
#property indicator_color1 clrLime
// Set the arrow code in OnInit:
PlotIndexSetInteger(0, PLOT_ARROW, 233);  // up arrow
// Common codes: 233=up arrow, 234=down arrow, 159=dot, 108=star

DRAW_COLOR_LINE — A line that changes color based on conditions. Uses 2 buffers (data + color index). Powerful for showing trend direction with color.

#property indicator_buffers 2
#property indicator_plots   1
#property indicator_type1   DRAW_COLOR_LINE
#property indicator_color1  clrLime,clrRed,clrGray  // up to 64 colors
#property indicator_width1  2

Multi-Color Line Example

A moving average that is green when price is above it and red when below:

#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   1
#property indicator_type1   DRAW_COLOR_LINE
#property indicator_color1  clrLime,clrRed
#property indicator_width1  2

input int MAPeriod = 20;

double maBuffer[];
double colorBuffer[];  // 0=green(above), 1=red(below)

int OnInit()
{
    SetIndexBuffer(0, maBuffer, INDICATOR_DATA);
    SetIndexBuffer(1, colorBuffer, INDICATOR_COLOR_INDEX);
    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[])
{
    int start = (prev_calculated == 0) ? MAPeriod : prev_calculated - 1;

    for(int i = start; i < rates_total; i++)
    {
        // Calculate SMA
        double sum = 0;
        for(int j = 0; j < MAPeriod; j++)
            sum += close[i - j];
        maBuffer[i] = sum / MAPeriod;

        // Set color: 0=green if price above MA, 1=red if below
        colorBuffer[i] = (close[i] >= maBuffer[i]) ? 0 : 1;
    }

    return rates_total;
}

Buy/Sell Signal Arrows

Using DRAW_ARROW to mark entry points:

#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2

#property indicator_type1  DRAW_ARROW
#property indicator_color1 clrLime
#property indicator_width1 2
#property indicator_type2  DRAW_ARROW
#property indicator_color2 clrRed
#property indicator_width2 2

double buySignal[];
double sellSignal[];

int OnInit()
{
    SetIndexBuffer(0, buySignal, INDICATOR_DATA);
    SetIndexBuffer(1, sellSignal, INDICATOR_DATA);

    // Arrow codes: 233=up arrow (buy), 234=down arrow (sell)
    PlotIndexSetInteger(0, PLOT_ARROW, 233);
    PlotIndexSetInteger(1, PLOT_ARROW, 234);

    // Show arrows slightly offset from price
    PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0);
    PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, 0);

    return INIT_SUCCEEDED;
}

In your OnCalculate, set buySignal[i] = low[i] - 10*_Point when a buy condition is met (places arrow below the candle), and sellSignal[i] = high[i] + 10*_Point for sells (above the candle). Set to 0 (empty value) when no signal.

Separate Window Indicators

For oscillators and indicators that do not overlay on the price chart:

#property indicator_separate_window    // draw in separate sub-window
#property indicator_minimum 0          // fix Y-axis minimum
#property indicator_maximum 100        // fix Y-axis maximum
#property indicator_level1 30          // horizontal reference line
#property indicator_level2 70          // horizontal reference line
#property indicator_levelcolor clrSilver

Use Indicator Buffers the Way the Terminal Expects, in Five Steps

A buffer is just an array — but it is an array the terminal owns, keeps between calls, and may draw. Three consequences follow, and each one produces a distinctive bug: an indicator that recomputes the whole chart on every tick and makes the platform crawl, an indicator whose hidden working values reset unpredictably, and an indicator that squashes the price into an unreadable strip the moment you attach it. In the next half hour you will measure all three. Every line of output below came from running these files.

1
Save the header that supplies MetaTrader's built-ins

Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.

Do: save this as mql5.h. If you already have it from an earlier page, reuse that copy and skip to step 3.

// mql5.h - a small shim that lets a useful SUBSET of MQL5 compile and run
// with an ordinary C++ compiler, so you can practise the language without
// MetaTrader. It is NOT MetaTrader and does not trade anything.
#pragma once
#include <cstdio>
#include <cmath>
#include <string>
#include <vector>
#include <iostream>
#include <sstream>

typedef std::string string;
typedef long long datetime;
#define input static

template <typename T> int ArraySize(const std::vector<T> &a) { return (int)a.size(); }
template <typename T> void ArrayResize(std::vector<T> &a, int n) { a.resize(n); }

inline void Print() { std::cout << std::endl; }
template <typename T, typename... Rest>
void Print(const T &first, const Rest &...rest) {
    std::cout << first;
    if constexpr (sizeof...(rest) > 0) Print(rest...);
    else std::cout << std::endl;
}

inline string DoubleToString(double v, int digits = 8) {
    char buf[64]; snprintf(buf, sizeof buf, "%.*f", digits, v); return string(buf);
}
inline string IntegerToString(long long v) { return std::to_string(v); }
inline double MathAbs(double v)  { return std::fabs(v); }
inline double MathMax(double a, double b) { return a > b ? a : b; }
inline double MathMin(double a, double b) { return a < b ? a : b; }
inline int    MathRound(double v) { return (int)std::lround(v); }

You should see: nothing — this is a header file. Confirm with ls mql5.h (Windows: dir mql5.h).

If not: no compiler? On Ubuntu or Debian sudo apt install g++; on macOS xcode-select --install; on Windows use the Windows Subsystem for Linux or MSYS2.

2
Save the build script

Go: the same folder.

Do: save this as build.sh. It compiles a .mq5 file and executes the result. Make it executable with chmod +x build.sh, or invoke it as sh build.sh.

#!/bin/sh
# build.sh NAME  ->  compiles NAME.mq5 and runs it.
# MQL5 is close enough to C++ that a C++ compiler can run the subset used here,
# with mql5.h supplying the handful of built-ins MetaTrader would provide.
# Uses whichever compiler you have.
set -e
NAME="$1"
if command -v g++ >/dev/null 2>&1 && g++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5" 2>/dev/null; then
    :
elif command -v clang++ >/dev/null 2>&1; then
    clang++ -x c++ -std=c++17 -o "$NAME" "$NAME.mq5"
else
    echo "No C++ compiler found. Install g++ (Linux: apt install g++, macOS: xcode-select --install)." >&2
    exit 1
fi
"./$NAME"

You should see: nothing — this is the toolchain. ls should show both mql5.h and build.sh.

If not: Permission denied means the chmod was skipped; use sh build.sh NAME, which needs no execute bit.

3
Measure what ignoring prev_calculated costs

Go: the same folder.

Do: save this as prevcalc.mq5 and run sh build.sh prevcalc.

#include "mql5.h"

//+------------------------------------------------------------------+
//| prev_calculated is why an indicator does not recompute 100,000    |
//| bars on every tick -- and why getting it wrong is invisible.      |
//+------------------------------------------------------------------+

input int MaPeriod = 3;
std::vector<double> MaBuffer;
long total_iterations = 0;              // just to count the work

int Calculate(const int rates_total, const int prev_calculated,
              const std::vector<double> &close, bool use_prev)
{
    ArrayResize(MaBuffer, rates_total);
    int start = MaPeriod - 1;
    if(use_prev && prev_calculated > 0)
        start = prev_calculated - 1;    // recompute only the last bar onward

    for(int i = start; i < rates_total; i++)
    {
        double sum = 0.0;
        for(int k = 0; k < MaPeriod; k++) { sum += close[i - k]; total_iterations++; }
        MaBuffer[i] = sum / MaPeriod;
    }
    return rates_total;
}

void OnStart()
{
    std::vector<double> close;
    for(int i = 0; i < 1000; i++) close.push_back(100.0 + (i % 7));

    // 50 ticks arrive on the same chart.
    total_iterations = 0;
    int prev = 0;
    for(int tick = 0; tick < 50; tick++)
        prev = Calculate(ArraySize(close), prev, close, false);   // ignores prev
    long naive = total_iterations;

    total_iterations = 0;
    prev = 0;
    for(int tick = 0; tick < 50; tick++)
        prev = Calculate(ArraySize(close), prev, close, true);    // uses prev
    long smart = total_iterations;

    Print("bars on the chart : ", ArraySize(close));
    Print("ticks processed   : 50");
    Print("");
    Print("ignoring prev_calculated : ", naive, " inner iterations");
    Print("using prev_calculated    : ", smart, " inner iterations");
    Print("ratio                    : ", (int)(naive / (smart > 0 ? smart : 1)), "x more work");
    Print("");
    Print("On a 1000-bar chart that is merely wasteful. On a 100,000-bar chart");
    Print("with several indicators it is why the terminal stops responding --");
    Print("and nothing errors, so it looks like the platform is slow.");
}

int main() { OnStart(); return 0; }

You should see: the same 50 ticks doing 47 times as much work:

bars on the chart : 1000
ticks processed   : 50

ignoring prev_calculated : 149700 inner iterations
using prev_calculated    : 3141 inner iterations
ratio                    : 47x more work

On a 1000-bar chart that is merely wasteful. On a 100,000-bar chart
with several indicators it is why the terminal stops responding --
and nothing errors, so it looks like the platform is slow.

The terminal passes prev_calculated so you can start where you left off. Ignore it and you recalculate every bar on the chart on every tick — correct results, catastrophic cost.

The reason this bug survives so long is that nothing reports it. The indicator is right, the chart looks fine, and the only symptom is that the terminal becomes sluggish with several charts open — which people attribute to the platform, their computer, or their broker. A 1000-bar chart hides it; a 100,000-bar chart with four indicators does not.

The one subtlety: start from prev_calculated - 1, not prev_calculated. The last bar you calculated was the forming bar, and it has changed since — so it must be recomputed, and only it.

If not: if both counts are equal, the use_prev argument is being passed the same way in both loops. If the ratio is far larger or smaller than 47, the bar count or tick count was changed, which moves it proportionally.

4
Find out why an indicator needs more arrays than it draws

Go: the same folder.

Do: save this as buffers.mq5 and run sh build.sh buffers.

#include "mql5.h"

//+------------------------------------------------------------------+
//| Not every buffer is drawn. The ones that are not do the work.     |
//+------------------------------------------------------------------+

// A MACD needs four arrays but shows two lines.
std::vector<double> MacdLine;      // INDICATOR_DATA        -- drawn
std::vector<double> SignalLine;    // INDICATOR_DATA        -- drawn
std::vector<double> FastEma;       // INDICATOR_CALCULATIONS -- hidden
std::vector<double> SlowEma;       // INDICATOR_CALCULATIONS -- hidden

void Ema(const std::vector<double> &src, std::vector<double> &dst, int period, int total)
{
    ArrayResize(dst, total);
    double k = 2.0 / (period + 1);
    double seed = 0.0;
    for(int i = 0; i < period; i++) seed += src[i];
    seed /= period;
    for(int i = 0; i < period - 1; i++) dst[i] = 0.0;
    dst[period - 1] = seed;
    for(int i = period; i < total; i++)
        dst[i] = src[i] * k + dst[i - 1] * (1 - k);
}

void OnStart()
{
    std::vector<double> close;
    for(int i = 0; i < 40; i++) close.push_back(100.0 + (i % 9) - (i % 5));
    int total = ArraySize(close);

    Ema(close, FastEma, 5, total);
    Ema(close, SlowEma, 12, total);

    ArrayResize(MacdLine, total);
    for(int i = 0; i < total; i++)
        MacdLine[i] = (i < 11) ? 0.0 : FastEma[i] - SlowEma[i];

    Ema(MacdLine, SignalLine, 4, total);

    Print("buffers declared : 4");
    Print("lines drawn      : 2   (indicator_plots)");
    Print("");
    Print("bar   FastEma   SlowEma    MACD   Signal");
    for(int i = 30; i < 35; i++)
        Print("  ", i, "  ", DoubleToString(FastEma[i], 4),
              "  ", DoubleToString(SlowEma[i], 4),
              "  ", DoubleToString(MacdLine[i], 4),
              "  ", DoubleToString(SignalLine[i], 4));

    Print("");
    Print("FastEma and SlowEma are never shown, but they must be BUFFERS --");
    Print("not local variables -- because their previous values are needed on");
    Print("the next call, and a local array is gone the moment OnCalculate");
    Print("returns.");
    Print("");
    Print("In MetaEditor:");
    Print("   #property indicator_buffers 4      <- how many arrays exist");
    Print("   #property indicator_plots   2      <- how many are drawn");
    Print("   SetIndexBuffer(0, MacdLine,   INDICATOR_DATA);");
    Print("   SetIndexBuffer(2, FastEma,    INDICATOR_CALCULATIONS);");
    Print("");
    Print("Declaring 2 buffers and using 4 arrays is the classic mistake:");
    Print("it compiles, and the two extra arrays are not preserved between");
    Print("calls, so the indicator's values drift or reset unpredictably.");
}

int main() { OnStart(); return 0; }

You should see: four arrays producing two lines:

buffers declared : 4
lines drawn      : 2   (indicator_plots)

bar   FastEma   SlowEma    MACD   Signal
  30  100.9864  101.4532  -0.4668  -0.4816
  31  101.6576  101.6912  -0.0336  -0.3024
  32  102.1051  101.8925  0.2125  -0.0964
  33  102.4034  102.0629  0.3405  0.0783
  34  102.6022  102.2071  0.3952  0.2051

FastEma and SlowEma are never shown, but they must be BUFFERS --
not local variables -- because their previous values are needed on
the next call, and a local array is gone the moment OnCalculate
returns.

In MetaEditor:
   #property indicator_buffers 4      <- how many arrays exist
   #property indicator_plots   2      <- how many are drawn
   SetIndexBuffer(0, MacdLine,   INDICATOR_DATA);
   SetIndexBuffer(2, FastEma,    INDICATOR_CALCULATIONS);

Declaring 2 buffers and using 4 arrays is the classic mistake:
it compiles, and the two extra arrays are not preserved between
calls, so the indicator's values drift or reset unpredictably.

indicator_buffers and indicator_plots are different numbers and both matter. The first says how many arrays the terminal must preserve between calls; the second says how many of them to draw.

The intermediate arrays cannot be local variables, because an EMA needs its own previous value and a local array ceases to exist when OnCalculate returns. Declaring them as buffers with INDICATOR_CALCULATIONS tells the terminal to keep them without drawing them.

The failure mode is worth recognising: declare indicator_buffers 2 while using four arrays and it compiles cleanly, the chart looks right at first, and the values drift or reset when the terminal recalculates — because two of your arrays were never preserved.

If not: if the MACD column is zero throughout, the guard i < 11 is excluding every bar — it must skip only the bars before the slower EMA exists, which with a 12-period EMA is the first eleven.

5
Write the right thing into positions that have no value

Go: the same folder.

Do: save this as emptyvalue.mq5 and run sh build.sh emptyvalue.

#include "mql5.h"

//+------------------------------------------------------------------+
//| What to put in a buffer position that has no value.               |
//+------------------------------------------------------------------+

const double EMPTY = 0.0;               // what many people use
// MQL5's own answer: PLOT_EMPTY_VALUE, usually EMPTY_VALUE (a huge number),
// which tells the terminal "draw nothing here" instead of "draw a zero".

void OnStart()
{
    // A 5-bar average on 8 bars: the first 4 positions have no value.
    std::vector<double> close = {100, 102, 101, 103, 105, 104, 106, 108};
    std::vector<double> with_zero(8), with_empty(8);

    for(int i = 0; i < 8; i++)
    {
        if(i < 4) { with_zero[i] = 0.0; with_empty[i] = 1e308; continue; }
        double s = 0.0;
        for(int k = 0; k < 5; k++) s += close[i - k];
        with_zero[i] = with_empty[i] = s / 5.0;
    }

    Print("bar  close   buffer filled with 0   buffer left EMPTY");
    for(int i = 0; i < 8; i++)
        Print("  ", i, "  ", DoubleToString(close[i], 1),
              "        ", DoubleToString(with_zero[i], 2),
              "              ", with_empty[i] > 1e300 ? "(not drawn)"
                                                      : DoubleToString(with_empty[i], 2));

    Print("");
    double lo = with_zero[0], hi = with_zero[0];
    for(int i = 0; i < 8; i++) { lo = MathMin(lo, with_zero[i]); hi = MathMax(hi, with_zero[i]); }
    Print("chart scale if zeros are drawn : ", DoubleToString(lo, 1),
          " to ", DoubleToString(hi, 1));

    lo = hi = with_empty[4];
    for(int i = 4; i < 8; i++) { lo = MathMin(lo, with_empty[i]); hi = MathMax(hi, with_empty[i]); }
    Print("chart scale if they are not    : ", DoubleToString(lo, 1),
          " to ", DoubleToString(hi, 1));

    Print("");
    Print("A zero in a price buffer is not 'no value' -- it is the price zero.");
    Print("The terminal draws a line plunging to the bottom of the window and");
    Print("rescales the whole chart around it, which is why a new indicator");
    Print("sometimes flattens the price into an unreadable strip.");
    Print("");
    Print("Set PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE) in OnInit,");
    Print("and write EMPTY_VALUE into positions you have not calculated.");
}

int main() { OnStart(); return 0; }

You should see: the same data producing a chart scale of 0–105 or 102–105:

bar  close   buffer filled with 0   buffer left EMPTY
  0  100.0        0.00              (not drawn)
  1  102.0        0.00              (not drawn)
  2  101.0        0.00              (not drawn)
  3  103.0        0.00              (not drawn)
  4  105.0        102.20              102.20
  5  104.0        103.00              103.00
  6  106.0        103.80              103.80
  7  108.0        105.20              105.20

chart scale if zeros are drawn : 0.0 to 105.2
chart scale if they are not    : 102.2 to 105.2

A zero in a price buffer is not 'no value' -- it is the price zero.
The terminal draws a line plunging to the bottom of the window and
rescales the whole chart around it, which is why a new indicator
sometimes flattens the price into an unreadable strip.

Set PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE) in OnInit,
and write EMPTY_VALUE into positions you have not calculated.

This is the bug behind a very recognisable experience: you attach a new indicator and the price candles collapse into a thin band at the top of the window. The indicator wrote zeros into the bars it had not calculated, the terminal drew them as the price zero, and rescaled the window to fit.

Zero is a price, not an absence. MQL5 has a separate concept for “nothing here”: set PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE) in OnInit() and write EMPTY_VALUE into uncalculated positions. The terminal then skips them when drawing and when scaling.

It matters beyond appearance. Another indicator or an EA reading your buffer sees those zeros too, and a comparison like “is price above my line?” is true for every zero — which is the same class of failure as the ignored return value on the functions page.

If not: if the two scale lines are identical, the with_empty array is being filled with zeros as well — the sentinel must be the very large value, which is what stands in for EMPTY_VALUE here.

🎉
Check yourself before moving on

Without scrolling up: your indicator draws correctly on a fresh chart, but after scrolling back through a few years of history the terminal becomes unusably slow, and after switching timeframe some of the older values are wrong. Name the two separate causes and the fix for each. Answer: the slowness is prev_calculated being ignored: step 3 measured 47 times more work on a 1000-bar chart, and scrolling back loads far more bars, so the cost grows with the history you have loaded rather than with anything you did. The fix is to begin the loop at prev_calculated - 1 rather than at the start of the array. The wrong older values are a different problem — almost certainly intermediate arrays that were not declared as buffers, so they are not preserved when the terminal recalculates after a timeframe switch, and any calculation that depends on its own previous value restarts from whatever happened to be in memory. The fix is to declare every array the calculation depends on in indicator_buffers and register the non-drawn ones with INDICATOR_CALCULATIONS. Both bugs are silent, which is why they usually arrive together and get blamed on the platform.

Now do it without the page: take buffers.mq5 and deliberately make FastEma a local array inside a function rather than a file-level buffer, then call the calculation twice in a row and compare the second result with the first. Watching the values change when nothing about the input changed is the clearest way to understand what “the terminal preserves buffers” actually buys you. Then add EMPTY_VALUE handling from step 5 to sma_ind.mq5 from the previous page, so the two habits arrive together.

Buffer Management Tips

  • Buffers used for drawing must come first, calculation-only buffers last
  • Use INDICATOR_CALCULATIONS for helper buffers that should not appear in the Data Window
  • Use PlotIndexSetDouble(index, PLOT_EMPTY_VALUE, 0) to define what "no data" looks like — the indicator skips drawing at those points
  • Set IndicatorSetString(INDICATOR_SHORTNAME, "Name") so your indicator shows a meaningful name in the chart and Data Window