MQL5 Indicator Development: Buffers, DRAW Styles and the No-Repaint Architecture
Most repainting complaints are not MetaTrader bugs — they are architecture failures: the indicator recalculated the forming bar or confirmed a signal too early. This guide covers the OnCalculate skeleton, buffer architecture, every DRAW style, and the locking pattern that makes a signal stay where it printed — the standard we apply on every build, and part of what clients should expect when they hire MQL4/MQL5 developers.
Table of Contents
The Indicator Skeleton
Every custom indicator starts from the same skeleton: #property indicator_buffers and #property indicator_plots declared, buffers bound in OnInit with SetIndexBuffer, and the computation in OnCalculate with its four parameters: rates_total, prev_calculated, begin and the price[] series.
The calculation mode is not a flag MetaTrader hands you — it is encoded in how prev_calculated relates to rates_total, and ignoring these cases breaks indicators in ways that only appear live:
- prev_calculated == 0. First load or rebuilt history — recalculate everything.
- prev_calculated == rates_total. No new bars — only the forming bar changed, update it alone.
- rates_total < prev_calculated. History repagination or a symbol swap — full recalculation, and cached per-bar state must be rebuilt.
An indicator handling only the second case works in the visual tester and produces garbage after a terminal update repaginates history. Most script-style indicators that "break after an update" broke here.
Buffers and Series
A buffer is just an array the terminal draws from — but how it is declared changes everything. INDICATOR_DATA marks a double array as drawable data; INDICATOR_COLOR marks a uchar array as per-bar color indexes for the color DRAW variants; INDICATOR_CALCULATIONS marks working arrays that are never drawn. MQL5 allows 512 buffers against MQL4's 8 — the gap that makes multi-timeframe caches and calculation history possible.
Series orientation is the second decision: ArraySetAsSeries flips indexing so element 0 is the forming bar and history counts right-to-left. Skipping it works, but every third bug in ported code is an indexing-direction bug. The rest of the platform differences are covered in MQL4 vs MQL5 development.
The rule that governs everything downstream: whatever an EA needs to read must exist in a buffer. Objects and screen text are for humans; buffers are the API for automation.
DRAW Styles Compared
Each DRAW_* style renders buffer values differently. Choosing the right one is the difference between a readable chart and a wall of noise.
| Style | What It Draws | Typical Use |
|---|---|---|
| DRAW_LINE | Connected line through consecutive non-empty values | Moving averages, oscillators |
| DRAW_SECTION | Horizontal segments between values | Levels, session highs and lows |
| DRAW_HISTOGRAM | Vertical bars from the zero line | MACD, momentum, volume deltas |
| DRAW_ARROW | A symbol printed at each non-empty value | Signal markers — arrow codes 233 and 234 for up and down |
| DRAW_ZIGZAG | Segments connecting pivot highs and lows | Swing structure, wave counts |
| DRAW_FILLING | Filled area between two buffers | Channels, envelopes, confidence bands |
| DRAW_BARS | OHLC bars from four buffers — open, high, low, close | Custom bar representations, range charts |
| DRAW_CANDLES | Candles from four buffers, bodies and wicks | Heikin-Ashi, smoothed candles |
| DRAW_COLOR_* variants | The same geometry with per-bar color from a color buffer | Trend-colored lines, bullish and bearish histograms |
PlotIndexSetInteger Essentials
Buffers carry values; plots control how they render, through PlotIndexSetInteger. The settings that matter in every indicator:
- PLOT_DRAW_TYPE. The DRAW style — unset, the plot never renders.
- PLOT_EMPTY_VALUE. Set to
EMPTY_VALUE, the marker for "draw nothing on this bar"; stray zeros print as data. - PLOT_DRAW_BEGIN. The first bar index drawn from — skips the warm-up region.
- PLOT_LINE_WIDTH and PLOT_LINE_STYLE. Thickness 1 to 5 and dash styling.
- PLOT_ARROW and PLOT_ARROW_SHIFT. The Wingdings symbol code and its vertical offset for arrow plots.
- Per-plot colors.
PLOT_LINE_COLORfor single-color plots, or#property indicator_color1palettes for the COLOR variants.
Most "flat line at zero" bug reports are an EMPTY_VALUE mismatch — code skipping bars with a literal zero instead of the empty marker.
Why Indicators Repaint and How to Prevent It
Repainting has three sources, and only one is a bug. First, the forming bar: bar 0 recalculates every tick until it closes, so signals computed on it can move or vanish. Second, confirmation structure: fractals and ZigZag confirm N bars after the event by design. Third, real lookahead bugs: code reading future indexes, which backtests look like magic and live charts miss.
The fix for source one is closed-bar locking: compute only on closed bars. Detect a new bar by comparing rates_total against the stored prev_calculated and recalculate only newly closed bars. The signal-lock pattern completes it: cache each locked signal keyed by bar time, and refuse to recompute any bar already present. Once locked, a bar never moves.
Source two has no fix — it is mathematics. If the logic needs future confirmation, delay publication by the lookback and document it. A ZigZag that revises is not broken; a ZigZag sold as non-repainting is a lie. When clients bring us TradingView systems, this is where ports fall apart — the no-repaint discipline is the first casualty of a naive conversion, as our PineScript conversion work shows.
Multi-Timeframe Indicators
Multi-timeframe indicators read another timeframe through CopyBuffer, CopyRates or iCustom, mapping each higher-timeframe bar onto the lower timeframe. Three pitfalls define the discipline:
- Insufficient history. Copy functions return what exists, or -1. Asking for 500 daily bars on a symbol with 60 days silently truncates — check
BarsCalculatedand degrade gracefully. - Stale static caches. Higher-timeframe data cached without a per-symbol key is shared when the same indicator loads on EURUSD and GBPUSD. Key caches by symbol and timeframe.
- Tester contexts. When an EA calls the indicator via
iCustom, the indicator runs simulated: no chart events, no real ticks. Everything must be deterministic from buffers alone.
On the EA side it is the same: create the handle once in OnInit, keep it for the EA's lifetime, and pull buffers with CopyBuffer when the signal engine runs. EAs read buffers, never drawn objects — the automation contract in the 9-layer MQL5 architecture blueprint.
Alerts and Notifications
A signal nobody sees is decoration. MQL5 offers three channels: Alert() for a popup and sound, SendNotification() for push messages, and SendMail() for email digests. Push needs the MetaQuotes ID configured; mail needs SMTP settings. Check availability at startup rather than failing silently at signal time.
Throttling is the professional half. Fire on every tick and a news candle produces forty notifications and a muted phone. One alert per bar per condition — track the last alert bar time in a static variable, reset on each new bar.
Performance Rules
An indicator runs on every tick of every chart it loads on, and a slow indicator degrades the whole terminal:
- Compute once per bar. Cache intermediates keyed by bar index; never recompute closed bars on a tick.
- No object creation per tick.
OBJ_TEXTandOBJ_TRENDchurn leaks objects and CPU. Create once, update coordinates only. - Redraw only on change.
ChartRedrawis terminal-wide. Call it when something actually changed, not defensively. - No strings or files per tick. Formatting and
FileWritein the tick path cause micro-freezes; buffer writes are cheap.
The acceptance test before delivery: load the indicator on an M1 chart with years of history and fast-forward the visual tester. CPU should stay in the low single digits.
From Pine Script to MQL5 Indicator
Pine Script and MQL5 look similar at the surface and diverge underneath. Pine recalculates the whole script over the visible series; MQL5's OnCalculate is incremental and expects you to manage what changed. security() becomes multi-timeframe reads; plot() becomes a DRAW style; color logic becomes color buffers.
The no-repaint discipline is the first casualty of naive ports, because Pine blurs confirmed and forming bar states. Our rule: lock every published signal to closed bars by default, and relax it only when the client wants forming-bar behavior. The full methodology — function mapping and parity backtests — lives in the PineScript conversion guide.
Frequently Asked Questions
How do you make a non-repainting MT5 indicator?
Compute signals only on closed bars, never on the forming bar zero. Detect new bars by comparing rates_total against the previous prev_calculated, and lock each bar's signal in a cache keyed by bar time so it can never be recomputed. If the logic needs future confirmation — fractals, ZigZag — delay publication by the lookback and document it, rather than selling it as locked.
How many buffers can an MQL5 indicator have?
Up to 512 buffers, against MQL4's limit of 8. Data buffers are double arrays declared with INDICATOR_DATA; per-bar color buffers are uchar arrays declared with INDICATOR_COLOR; INDICATOR_CALCULATIONS marks working arrays. Plots are limited to 64 per indicator, but each can draw from a data buffer plus a color buffer.
What is OnCalculate's prev_calculated for?
prev_calculated tells OnCalculate how many bars were already processed, so the indicator only recomputes what changed. Zero means first load or rebuilt history, so recalculate everything. Equal to rates_total means no new bars, so update only the forming bar. rates_total below prev_calculated means history repagination, so full recalculation is required. Ignoring the third case breaks indicators after terminal updates.
How do I draw arrows on an MT5 chart?
Use a buffer with DRAW_ARROW (or DRAW_COLOR_ARROW). Set PlotIndexSetInteger(plot, PLOT_DRAW_TYPE, DRAW_ARROW), choose the symbol via PlotIndexSetInteger(plot, PLOT_ARROW, code) — 233 for up, 234 for down — and write a non-empty value at the bar where the arrow should print. EMPTY_VALUE elsewhere means nothing is drawn.
Can an EA call a custom indicator?
Yes — via iCustom in OnInit, which returns a handle, then CopyBuffer pulls the indicator's buffer values into the EA. The EA reads buffers, never drawn objects — which is why every value an EA consumes must exist in a buffer. Keep the handle across the EA's lifetime and validate it before every read.
Get Your Signal Locked, Buffered and Delivered
A professional indicator is boring in all the right ways: it prints once, stays printed, reads fast on ten years of M1 history, and hands every value to your EA through buffers. Whether it starts as a sketch, a spec or a TradingView script, we build it on this architecture and deliver the source.