Building an Object-Oriented FVG Scanner in MQL5
Building an Object-Oriented FVG Scanner in MQL5
The transition from retail indicator-based trading to institutional algorithmic modeling requires a fundamental shift in how a developer interprets historical data. Traditional oscillators and moving averages use smoothing formulas. This introduces mathematical lag. Institutional algorithms operate on raw market microstructure. They analyze liquidity voids and structural imbalances created by high-frequency trading.
One of the most critical footprints of institutional order flow is the Fair Value Gap, commonly referred to as an FVG or a liquidity void. Automating the detection of these imbalances provides algorithmic systems with highly precise, unlagged zones of interest for entry and exit routing. This article describes how to build an object-oriented Fair Value Gap scanner in MQL5. We define the three-candle geometry, load data with CopyRates, manage chart objects safely, and add mitigation checks to detect when gaps are filled.
The Microstructure and Mathematical Geometry of Imbalances
Before translating the concept into MQL5 syntax, we must rigidly define the geometry of a Fair Value Gap to prevent false positives. A Fair Value Gap is strictly a three-candle formation that exposes a temporary lack of liquidity on one side of the market order book. In a bullish macroeconomic environment, a bullish FVG occurs when the high of the first candle fails to overlap with the low of the third candle, leaving the entire body of the massive second candle completely unchecked by opposing sellers. Define the indices as follows: 1 = most recently closed candle, 2 = displacement candle, 3 = origin candle. A bullish imbalance is the range between the high of index 3 and the low of index 1.
Conversely, a bearish liquidity void occurs during a violent downside displacement. In this scenario, the low of the origin candle (index 3) remains significantly higher than the high of the most recently closed candle (index 1). The unmitigated space between these two specific price points represents a severe lack of buying liquidity. Institutional algorithms target these coordinates because price often returns to the void to fill orders, and then continues the main trend. Hardcoding this precise three-candle comparative logic is the absolute foundation of our scanning architecture.

Fractal Liquidity and Multi-Timeframe Alignment
Financial markets exhibit a strictly fractal nature, meaning that the micro-structural imbalances detected on a one-minute chart are mathematically and functionally identical to those forming on a weekly timeframe. However, programming an algorithmic system to trade an intraday Fair Value Gap in absolute isolation is a severe statistical flaw. Institutional models overlay a multi-timeframe matrix, demanding that a micro bullish FVG only be validated if it forms within the premium discount zone of a higher-timeframe order block.
This fractal alignment acts as a primary directional filter, aggressively reducing the win-rate degradation caused by market noise. Developers building upon our core FVG scanner class are strongly encouraged to instantiate two separate objects—one reading the hourly baseline and another targeting the five-minute execution chart—ensuring that the local geometric imbalance is heavily supported by the macroeconomic tide before any graphical rendering is authorized on the terminal screen.
Object-Oriented Encapsulation of Price Action
A scanner that evaluates history and draws many chart objects can cause memory issues if implemented linearly. To ensure institutional-grade stability, we must encapsulate the entire detection and rendering pipeline within a dedicated MQL5 class. This approach avoids global variables and allows multiple scanners on different timeframes without shared state. We declare private variables to track the maximum number of historical bars to scan, the specific color configurations for bullish and bearish voids, and a critical string prefix to identify and isolate our specific graphical objects from other manual drawings on the user's chart.
//+------------------------------------------------------------------+ //| Class: CFVGScanner | //| Purpose: Detects, draws, and manages Fair Value Gaps | //+------------------------------------------------------------------+ class CFVGScanner { private: string m_symbol; // Target symbol for the scanner ENUM_TIMEFRAMES m_timeframe; // Target timeframe int m_history_bars; // Depth of historical scan string m_obj_prefix; // Prefix for graphical objects color m_color_bullish; // Color for bullish imbalances color m_color_bearish; // Color for bearish imbalances void DrawRectangle(string name, datetime time1, double price1, datetime time2, double price2, color clr); void ClearOldObjects(void); public: CFVGScanner(string symbol, ENUM_TIMEFRAMES tf, int depth); ~CFVGScanner(void); void ScanAndDraw(void); void SetColors(color bull, color bear); }; //+------------------------------------------------------------------+ //| Constructor: Initializes the environment and routing variables | //+------------------------------------------------------------------+ CFVGScanner::CFVGScanner(string symbol, ENUM_TIMEFRAMES tf, int depth) { m_symbol = (symbol == "") ? _Symbol : symbol; m_timeframe = tf; m_history_bars = (depth < 10) ? 10 : depth; m_obj_prefix = "FVG_" + m_symbol + "_" + EnumToString(m_timeframe) + "_"; m_color_bullish = clrDarkGreen; m_color_bearish = clrMaroon; }
Dynamic Array Synchronization via CopyRates
To evaluate the structural geometry of the market, the algorithm must pull raw Open, High, Low, and Close (OHLC) data directly from the broker's historical server cache. MQL5 provides the highly optimized CopyRates function for this exact operation, populating an array of MqlRates structures. Because we are analyzing a three-candle sequence, we must extract the data dynamically on every new bar formation. We utilize the ArraySetAsSeries function to rigorously format the extracted array as a chronological time series. ArraySetAsSeries reverses the internal indexing so that index zero always permanently represents the current, actively fluctuating, unfinished candle, while index one represents the last fully confirmed historical block.
A crucial element of algorithmic safety involves validating the return value of the CopyRates request. If the terminal is not synchronized or the server disconnects, CopyRates may return fewer bars than requested or a negative error code. If you proceed, the code may access invalid array elements and crash due to an out-of-range error. Our engine intercepts this integer and silently aborts the scan, waiting for the terminal to successfully synchronize the missing data packets.
The Scanning Engine and Mitigation Logic
The core scanning loop iterates backwards through the securely populated MqlRates array. For every confirmed candle, the engine evaluates the relationship between index i (the origin candle), index i-1 (the displacement candle), and index i-2 (the confirmation candle). If the high of the origin candle is strictly lower than the low of the confirmation candle, a bullish liquidity void is mathematically confirmed. However, detecting the gap is only the first logical phase. Financial markets are dynamic, and these gaps are frequently mitigated or filled by subsequent price action hours or days later.
To address this, the algorithm executes a secondary look-ahead loop over subsequent candles immediately after detecting a valid imbalance. The nested loop checks later candles. It marks a bullish gap as mitigated if a low touches the gap, and a bearish gap if a high touches it. If the price action has successfully retraced and touched the origin coordinates, the gap is classified as mitigated and is mathematically disqualified from being rendered on the chart. This rigorous filtration ensures that the algorithmic system only reacts to fresh, active institutional imbalances rather than exhausted historical data.
//+------------------------------------------------------------------+ //| Core Engine: Extracts data, evaluates geometry, and filters voids| //+------------------------------------------------------------------+ void CFVGScanner::ScanAndDraw(void) { MqlRates rates[]; ArraySetAsSeries(rates, true); if(CopyRates(m_symbol, m_timeframe, 0, m_history_bars, rates) < m_history_bars) { PrintFormat("Warning: Failed to synchronize %s history for FVG scanner.", m_symbol); return; } ClearOldObjects(); for(int i = m_history_bars - 1; i >= 3; i--) { double origin_high = rates[i].high; double origin_low = rates[i].low; double confirm_high = rates[i-2].high; double confirm_low = rates[i-2].low; bool is_bullish_fvg = (origin_high < confirm_low); bool is_bearish_fvg = (origin_low > confirm_high); if(is_bullish_fvg) { bool is_mitigated = false; for(int j = i - 3; j >= 1; j--) { if(rates[j].low <= origin_high) { is_mitigated = true; break; } } if(!is_mitigated) { string name = m_obj_prefix + "BULL_" + IntegerToString(i); DrawRectangle(name, rates[i].time, confirm_low, rates[1].time, origin_high, m_color_bullish); } } if(is_bearish_fvg) { bool is_mitigated = false; for(int j = i - 3; j >= 1; j--) { if(rates[j].high >= origin_low) { is_mitigated = true; break; } } if(!is_mitigated) { string name = m_obj_prefix + "BEAR_" + IntegerToString(i); DrawRectangle(name, rates[i].time, origin_low, rates[1].time, confirm_high, m_color_bearish); } } } }
Computational Complexity and Array Traversal Efficiency
When dealing with massive arrays containing thousands of historical MqlRates structures, algorithmic latency becomes a critical performance bottleneck. Inefficiently programmed scanners often deploy brute-force nested loops that evaluate the entire historical chart from start to finish on every tick, exhibiting an exponential Big O complexity of O(N²). This catastrophic design choice causes the terminal to completely freeze during initialization and destroys the frame rate of the user interface.
Our scanning architecture deliberately utilizes a mathematically bounded, single backward-iterating primary loop, maintaining a strictly linear O(N) execution profile. Furthermore, the nested mitigation loop breaks and terminates its execution the exact millisecond a mitigation wick is confirmed, actively preventing redundant CPU cycles. This high-performance memory traversal ensures that the engine can process decades of tick data seamlessly without bottlenecking the trade execution router.
Volume-Backed Displacement and Institutional Validation
Detecting a geometric gap in price action is fundamentally meaningless if the displacement was caused by low-liquidity retail panic rather than true institutional sponsorship. A genuine Fair Value Gap requires massive volume injection during the creation of the displacement candle. Developers must conceptually integrate volume analysis alongside the geometric detection to prevent the algorithm from buying into false breakouts. If the second candle in our three-bar sequence forms a massive body but registers abysmal tick volume from the broker feed, the overarching algorithm should theoretically flag this zone as a low-probability trap.
While our geometric class maps the exact coordinates of the void, advanced quantitative models will overlay the native iVolume function to ensure the displacement candle holds a volume metric significantly higher than the rolling average of the previous twenty periods. This cross-validation filters out algorithmic noise generated during illiquid market sessions and isolates authentic macroeconomic order blocks.
Algorithmic Execution Mechanics: Limit Orders vs. Market Confirmation
Once the scanner accurately maps an unmitigated liquidity void on the terminal chart, the execution engine must decide how to use this data for execution decisions. Novice algorithmic models often place static limit orders exactly at the proximal edge of the Fair Value Gap. This aggressive approach guarantees a rapid entry but exposes the trading account to severe drawdowns if the institutional flow intends to sweep the entire gap violently rather than bounce cleanly off its surface.
A vastly superior architectural design utilizes the FVG strictly as a designated zone of interest rather than a hardcoded execution trigger. The system waits for the price action to penetrate the graphical rectangle and then actively monitors lower-timeframe microstructures for a confirmed reversal signature, such as a moving average crossover or a local break of structure, before firing a market order. This multi-layered execution protocol drastically reduces the overall trade frequency but exponentially increases the mathematical expectancy of the portfolio by tightening the stop-loss parameters precisely around the point of confirmed mitigation.
Bid-Ask Spread Dynamics and Front-Running Phenomena
A theoretical liquidity void mapped perfectly on a charting canvas does not guarantee an immediate or precise order fill due to the mechanical nature of the bid-ask spread and the documented phenomena of institutional front-running. Major liquidity providers frequently defend their positions by placing massive limit order walls just a few fractional pips ahead of the actual geometric gap boundary to ensure execution before the retail crowd.
If an algorithm strictly demands a pixel-perfect touch of the origin candle's extreme price point before authorizing an entry, it will systematically miss highly profitable macroeconomic reversals. Professional developers address this slippage asymmetry by injecting a fractional spread buffer directly into the graphical coordinates, artificially expanding the zone of interest by one or two points. This programmatic tolerance absorbs latency delays and guarantees that the Expert Advisor participates in the institutional mitigation wave even if the market price falls infinitesimally short of absolute structural equilibrium.
Memory Management for Graphical Rectangles
Rendering visual elements directly onto the MetaTrader 5 chart canvas requires the strict manipulation of the ObjectCreate and ObjectSet parameters. A Fair Value Gap is best represented by the OBJ_RECTANGLE class, which requires two distinct time coordinates and two distinct price coordinates to form a bounding box. We project the rectangle from the time of the origin candle forward into the future, stopping at the time of the most recently closed candle to visually drag the zone across the screen. To maintain a professional aesthetic, we explicitly disable the background fill and configure the bounding lines to remain immutable against accidental user clicks.
When creating chart objects programmatically, memory and object lifecycle management are critical. An algorithm running on a one-minute timeframe will execute the scanning loop over a thousand times per day. If old rectangles are not deleted, the terminal may accumulate thousands of objects. This can exhaust resources and freeze the terminal (and possibly the OS). We implement a dedicated destructor and a pre-scan cleanup method. By utilizing the ObjectsDeleteAll function coupled with our unique string prefix, the engine surgically identifies and deletes only its own historical rectangles, leaving the user's manual trendlines completely untouched.
//+------------------------------------------------------------------+ //| Renders the institutional imbalance zone on the chart canvas | //+------------------------------------------------------------------+ void CFVGScanner::DrawRectangle(string name, datetime time1, double price1, datetime time2, double price2, color clr) { if(ObjectFind(0, name) < 0) { ObjectCreate(0, name, OBJ_RECTANGLE, 0, time1, price1, time2, price2); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, name, OBJPROP_WIDTH, 1); ObjectSetInteger(0, name, OBJPROP_BACK, false); ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); } else { ObjectSetInteger(0, name, OBJPROP_TIME, 1, time2); } } //+------------------------------------------------------------------+ //| Prevents memory leaks by purging outdated graphical objects | //+------------------------------------------------------------------+ void CFVGScanner::ClearOldObjects(void) { ObjectsDeleteAll(0, m_obj_prefix); } //+------------------------------------------------------------------+ //| Public method to allow developers to customize the visual output | //+------------------------------------------------------------------+ void CFVGScanner::SetColors(color bull, color bear) { m_color_bullish = bull; m_color_bearish = bear; }
The Institutional Bridge and Final Architecture
To transition this advanced mathematical library into a fully operational trading system, we construct an Expert Advisor that instantiates the CFVGScanner class. A fundamental rule of high-performance quantitative development is protecting the central processing unit from redundant calculations. Market structure geometries, by their very definition, can only physically alter their state when a candle actively closes on the server. Running a heavy array traversal loop on every single incoming market tick is a severe architectural blunder that will overwhelm the Strategy Tester environment and degrade live performance.
We avoid redundant scans by running ScanAndDraw only when a new bar starts. By perpetually tracking the opening time of the current active candle, the algorithm mathematically locks the execution gate, allowing the heavy scanning method to fire exclusively once per timeframe cycle. The development of this automated scanner serves as a crucial bridge between retail indicator programming and institutional price action modeling in MQL5. Implementing strict synchronization protocols with the CopyRates function guarantees that the algorithm reacts exclusively to verified historical data, while aggressive memory management techniques protect the terminal from systemic crashes, leaving the developer with a highly optimized framework capable of integrating Smart Money Concepts seamlessly into any automated portfolio.
//+------------------------------------------------------------------+ //| EA_FVG_Scanner.mq5 | //| Copyright 2026, MetaQuotes Ltd. | //+------------------------------------------------------------------+ #property copyright "Open Source" #property version "1.00" //--- Input Parameters for the Scanner Engine input int InpHistoryDepth = 100; // Historical Bars to Evaluate //--- Global Pointer to the encapsulated scanner object CFVGScanner *g_fvg_engine; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { g_fvg_engine = new CFVGScanner(_Symbol, PERIOD_CURRENT, InpHistoryDepth); g_fvg_engine.SetColors(clrLimeGreen, clrRed); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(CheckPointer(g_fvg_engine) == POINTER_DYNAMIC) { delete g_fvg_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_fvg_engine) != POINTER_INVALID) { g_fvg_engine.ScanAndDraw(); } } } //+------------------------------------------------------------------+
Conclusion
Following the stated requirements, the article produces a compact, extendable implementation that bridges concept to production. The delivered artifacts and guarantees include an object-oriented CFVGScanner class that encapsulates detection, drawing, and lifecycle management with configurable symbol, timeframe, and history depth. It provides deterministic FVG geometry utilizing a strict three-candle comparison for bullish and bearish voids.
The architecture ensures robust data handling through CopyRates and ArraySetAsSeries, featuring validation and a graceful abort when history is insufficient to prevent out-of-range access. Furthermore, a mitigation filter uses a nested look-ahead loop to reject gaps that subsequent price action has already touched, ensuring only active, unmitigated zones are presented. Safe chart rendering is achieved by naming objects with a unique prefix and selectively deleting them to avoid contaminating user drawings and prevent resource leaks. Finally, CPU protection is guaranteed as the EA integration triggers the ScanAndDraw method only on new bar formation rather than on every tick.
The scanner is intentionally designed as a zone-of-interest engine rather than an execution module. Recommended next steps involve overlaying a volume-validation check by comparing the displacement candle volume to a rolling average, and wiring a separate execution layer that uses lower-timeframe confirmation before placing market orders. Because the architecture is completely modular, these extensions can be seamlessly added without changing the core synchronization, filtration, and object-management guarantees.
| File Name | Description |
|---|---|
| EA_FVG_Scanner.mq5 | Complete source code integrating the FVG Class and the CPU optimization loop. |
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.
Engineering Trading Discipline into Code (Part 5): Account-Level Risk Enforcement in MQL5
How to implement AutoARIMA forecasting in MQL5
From Novice to Expert: Creating an MTF CRT Overlay Indicator in MQL5
Graph Theory: Heuristic Search Algorithm (A-Star) Applied in Trading
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use