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
Architectural Upgrades for Pine Script
One-and-Done Garbage Collection: In the MT5/cTrader versions, the lines were drawn statically. In this script, once the M15 candle sweeps the exact level and closes back inside, the array.remove() function deletes that specific EQH/EQL price from active memory. It marks the chart with a colored triangle and will never alert you for that specific liquidity pool again, preventing duplicate webhook executions on choppy consolidations.
barmerge.lookahead_off: This guarantees zero repainting. If you use this script on a live chart, the H4/D1 structures will only lock in and draw the line after the higher timeframe candle has fully closed and confirmed the right shoulder of the swing.
One-and-Done Garbage Collection: In the MT5/cTrader versions, the lines were drawn statically. In this script, once the M15 candle sweeps the exact level and closes back inside, the array.remove() function deletes that specific EQH/EQL price from active memory. It marks the chart with a colored triangle and will never alert you for that specific liquidity pool again, preventing duplicate webhook executions on choppy consolidations.
barmerge.lookahead_off: This guarantees zero repainting. If you use this script on a live chart, the H4/D1 structures will only lock in and draw the line after the higher timeframe candle has fully closed and confirmed the right shoulder of the swing.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Transitioning this logic to MQL4 for MetaTrader 4 actually simplifies the data retrieval process compared to MQL5. Because MT4 treats historical price data natively as timeseries arrays (newest bar is index 0), we do not need to use CopyHigh, CopyLow, or ArraySetAsSeries. We can query the higher timeframes directly using iHigh() and iLow().
Re: Checklist fatigue: how I kept my pre-trade list to five lines
This is built as a Custom Indicator (.mq4). Drop it into your MQL4\Indicators folder and compile it in MetaEditor.
Code: Select all
//+------------------------------------------------------------------+
//| Liquidity_Sweeps_EQH_EQL_MT4.mq4 |
//| Mechanical MT4 Indicator |
//+------------------------------------------------------------------+
#property copyright "Forex, Stocks & Me"
#property link "https://forex-scalping.com"
#property version "1.00"
#property strict
#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 when a new M15 bar opens 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_DASHDOT, 1);
ChartRedraw(0);
return(rates_total);
}
//+------------------------------------------------------------------+
//| Core logic to find and pair EQH / EQL pools |
//+------------------------------------------------------------------+
void MapEqualLiquidity(int tf, string prefix, color clr, int style, int width) {
SwingPoint highs[];
SwingPoint lows[];
// Step 1: Scan for strict structural extremes using MT4 native iHigh/iLow
// Index 0 is current forming bar, we scan backwards into history
for(int i = InpSwingBars; i < InpHistory - InpSwingBars; i++) {
bool isRes = true;
bool isSup = true;
for(int j = 1; j <= InpSwingBars; j++) {
if(iHigh(Symbol(), tf, i) <= iHigh(Symbol(), tf, i-j) || iHigh(Symbol(), tf, i) <= iHigh(Symbol(), tf, i+j)) isRes = false;
if(iLow(Symbol(), tf, i) >= iLow(Symbol(), tf, i-j) || iLow(Symbol(), tf, i) >= iLow(Symbol(), tf, i+j)) isSup = false;
}
if(isRes) {
int s = ArraySize(highs);
ArrayResize(highs, s + 1);
highs[s].time = iTime(Symbol(), tf, i);
highs[s].price = iHigh(Symbol(), tf, i);
highs[s].used = false;
}
if(isSup) {
int s = ArraySize(lows);
ArrayResize(lows, s + 1);
lows[s].time = iTime(Symbol(), tf, i);
lows[s].price = iLow(Symbol(), tf, i);
lows[s].used = false;
}
}
// MT4 natively provides the Point variable based on broker digits
double tolerance = InpPointTolerance * Point;
// Step 2 & 3: Find EQH pairs (Double/Triple Tops)
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) {
// Connect older touch (j) to recent touch (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;
}
}
}
// 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
MT4-Specific Mechanics
The Point Variable: Unlike MQL5 which requires calling SymbolInfoDouble(), MQL4 automatically assigns the Point variable per chart. On IC Markets MT4 (5-digit broker), Point equals 0.00001. Inputting 150 into InpPointTolerance cleanly translates to 15 pips.
Time Series Indexing: iHigh(), iLow(), and iTime() automatically pull data backward in time. i = 0 is the active candle, and i = 10 is ten candles ago. The loop logic handles this natively without needing ArraySetAsSeries().
Execution Limitations: This is purely a visual indicator. If you want this to auto-execute your exponential risk management model on MT4 the way the cBot does, you must compile it as an Expert Advisor (.mq4 placed in the Experts folder) and use MT4's OrderSend() function instead of drawing lines.
The Point Variable: Unlike MQL5 which requires calling SymbolInfoDouble(), MQL4 automatically assigns the Point variable per chart. On IC Markets MT4 (5-digit broker), Point equals 0.00001. Inputting 150 into InpPointTolerance cleanly translates to 15 pips.
Time Series Indexing: iHigh(), iLow(), and iTime() automatically pull data backward in time. i = 0 is the active candle, and i = 10 is ten candles ago. The loop logic handles this natively without needing ArraySetAsSeries().
Execution Limitations: This is purely a visual indicator. If you want this to auto-execute your exponential risk management model on MT4 the way the cBot does, you must compile it as an Expert Advisor (.mq4 placed in the Experts folder) and use MT4's OrderSend() function instead of drawing lines.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Since we already built the MQL5 Indicator earlier in this thread, bringing "that" back to MT5 means upgrading from a simple visual indicator to a full-blown Expert Advisor (EA).
This will take all the advanced logic we just engineered for the cBot—the Exponential Risk Engine, Partial Profit Closures, Breakeven Stop, Trailing Stop, and Auto-Execution—and translate it directly into MQL5 object-oriented code.
Because MT5 handles partial closures and tick-events differently than cTrader, I have implemented an array-based state tracker (_partiallyClosedTickets) to ensure the EA doesn't get caught in an infinite loop when closing 50% of your position at the 1:1 target.
This will take all the advanced logic we just engineered for the cBot—the Exponential Risk Engine, Partial Profit Closures, Breakeven Stop, Trailing Stop, and Auto-Execution—and translate it directly into MQL5 object-oriented code.
Because MT5 handles partial closures and tick-events differently than cTrader, I have implemented an array-based state tracker (_partiallyClosedTickets) to ensure the EA doesn't get caught in an infinite loop when closing 50% of your position at the 1:1 target.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Save this in your MQL5\Experts folder (not the Indicators folder) and compile it as LiquiditySweeps_Master_EA.mq5.
Code: Select all
//+------------------------------------------------------------------+
//| LiquiditySweeps_Master_EA.mq5|
//| Mechanical MT5 Auto-Exec EA |
//+------------------------------------------------------------------+
#property copyright "Forex, Stocks & Me"
#property link "https://forex-scalping.com"
#property version "1.00"
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
// === INPUTS ===
sinput string grp1 = "--- Structural Mapping ---";
input int InpSwingBars = 5; // Fractal Swing (Bars left & right)
input int InpHistory = 500; // HTF Bars to scan
input double InpTolerancePips = 15.0; // EQH/EQL Tolerance (Pips)
sinput string grp2 = "--- Exponential Risk Engine ---";
input bool InpUseExponential = true; // Use Accrued Profit Scaling?
input double InpStartingBalance= 1000.0; // Series Starting Balance
input double InpBaseRisk = 20.0; // Base Risk (Currency)
input double InpProfitScalePct = 50.0; // Profit Reinvestment (%)
input bool InpLockPrincipal = true; // Lock Principal (Risk ONLY Profits)
input double InpStaticLots = 1.0; // Static Fallback Volume
sinput string grp3 = "--- Execution Rules ---";
input double InpStopLossPips = 10.0; // Stop Loss (Pips)
input double InpTakeProfitPips = 50.0; // Take Profit (Pips)
input double InpMaxSpreadPips = 1.5; // Max Spread (Pips)
sinput string grp4 = "--- Trade Management (BE, Trail, Partial) ---";
input double InpManagementRR = 1.0; // Trigger Management at R:R
input bool InpUsePartial = true; // Take Partial Profit?
input double InpPartialPct = 50.0; // Partial Profit (%)
input bool InpUseBreakeven = true; // Move to Breakeven?
input double InpCommBufferPips = 0.6; // Commission Buffer (Pips)
input bool InpUseTrailing = true; // Use Trailing Stop after BE?
input double InpTrailDistPips = 15.0; // Trailing Distance (Pips)
input double InpTrailStepPips = 2.0; // Trailing Step (Pips)
input ulong InpMagicNumber = 888888; // EA Magic Number
// === GLOBALS & CLASSES ===
CTrade trade;
CPositionInfo pos;
ulong partiallyClosedTickets[]; // State tracker to prevent infinite loops
struct SwingPoint {
datetime time;
double price;
bool used;
};
//+------------------------------------------------------------------+
//| Initialization |
//+------------------------------------------------------------------+
int OnInit() {
trade.SetExpertMagicNumber(InpMagicNumber);
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason) {
ObjectsDeleteAll(0, "EQH_EQL_");
}
//+------------------------------------------------------------------+
//| Helper: Dynamic Pip Sizing for 5-Digit Brokers (IC Markets) |
//+------------------------------------------------------------------+
double GetPipSize() {
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
return (digits == 5 || digits == 3) ? _Point * 10.0 : _Point;
}
//+------------------------------------------------------------------+
//| Helper: State Tracking for Partial Closes |
//+------------------------------------------------------------------+
bool IsPartiallyClosed(ulong ticket) {
for(int i = 0; i < ArraySize(partiallyClosedTickets); i++) {
if(partiallyClosedTickets[i] == ticket) return true;
}
return false;
}
void AddPartiallyClosed(ulong ticket) {
int s = ArraySize(partiallyClosedTickets);
ArrayResize(partiallyClosedTickets, s + 1);
partiallyClosedTickets[s] = ticket;
}
//+------------------------------------------------------------------+
//| Core Tick Loop (Trade Management) |
//+------------------------------------------------------------------+
void OnTick() {
ManageActivePositions();
// Check for new M15 Bar for mapping and entry logic
static datetime last_bar = 0;
datetime curr_bar = (datetime)SeriesInfoInteger(_Symbol, PERIOD_M15, SERIES_LASTBAR_DATE);
if(curr_bar != last_bar) {
last_bar = curr_bar;
ProcessM15BarClose();
}
}
//+------------------------------------------------------------------+
//| Trade Management Module (Tick by Tick) |
//+------------------------------------------------------------------+
void ManageActivePositions() {
double pip = GetPipSize();
double trigger_dist = InpStopLossPips * InpManagementRR * pip;
for(int i = PositionsTotal() - 1; i >= 0; i--) {
if(pos.SelectByIndex(i) && pos.Symbol() == _Symbol && pos.Magic() == InpMagicNumber) {
double open_price = pos.PriceOpen();
double current_sl = pos.StopLoss();
ulong ticket = pos.Ticket();
double profit_dist = 0;
if(pos.PositionType() == POSITION_TYPE_BUY) profit_dist = SymbolInfoDouble(_Symbol, SYMBOL_BID) - open_price;
if(pos.PositionType() == POSITION_TYPE_SELL) profit_dist = open_price - SymbolInfoDouble(_Symbol, SYMBOL_ASK);
// Activate management when R:R target is hit
if(profit_dist >= trigger_dist) {
// 1. Partial Close
if(InpUsePartial && !IsPartiallyClosed(ticket)) {
double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double min_vol = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double vol_to_close = MathFloor((pos.Volume() * (InpPartialPct / 100.0)) / step) * step;
if(vol_to_close >= min_vol && pos.Volume() > vol_to_close) {
if(trade.PositionClosePartial(ticket, vol_to_close)) {
AddPartiallyClosed(ticket); // Lock to prevent loop
Print("Partial 50% closed for ticket: ", ticket);
}
}
}
// 2. Breakeven & Trailing Stop
if(InpUseBreakeven) {
double comm_buffer = InpCommBufferPips * pip;
double be_sl = (pos.PositionType() == POSITION_TYPE_BUY) ? open_price + comm_buffer : open_price - comm_buffer;
double target_sl = be_sl;
if(InpUseTrailing) {
double trail_sl = (pos.PositionType() == POSITION_TYPE_BUY)
? SymbolInfoDouble(_Symbol, SYMBOL_BID) - (InpTrailDistPips * pip)
: SymbolInfoDouble(_Symbol, SYMBOL_ASK) + (InpTrailDistPips * pip);
target_sl = (pos.PositionType() == POSITION_TYPE_BUY) ? MathMax(target_sl, trail_sl) : MathMin(target_sl, trail_sl);
}
double step_buffer = InpTrailStepPips * pip;
bool requires_update = false;
if(pos.PositionType() == POSITION_TYPE_BUY && (current_sl == 0 || target_sl > current_sl + step_buffer)) requires_update = true;
if(pos.PositionType() == POSITION_TYPE_SELL && (current_sl == 0 || target_sl < current_sl - step_buffer)) requires_update = true;
if(requires_update) trade.PositionModify(ticket, NormalizeDouble(target_sl, _Digits), pos.TakeProfit());
}
}
}
}
}
//+------------------------------------------------------------------+
//| Structural Mapping & Entry Module (M15 Bar Close) |
//+------------------------------------------------------------------+
void ProcessM15BarClose() {
ObjectsDeleteAll(0, "EQH_EQL_");
double activeEQH[], activeEQL[];
MapEqualLiquidity(PERIOD_D1, "D1", clrCrimson, STYLE_SOLID, 2, activeEQH, activeEQL);
MapEqualLiquidity(PERIOD_H4, "H4", clrDarkOrange, STYLE_DASHDOT, 1, activeEQH, activeEQL);
ChartRedraw(0);
// Spread Check Circuit Breaker
double spread = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / GetPipSize();
if(spread > InpMaxSpreadPips) return;
// Ensure we don't open multiple trades for the same setup
if(PositionsTotal() > 0) return;
// M15 execution trigger (Index 1 is the recently closed bar)
double o = iOpen(_Symbol, PERIOD_M15, 1);
double h = iHigh(_Symbol, PERIOD_M15, 1);
double l = iLow(_Symbol, PERIOD_M15, 1);
double c = iClose(_Symbol, PERIOD_M15, 1);
double volume = GetDynamicVolume();
if(volume < SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)) return;
double pip = GetPipSize();
// Check Short Sweeps
for(int i = 0; i < ArraySize(activeEQH); i++) {
if(o < activeEQH[i] && h > activeEQH[i] && c < activeEQH[i]) {
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
trade.Sell(volume, _Symbol, bid, bid + (InpStopLossPips * pip), bid - (InpTakeProfitPips * pip), "LiqSweep");
return;
}
}
// Check Long Sweeps
for(int i = 0; i < ArraySize(activeEQL); i++) {
if(o > activeEQL[i] && l < activeEQL[i] && c > activeEQL[i]) {
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
trade.Buy(volume, _Symbol, ask, ask - (InpStopLossPips * pip), ask + (InpTakeProfitPips * pip), "LiqSweep");
return;
}
}
}
//+------------------------------------------------------------------+
//| Exponential Risk Engine Calculation |
//+------------------------------------------------------------------+
double GetDynamicVolume() {
double risk_currency = InpBaseRisk;
if(InpUseExponential) {
double accrued_profit = AccountInfoDouble(ACCOUNT_EQUITY) - InpStartingBalance;
if(accrued_profit > 0) {
risk_currency = InpLockPrincipal
? (accrued_profit * (InpProfitScalePct / 100.0))
: InpBaseRisk + (accrued_profit * (InpProfitScalePct / 100.0));
}
} else {
return InpStaticLots;
}
double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double pip_val_per_lot = tick_value * (GetPipSize() / tick_size);
double calculated_vol = risk_currency / (InpStopLossPips * pip_val_per_lot);
// Floor to nearest lot step (e.g. 0.01)
double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
return MathFloor(calculated_vol / step) * step;
}
//+------------------------------------------------------------------+
//| HTF Liquidity Mapper |
//+------------------------------------------------------------------+
void MapEqualLiquidity(ENUM_TIMEFRAMES tf, string prefix, color clr, int style, int width, double &outEQH[], double &outEQL[]) {
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[], lows[];
for(int i = InpSwingBars; i < InpHistory - InpSwingBars; i++) {
bool isRes = true, 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 tolerance = InpTolerancePips * GetPipSize();
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) {
DrawLine("EQH_" + prefix + "_" + TimeToString(highs[i].time), highs[j].time, highs[j].price, highs[i].time, highs[i].price, clr, style, width);
int s = ArraySize(outEQH); ArrayResize(outEQH, s+1); outEQH[s] = highs[i].price;
highs[i].used = true; highs[j].used = true;
break;
}
}
}
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("EQL_" + prefix + "_" + TimeToString(lows[i].time), lows[j].time, lows[j].price, lows[i].time, lows[i].price, clr, style, width);
int s = ArraySize(outEQL); ArrayResize(outEQL, s+1); outEQL[s] = lows[i].price;
lows[i].used = true; lows[j].used = true;
break;
}
}
}
}
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
To upgrade this into an institutional-grade, "Pro" Expert Advisor, we need to add the three protective layers that separate retail traders from funded professionals and algorithmic desks:
Prop-Firm Style Daily Drawdown Limit (Circuit Breaker): If market chop chips away at your capital, the EA calculates your total realized + floating PnL for the current day. If it hits your maximum daily loss percentage, the EA hard-stops all new entries until midnight.
Session Filters: Liquidity sweeps only work when volume is present. The Pro version includes strict broker-time trading windows so you only execute during the London or New York overlap, ignoring the dead Asian session.
On-Chart Telemetry HUD: A real-time dashboard printed directly on your chart so you can instantly monitor your Daily PnL, active Risk Mode, Spread, and whether the Session/Drawdown filters are blocking trades.
Prop-Firm Style Daily Drawdown Limit (Circuit Breaker): If market chop chips away at your capital, the EA calculates your total realized + floating PnL for the current day. If it hits your maximum daily loss percentage, the EA hard-stops all new entries until midnight.
Session Filters: Liquidity sweeps only work when volume is present. The Pro version includes strict broker-time trading windows so you only execute during the London or New York overlap, ignoring the dead Asian session.
On-Chart Telemetry HUD: A real-time dashboard printed directly on your chart so you can instantly monitor your Daily PnL, active Risk Mode, Spread, and whether the Session/Drawdown filters are blocking trades.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
Here is the upgraded LiquiditySweeps_PRO_EA.mq5.
Code: Select all
//+------------------------------------------------------------------+
//| LiquiditySweeps_PRO_EA.mq5 |
//| Institutional Auto-Exec EA |
//+------------------------------------------------------------------+
#property copyright "Forex, Stocks & Me"
#property link "https://forex-scalping.com"
#property version "2.00"
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\HistoryOrderInfo.mqh>
// === INPUTS ===
sinput string grp1 = "--- Structural Mapping ---";
input int InpSwingBars = 5; // Fractal Swing (Bars left & right)
input int InpHistory = 500; // HTF Bars to scan
input double InpTolerancePips = 15.0; // EQH/EQL Tolerance (Pips)
sinput string grp2 = "--- Exponential Risk Engine ---";
input bool InpUseExponential = true; // Use Accrued Profit Scaling?
input double InpStartingBalance= 1000.0; // Series Starting Balance
input double InpBaseRisk = 20.0; // Base Risk (Currency)
input double InpProfitScalePct = 50.0; // Profit Reinvestment (%)
input bool InpLockPrincipal = true; // Lock Principal?
input double InpStaticLots = 1.0; // Static Fallback Volume
sinput string grp3 = "--- Execution Rules ---";
input double InpStopLossPips = 10.0; // Stop Loss (Pips)
input double InpTakeProfitPips = 50.0; // Take Profit (Pips)
input double InpMaxSpreadPips = 1.5; // Max Spread (Pips)
input ulong InpMaxSlippagePts = 30; // Max Slippage (Points)
sinput string grp4 = "--- Pro Circuit Breakers & Sessions ---";
input double InpMaxDailyLossPct= 2.0; // Max Daily Loss (%) - Halts EA
input string InpSessionStart = "14:00"; // Trading Start (Broker Time)
input string InpSessionEnd = "18:00"; // Trading End (Broker Time)
sinput string grp5 = "--- Trade Management (BE, Trail, Partial) ---";
input double InpManagementRR = 1.0; // Trigger Management at R:R
input bool InpUsePartial = true; // Take Partial Profit?
input double InpPartialPct = 50.0; // Partial Profit (%)
input bool InpUseBreakeven = true; // Move to Breakeven?
input double InpCommBufferPips = 0.6; // Commission Buffer (Pips)
input bool InpUseTrailing = true; // Use Trailing Stop after BE?
input double InpTrailDistPips = 15.0; // Trailing Distance (Pips)
input double InpTrailStepPips = 2.0; // Trailing Step (Pips)
input ulong InpMagicNumber = 888899; // EA Magic Number
// === GLOBALS & CLASSES ===
CTrade trade;
CPositionInfo pos;
ulong partiallyClosedTickets[]; // State tracker
double startOfDayBalance = 0; // For Daily Drawdown Tracker
struct SwingPoint {
datetime time;
double price;
bool used;
};
//+------------------------------------------------------------------+
//| Initialization |
//+------------------------------------------------------------------+
int OnInit() {
trade.SetExpertMagicNumber(InpMagicNumber);
trade.SetDeviationInPoints(InpMaxSlippagePts); // Pro slippage control
startOfDayBalance = AccountInfoDouble(ACCOUNT_BALANCE);
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason) {
ObjectsDeleteAll(0, "EQH_EQL_");
Comment(""); // Clear Telemetry HUD
}
//+------------------------------------------------------------------+
//| Helper: Dynamic Pip Sizing |
//+------------------------------------------------------------------+
double GetPipSize() {
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
return (digits == 5 || digits == 3) ? _Point * 10.0 : _Point;
}
//+------------------------------------------------------------------+
//| Core Tick Loop |
//+------------------------------------------------------------------+
void OnTick() {
// 1. Check if a new day started to reset the Daily Drawdown balance
static int current_day = -1;
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(dt.day_of_year != current_day) {
current_day = dt.day_of_year;
startOfDayBalance = AccountInfoDouble(ACCOUNT_BALANCE);
ArrayResize(partiallyClosedTickets, 0); // Clear partial state tracker
}
// 2. Active Trade Management
ManageActivePositions();
// 3. Update HUD
UpdateTelemetryHUD();
// 4. M15 Bar Close Mapping & Entry
static datetime last_bar = 0;
datetime curr_bar = (datetime)SeriesInfoInteger(_Symbol, PERIOD_M15, SERIES_LASTBAR_DATE);
if(curr_bar != last_bar) {
last_bar = curr_bar;
ProcessM15BarClose();
}
}
//+------------------------------------------------------------------+
//| Pro Circuit Breaker: Max Daily Loss Tracker |
//+------------------------------------------------------------------+
bool IsDailyDrawdownHit() {
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
double dailyPnL = currentEquity - startOfDayBalance;
double maxLossAllowed = startOfDayBalance * (InpMaxDailyLossPct / 100.0);
// If daily PnL is negative and exceeds our max loss threshold
if(dailyPnL < 0 && MathAbs(dailyPnL) >= maxLossAllowed) return true;
return false;
}
//+------------------------------------------------------------------+
//| Pro Circuit Breaker: Session Window |
//+------------------------------------------------------------------+
bool IsWithinTradingSession() {
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
int currentMins = dt.hour * 60 + dt.min;
string startArr[], endArr[];
StringSplit(InpSessionStart, ':', startArr);
StringSplit(InpSessionEnd, ':', endArr);
int startMins = (int)StringToInteger(startArr[0]) * 60 + (int)StringToInteger(startArr[1]);
int endMins = (int)StringToInteger(endArr[0]) * 60 + (int)StringToInteger(endArr[1]);
return (currentMins >= startMins && currentMins <= endMins);
}
//+------------------------------------------------------------------+
//| Telemetry HUD |
//+------------------------------------------------------------------+
void UpdateTelemetryHUD() {
double currentEquity = AccountInfoDouble(ACCOUNT_EQUITY);
double dailyPnL = currentEquity - startOfDayBalance;
double spread = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / GetPipSize();
string ddStatus = IsDailyDrawdownHit() ? "[HALTED: Daily Limit Hit]" : "[OK]";
string sessionStatus = IsWithinTradingSession() ? "ACTIVE" : "WAITING";
string riskMode = InpUseExponential ? (InpLockPrincipal ? "EXPONENTIAL (Locked)" : "EXPONENTIAL (Compounding)") : "STATIC";
string hud = "\n=== PRO TELEMETRY ===\n" +
"Risk Mode: " + riskMode + "\n" +
"Session: " + sessionStatus + "\n" +
"Spread: " + DoubleToString(spread, 1) + " pips\n" +
"Daily PnL: " + DoubleToString(dailyPnL, 2) + " " + AccountInfoString(ACCOUNT_CURRENCY) + " " + ddStatus + "\n" +
"Active Target R:R: 1:" + DoubleToString(InpTakeProfitPips/InpStopLossPips, 1);
Comment(hud);
}
//+------------------------------------------------------------------+
//| Trade Management Module (BE, Trail, Partial) |
//+------------------------------------------------------------------+
void ManageActivePositions() {
double pip = GetPipSize();
double trigger_dist = InpStopLossPips * InpManagementRR * pip;
for(int i = PositionsTotal() - 1; i >= 0; i--) {
if(pos.SelectByIndex(i) && pos.Symbol() == _Symbol && pos.Magic() == InpMagicNumber) {
double open_price = pos.PriceOpen();
double current_sl = pos.StopLoss();
ulong ticket = pos.Ticket();
double profit_dist = (pos.PositionType() == POSITION_TYPE_BUY)
? SymbolInfoDouble(_Symbol, SYMBOL_BID) - open_price
: open_price - SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(profit_dist >= trigger_dist) {
// 1. Partial Close
bool isPartiallyClosed = false;
for(int j=0; j<ArraySize(partiallyClosedTickets); j++) { if(partiallyClosedTickets[j]==ticket) isPartiallyClosed = true; }
if(InpUsePartial && !isPartiallyClosed) {
double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
double vol_to_close = MathFloor((pos.Volume() * (InpPartialPct / 100.0)) / step) * step;
if(vol_to_close >= SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN) && pos.Volume() > vol_to_close) {
if(trade.PositionClosePartial(ticket, vol_to_close)) {
int s = ArraySize(partiallyClosedTickets);
ArrayResize(partiallyClosedTickets, s + 1);
partiallyClosedTickets[s] = ticket;
Print("PRO EXECUTION: Partial 50% closed at Target R:R.");
}
}
}
// 2. Breakeven & Trailing Stop
if(InpUseBreakeven) {
double comm_buffer = InpCommBufferPips * pip;
double be_sl = (pos.PositionType() == POSITION_TYPE_BUY) ? open_price + comm_buffer : open_price - comm_buffer;
double target_sl = be_sl;
if(InpUseTrailing) {
double trail_sl = (pos.PositionType() == POSITION_TYPE_BUY)
? SymbolInfoDouble(_Symbol, SYMBOL_BID) - (InpTrailDistPips * pip)
: SymbolInfoDouble(_Symbol, SYMBOL_ASK) + (InpTrailDistPips * pip);
target_sl = (pos.PositionType() == POSITION_TYPE_BUY) ? MathMax(target_sl, trail_sl) : MathMin(target_sl, trail_sl);
}
double step_buffer = InpTrailStepPips * pip;
bool requires_update = false;
if(pos.PositionType() == POSITION_TYPE_BUY && (current_sl == 0 || target_sl > current_sl + step_buffer)) requires_update = true;
if(pos.PositionType() == POSITION_TYPE_SELL && (current_sl == 0 || target_sl < current_sl - step_buffer)) requires_update = true;
if(requires_update) trade.PositionModify(ticket, NormalizeDouble(target_sl, _Digits), pos.TakeProfit());
}
}
}
}
}
//+------------------------------------------------------------------+
//| Structural Mapping & Entry Module (M15 Bar Close) |
//+------------------------------------------------------------------+
void ProcessM15BarClose() {
ObjectsDeleteAll(0, "EQH_EQL_");
double activeEQH[], activeEQL[];
MapEqualLiquidity(PERIOD_D1, "D1", clrCrimson, STYLE_SOLID, 2, activeEQH, activeEQL);
MapEqualLiquidity(PERIOD_H4, "H4", clrDarkOrange, STYLE_DASHDOT, 1, activeEQH, activeEQL);
ChartRedraw(0);
// --- PRO CIRCUIT BREAKERS ---
if(IsDailyDrawdownHit()) return;
if(!IsWithinTradingSession()) return;
double spread = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / GetPipSize();
if(spread > InpMaxSpreadPips) return;
if(PositionsTotal() > 0) return;
double o = iOpen(_Symbol, PERIOD_M15, 1);
double h = iHigh(_Symbol, PERIOD_M15, 1);
double l = iLow(_Symbol, PERIOD_M15, 1);
double c = iClose(_Symbol, PERIOD_M15, 1);
double volume = GetDynamicVolume();
if(volume < SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)) return;
double pip = GetPipSize();
for(int i = 0; i < ArraySize(activeEQH); i++) {
if(o < activeEQH[i] && h > activeEQH[i] && c < activeEQH[i]) {
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
trade.Sell(volume, _Symbol, bid, bid + (InpStopLossPips * pip), bid - (InpTakeProfitPips * pip), "PRO_Sweep");
return;
}
}
for(int i = 0; i < ArraySize(activeEQL); i++) {
if(o > activeEQL[i] && l < activeEQL[i] && c > activeEQL[i]) {
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
trade.Buy(volume, _Symbol, ask, ask - (InpStopLossPips * pip), ask + (InpTakeProfitPips * pip), "PRO_Sweep");
return;
}
}
}
//+------------------------------------------------------------------+
//| Exponential Risk Engine Calculation |
//+------------------------------------------------------------------+
double GetDynamicVolume() {
double risk_currency = InpBaseRisk;
if(InpUseExponential) {
double accrued_profit = AccountInfoDouble(ACCOUNT_EQUITY) - InpStartingBalance;
if(accrued_profit > 0) {
risk_currency = InpLockPrincipal
? (accrued_profit * (InpProfitScalePct / 100.0))
: InpBaseRisk + (accrued_profit * (InpProfitScalePct / 100.0));
}
} else return InpStaticLots;
double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double pip_val_per_lot = tick_value * (GetPipSize() / tick_size);
double calculated_vol = risk_currency / (InpStopLossPips * pip_val_per_lot);
double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
return MathFloor(calculated_vol / step) * step;
}
//+------------------------------------------------------------------+
//| HTF Liquidity Mapper |
//+------------------------------------------------------------------+
void MapEqualLiquidity(ENUM_TIMEFRAMES tf, string prefix, color clr, int style, int width, double &outEQH[], double &outEQL[]) {
double h[], l[];
datetime t[];
ArraySetAsSeries(h, true); ArraySetAsSeries(l, true); ArraySetAsSeries(t, true);
if(CopyHigh(_Symbol, tf, 0, InpHistory, h) < InpHistory || CopyLow(_Symbol, tf, 0, InpHistory, l) < InpHistory || CopyTime(_Symbol, tf, 0, InpHistory, t) < InpHistory) return;
SwingPoint highs[], lows[];
for(int i = InpSwingBars; i < InpHistory - InpSwingBars; i++) {
bool isRes = true, 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 tolerance = InpTolerancePips * GetPipSize();
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) {
DrawLine("EQH_" + prefix + "_" + TimeToString(highs[i].time), highs[j].time, highs[j].price, highs[i].time, highs[i].price, clr, style, width);
int s = ArraySize(outEQH); ArrayResize(outEQH, s+1); outEQH[s] = highs[i].price;
highs[i].used = true; highs[j].used = true; break;
}
}
}
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("EQL_" + prefix + "_" + TimeToString(lows[i].time), lows[j].time, lows[j].price, lows[i].time, lows[i].price, clr, style, width);
int s = ArraySize(outEQL); ArrayResize(outEQL, s+1); outEQL[s] = lows[i].price;
lows[i].used = true; lows[j].used = true; break;
}
}
}
}
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
Pro Upgrades Explained
Drawdown Halt Mechanism: IsDailyDrawdownHit() tracks the ACCOUNT_BALANCE at midnight and compares it against your live equity throughout the day. If your floating/closed losses cross the InpMaxDailyLossPct threshold (e.g., -2.0%), it shuts off the execution engine until the next day starts, protecting your capital from tilt or a chopping market.
Session Boundary Control: The IsWithinTradingSession() function enforces a strict time window (default 14:00 to 18:00 Broker Time, which aligns with the New York overlap). Sweeps that trigger outside of high-liquidity windows are completely ignored.
Telemetry HUD: Using the Comment() function, the EA now actively paints a clean readout on the top left of your M15 chart showing your exact active risk state, spread, daily PnL, and whether your session is active or halted by a circuit breaker.
Drawdown Halt Mechanism: IsDailyDrawdownHit() tracks the ACCOUNT_BALANCE at midnight and compares it against your live equity throughout the day. If your floating/closed losses cross the InpMaxDailyLossPct threshold (e.g., -2.0%), it shuts off the execution engine until the next day starts, protecting your capital from tilt or a chopping market.
Session Boundary Control: The IsWithinTradingSession() function enforces a strict time window (default 14:00 to 18:00 Broker Time, which aligns with the New York overlap). Sweeps that trigger outside of high-liquidity windows are completely ignored.
Telemetry HUD: Using the Comment() function, the EA now actively paints a clean readout on the top left of your M15 chart showing your exact active risk state, spread, daily PnL, and whether your session is active or halted by a circuit breaker.
Re: Checklist fatigue: how I kept my pre-trade list to five lines
To elevate this Expert Advisor from a retail script to a quantitative, institutional-grade execution engine (v3.00 Quant), six architectural flaws common to basic EAs have been re-engineered:
Unmitigated (Virgin) Liquidity Validation: Previous versions paired historical swings without checking what happened afterward. The engine now validates that price never broke the level between the two touches and never swept the level in the historical bars following the second touch. Once an M15 bar sweeps a pool, that level is locked as mitigated so it can never trigger duplicate trades.
Native MQL5 Economic Calendar Filter (MqlCalendarValue): Directly enforces Item #1 of the checklist ("News: nothing red in the next 15 minutes?"). The EA queries the native MetaTrader 5 economic calendar database for high-impact events affecting the active pair's base or quote currency and halts execution before and after the release.
Unmitigated (Virgin) Liquidity Validation: Previous versions paired historical swings without checking what happened afterward. The engine now validates that price never broke the level between the two touches and never swept the level in the historical bars following the second touch. Once an M15 bar sweeps a pool, that level is locked as mitigated so it can never trigger duplicate trades.
Native MQL5 Economic Calendar Filter (MqlCalendarValue): Directly enforces Item #1 of the checklist ("News: nothing red in the next 15 minutes?"). The EA queries the native MetaTrader 5 economic calendar database for high-impact events affecting the active pair's base or quote currency and halts execution before and after the release.