Why Most Order Block Indicators Lie to You
Let me save you some time: 90% of the free Order Block indicators you find on forums repaint. They look amazing on historical data, showing perfect entries where price reversed. Then you put them on a live chart and watch those beautiful levels vanish seconds after they appear. It's not a bug - it's how most are coded. They recalculate every tick using the latest closed bars, which means yesterday's "valid" order block disappears when today's price action changes the swing structure.
I've been coding MQL4 EAs for over seven years, and I've seen this pattern more times than I can count. The fix isn't complicated, but it requires a fundamental shift in how you think about swing point detection. You have to commit to a bar once it's confirmed, then never change it. This article walks you through exactly that approach, with full MQL4 code you can compile and test today.
This is not a "copy-paste and get rich" post. I'll show you the code, explain why each piece matters, and tell you where the approach falls short. If you're serious about building reliable SMC tools for MetaTrader 4, read on.
Most traders don't realize that repainting indicators can distort your entire trading psychology. You start second-guessing every level, wondering if it'll hold. I've had students tell me they spent months trading a repainting indicator, only to blow accounts when the levels disappeared mid-trade. The code I'm about to show you solves that by design.
What an Order Block Actually Is (And Isn't)
In Smart Money Concepts (SMC) - popularized by ICT - an Order Block is a single candle where institutional traders placed large pending orders. The theory goes that price will return to this zone to "liquidity grab" before continuing in the trend direction. In practice, it's a supply or demand zone condensed to one bar.
Key characteristics of a valid Order Block:
- Formed at a swing point - either a swing high or swing low in price
- Has an imbalance - the candle body is significantly larger than preceding candles, or there's a gap in price range between consecutive candles
- Is the last candle before the swing reversal - the move that breaks the prior structure
The problem most coders face: how do you define "swing point" in code without repainting? The built-in ZigZag indicator repaints by design - it's a lagging indicator that recalculates as new bars form. You can't use it directly. Even if you set ZigZag's Depth to 12 and Deviation to 5, those values are just thresholds for recalculating peaks and troughs. Every new tick can shift previous swing points. That's why you'll see a perfect-looking setup in the tester, but live it's a mess.
Another common mistake: using the iCustom() function to pull ZigZag values in an EA. That doesn't fix repainting; it just reads the repainted values. You need your own detection logic.
Let's get more specific about the repainting mechanics. When a ZigZag indicator runs in MetaTrader, it uses a recursive algorithm that looks back a defined number of bars (the Depth parameter). If a new bar forms with a higher high than the current peak, the algorithm shifts that peak forward, erasing the previous one. The old peak now shows a lower value or disappears entirely. In the Strategy Tester, this looks perfect because the tester recalculates the entire history on every tick. But on a live chart, you're watching the indicator constantly adjust. I've seen cases where a swing high moves by 20 pips between ticks on EURUSD during news events.
There's also the issue of "look-ahead bias" in repainting indicators. When you backtest a strategy using a repainting Order Block indicator, you're essentially peeking into future data. The indicator shows entries that wouldn't have been visible at the time of the trade. This is why many traders think they've found a holy grail in backtests, only to lose money live.
The Core Architecture: Locked Swing Points
My approach uses a locked swing point buffer. Once a bar closes and we identify it as a swing high or low, we store its index and price permanently. No recalculation. The only thing that changes is whether we draw an Order Block at that location - and that decision depends on subsequent price action.
Here's the flow:
- On each new bar, check if the previous bar is a swing extreme by comparing its high/low to surrounding bars
- If confirmed, store that bar's index and price in an array
- Once stored, never modify that entry
- Check if price has returned to within the range of a stored swing bar - if yes, draw the Order Block zone
This is fundamentally different from repainting indicators that delete and redraw their levels. The trade-off: your zones appear later (after the swing is confirmed), but they stay put. You'll miss the first few pips of the move, but you won't get faked out by disappearing levels.
One nuance most tutorials skip: you need to handle the case where a new bar forms while the previous bar is still being evaluated. In MQL4, the OnCalculate() function runs on each tick. I use a static variable to track the last processed bar index. If prev_calculated == 0 (first run), I scan all historical bars. Otherwise, I only process the new bar. This keeps performance clean.
Let me walk through the exact variable structure. I use a static array of structs, where each struct holds the bar index, the high and low of the Order Block zone, a boolean for bullish or bearish, and a timestamp of when the zone was first identified. The timestamp helps with the expiry logic. I also store the swing point price itself for validation purposes. Here's the struct definition I use in practice:
struct SwingPoint
{
int barIndex;
double price; // The extreme price (high for swing high, low for swing low)
double zoneHigh; // Upper boundary of the Order Block zone
double zoneLow; // Lower boundary
bool isBullish; // true = swing low (bullish OB), false = swing high (bearish OB)
datetime firstSeen; // Bar open time when first detected
bool isActive; // true if price hasn't yet violated the zone
};
SwingPoint swingPoints[];I initialize the array with a size of 200 in the OnInit() function. That's enough for most charts. If you're trading on M1 with thousands of bars, you might need to increase it. The array is dynamically resized if we hit the limit, but I've never needed more than 200 on H1 for a week's worth of data.
Parameter Design for Real-World Use
Let me show you the inputs I use. I've tested these across EURUSD, GBPUSD, and XAUUSD on H1 and H4 timeframes. They're not magic numbers - they're starting points you'll need to adjust per instrument.
| Parameter | Type | Default | Description |
|---|---|---|---|
| SwingBars | int | 3 | Number of bars on each side to confirm a swing extreme (3 = look 3 bars left and right) |
| MinBodyRatio | double | 1.5 | Minimum ratio of candle body to average body of last 10 bars (1.5 = 50% larger) |
| MaxZones | int | 10 | Maximum number of zones to display (older ones are hidden) |
| ShowBullishOB | bool | true | Display bullish Order Blocks (at swing lows) |
| ShowBearishOB | bool | true | Display bearish Order Blocks (at swing highs) |
| ZoneExpiryBars | int | 50 | Remove zone if price hasn't returned within this many bars (0 = never expire) |
The ZoneExpiryBars parameter is something I added after noticing old zones cluttering charts on slow-moving pairs like USDCHF. On H4, 50 bars is about 8 days. If price hasn't revisited by then, that zone is probably dead.
One more parameter I didn't include in the table but use in my personal version: MinZoneWidthPoints. This sets a minimum width for the zone in points (e.g., 10 points = 1 pip for most forex pairs). Without it, you sometimes get zones that are just 1-2 points wide from a doji candle, which isn't useful for trading. I set it to 15 points for EURUSD, but you'll want to adjust for pairs like USDJPY where a point is different.
MQL4 Implementation: The Core Logic
Here's the heart of the indicator. I'm showing the key functions - not the full boilerplate - because that's where the value is. You'll need to wrap this in a standard #property indicator_chart_window framework with OnCalculate(). I assume you know how to set up the buffer declarations and #property indicator_buffers for drawing rectangles. If not, check the MQL4 documentation for OBJ_RECTANGLE - that's what I use for the zones.
Before diving into the code, let me explain the buffer setup. I use two indicator buffers: one for the zone high and one for the zone low. But since we're drawing rectangles, not lines, the buffers are mostly for tracking internal state. The actual drawing happens through ObjectCreate() calls. I allocate buffer 0 for zone high and buffer 1 for zone low, but they're set to EMPTY_VALUE by default. Only when a zone is active do I populate the buffer values at the bar index. This lets me use the buffers for quick lookups without repainting.
1. Swing Point Detection (Non-Repainting)
//+------------------------------------------------------------------+
//| Detect if bar at index is a swing high |
//+------------------------------------------------------------------+
bool IsSwingHigh(int index, int barsLookback)
{
double currentHigh = High[index];
for(int i = 1; i <= barsLookback; i++)
{
// Check bars to the left (older)
if(index + i < Bars && High[index + i] >= currentHigh)
return false;
// Check bars to the right (newer)
if(index - i >= 0 && High[index - i] >= currentHigh)
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Detect if bar at index is a swing low |
//+------------------------------------------------------------------+
bool IsSwingLow(int index, int barsLookback)
{
double currentLow = Low[index];
for(int i = 1; i <= barsLookback; i++)
{
if(index + i < Bars && Low[index + i] <= currentLow)
return false;
if(index -





