Why Most Order Block Indicators Repaint (And Why That's a Problem)
If you've spent any time in the Smart Money Concepts (SMC) space, you've probably downloaded half a dozen Order Block indicators that looked great on the chart—until you refreshed and half the zones disappeared. That's repainting, and it's the fastest way to destroy trust in your trading system. A repainting indicator changes its past signals based on new price data, meaning the perfect entry you saw at 2 PM never actually existed.
I've been coding MQL indicators for over six years, and I've lost count of how many "non-repainting" claims turned out to be lies. The problem is structural: most coders calculate zones on every tick, which means a swing that wasn't confirmed yesterday gets replaced today. You end up chasing phantom levels. I've seen traders blow accounts because they entered a position based on a zone that vanished mid-trade.
In this post, I'll walk you through coding a non-repainting Order Block indicator in MQL5 that identifies genuine institutional order flow zones. This isn't a copy-paste of some generic supply/demand script. We're building something that respects the ICT (Inner Circle Trader) logic: order blocks form at the last candle before a strong move away from a swing point, and they stay put once drawn.
What you'll learn:
- The exact algorithm to detect bullish and bearish order blocks without repainting
- How to handle swing point detection using a ZigZag-style approach in MQL5
- Parameter tuning that actually matters (and which ones to leave alone)
- Why "non-repainting" is harder than it sounds in MQL5, and how to guarantee it
- Common pitfalls with buffer indexing and object naming that will break your indicator silently
This article is for intermediate MQL5 coders. You should know basic array handling, OnCalculate, and indicator buffers. If you're new to MQL5, I'd suggest starting with a simple moving average crossover indicator first—this builds on those fundamentals. If you're coming from MQL4, expect some pain with the zero-indexed timeseries and the stricter buffer handling.
What Makes an Order Block Different from Supply/Demand?
Most traders confuse order blocks with standard supply/demand zones. They're related but not identical. A supply zone is any area where sellers overwhelmed buyers—it's a broad concept. An order block is more specific: it's the last candle (or a consolidation of a few candles) before a strong impulsive move away from a swing high or low. The logic is that institutional traders placed large pending orders in that zone, and once price returns there, those orders get triggered again.
For a bullish order block: look for the last bearish candle (or series of candles) just before price breaks above a swing high. For a bearish order block: the last bullish candle before price breaks below a swing low.
The key distinction: order blocks are directional—they only work for reversals. A supply zone might work for both continuation and reversal, but an order block assumes the move was initiated by smart money, so price should respect it on the way back. I've seen traders use order blocks as continuation entries, and it usually ends badly. Stick to reversal setups unless you have strong confluence.
Most MQL5 supply demand indicators I've seen just draw horizontal boxes around consolidation areas. That's not what we're doing. Our indicator will draw specific candle-level zones that align with swing structure. The difference is subtle but critical: a supply zone drawn from a 20-bar consolidation might span 50 pips, while an order block from a single candle might be 10 pips. The tighter zone gives you a better risk/reward ratio.
The ICT Context: Why Order Blocks Matter
ICT's core premise is that price moves in waves driven by institutional order flow. When a swing low is broken by a strong bullish move, the last candle before that breakout represents where institutions placed their buy orders. If price returns to that zone, those same orders should provide support. It's not magic—it's just order flow dynamics. The issue is that most implementations are sloppy. They draw zones that repaint, or they use arbitrary lookback periods that don't align with actual structure.
Our implementation solves that by anchoring zones to confirmed swing points. No guesswork, no subjective drawing.
The Non-Repainting Algorithm: Step by Step
Here's the core logic. It runs on each new bar (tick updates only redraw, never recalculate history). This is critical: if you run the full calculation on every tick, you'll introduce repainting because the ZigZag might change its mind mid-bar. We only recalculate when a new bar forms, detected by checking prev_calculated in OnCalculate.
- Find swing highs and lows using a ZigZag algorithm. We need at least two consecutive swings to confirm direction.
- Identify the breakout candle—the first candle that closes beyond the swing high/low.
- Go back one candle (or more if we use a consolidation filter). That candle (or group) is the order block.
- Store the zone with its high, low, and direction. Once stored, it never changes.
- Draw the zone as a rectangle extending to the right, so you can see where price might return.
The non-repainting guarantee comes from step 4. We only write to the indicator buffers when a new swing is confirmed. On every subsequent tick, we read from buffers—we never recalculate past zones. The ZigZag itself can repaint (that's a separate debate), but the order block zones are fixed once drawn. I'll show you exactly how to implement this with a confirmed flag in a structure array.
This is where most coders mess up. They calculate order blocks on every tick, which means a swing that wasn't confirmed yesterday gets replaced today. To avoid that, use a static array or a CSV file to store confirmed zones. In MQL5, I prefer a simple array of structures with a confirmed flag. The array persists across ticks because it's declared at global scope. Just make sure you don't accidentally reinitialize it in OnInit—I've made that mistake and lost hours debugging.
MQL5 Implementation: The Core Code
Let's get into the actual MQL5 code. I'll show you the essential parts—the full indicator is about 400 lines, but these are the critical functions. I'm assuming you're working in MetaEditor with a standard indicator template. If you haven't created one yet, go to File > New > Expert Advisor/Indicator > Custom Indicator and set the drawing style to DRAW_NONE since we're using graphical objects, not buffer lines.
Data Structures
First, we need a structure to hold each order block zone. I use a simple struct with a confirmed flag to prevent repainting.
//+------------------------------------------------------------------+
//| Structure for a single order block zone |
//+------------------------------------------------------------------+
struct OrderBlock
{
datetime time; // bar time when zone was formed
double high; // zone high price
double low; // zone low price
bool isBullish; // true = bullish, false = bearish
bool confirmed; // true = permanently stored
};
OrderBlock zones[];The confirmed flag is key. When we first detect a potential zone, we set it to false. Only after the breakout candle closes do we set it to true. This prevents partial zones from being drawn. I've seen indicators that draw zones as soon as a swing is detected, then remove them when the breakout fails. That's repainting by another name.
Swing Point Detection
I use a simple ZigZag with a minimum deviation parameter. This isn't the most sophisticated method, but it's reliable and non-repainting for the purpose of order blocks. The key is to only update the swing array when a new bar forms.
//+------------------------------------------------------------------+
//| Detect swing high/low using ZigZag logic |
//+------------------------------------------------------------------+
bool IsSwingHigh(int index, int depth, double deviation)
{
// Check if price at index is higher than 'depth' bars on each side
double currentHigh = high[index];
for(int i = 1; i <= depth; i++)
{
if(high[index + i] >= currentHigh) return false;
if(high[index - i] >= currentHigh) return false;
}
// Check minimum price deviation from surrounding bars
double leftAvg = (high[index - depth] + low[index - depth]) / 2;
double rightAvg = (high[index + depth] + low[index + depth]) / 2;
double avg = (leftAvg + rightAvg) / 2;
if(MathAbs(currentHigh - avg) < deviation * Point) return false;
return true;
}Same logic for IsSwingLow. This runs on each new bar, not every tick. The deviation parameter filters out minor noise—I typically use 10-20 points on EURUSD H1. One thing to watch: the high[] and low[] arrays are zero-indexed in MQL5, so index 0 is the current bar. When you're scanning back, make sure you don't go out of bounds. I add a check at the start of OnCalculate to limit the lookback to rates_total - depth - 1.
Order Block Identification
Once we have a confirmed swing high and a subsequent breakout candle that closes above it, the order block is the candle immediately before the breakout.
//+------------------------------------------------------------------+
//| Identify and store order blocks |
//+------------------------------------------------------------------+
void FindOrderBlocks()
{
// We need at least 2 swings to determine direction
if(swingCount < 2) return;
// Get the last two swings
int lastSwing = swingIndices[swingCount - 1];
int prevSwing = swingIndices[swingCount - 2];
// Determine if we have a bullish or bearish setup
bool isBullish = (high[lastSwing] > high[prevSwing]); // higher high
// Find breakout candle: first candle that closes beyond the swing
int startBar = MathMin(lastSwing, prevSwing) + 1;
int breakoutBar = -1;
for(int i = startBar; i < ratesTotal; i++)
{
if(isBullish && close[i] > high[lastSwing])
{
breakoutBar = i;
break;
}
if(!isBullish && close[i] < low[lastSwing])
{
breakoutBar = i;
break;
}
}
if(breakoutBar == -1) return;
// Order block is the candle before breakout
int obBar = breakoutBar - 1;
if(obBar < 0) return;
// Check if this zone already exists
for(int i = 0; i < ArraySize(zones); i++)
{
if(zones[i].time == time[obBar]) return; // already stored
}
// Store new zone
int size = ArraySize(zones);
ArrayResize(zones, size + 1);
zones[size].time = time[obBar];
zones[size].high = high[obBar];
zones[size].low = low[obBar];
zones[size].isBullish = isBullish;
zones[size].confirmed = true;
}Notice the duplicate check: we only add a zone if its bar time hasn't been stored before. That's the non-repainting guarantee in action. The time[] array gives us the open time of each bar, which is unique. If you're on a timeframe like M1 where bars can have the same time across different days, add a date check too. I've never had issues on H1 or higher, but M1 can be tricky.
One edge case: what if the breakout candle is also the swing candle? That can happen on very fast moves. In that case, obBar equals lastSwing, which means the order block is the swing candle itself. Some traders argue that's invalid because the order block should be before the move. I handle it by checking if obBar == lastSwing and skipping the zone in that case. You can decide based on your own testing.
Drawing the Zones
I use ObjectCreate with OBJ_RECTANGLE for each zone. The rectangle extends from the order block candle to the right edge of the chart. Color coding: green for bullish, red for bearish.
void DrawZones()
{
for(int i = 0; i < ArraySize(zones); i++)
{
string objName = "OB_" + IntegerToString(zones[i].time);
if(ObjectFind(0, objName) >= 0) continue;
ObjectCreate(0, objName, OBJ_RECTANGLE, 0,
(datetime)zones[i].time, zones[i].high,
TimeCurrent() + 3600 * 24 * 365, zones[i].low);
ObjectSetInteger(0, objName, OBJPROP_COLOR,
zones[i].isBullish ? clrLimeGreen : clrRed);
ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);
ObjectSetInteger(0, objName, OBJPROP_FILL, true);
ObjectSetInteger(0, objName, OBJPROP_BACK, true);
}
}The rectangle extends 365 days to the right. You can adjust that or make it dynamic based on the current chart timeframe. I prefer a fixed extension because it keeps the zones visible even on longer timeframes. If you're on M15, 365 days might be overkill—set it to ZoneExtensionBars * PeriodSeconds() instead.
One gotcha: ObjectFind returns -1 if the object doesn't exist, but in MQL5 it can also return the chart ID. Always check >= 0 to avoid false negatives. I also add a cleanup function in OnDeinit to remove all objects with the "OB_" prefix, otherwise you'll clutter the chart on recompile.
Parameter Tuning: What Actually Matters
Here's where most traders overcomplicate things. You don't need 30 parameters. These five will cover 95% of use cases. I've seen indicators with 20+ inputs, and half of them just add noise. Keep it simple.
| Parameter | Type | Default | Description |
|---|---|---|---|
| ZigZag Depth | int |






