Page 2 of 2
Re: EURUSD liquidity voids left by stops: how I mark them
Posted: Tue Sep 29, 2026 8:05 am
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.
Re: EURUSD liquidity voids left by stops: how I mark them
Posted: Tue Sep 29, 2026 8:05 am
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);
}
Re: EURUSD liquidity voids left by stops: how I mark them
Posted: Tue Sep 29, 2026 8:06 am
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.
Re: EURUSD liquidity voids left by stops: how I mark them
Posted: Tue Sep 29, 2026 8:07 am
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);
}
Re: EURUSD liquidity voids left by stops: how I mark them
Posted: Tue Sep 29, 2026 8:07 am
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.
Re: EURUSD liquidity voids left by stops: how I mark them
Posted: Tue Sep 29, 2026 8:08 am
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.
Re: EURUSD liquidity voids left by stops: how I mark them
Posted: Tue Sep 29, 2026 8:08 am
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;
}
}
}
Re: EURUSD liquidity voids left by stops: how I mark them
Posted: Tue Sep 29, 2026 8:09 am
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.