preview
Building an Object-Oriented Order Block Engine in MQL5

Building an Object-Oriented Order Block Engine in MQL5

MetaTrader 5Trading systems |
488 0
Amanda Vitoria De Paula Pereira
Amanda Vitoria De Paula Pereira

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.

      Print

      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.

      Print

      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.

      Attached files |
      EA_OrderBlock.mq5 (2.74 KB)
      Ind_OrderBlock.mq5 (3.53 KB)
      MQL5.zip (4.63 KB)
      Persistence Entropy as a Market Regime Indicator in MQL5 Persistence Entropy as a Market Regime Indicator in MQL5
      This article turns the verified TDA pipeline into a live MQL5 indicator. It reduces each price window to two persistence-entropy lines (H0 and H1), computes a normalized loop-strength metric with an adaptive percentile band, and places fade marks only when loop strength is high and price hits a window extreme. You can attach the indicator, read six buffers from an Expert Advisor, and tune key window, ranking, and performance parameters.
      Encoding Candlestick Patterns (Part 4): Frequency Analysis for Double-Candlestick Structures Encoding Candlestick Patterns (Part 4): Frequency Analysis for Double-Candlestick Structures
      This article extends single-candlestick analysis to ordered double-candlestick patterns using an MQL5 script. The script encodes candles into symbols, extracts every consecutive two-symbol sequence (treating Aa and aA as different), counts occurrences and percentages, and writes sorted frequency tables to a text file. Readers can quickly identify the most recurrent transitions by symbol, timeframe, and lookback for further statistical testing.
      Interactive Supply and Demand Zone Manager in MQL5 (Part III): Zone Analysis, Stateful Interaction, and Pending Event Management Interactive Supply and Demand Zone Manager in MQL5 (Part III): Zone Analysis, Stateful Interaction, and Pending Event Management
      We extend the stateful supply and demand framework for MetaTrader 5 with a quantitative admission model and a dedicated interaction engine. Candidate zones are scored by structural symmetry, volume participation, and ATR‑normalized displacement, then classified into objective tiers. Admitted zones follow a deterministic lifecycle that tracks first touch, validates bounces, or confirms breakouts, with full telemetry for analysis and reproducibility.
      Neural network trading EA based on PatchTST Neural network trading EA based on PatchTST
      The article presents the revolutionary architecture of PatchTST, a tailored transformer for financial time series analysis that breaks market data into 16-bar patches for efficient processing. We will discuss the full implementation of a trading robot in MQL5 covering everything from mathematical fundamentals and data structures to a ready-made EA with risk management and continuous learning systems.