Checklist fatigue: how I kept my pre-trade list to five lines
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Here is the mechanical translation in MQL5:
Store the Swings: Instead of drawing a line immediately when a fractal is found, we store the time and price of every valid H4/D1 swing in a custom struct.
Calculate the Delta: We iterate through the stored swings and compare recent swings against historical ones.
Apply Tolerance Filter: If the absolute price difference between two swings is less than or equal to InpPointTolerance (e.g., 15 pips), they are flagged as an EQH/EQL pair.
Draw the Zone: A trendline is drawn connecting the older touch to the recent touch and projected into the future as a ray.
Store the Swings: Instead of drawing a line immediately when a fractal is found, we store the time and price of every valid H4/D1 swing in a custom struct.
Calculate the Delta: We iterate through the stored swings and compare recent swings against historical ones.
Apply Tolerance Filter: If the absolute price difference between two swings is less than or equal to InpPointTolerance (e.g., 15 pips), they are flagged as an EQH/EQL pair.
Draw the Zone: A trendline is drawn connecting the older touch to the recent touch and projected into the future as a ray.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Here is the modified custom indicator. Drop this onto your M15 chart during your workstation prep:
Code: Select all
//+------------------------------------------------------------------+
//| Liquidity_Sweeps_EQH_EQL.mq5 |
//| Mechanical MT5 Indicator |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_plots 0
input int InpSwingBars = 5; // Fractal Swing Length (Bars left & right)
input int InpHistory = 500; // Number of HTF bars to scan
input int InpPointTolerance = 150; // Tolerance in Points (150 = 15 pips)
input color InpColorD1 = clrCrimson; // D1 EQH/EQL Color
input color InpColorH4 = clrDarkOrange; // H4 EQH/EQL Color
// Structure to store swing data in memory
struct SwingPoint {
datetime time;
double price;
bool used;
};
//+------------------------------------------------------------------+
//| Custom indicator initialization function |
//+------------------------------------------------------------------+
int OnInit() {
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
ObjectsDeleteAll(0, "EQH_EQL_");
}
//+------------------------------------------------------------------+
//| 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[]) {
// Only recalculate on a new bar to save CPU cycles
if(rates_total == prev_calculated) return rates_total;
ObjectsDeleteAll(0, "EQH_EQL_");
MapEqualLiquidity(PERIOD_D1, "D1", InpColorD1, STYLE_SOLID, 2);
MapEqualLiquidity(PERIOD_H4, "H4", InpColorH4, STYLE_DASH, 1);
ChartRedraw(0);
return(rates_total);
}
//+------------------------------------------------------------------+
//| Core logic to find and pair EQH / EQL pools |
//+------------------------------------------------------------------+
void MapEqualLiquidity(ENUM_TIMEFRAMES tf, string prefix, color clr, int style, int width) {
double h[], l[];
datetime t[];
ArraySetAsSeries(h, true);
ArraySetAsSeries(l, true);
ArraySetAsSeries(t, true);
if(CopyHigh(Symbol(), tf, 0, InpHistory, h) < InpHistory) return;
if(CopyLow(Symbol(), tf, 0, InpHistory, l) < InpHistory) return;
if(CopyTime(Symbol(), tf, 0, InpHistory, t) < InpHistory) return;
SwingPoint highs[];
SwingPoint lows[];
// Step 1: Scan for strict structural extremes and store them
for(int i = InpSwingBars; i < InpHistory - InpSwingBars; i++) {
bool isRes = true;
bool isSup = true;
for(int j = 1; j <= InpSwingBars; j++) {
if(h[i] <= h[i-j] || h[i] <= h[i+j]) isRes = false;
if(l[i] >= l[i-j] || l[i] >= l[i+j]) isSup = false;
}
if(isRes) {
int s = ArraySize(highs);
ArrayResize(highs, s + 1);
highs[s].time = t[i];
highs[s].price = h[i];
highs[s].used = false;
}
if(isSup) {
int s = ArraySize(lows);
ArrayResize(lows, s + 1);
lows[s].time = t[i];
lows[s].price = l[i];
lows[s].used = false;
}
}
double point_size = SymbolInfoDouble(Symbol(), SYMBOL_POINT);
double tolerance = InpPointTolerance * point_size;
// Step 2 & 3: Find EQH pairs (Double/Triple Tops)
// Array is filled from recent (index 0) to older history.
for(int i = 0; i < ArraySize(highs); i++) {
if(highs[i].used) continue;
for(int j = i + 1; j < ArraySize(highs); j++) {
if(!highs[j].used && MathAbs(highs[i].price - highs[j].price) <= tolerance) {
// Pair found! Draw line connecting older point (j) to recent point (i)
DrawLine("EQH_EQL_" + prefix + "_Res_" + TimeToString(highs[i].time), highs[j].time, highs[j].price, highs[i].time, highs[i].price, clr, style, width);
highs[i].used = true;
highs[j].used = true;
break; // Stop after finding the first valid historical pair for this recent swing
}
}
}
// Step 4: Find EQL pairs (Double/Triple Bottoms)
for(int i = 0; i < ArraySize(lows); i++) {
if(lows[i].used) continue;
for(int j = i + 1; j < ArraySize(lows); j++) {
if(!lows[j].used && MathAbs(lows[i].price - lows[j].price) <= tolerance) {
DrawLine("EQH_EQL_" + prefix + "_Sup_" + TimeToString(lows[i].time), lows[j].time, lows[j].price, lows[i].time, lows[i].price, clr, style, width);
lows[i].used = true;
lows[j].used = true;
break;
}
}
}
}
//+------------------------------------------------------------------+
//| Renders a trendline connecting the touches, extended right |
//+------------------------------------------------------------------+
void DrawLine(string name, datetime t1, double p1, datetime t2, double p2, color clr, int style, int width) {
if(ObjectCreate(0, name, OBJ_TREND, 0, t1, p1, t2, p2)) {
ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, true);
ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
ObjectSetInteger(0, name, OBJPROP_STYLE, style);
ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
ObjectSetInteger(0, name, OBJPROP_BACK, true);
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
}
}
//+------------------------------------------------------------------+Re: Checklist fatigue: how I kept my pre-trade list to five lines
Key Differences & Broker Adjustments
InpPointTolerance: Because IC Markets is a 5-digit broker, 150 points equates to 15 pips. For H4/D1 structures, a 10 to 20 pip deviation between two structural touches is entirely standard for a retail double top/bottom. If you want it tighter, lower this to 50 (5 pips).
Visual Presentation: Instead of generating a flat horizontal ray from a single point, the DrawLine function now connects time1 (the older touch) directly to time2 (the recent touch) and projects that exact trajectory forward. This visually confirms exactly which two peaks or valleys formed the retail trap.
InpPointTolerance: Because IC Markets is a 5-digit broker, 150 points equates to 15 pips. For H4/D1 structures, a 10 to 20 pip deviation between two structural touches is entirely standard for a retail double top/bottom. If you want it tighter, lower this to 50 (5 pips).
Visual Presentation: Instead of generating a flat horizontal ray from a single point, the DrawLine function now connects time1 (the older touch) directly to time2 (the recent touch) and projects that exact trajectory forward. This visually confirms exactly which two peaks or valleys formed the retail trap.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Transitioning from MQL5 to C# for cTrader (cAlgo) significantly cleans up the data retrieval process. Instead of manually copying arrays via CopyHigh and CopyLow, cTrader's MarketData.GetBars(TimeFrame) exposes the structural history natively.
Because you requested a cBot rather than an indicator, this is structured as a Robot class. It runs the mapping logic inside the OnBar() method to emulate MT5's prev_calculated == rates_total cycle-saving behavior, recalculating the HTF levels only when a new M15 bar opens.
Because you requested a cBot rather than an indicator, this is structured as a Robot class. It runs the mapping logic inside the OnBar() method to emulate MT5's prev_calculated == rates_total cycle-saving behavior, recalculating the HTF levels only when a new M15 bar opens.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Compile this in cTrader Automate and attach it to your M15 chart:
Code: Select all
using System;
using System.Collections.Generic;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class LiquiditySweepsEQHEQL : Robot
{
[Parameter("Swing Bars (Left & Right)", DefaultValue = 5)]
public int SwingBars { get; set; }
[Parameter("History Bars", DefaultValue = 500)]
public int HistoryBars { get; set; }
[Parameter("Tolerance (Pips)", DefaultValue = 15.0)]
public double TolerancePips { get; set; }
[Parameter("H4 Color", DefaultValue = "DarkOrange")]
public string H4ColorName { get; set; }
[Parameter("D1 Color", DefaultValue = "Crimson")]
public string D1ColorName { get; set; }
private Color _h4Color;
private Color _d1Color;
private class SwingPoint
{
public DateTime Time { get; set; }
public double Price { get; set; }
public bool Used { get; set; }
}
protected override void OnStart()
{
_h4Color = Color.FromName(H4ColorName);
_d1Color = Color.FromName(D1ColorName);
}
protected override void OnBar()
{
// Clear only the bot's generated lines, preserving manual chart markups
var oldLines = Chart.Objects.Where(obj => obj.Name.StartsWith("EQH_") || obj.Name.StartsWith("EQL_")).ToList();
foreach (var line in oldLines)
{
Chart.RemoveObject(line.Name);
}
var d1Bars = MarketData.GetBars(TimeFrame.Daily);
var h4Bars = MarketData.GetBars(TimeFrame.Hour4);
MapEqualLiquidity(d1Bars, "D1", _d1Color, LineStyle.Lines, 2);
MapEqualLiquidity(h4Bars, "H4", _h4Color, LineStyle.LinesToDots, 1);
}
private void MapEqualLiquidity(Bars htfBars, string prefix, Color clr, LineStyle style, int width)
{
var highs = new List<SwingPoint>();
var lows = new List<SwingPoint>();
int startIndex = htfBars.Count - 1 - SwingBars;
int endIndex = Math.Max(0, htfBars.Count - HistoryBars);
// Step 1: Scan for strict structural extremes (Iterating newest to oldest)
for (int i = startIndex; i >= endIndex; i--)
{
bool isRes = true;
bool isSup = true;
for (int j = 1; j <= SwingBars; j++)
{
if (htfBars.HighPrices[i] <= htfBars.HighPrices[i - j] || htfBars.HighPrices[i] <= htfBars.HighPrices[i + j])
isRes = false;
if (htfBars.LowPrices[i] >= htfBars.LowPrices[i - j] || htfBars.LowPrices[i] >= htfBars.LowPrices[i + j])
isSup = false;
}
if (isRes) highs.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.HighPrices[i] });
if (isSup) lows.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.LowPrices[i] });
}
double tolerance = TolerancePips * Symbol.PipSize;
// Step 2 & 3: Pair EQH (Double/Triple Tops)
for (int i = 0; i < highs.Count; i++)
{
if (highs[i].Used) continue;
for (int j = i + 1; j < highs.Count; j++)
{
if (!highs[j].Used && Math.Abs(highs[i].Price - highs[j].Price) <= tolerance)
{
string name = $"EQH_{prefix}_{highs[i].Time:yyyyMMdd_HHmm}";
DrawLine(name, highs[j].Time, highs[j].Price, highs[i].Time, highs[i].Price, clr, style, width);
highs[i].Used = true;
highs[j].Used = true;
break;
}
}
}
// Step 4: Pair EQL (Double/Triple Bottoms)
for (int i = 0; i < lows.Count; i++)
{
if (lows[i].Used) continue;
for (int j = i + 1; j < lows.Count; j++)
{
if (!lows[j].Used && Math.Abs(lows[i].Price - lows[j].Price) <= tolerance)
{
string name = $"EQL_{prefix}_{lows[i].Time:yyyyMMdd_HHmm}";
DrawLine(name, lows[j].Time, lows[j].Price, lows[i].Time, lows[i].Price, clr, style, width);
lows[i].Used = true;
lows[j].Used = true;
break;
}
}
}
}
private void DrawLine(string name, DateTime t1, double p1, DateTime t2, double p2, Color clr, LineStyle style, int width)
{
var line = Chart.DrawTrendLine(name, t1, p1, t2, p2, clr, width, style);
line.IsInteractive = false;
line.ExtendToRight = true; // Projects the trajectory as a ray
}
}
}Re: Checklist fatigue: how I kept my pre-trade list to five lines
Key architectural differences in this .NET implementation:
Pip Scaling: MQL5 requires manual SymbolInfoDouble point conversions. cTrader abstracts this natively via Symbol.PipSize. Passing 15 to TolerancePips scales perfectly across EUR/USD, XAU/USD, or JPY pairs on IC Markets without math adjustments.
Object State Management: Instead of relying on a string prefix and deleting everything, the cBot uses LINQ on Chart.Objects to target and wipe only the script's specific EQH/EQL trendlines. This prevents it from erasing any manual rectangles or fibs you drew during your morning prep.
Array Indexing: cTrader Bars collections are indexed chronologically (index 0 is the oldest bar, Count - 1 is the newest). The primary mapping loop reverses the iteration (i--) to scan from right to left, perfectly mimicking the ArraySetAsSeries behavior from MT5.
Pip Scaling: MQL5 requires manual SymbolInfoDouble point conversions. cTrader abstracts this natively via Symbol.PipSize. Passing 15 to TolerancePips scales perfectly across EUR/USD, XAU/USD, or JPY pairs on IC Markets without math adjustments.
Object State Management: Instead of relying on a string prefix and deleting everything, the cBot uses LINQ on Chart.Objects to target and wipe only the script's specific EQH/EQL trendlines. This prevents it from erasing any manual rectangles or fibs you drew during your morning prep.
Array Indexing: cTrader Bars collections are indexed chronologically (index 0 is the oldest bar, Count - 1 is the newest). The primary mapping loop reverses the iteration (i--) to scan from right to left, perfectly mimicking the ArraySetAsSeries behavior from MT5.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
To automate the execution on the M15 timeframe, we must translate your mental checklist directly into the OnBar() method.
The cBot needs to look at the last completed M15 bar (the one that just closed) and evaluate three things:
The Sweep: Did the bar's Low pierce below the EQL, but Close back above it? (Or vice versa for EQH).
The Open: Did the bar open above the EQL? If it opened below, the structure was already broken, invalidating the trap.
The Spread: Is the IC Markets Raw spread within normal bounds? Sweeps often happen during news, where spreads can blow out. If the spread is wider than your maximum tolerance, the bot aborts the execution to protect you from extreme slippage.
The cBot needs to look at the last completed M15 bar (the one that just closed) and evaluate three things:
The Sweep: Did the bar's Low pierce below the EQL, but Close back above it? (Or vice versa for EQH).
The Open: Did the bar open above the EQL? If it opened below, the structure was already broken, invalidating the trap.
The Spread: Is the IC Markets Raw spread within normal bounds? Sweeps often happen during news, where spreads can blow out. If the spread is wider than your maximum tolerance, the bot aborts the execution to protect you from extreme slippage.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Here is the complete, modified cBot with the execution and spread-filter logic added:
Code: Select all
using System;
using System.Collections.Generic;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class LiquiditySweepsEQHEQL_AutoExec : Robot
{
[Header("Structural Mapping")]
[Parameter("Swing Bars (Left & Right)", DefaultValue = 5)]
public int SwingBars { get; set; }
[Parameter("History Bars", DefaultValue = 500)]
public int HistoryBars { get; set; }
[Parameter("EQH/EQL Tolerance (Pips)", DefaultValue = 15.0)]
public double TolerancePips { get; set; }
[Header("Execution Rules")]
[Parameter("Trade Volume (Lots)", DefaultValue = 1.0)]
public double QuantityInLots { get; set; }
[Parameter("Stop Loss (Pips)", DefaultValue = 10.0)]
public double StopLossPips { get; set; }
[Parameter("Take Profit (Pips)", DefaultValue = 30.0)]
public double TakeProfitPips { get; set; }
[Parameter("Max Spread (Pips)", DefaultValue = 1.5)]
public double MaxSpreadPips { get; set; }
private Color _h4Color = Color.DarkOrange;
private Color _d1Color = Color.Crimson;
private class SwingPoint
{
public DateTime Time { get; set; }
public double Price { get; set; }
public bool Used { get; set; }
}
protected override void OnBar()
{
// 1. Clear old drawn lines
var oldLines = Chart.Objects.Where(obj => obj.Name.StartsWith("EQH_") || obj.Name.StartsWith("EQL_")).ToList();
foreach (var line in oldLines)
{
Chart.RemoveObject(line.Name);
}
// 2. Map structural levels and retrieve active prices
var d1Bars = MarketData.GetBars(TimeFrame.Daily);
var h4Bars = MarketData.GetBars(TimeFrame.Hour4);
List<double> d1EQH, d1EQL, h4EQH, h4EQL;
MapEqualLiquidity(d1Bars, "D1", _d1Color, LineStyle.Lines, 2, out d1EQH, out d1EQL);
MapEqualLiquidity(h4Bars, "H4", _h4Color, LineStyle.LinesToDots, 1, out h4EQH, out h4EQL);
var activeEQH = d1EQH.Concat(h4EQH).ToList();
var activeEQL = d1EQL.Concat(h4EQL).ToList();
// 3. Evaluate the M15 Execution Trigger
EvaluateSweepExecution(activeEQH, activeEQL);
}
private void EvaluateSweepExecution(List<double> activeEQH, List<double> activeEQL)
{
// Only trigger if we don't already have an open position
if (Positions.FindAll("LiqSweep", SymbolName).Length > 0) return;
// Enforce Checklist Item #3: Spread normality
if ((Symbol.Spread / Symbol.PipSize) > MaxSpreadPips) return;
// Get the M15 bar that just closed (Last(1))
var lastBar = Bars.Last(1);
double volumeInUnits = Symbol.QuantityToVolumeInUnits(QuantityInLots);
// Check Bearish Reversals (Shorting an EQH Sweep)
foreach (var eqh in activeEQH)
{
// Must open below, pierce above, and close back below
if (lastBar.Open < eqh && lastBar.High > eqh && lastBar.Close < eqh)
{
ExecuteMarketOrder(TradeType.Sell, SymbolName, volumeInUnits, "LiqSweep", StopLossPips, TakeProfitPips);
return; // Prevent multiple executions if levels overlap
}
}
// Check Bullish Reversals (Buying an EQL Sweep)
foreach (var eql in activeEQL)
{
// Must open above, pierce below, and close back above
if (lastBar.Open > eql && lastBar.Low < eql && lastBar.Close > eql)
{
ExecuteMarketOrder(TradeType.Buy, SymbolName, volumeInUnits, "LiqSweep", StopLossPips, TakeProfitPips);
return;
}
}
}
private void MapEqualLiquidity(Bars htfBars, string prefix, Color clr, LineStyle style, int width, out List<double> activeEQH, out List<double> activeEQL)
{
activeEQH = new List<double>();
activeEQL = new List<double>();
var highs = new List<SwingPoint>();
var lows = new List<SwingPoint>();
int startIndex = htfBars.Count - 1 - SwingBars;
int endIndex = Math.Max(0, htfBars.Count - HistoryBars);
for (int i = startIndex; i >= endIndex; i--)
{
bool isRes = true, isSup = true;
for (int j = 1; j <= SwingBars; j++)
{
if (htfBars.HighPrices[i] <= htfBars.HighPrices[i - j] || htfBars.HighPrices[i] <= htfBars.HighPrices[i + j]) isRes = false;
if (htfBars.LowPrices[i] >= htfBars.LowPrices[i - j] || htfBars.LowPrices[i] >= htfBars.LowPrices[i + j]) isSup = false;
}
if (isRes) highs.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.HighPrices[i] });
if (isSup) lows.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.LowPrices[i] });
}
double tolerance = TolerancePips * Symbol.PipSize;
for (int i = 0; i < highs.Count; i++)
{
if (highs[i].Used) continue;
for (int j = i + 1; j < highs.Count; j++)
{
if (!highs[j].Used && Math.Abs(highs[i].Price - highs[j].Price) <= tolerance)
{
DrawLine($"EQH_{prefix}_{highs[i].Time:yyyyMMdd_HHmm}", highs[j].Time, highs[j].Price, highs[i].Time, highs[i].Price, clr, style, width);
activeEQH.Add(highs[i].Price);
highs[i].Used = true; highs[j].Used = true;
break;
}
}
}
for (int i = 0; i < lows.Count; i++)
{
if (lows[i].Used) continue;
for (int j = i + 1; j < lows.Count; j++)
{
if (!lows[j].Used && Math.Abs(lows[i].Price - lows[j].Price) <= tolerance)
{
DrawLine($"EQL_{prefix}_{lows[i].Time:yyyyMMdd_HHmm}", lows[j].Time, lows[j].Price, lows[i].Time, lows[i].Price, clr, style, width);
activeEQL.Add(lows[i].Price);
lows[i].Used = true; lows[j].Used = true;
break;
}
}
}
}
private void DrawLine(string name, DateTime t1, double p1, DateTime t2, double p2, Color clr, LineStyle style, int width)
{
var line = Chart.DrawTrendLine(name, t1, p1, t2, p2, clr, width, style);
line.IsInteractive = false;
line.ExtendToRight = true;
}
}
}Re: Checklist fatigue: how I kept my pre-trade list to five lines
Because im executing M15 lower-timeframe scalps on IC Markets Raw ECN accounts, moving the stop loss to breakeven requires two critical architectural adjustments:
Shift to OnTick() for Management: Your entry logic sits perfectly in OnBar() to wait for confirmed closures, but trade management must run in OnTick(). If price spikes to your 1:1 R:R target and immediately reverses within the same 15-minute candle, an OnBar() manager will completely miss the trigger and let a winning scalp turn into a full loss.
The Commission Buffer: A true "breakeven" (entry price) on a Raw ECN account actually results in a minor loss due to the commission ($7 per standard lot round trip). To make it a true risk-free trade, the bot must lock the stop loss at Entry + a fractional pip buffer (usually 0.5 to 0.7 pips) to cover the IC Markets commission structure.
Shift to OnTick() for Management: Your entry logic sits perfectly in OnBar() to wait for confirmed closures, but trade management must run in OnTick(). If price spikes to your 1:1 R:R target and immediately reverses within the same 15-minute candle, an OnBar() manager will completely miss the trigger and let a winning scalp turn into a full loss.
The Commission Buffer: A true "breakeven" (entry price) on a Raw ECN account actually results in a minor loss due to the commission ($7 per standard lot round trip). To make it a true risk-free trade, the bot must lock the stop loss at Entry + a fractional pip buffer (usually 0.5 to 0.7 pips) to cover the IC Markets commission structure.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Here is the fully integrated C# cBot with the dynamic R:R Breakeven module added.
Code: Select all
using System;
using System.Collections.Generic;
using System.Linq;
using cAlgo.API;
using cAlgo.API.Internals;
namespace cAlgo.Robots
{
[Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)]
public class LiquiditySweepsEQHEQL_AutoExec : Robot
{
[Header("Structural Mapping")]
[Parameter("Swing Bars (Left & Right)", DefaultValue = 5)]
public int SwingBars { get; set; }
[Parameter("History Bars", DefaultValue = 500)]
public int HistoryBars { get; set; }
[Parameter("EQH/EQL Tolerance (Pips)", DefaultValue = 15.0)]
public double TolerancePips { get; set; }
[Header("Execution Rules")]
[Parameter("Trade Volume (Lots)", DefaultValue = 1.0)]
public double QuantityInLots { get; set; }
[Parameter("Stop Loss (Pips)", DefaultValue = 10.0)]
public double StopLossPips { get; set; }
[Parameter("Take Profit (Pips)", DefaultValue = 30.0)]
public double TakeProfitPips { get; set; }
[Parameter("Max Spread (Pips)", DefaultValue = 1.5)]
public double MaxSpreadPips { get; set; }
[Header("Trade Management (Breakeven)")]
[Parameter("Use Auto Breakeven", DefaultValue = true)]
public bool UseBreakeven { get; set; }
[Parameter("Trigger at R:R (1.0 = 1:1 Target)", DefaultValue = 1.0)]
public double BreakevenTriggerRR { get; set; }
[Parameter("Commission Buffer (Pips)", DefaultValue = 0.6)]
public double CommissionBufferPips { get; set; }
private Color _h4Color = Color.DarkOrange;
private Color _d1Color = Color.Crimson;
private class SwingPoint
{
public DateTime Time { get; set; }
public double Price { get; set; }
public bool Used { get; set; }
}
// Executed every tick (Price change)
protected override void OnTick()
{
if (!UseBreakeven) return;
var positions = Positions.FindAll("LiqSweep", SymbolName);
foreach (var position in positions)
{
double triggerPips = StopLossPips * BreakevenTriggerRR;
double profitInPips = position.TradeType == TradeType.Buy
? (Symbol.Bid - position.EntryPrice) / Symbol.PipSize
: (position.EntryPrice - Symbol.Ask) / Symbol.PipSize;
// Check if R:R threshold is reached
if (profitInPips >= triggerPips)
{
double newStopLoss = position.TradeType == TradeType.Buy
? position.EntryPrice + (CommissionBufferPips * Symbol.PipSize)
: position.EntryPrice - (CommissionBufferPips * Symbol.PipSize);
// Ensure we don't spam server if SL is already at or better than BE
bool requiresUpdate = false;
if (position.TradeType == TradeType.Buy && (position.StopLoss == null || position.StopLoss < newStopLoss - (0.1 * Symbol.PipSize)))
requiresUpdate = true;
if (position.TradeType == TradeType.Sell && (position.StopLoss == null || position.StopLoss > newStopLoss + (0.1 * Symbol.PipSize)))
requiresUpdate = true;
if (requiresUpdate)
{
// Fire asynchronously to avoid blocking the tick thread
ModifyPositionAsync(position, newStopLoss, position.TakeProfit);
Print($"Breakeven triggered for position {position.Id} at {BreakevenTriggerRR} R:R.");
}
}
}
}
// Executed only on candle close (M15 mapping & entry)
protected override void OnBar()
{
var oldLines = Chart.Objects.Where(obj => obj.Name.StartsWith("EQH_") || obj.Name.StartsWith("EQL_")).ToList();
foreach (var line in oldLines) Chart.RemoveObject(line.Name);
var d1Bars = MarketData.GetBars(TimeFrame.Daily);
var h4Bars = MarketData.GetBars(TimeFrame.Hour4);
List<double> d1EQH, d1EQL, h4EQH, h4EQL;
MapEqualLiquidity(d1Bars, "D1", _d1Color, LineStyle.Lines, 2, out d1EQH, out d1EQL);
MapEqualLiquidity(h4Bars, "H4", _h4Color, LineStyle.LinesToDots, 1, out h4EQH, out h4EQL);
var activeEQH = d1EQH.Concat(h4EQH).ToList();
var activeEQL = d1EQL.Concat(h4EQL).ToList();
EvaluateSweepExecution(activeEQH, activeEQL);
}
private void EvaluateSweepExecution(List<double> activeEQH, List<double> activeEQL)
{
if (Positions.FindAll("LiqSweep", SymbolName).Length > 0) return;
if ((Symbol.Spread / Symbol.PipSize) > MaxSpreadPips) return;
var lastBar = Bars.Last(1);
double volumeInUnits = Symbol.QuantityToVolumeInUnits(QuantityInLots);
foreach (var eqh in activeEQH)
{
if (lastBar.Open < eqh && lastBar.High > eqh && lastBar.Close < eqh)
{
ExecuteMarketOrder(TradeType.Sell, SymbolName, volumeInUnits, "LiqSweep", StopLossPips, TakeProfitPips);
return;
}
}
foreach (var eql in activeEQL)
{
if (lastBar.Open > eql && lastBar.Low < eql && lastBar.Close > eql)
{
ExecuteMarketOrder(TradeType.Buy, SymbolName, volumeInUnits, "LiqSweep", StopLossPips, TakeProfitPips);
return;
}
}
}
private void MapEqualLiquidity(Bars htfBars, string prefix, Color clr, LineStyle style, int width, out List<double> activeEQH, out List<double> activeEQL)
{
activeEQH = new List<double>();
activeEQL = new List<double>();
var highs = new List<SwingPoint>();
var lows = new List<SwingPoint>();
int startIndex = htfBars.Count - 1 - SwingBars;
int endIndex = Math.Max(0, htfBars.Count - HistoryBars);
for (int i = startIndex; i >= endIndex; i--)
{
bool isRes = true, isSup = true;
for (int j = 1; j <= SwingBars; j++)
{
if (htfBars.HighPrices[i] <= htfBars.HighPrices[i - j] || htfBars.HighPrices[i] <= htfBars.HighPrices[i + j]) isRes = false;
if (htfBars.LowPrices[i] >= htfBars.LowPrices[i - j] || htfBars.LowPrices[i] >= htfBars.LowPrices[i + j]) isSup = false;
}
if (isRes) highs.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.HighPrices[i], Used = false });
if (isSup) lows.Add(new SwingPoint { Time = htfBars.OpenTimes[i], Price = htfBars.LowPrices[i], Used = false });
}
double tolerance = TolerancePips * Symbol.PipSize;
for (int i = 0; i < highs.Count; i++)
{
if (highs[i].Used) continue;
for (int j = i + 1; j < highs.Count; j++)
{
if (!highs[j].Used && Math.Abs(highs[i].Price - highs[j].Price) <= tolerance)
{
DrawLine($"EQH_{prefix}_{highs[i].Time:yyyyMMdd_HHmm}", highs[j].Time, highs[j].Price, highs[i].Time, highs[i].Price, clr, style, width);
activeEQH.Add(highs[i].Price);
highs[i].Used = true; highs[j].Used = true;
break;
}
}
}
for (int i = 0; i < lows.Count; i++)
{
if (lows[i].Used) continue;
for (int j = i + 1; j < lows.Count; j++)
{
if (!lows[j].Used && Math.Abs(lows[i].Price - lows[j].Price) <= tolerance)
{
DrawLine($"EQL_{prefix}_{lows[i].Time:yyyyMMdd_HHmm}", lows[j].Time, lows[j].Price, lows[i].Time, lows[i].Price, clr, style, width);
activeEQL.Add(lows[i].Price);
lows[i].Used = true; lows[j].Used = true;
break;
}
}
}
}
private void DrawLine(string name, DateTime t1, double p1, DateTime t2, double p2, Color clr, LineStyle style, int width)
{
var line = Chart.DrawTrendLine(name, t1, p1, t2, p2, clr, width, style);
line.IsInteractive = false; line.ExtendToRight = true;
}
}
}