Building an Object-Oriented Order Block Engine in MQL5
Introduction and Practical Pain Points
When developing an intraday EA in MQL5, I run into a recurring problem: standard indicators provide a lagging filter and often trigger entries precisely at the moment of a structural reversal on liquid instruments. What's needed isn't "yet another oscillator," but a module that detects real price imbalance zones: the last opposite candle before the impulse shift that breaks through the nearest structure (MSS), and keeps this zone active until its retest (mitigation). MQL5 doesn't have a ready-made mechanism that can be quickly plugged in and used equally in an indicator (for testing) and in an expert advisor (for trading). Therefore, the goal of this article is to formalize the zone rules and package them into a reusable engine.
The Logic of Candlestick Displacement
The calculation path behind an order block is highly rule-based. It avoids the smoothing lag found in technical indicators. To identify a valid institutional zone, the engine monitors price displacement. A bullish order block forms when a bearish candlestick is followed by a sharp upward expansion. This expansion must break the previous structural high. This structural shift is a Market Structure Shift (MSS). Conversely, a bearish order block represents the last bullish candlestick before an aggressive downward displacement that breaks previous swing lows.
The mechanical identification relies on three main validation checkpoints:
- Displacement Intensity: The expansion candlesticks must show a clear size imbalance compared to previous quiet ranges.
- Structure Break: The price expansion must fully close beyond the immediate structural high/low.
- Mitigation State: The zone remains active until a later bar closes and retests the boundary.
When the price returns to an unmitigated order block, the market test often finds residual institutional liquidity. The automated engine does not treat these zones as absolute boundaries. Aggressive macroeconomic news can break these levels easily. Instead, our engine treats them as structural filters. They confirm if a pullback is returning to an authentic zone of historical institutional interest before an order is authorized.
The Signal Contract and Mitigation Rules
Before translating this structural logic into code, we must establish a clear agreement for our execution layers. This contract defines how the engine identifies active zones and ignores old data. To avoid intraday noise, the calculation loop runs only on fully closed bars. We enforce this in both modules by calling calculations with shift = 1. This ensures that the algorithm entirely ignores active bar index 0.
An active order block can experience two structural states:
- Unmitigated Zone: The price has not returned to the historical block coordinate range since its creation.
- Mitigated Zone: A candle's wick on a closed bar has crossed into the zone boundaries, neutralizing the residual liquidity filter.
Our engine tracks the highest high and lowest low of the original base candlestick. The algorithm removes the zone from the active tracking queue the moment a closed bar mitigates the price levels. This contract ensures that the execution layer evaluates fresh setups. It ignores stale zones that lack real institutional interest.
Dynamic Memory Allocation and Pointer Safety
When creating modular code blocks shared by indicators and expert advisors, managing memory allocation footprints becomes a primary engineering responsibility. In MQL5, creating object class instances on the stack can limit operational scalability. If a strategy requires monitoring multiple symbols and timeframes simultaneously, static stack allocations restrict runtime configuration changes. To build a production-grade environment, our architecture instantiates the core order block engine dynamically on the system heap using the new operator.
This dynamic approach grants complete control over the object lifecycle, allowing the trading robot to load, configure, and purge tracking modules in response to market events. However, working with heap memory introduces the risk of critical memory leaks. If the expert advisor allocates heap memory and exits without freeing it, allocations may leak and degrade performance, eventually crashing the Strategy Tester.
To eliminate this vulnerability, our framework implements a strict object lifecycle pattern. We use the native CheckPointer function to monitor instance states continuously. Before invoking engine calculations, the calling module verifies that the underlying pointer maps to a valid dynamic memory address. Upon deinitialization, the OnDeinit event handler triggers a defensive tracking routine. It identifies whether the pointer is flagged as POINTER_DYNAMIC and executes the delete operator to return the allocated memory blocks to the system heap.

Figure 1. Architecture Overview
Architecting the Reusable Include File
Placing heavy structural search loops inside the main event handler of an expert advisor creates rigid architecture. If you later decide to track multiple symbols or deploy the scanner across different chart timeframes, the code quickly becomes unmanageable. To ensure robust performance, we isolate the detection engine inside a custom include file named OrderBlock_Engine.mqh. This object-oriented approach achieves a clean separation between structural processing and order routing mechanics. The visual indicator and the trading robot will call the same class from the include file, ensuring identical structural output across both modules.
To comply with clean platform repository guidelines, all project resources are isolated inside a dedicated project subfolder named OrderBlockEngine. Let's define the interface and private structural members of our object class.
//+------------------------------------------------------------------+ //| OrderBlock_Engine.mqh | //| Copyright 2026, MetaQuotes Ltd. | //+------------------------------------------------------------------+ #property copyright "Open Source" #property version "1.10" //--- Structure to store order block data properties struct OrderBlock { double open_price; // Base candle open coordinate double high_price; // Base candle high limit double low_price; // Base candle low limit double close_price; // Base candle close coordinate datetime creation_time; // Timestamp of block formation int direction; // 1 = Bullish, -1 = Bearish bool is_mitigated; // Mitigation status flag }; //--- Class definition for institutional zone profiling class COrderBlockEngine { private: string m_symbol; // Target asset symbol name ENUM_TIMEFRAMES m_timeframe; // Working calculation timeframe int m_max_zones; // Maximum historical zones tracking limit public: COrderBlockEngine(string symbol, ENUM_TIMEFRAMES tf, int max_zones = 20); ~COrderBlockEngine(void); bool GetLatestZone(int shift, OrderBlock &zone); }; //+------------------------------------------------------------------+ //| Constructor: Setup parameters and target tracking state | //+------------------------------------------------------------------+ COrderBlockEngine::COrderBlockEngine(string symbol, ENUM_TIMEFRAMES tf, int max_zones) { //--- Initialize internal environment variables m_symbol = (symbol == "") ? _Symbol : symbol; m_timeframe = tf; m_max_zones = max_zones; } //+------------------------------------------------------------------+ //| Destructor: Standard lifecycle cleanup | //+------------------------------------------------------------------+ COrderBlockEngine::~COrderBlockEngine(void) { }
The private section of the COrderBlockEngine class strictly manages our environment variables, locking the targeted financial instrument and timeframe upon initialization. Exposing only the necessary data retrieval method to the public scope protects the internal calculation properties from unauthorized external modifications. GetLatestZone returns a boolean instead of an array. It passes the actual mathematical results by reference using the ampersand operator. This design pattern is a vital performance optimization in MQL5. It allows a single function execution to update the structural parameters without requiring redundant memory copy operations on every tick.
Implementing the Scan and Mitigation Loop
With the class structure declared, we implement the historical scanning logic inside the same include file. The GetLatestZone method processes a local array of MqlRates structures to look for price imbalances. To protect terminal responsiveness during initialization, the algorithm limits its lookup sequence to a tight historical array sample. The code explicitly applies ArraySetAsSeries to ensure chronological tracking coordinates match standard MetaTrader systems.
The core implementation handles the structural search loop inside the class layer. Here, we parse historical candle data arrays step-by-step to isolate institutional footprints and monitor retests.
//+------------------------------------------------------------------+ //| Scans historical bars to detect unmitigated institutional zones | //+------------------------------------------------------------------+ bool COrderBlockEngine::GetLatestZone(int shift, OrderBlock &zone) { //--- Read raw candle data over a window based on max historical lookback MqlRates rates[]; int copied = CopyRates(m_symbol, m_timeframe, shift, m_max_zones + 5, rates); if(copied < 5) return false; ArraySetAsSeries(rates, true); //--- Traverse history from the newest bars backward. for(int i = 1; i < copied - 1; i++) { double body_base = MathAbs(rates[i].close - rates[i].open); double body_displaced = MathAbs(rates[i-1].close - rates[i-1].open); //--- Detect bullish pattern candidate (Bearish candle engulfed/displaced upward) if(rates[i].close < rates[i].open && rates[i-1].close > rates[i-1].open && rates[i-1].close > rates[i].high) { //--- Displacement intensity check: expansion body must be at least 1.5x base body if(body_displaced > body_base * 1.5) { bool mitigated = false; //--- Verify real-time mitigation state between formation bar and requested shift for(int j = i - 1; j >= 0; j--) { if(rates[j].low <= rates[i].low) { mitigated = true; break; } } //--- Populate structural reference if the zone remains fully unmitigated if(!mitigated) { zone.open_price = rates[i].open; zone.high_price = rates[i].high; zone.low_price = rates[i].low; zone.close_price = rates[i].close; zone.creation_time = rates[i].time; zone.direction = 1; zone.is_mitigated = false; return true; } } } //--- Detect bearish pattern candidate (Bullish candle engulfed/displaced downward) if(rates[i].close > rates[i].open && rates[i-1].close < rates[i-1].open && rates[i-1].close < rates[i].low) { if(body_displaced > body_base * 1.5) { bool mitigated = false; for(int j = i - 1; j >= 0; j--) { if(rates[j].high >= rates[i].high) { mitigated = true; break; } } if(!mitigated) { zone.open_price = rates[i].open; zone.high_price = rates[i].high; zone.low_price = rates[i].low; zone.close_price = rates[i].close; zone.creation_time = rates[i].time; zone.direction = -1; zone.is_mitigated = false; return true; } } } } return false; }
The algorithm reads structural candle displacement across the internal data array. The first pass evaluates the open, high, low, and close coordinates of the validation bar. If a bullish candle gets instantly followed by a massive bearish candle, the algorithm flags a bearish zone. One critical trap involves low-liquidity environments. Markets often print small, noisy bars during session rollovers. To filter this out, the engine checks that the breakout candle fully breaks the high or low of the previous base candle. If the breakout candle fails to breach this boundary, the engine drops the pattern instantly to avoid recording false zones.
Building the Visual Diagnostic Indicator
Before plugging this include file into live automated market orders, we must visually verify how zones update across different sessions. We instantiate the COrderBlockEngine class within a custom indicator named Ind_OrderBlock.mq5. This script configures dynamic plotting streams using chart indicator buffers to draw the lines directly onto the price chart.
The header and initialization sections establish the plotting properties and safely bind the data channels to the terminal's drawing layer.
//+------------------------------------------------------------------+ //| Ind_OrderBlock.mq5 | //| Copyright 2026, MetaQuotes Ltd. | //+------------------------------------------------------------------+ #property copyright "Open Source" #property version "1.02" #property indicator_chart_window #property indicator_buffers 2 #property indicator_plots 2 #property indicator_label1 "Bullish OB" #property indicator_type1 DRAW_ARROW #property indicator_color1 clrMediumSeaGreen #property indicator_label2 "Bearish OB" #property indicator_type2 DRAW_ARROW #property indicator_color2 clrCrimson #include <OrderBlockEngine\OrderBlock_Engine.mqh> //--- Buffers and Engine double BufferBull[]; double BufferBear[]; COrderBlockEngine *g_ob_engine; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Bind data array series to terminal drawing layer SetIndexBuffer(0, BufferBull, INDICATOR_DATA); SetIndexBuffer(1, BufferBear, INDICATOR_DATA); //--- Set bullet dot characters for isolated pattern visualization PlotIndexSetInteger(0, PLOT_ARROW, 159); PlotIndexSetInteger(1, PLOT_ARROW, 159); //--- Allocate processing engine dynamically on system heap g_ob_engine = new COrderBlockEngine(_Symbol, PERIOD_CURRENT); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release the pointer to prevent memory leaks if(CheckPointer(g_ob_engine) == POINTER_DYNAMIC) { delete g_ob_engine; } } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ 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[]) { //--- Check total available historical timeline dataset if(rates_total < 5) return 0; int start = (prev_calculated > 0) ? prev_calculated - 1 : 0; //--- Traverse chart history bar by bar sequentially for(int i = start; i < rates_total && !IsStopped(); i++) { int shift = rates_total - 1 - i; OrderBlock active_zone; BufferBull[i] = EMPTY_VALUE; BufferBear[i] = EMPTY_VALUE; if(g_ob_engine.GetLatestZone(shift, active_zone)) { if(active_zone.direction == 1) { BufferBull[i] = active_zone.high_price; } else if(active_zone.direction == -1) { BufferBear[i] = active_zone.low_price; } } } return(rates_total); }
To protect platform CPU efficiency, the calculation script utilizes the prev_calculated parameter. When the indicator first loads, it runs across the entire visible chart history. On every subsequent incoming price tick, the algorithm updates only the current bar. This optimization check prevents the terminal from recalculating thousands of historical bars simultaneously, saving processing power during intense trading hours.

Setting EMPTY VALUE for non-pattern bars and plotting only valid points keeps the chart clean and avoids unnecessary drawing work. Since order blocks are discrete price formations rather than continuous mathematical values, using a standard line plotting buffer would cause the terminal to draw distorted diagonal connections across empty chart bars. Assigning an explicit EMPTY_VALUE to non-pattern bars and mapping valid coordinate instances to specific arrow objects ensures clean visual chart tracking without performance degradation.
With visual confirmation that the engine tracks zones correctly, we build the automated execution shell named EA_OrderBlock.mq5. The script handles market orders by leveraging the native CTrade class library. The global pointer instantiates our inclusion module, keeping the trade execution logic separated from the raw pattern scanning layers.
The execution shell runs within an optimized tick structure to manage positions.
//+------------------------------------------------------------------+ //| EA_OrderBlock.mq5 | //| Copyright 2026, MetaQuotes Ltd. | //+------------------------------------------------------------------+ #property copyright "Open Source" #property version "1.00" #include <Trade\Trade.mqh> #include <OrderBlockEngine\OrderBlock_Engine.mqh> //--- Input Configuration input double InpLotSize = 0.10; // Working fixed order lot size //--- Global Objects COrderBlockEngine *g_ob_engine; CTrade g_trade; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Setup memory space allocation for include engine module g_ob_engine = new COrderBlockEngine(_Symbol, PERIOD_CURRENT); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Remove heap dynamic memory footprints safely if(CheckPointer(g_ob_engine) == POINTER_DYNAMIC) { delete g_ob_engine; } } //+------------------------------------------------------------------+ //| Expert tick function with strict CPU optimization | //+------------------------------------------------------------------+ void OnTick() { static datetime last_bar_time = 0; datetime current_bar_time = iTime(_Symbol, PERIOD_CURRENT, 0); if(current_bar_time != last_bar_time) { last_bar_time = current_bar_time; if(CheckPointer(g_ob_engine) != POINTER_INVALID) { OrderBlock zone; //--- Read structural metrics from the last closed bar index if(g_ob_engine.GetLatestZone(1, zone)) { double current_close = iClose(_Symbol, PERIOD_CURRENT, 1); //--- Verify active netting exposure before routing entry signals if(!PositionSelect(_Symbol)) { if(zone.direction == 1 && current_close > zone.high_price) { g_trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "OB Bullish Displacement Break"); } else if(zone.direction == -1 && current_close < zone.low_price) { g_trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "OB Bearish Displacement Break"); } } } } } }
To avoid intraday noise, the calculation loop runs only on fully closed bars. We avoid this trap by installing a strict new bar gate inside the OnTick handler. By checking the opening timestamp of the current candle, the algorithm authorizes the main loop to execute only once per timeframe cycle.

Note on EA Execution and Level Filtering: As shown in the USDJPY M5 chart above, the engine detects multiple intermediate order block levels while the trade is active. The Expert Advisor intentionally ignores these structural levels because its operational logic gates new entries to periods when no position is active, preventing over-trading. The short position runs undisturbed from 162.491 and automatically closes at 161.444 upon hitting the predefined Take Profit target.
When deploying this expert advisor template to live environments, you must evaluate the account tracking configuration of your target broker. The operational logic uses PositionSelect to identify active market exposure before generating new trade requests. In a standard netting environment, this approach functions perfectly because the platform consolidates transactions into a single directional contract ticket per asset.
However, if you execute this setup on a hedging account, PositionSelect will select only one existing position (depending on the mode). If the user manually opens a separate position or another robot trades on the same symbol, the position lock can mask the core strategy signals. To deploy this framework into a live hedging portfolio safely, you should enhance the selection layer with a magic number filter and a comprehensive position enumeration loop.
Conclusion and Reusable Artifacts
We converted the qualitative idea of “institutional zones” into a small, testable engineering contract and a reusable codebase. Rather than embedding structural logic directly into an Expert Advisor, the solution separates detection, visualization, and execution into independent modules that can be developed and tested consistently.
The resulting framework provides:
-
A clear signal contract: calculations run only on fully closed bars; each zone exposes its base OHLC, creation time, direction, and mitigation state. Zones are automatically removed from active tracking once a later closed bar retests their boundaries.
-
A reusable detection engine: OrderBlock_Engine.mqh implements the displacement and Market Structure Shift (MSS) rules, returns the latest unmitigated zone by reference, and uses safe dynamic allocation with pointer validation.
-
Two integration examples: Ind_OrderBlock.mq5 for visual validation of detected zones, and EA_OrderBlock.mq5 as a production-oriented integration template with a new-bar execution gate and basic position management.
This architecture is intended to serve as a stable foundation rather than a finished trading system. Additional filters such as ATR confirmation, multi-symbol processing, or magic-number-aware position management can be added without changing the underlying detection logic. By keeping structural analysis independent from visualization and trade execution, the same engine can be reused across indicators, Expert Advisors, and future projects while producing consistent results in both testing and live deployment.
File Structure Table
| File Name | Description |
|---|---|
| OrderBlock_Engine.mqh | Source code for the object-oriented tracking class and pattern search loop. |
| Ind_OrderBlock.mq5 | Custom chart indicator for plotting active institutional supply and demand levels. |
| EA_OrderBlock.mq5 | Automated breakout trading expert advisor with a built-in new candle gate filter. |
| MQL5.zip | Complete project archive that can be unpacked directly into the terminal installation directory. |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Persistence Entropy as a Market Regime Indicator in MQL5
Encoding Candlestick Patterns (Part 4): Frequency Analysis for Double-Candlestick Structures
Interactive Supply and Demand Zone Manager in MQL5 (Part III): Zone Analysis, Stateful Interaction, and Pending Event Management
Neural network trading EA based on PatchTST
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use