Advertisement IC Markets

EURUSD liquidity voids left by stops: how I mark them

Discuss 1-minute to 15-minute price action setups, fading intraday momentum, key support/resistance zones, and proven short-term trading methodologies.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: EURUSD liquidity voids left by stops: how I mark them

Post by PTScalper »

To replicate this state management in MQL5 using an Object-Oriented approach, you can leverage the Standard Library’s CArrayObj collection. This allows you to encapsulate the visual MT5 chart object (the rectangle) and its business logic (extension and mitigation) inside a dedicated class.

Because MQL5 separates the C++ class instance in memory from the actual chart object rendered in the terminal, you can safely destroy the tracking class instance once the void is mitigated, while leaving the historical rectangle on the chart.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Recommended broker for automated trading & scalping IC Markets
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: EURUSD liquidity voids left by stops: how I mark them

Post by PTScalper »

Here is the complete OOP architecture for the indicator:

Code: Select all

#property indicator_chart_window
#include <Arrays\ArrayObj.mqh>

// --- 1. Encapsulate the Void Box Logic ---
class CVoidBox : public CObject
{
private:
    string  m_name;
    double  m_top;
    double  m_bottom;

public:
    // Constructor: Creates the chart object
    CVoidBox(datetime startTime, datetime endTime, double topPrice, double bottomPrice)
    {
        m_top = topPrice;
        m_bottom = bottomPrice;
        
        // Generate a unique name for the MT5 chart object
        m_name = "Void_" + IntegerToString(TimeCurrent()) + "_" + IntegerToString(MathRand());
        
        // Create the rectangle on the main chart (window 0)
        ObjectCreate(0, m_name, OBJ_RECTANGLE, 0, startTime, m_top, endTime, m_bottom);
        
        // Styling: Unmitigated (Red)
        ObjectSetInteger(0, m_name, OBJPROP_COLOR, clrRed);
        ObjectSetInteger(0, m_name, OBJPROP_BACK, true);  // Draw in background
        ObjectSetInteger(0, m_name, OBJPROP_FILL, true);  // Fill the rectangle
    }

    // Destructor: Clean up memory, but DO NOT delete the chart object
    ~CVoidBox()
    {
        // Leaving this empty ensures the gray rectangle remains on the chart historically
    }

    // Extends the right boundary of the box to the current candle
    void Extend(datetime currentTime)
    {
        // OBJPROP_TIME with modifier 1 targets the second time anchor (the right side)
        ObjectSetInteger(0, m_name, OBJPROP_TIME, 1, currentTime);
    }

    // Checks if the current price action has pierced the void
    bool CheckMitigation(double currentHigh)
    {
        if(currentHigh > m_bottom)
        {
            // Update styling to indicate mitigation
            ObjectSetInteger(0, m_name, OBJPROP_COLOR, clrGray);
            return true;
        }
        return false;
    }
};

// --- 2. Global State ---
CArrayObj *ActiveVoids;

// --- 3. Initialization & Cleanup ---
int OnInit()
{
    ActiveVoids = new CArrayObj();
    return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason)
{
    // Clean up memory array
    delete ActiveVoids;
    
    // Sweep the chart to remove all objects generated by this indicator
    ObjectsDeleteAll(0, "Void_");
}

// --- 4. Main Calculation Loop ---
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[])
{
    // Ensure arrays are evaluated chronologically (like Pine Script)
    ArraySetAsSeries(time, true);
    ArraySetAsSeries(high, true);
    ArraySetAsSeries(low, true);
    ArraySetAsSeries(close, true);

    if(rates_total < 5) return(0);

    // Track active voids on every tick
    if(ActiveVoids.Total() > 0)
    {
        // Iterate backwards to safely remove mitigated objects without index shifting
        for(int i = ActiveVoids.Total() - 1; i >= 0; i--)
        {
            CVoidBox *box = ActiveVoids.At(i);
            
            // Extend to current candle time
            box.Extend(time[0]);
            
            // Check for mitigation
            if(box.CheckMitigation(high[0]))
            {
                // Delete(i) removes the pointer from CArrayObj AND calls the CVoidBox destructor.
                // Because our destructor doesn't call ObjectDelete(), the MT5 rectangle stays on chart.
                ActiveVoids.Delete(i); 
            }
        }
    }

    // Limit new detection strictly to newly closed bars
    int limit = rates_total - prev_calculated;
    if(limit > 1) 
    {
        for(int i = limit; i >= 1; i--)
        {
            // Note: You would insert your pivot/fractal detection and sweep validation here.
            // Assuming variables: bool isBearishVoid, double voidTop, double voidBottom
            
            bool isBearishVoid = false; // Replace with actual trigger
            
            if(isBearishVoid)
            {
                double voidTop = low[i+2];
                double voidBottom = high[i];
                
                // Instantiate the new void box and add it to our managed collection
                CVoidBox *newVoid = new CVoidBox(time[i+2], time[i], voidTop, voidBottom);
                ActiveVoids.Add(newVoid);
            }
        }
    }

    return(rates_total);
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: EURUSD liquidity voids left by stops: how I mark them

Post by PTScalper »

Architectural Mechanics

Memory vs. Chart Separation: When ActiveVoids.Delete(i) is called, CArrayObj automatically triggers the class destructor ~CVoidBox() and frees the memory. By leaving the destructor empty, we intentionally leak the visual OBJ_RECTANGLE onto the terminal, turning it into historical context.

Anchor Management: MT5 rectangles are defined by two coordinate pairs (Time1/Price1 and Time2/Price2). ObjectSetInteger(0, m_name, OBJPROP_TIME, 1, currentTime) specifically grabs that second time anchor and pulls it forward to match the live tape.

Array Polarity: ArraySetAsSeries(..., true) is critical when porting from Pine. It forces the MQL5 arrays to act like TradingView's history buffer, where index 0 is the live candle, index 1 is the previous candle, etc.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: EURUSD liquidity voids left by stops: how I mark them

Post by PTScalper »

Because modern MetaTrader 4 (build 600 and later) runs on an under-the-hood MQL5 compiler, you can use the exact same Object-Oriented architecture and Standard Library (CArrayObj) in MQL4.

The primary difference required to make this work in MT4 is adding #property strict at the top of the file. This forces the MT4 compiler to abandon legacy MQL4 rules and enforce modern object-oriented memory management, arrays, and classes.

Here is the exact logic ported natively for MQL4:

Code: Select all

#property strict
#property indicator_chart_window
#include <Arrays\ArrayObj.mqh>

// --- 1. Encapsulate the Void Box Logic ---
class CVoidBox : public CObject
{
private:
    string  m_name;
    double  m_top;
    double  m_bottom;

public:
    // Constructor: Creates the chart object
    CVoidBox(datetime startTime, datetime endTime, double topPrice, double bottomPrice)
    {
        m_top = topPrice;
        m_bottom = bottomPrice;
        
        // Generate a unique name for the MT4 chart object
        m_name = "Void_" + IntegerToString((long)TimeCurrent()) + "_" + IntegerToString(MathRand());
        
        // Create the rectangle on the main chart (window 0)
        ObjectCreate(0, m_name, OBJ_RECTANGLE, 0, startTime, m_top, endTime, m_bottom);
        
        // Styling: Unmitigated (Red)
        ObjectSetInteger(0, m_name, OBJPROP_COLOR, clrRed);
        ObjectSetInteger(0, m_name, OBJPROP_BACK, true);  // Draw in background
        ObjectSetInteger(0, m_name, OBJPROP_FILL, true);  // Fill the rectangle
    }

    // Destructor: Clean up memory, but DO NOT delete the chart object
    ~CVoidBox()
    {
        // Leaving this empty ensures the gray rectangle remains on the chart historically
    }

    // Extends the right boundary of the box to the current candle
    void Extend(datetime currentTime)
    {
        // OBJPROP_TIME with modifier 1 targets the second time anchor (the right side)
        ObjectSetInteger(0, m_name, OBJPROP_TIME, 1, currentTime);
    }

    // Checks if the current price action has pierced the void
    bool CheckMitigation(double currentHigh)
    {
        if(currentHigh > m_bottom)
        {
            // Update styling to indicate mitigation
            ObjectSetInteger(0, m_name, OBJPROP_COLOR, clrGray);
            return true;
        }
        return false;
    }
};

// --- 2. Global State ---
CArrayObj *ActiveVoids;

// --- 3. Initialization & Cleanup ---
int OnInit()
{
    ActiveVoids = new CArrayObj();
    return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason)
{
    // Clean up memory array to prevent MT4 memory leaks
    if(ActiveVoids != NULL) 
    {
        delete ActiveVoids;
    }
    
    // Sweep the chart to remove all objects generated by this indicator
    ObjectsDeleteAll(0, "Void_");
}

// --- 4. Main Calculation Loop ---
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[])
{
    // MT4 passes arrays chronologically (oldest to newest) by default. 
    // Setting them as series ensures index [0] is the current live candle.
    ArraySetAsSeries(time, true);
    ArraySetAsSeries(high, true);
    ArraySetAsSeries(low, true);
    ArraySetAsSeries(close, true);

    if(rates_total < 5) return(0);

    // Track active voids on every tick
    if(ActiveVoids.Total() > 0)
    {
        // Iterate backwards to safely remove mitigated objects without index shifting
        for(int i = ActiveVoids.Total() - 1; i >= 0; i--)
        {
            CVoidBox *box = ActiveVoids.At(i);
            
            // Extend to current candle time
            box.Extend(time[0]);
            
            // Check for mitigation
            if(box.CheckMitigation(high[0]))
            {
                // Deleting the index destroys the pointer and triggers the CVoidBox destructor
                ActiveVoids.Delete(i); 
            }
        }
    }

    // Limit new detection strictly to newly closed bars
    int limit = rates_total - prev_calculated;
    if(limit > 1) 
    {
        for(int i = limit; i >= 1; i--)
        {
            // Insert your MQL4 pivot/fractal detection and sweep validation here.
            // Example placeholders:
            bool isBearishVoid = false; 
            
            if(isBearishVoid)
            {
                double voidTop = low[i+2];
                double voidBottom = high[i];
                
                // Instantiate the new void box and add it to our managed collection
                CVoidBox *newVoid = new CVoidBox(time[i+2], time[i], voidTop, voidBottom);
                ActiveVoids.Add(newVoid);
            }
        }
    }

    return(rates_total);
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: EURUSD liquidity voids left by stops: how I mark them

Post by PTScalper »

MQL4-Specific Details

#property strict: Without this line, MT4 treats arrays and objects using the old MQL4 paradigms, which will cause CArrayObj to throw compilation errors and memory faults.

Type Casting (long): In MQL4, TimeCurrent() returns a datetime type. To concatenate it into the object name safely without compiler warnings under strict mode, it is explicitly cast as a (long) before passing it to IntegerToString().

Pointer Safety Check: MQL4 can occasionally be more sensitive to terminal closures. Wrapping the cleanup inside if(ActiveVoids != NULL) { delete ActiveVoids; } during OnDeinit prevents terminal crashing if you remove the indicator while MT4 is already shutting down.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: EURUSD liquidity voids left by stops: how I mark them

Post by PTScalper »

Because you are working in a modern C# environment with cTrader's Automate API (cAlgo), you have access to strong typing, standard generic collections like List<T>, and a highly responsive chart object model.

In cTrader, Chart.DrawRectangle immediately returns a mutable ChartRectangle object. This makes tracking and extending the objects significantly cleaner than MQL, as you can directly update the .Time2 or .Color properties of the referenced object in memory without needing to query the chart by a string name on every tick.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: EURUSD liquidity voids left by stops: how I mark them

Post by PTScalper »

Here is the complete idiomatic C# implementation for cTrader:

Code: Select all

using System;
using System.Collections.Generic;
using cAlgo.API;
using cAlgo.API.Internals;

namespace cAlgo
{
    [Indicator(IsOverlay = true, TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
    public class PostSweepLiquidityVoids : Indicator
    {
        [Parameter("Pivot Left", DefaultValue = 5)]
        public int PivotLeft { get; set; }

        [Parameter("Pivot Right", DefaultValue = 5)]
        public int PivotRight { get; set; }

        // Encapsulate the chart object and price boundaries
        public class VoidBox
        {
            public ChartRectangle Rectangle { get; set; }
            public double Top { get; set; }
            public double Bottom { get; set; }
        }

        private List<VoidBox> _activeBearishVoids;
        private double _lastPivotHigh = double.NaN;
        private int _lastEvaluatedBar = -1;

        protected override void Initialize()
        {
            _activeBearishVoids = new List<VoidBox>();
        }

        public override void Calculate(int index)
        {
            // Ensure we have enough history
            if (index < PivotLeft + PivotRight + 3)
                return;

            // --- 1. TICK-LEVEL LOGIC: Extend and Mitigate ---
            // cTrader fires Calculate(index) on every incoming tick for the live bar.
            for (int i = _activeBearishVoids.Count - 1; i >= 0; i--)
            {
                var voidBox = _activeBearishVoids[i];
                
                // Dynamically pull the right side of the rectangle forward
                voidBox.Rectangle.Time2 = Bars.OpenTimes[index];

                // Check for mitigation
                if (Bars.HighPrices[index] > voidBox.Bottom)
                {
                    // Visual update for mitigated state
                    voidBox.Rectangle.Color = Color.FromArgb(60, Color.Gray);
                    
                    // Remove from active tracking so we stop extending it
                    _activeBearishVoids.RemoveAt(i);
                }
            }

            // --- 2. BAR-CLOSE LOGIC: Detect New Voids ---
            // Lock this block to execute only once per completed bar
            if (index == _lastEvaluatedBar)
                return;

            int closedIndex = index - 1;

            // Detect and store the Origin Pivot
            int potentialPivotIndex = closedIndex - PivotRight;
            if (IsPivotHigh(potentialPivotIndex, PivotLeft, PivotRight))
            {
                _lastPivotHigh = Bars.HighPrices[potentialPivotIndex];
            }

            // A 3-candle bearish displacement: Low of candle 1 > High of candle 3
            bool isBearishVoid = Bars.LowPrices[closedIndex - 2] > Bars.HighPrices[closedIndex];

            // Validate that the trap/sweep occurred recently (within the last 3 closed bars)
            bool recentSweep = false;
            if (!double.IsNaN(_lastPivotHigh))
            {
                for (int i = 0; i <= 3; i++)
                {
                    int checkIdx = closedIndex - i;
                    // Wick goes above pivot, but body closes below it
                    if (Bars.HighPrices[checkIdx] > _lastPivotHigh && Bars.ClosePrices[checkIdx] < _lastPivotHigh)
                    {
                        recentSweep = true;
                        break;
                    }
                }
            }

            // Create the new void
            if (isBearishVoid && recentSweep)
            {
                double voidTop = Bars.LowPrices[closedIndex - 2];
                double voidBottom = Bars.HighPrices[closedIndex];

                // Ensure a unique ID to prevent chart object collisions
                string boxName = $"Void_{Bars.OpenTimes[closedIndex].Ticks}_{index}";

                // Chart.DrawRectangle returns the object reference immediately
                var rect = Chart.DrawRectangle(boxName, 
                                               Bars.OpenTimes[closedIndex - 2], voidTop, 
                                               Bars.OpenTimes[index], voidBottom, 
                                               Color.FromArgb(85, Color.Red));
                                               
                rect.IsFilled = true;
                rect.IsInteractive = false; // Locks the UI so you don't accidentally drag it

                _activeBearishVoids.Add(new VoidBox
                {
                    Rectangle = rect,
                    Top = voidTop,
                    Bottom = voidBottom
                });
            }

            _lastEvaluatedBar = index;
        }

        // Standard swing high fractal detection
        private bool IsPivotHigh(int index, int left, int right)
        {
            if (index - left < 0 || index + right >= Bars.Count)
                return false;

            double centerHigh = Bars.HighPrices[index];
            
            for (int i = 1; i <= left; i++)
                if (Bars.HighPrices[index - i] > centerHigh) return false;
                
            for (int i = 1; i <= right; i++)
                if (Bars.HighPrices[index + i] >= centerHigh) return false;

            return true;
        }
    }
}
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
PTScalper
Site Admin
Posts: 3349
Joined: Mon Jul 20, 2026 1:28 pm

Re: EURUSD liquidity voids left by stops: how I mark them

Post by PTScalper »

Architectural Notes for cTrader

The _lastEvaluatedBar Lock: In cTrader, Calculate(int index) triggers on every tick for the active, forming bar (index). The code separates the concerns by running the extension/mitigation loop dynamically on every tick, while locking the mathematical detection (pivots and 3-bar displacements) to only run once per completed historical bar (index - 1).

Color.FromArgb: Because you're working natively in .NET, opacity (alpha) is passed as the first parameter (0-255). This allows you to stack shaded voids on top of each other without obscuring the candlesticks underneath.

Garbage Collection: Unlike MQL, there is no need for explicit destructors. When _activeBearishVoids.RemoveAt(i) drops the reference, C#'s garbage collector handles the object in memory, while the ChartRectangle safely persists on the UI thread for historical review.
Preserve your own money. Scale with the market's money. Exponential growth is the ultimate key.
Post Reply