feat: Implement level ladder strategy, Replaced strict previous-close gate with continuation wick-retest detection, Removed intrabar reversal flipping from this retest path

This commit is contained in:
Naji El Chemaly
2026-06-10 11:13:01 +03:00
parent 192f2ac667
commit 7182736a57
2 changed files with 261 additions and 51 deletions
+245 -51
View File
@@ -5,7 +5,7 @@
//| Based on: ORB-All-Sessions.pine + LuxAlgo Order Block Detector |
//+------------------------------------------------------------------+
#property copyright "NANDR"
#property version "1.38"
#property version "1.39"
#property strict
#include <Trade\Trade.mqh>
@@ -53,6 +53,12 @@ enum ENUM_OB_MITIGATION
MITIG_CLOSE // Close (close penetrates zone)
};
enum ENUM_LADDER_SL_MODE
{
LADDER_SL_FIXED_PIPS, // Use InpSLPips
LADDER_SL_LEVEL_BUFFER // Buffer beyond source level
};
//+------------------------------------------------------------------+
//| Input Parameters |
//+------------------------------------------------------------------+
@@ -92,6 +98,18 @@ input bool InpUseOBRetestEntry = true; // OB Retest Entry (ent
input int InpMaxRetestsPerSession = 3; // Max Retest Entries Per Session (per breakout)
input bool InpUseSessionBiasFilter = true; // Session Bias Filter: block signals against day bias
// --- Session Level Ladder ---
input group "═══ Session Level Ladder ═══"
input bool InpUseSessionLevelLadder = false; // Enable Session Level Ladder Entries
input bool InpLadderOnDailyOpen = true; // Ladder on Daily Open session
input bool InpLadderOnTokyo = true; // Ladder on Tokyo session
input bool InpLadderOnLondon = true; // Ladder on London session
input bool InpLadderOnNY = true; // Ladder on NY session
input bool InpLadderOnNYORB = true; // Ladder on NY ORB session
input ENUM_LADDER_SL_MODE InpLadderSLMode = LADDER_SL_FIXED_PIPS; // Ladder SL Mode
input double InpLadderSLBufferPips = 5.0; // Ladder SL Buffer Pips (when level-buffer mode)
input int InpLadderMaxTradesPerSession = 2; // Max Ladder Trades Per Session (0=unlimited)
// --- Lot Size & Risk ---
input group "═══ Lot Size & Risk ═══"
input ENUM_LOT_MODE InpLotMode = LOT_RISK_PERCENT; // Lot Mode
@@ -157,6 +175,13 @@ struct SSession
int breakoutBarsAgo;
bool breakoutEntryTaken; // one entry max per breakout confirmation leg
bool ladderBullLowDone; // Bull ladder: low -> mid done
bool ladderBullMidDone; // Bull ladder: mid -> high done
bool ladderBearHighDone; // Bear ladder: high -> mid done
bool ladderBearMidDone; // Bear ladder: mid -> low done
int ladderTradesThisSession;
int tradesThisSession;
bool retestFiredThisBar; // blocks duplicate entries on consecutive ticks within same bar
@@ -172,6 +197,13 @@ struct SSession
inRetest = false;
breakoutBarsAgo = 0;
breakoutEntryTaken = false;
ladderBullLowDone = false;
ladderBullMidDone = false;
ladderBearHighDone = false;
ladderBearMidDone = false;
ladderTradesThisSession = 0;
tradesThisSession = 0;
retestFiredThisBar = false;
}
@@ -522,6 +554,7 @@ void UpdateORBSessions()
g_Sessions[s].inRetest = false;
g_Sessions[s].breakoutBarsAgo = 0;
g_Sessions[s].tradesThisSession= 0;
ResetLadderProgress(s);
if(g_Sessions[s].orbComplete)
PrintFormat("NANDR EA: [%s] ORB complete. High=%.2f Low=%.2f",
@@ -640,6 +673,7 @@ void DetectBreakouts()
g_Sessions[s].inBreakout = true;
g_Sessions[s].breakoutBarsAgo= 0;
g_Sessions[s].breakoutEntryTaken = false;
ResetLadderDirection(s, 1);
if(g_GlobalBiasDir == 0) g_GlobalBiasDir = 1; // first breakout of the day sets bias
PrintFormat("NANDR EA: [%s] Bullish ORB breakout at %.2f [DayBias=%d]",
g_Sessions[s].name, g_Sessions[s].orbHigh, g_GlobalBiasDir);
@@ -657,6 +691,7 @@ void DetectBreakouts()
g_Sessions[s].inBreakout = true;
g_Sessions[s].breakoutBarsAgo= 0;
g_Sessions[s].breakoutEntryTaken = false;
ResetLadderDirection(s, -1);
if(g_GlobalBiasDir == 0) g_GlobalBiasDir = -1; // first breakout of the day sets bias
PrintFormat("NANDR EA: [%s] Bearish ORB breakout at %.2f [DayBias=%d]",
g_Sessions[s].name, g_Sessions[s].orbLow, g_GlobalBiasDir);
@@ -690,6 +725,7 @@ void UpdateRetestState(int sessIdx)
{
g_Sessions[sessIdx].breakoutDir = -1;
g_Sessions[sessIdx].breakoutEntryTaken = false;
ResetLadderDirection(sessIdx, -1);
g_GlobalBiasDir = -1; // price swept the level and closed below — day bias now bearish
PrintFormat("NANDR EA: [%s] Bullish sweep reversal at orbHigh %.2f — flipping to BEARISH [DayBias=%d]",
g_Sessions[sessIdx].name, orbHigh, g_GlobalBiasDir);
@@ -702,6 +738,7 @@ void UpdateRetestState(int sessIdx)
g_Sessions[sessIdx].inBreakout = false;
g_Sessions[sessIdx].breakoutDir = 0;
g_Sessions[sessIdx].breakoutEntryTaken = false;
ResetLadderProgress(sessIdx);
}
}
else if(g_Sessions[sessIdx].breakoutDir == -1)
@@ -712,6 +749,7 @@ void UpdateRetestState(int sessIdx)
{
g_Sessions[sessIdx].breakoutDir = 1;
g_Sessions[sessIdx].breakoutEntryTaken = false;
ResetLadderDirection(sessIdx, 1);
g_GlobalBiasDir = 1; // price swept the level and closed above — day bias now bullish
PrintFormat("NANDR EA: [%s] Bearish sweep reversal at orbLow %.2f — flipping to BULLISH [DayBias=%d]",
g_Sessions[sessIdx].name, orbLow, g_GlobalBiasDir);
@@ -724,6 +762,7 @@ void UpdateRetestState(int sessIdx)
g_Sessions[sessIdx].inBreakout = false;
g_Sessions[sessIdx].breakoutDir = 0;
g_Sessions[sessIdx].breakoutEntryTaken = false;
ResetLadderProgress(sessIdx);
}
}
}
@@ -949,6 +988,34 @@ bool HasActiveExposure()
return (CountOpenPositions() + CountPendingOrders()) > 0;
}
double SessionMid(const SSession &sess)
{
return (sess.orbHigh + sess.orbLow) / 2.0;
}
void ResetLadderProgress(int sessIdx)
{
g_Sessions[sessIdx].ladderBullLowDone = false;
g_Sessions[sessIdx].ladderBullMidDone = false;
g_Sessions[sessIdx].ladderBearHighDone = false;
g_Sessions[sessIdx].ladderBearMidDone = false;
g_Sessions[sessIdx].ladderTradesThisSession = 0;
}
void ResetLadderDirection(int sessIdx, int dir)
{
if(dir > 0)
{
g_Sessions[sessIdx].ladderBullLowDone = false;
g_Sessions[sessIdx].ladderBullMidDone = false;
}
else
{
g_Sessions[sessIdx].ladderBearHighDone = false;
g_Sessions[sessIdx].ladderBearMidDone = false;
}
}
void RefreshBreakoutEntryLocks()
{
// Allow a new entry from the same breakout leg only after all managed
@@ -962,6 +1029,157 @@ void RefreshBreakoutEntryLocks()
}
}
bool OpenLadderTrade(int sessIdx, int dir, double srcLevel, double dstLevel, const string stepTag)
{
if(g_TradingHalted) return false;
if(g_TodayTrades >= InpMaxTradesPerDay) return false;
if(HasActiveExposure()) return false;
if(InpLadderMaxTradesPerSession > 0
&& g_Sessions[sessIdx].ladderTradesThisSession >= InpLadderMaxTradesPerSession)
return false;
double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID);
double entry = (dir > 0) ? ask : bid;
double tp = NormalizeDouble(dstLevel, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
double sl = 0.0;
if(InpLadderSLMode == LADDER_SL_LEVEL_BUFFER)
{
sl = (dir > 0) ? srcLevel - PipsToPrice(InpLadderSLBufferPips)
: srcLevel + PipsToPrice(InpLadderSLBufferPips);
}
else
{
sl = (dir > 0) ? entry - PipsToPrice(InpSLPips)
: entry + PipsToPrice(InpSLPips);
}
sl = NormalizeDouble(sl, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
if((dir > 0 && (tp <= entry || sl >= entry))
|| (dir < 0 && (tp >= entry || sl <= entry)))
return false;
double slPips = PriceToPips(MathAbs(entry - sl));
if(slPips <= 0) return false;
double lots = CalcLotSize(slPips);
string comment = StringFormat("NANDR|Ladder|%s|%s|%s",
g_Sessions[sessIdx].name,
(dir > 0 ? "BUY" : "SELL"),
stepTag);
bool result = (dir > 0)
? g_Trade.Buy(lots, g_Symbol, 0, sl, tp, comment)
: g_Trade.Sell(lots, g_Symbol, 0, sl, tp, comment);
if(!result)
{
PrintFormat("NANDR EA: Ladder trade failed. Step=%s Session=%s Error=%d",
stepTag, g_Sessions[sessIdx].name, GetLastError());
return false;
}
g_TodayTrades++;
g_Sessions[sessIdx].tradesThisSession++;
g_Sessions[sessIdx].ladderTradesThisSession++;
g_Sessions[sessIdx].breakoutEntryTaken = true;
PrintFormat("NANDR EA: Ladder trade opened. Step=%s Dir=%s Lots=%.2f Entry=%.2f SL=%.2f TP=%.2f Session=%s",
stepTag, (dir > 0 ? "BUY" : "SELL"), lots, entry, sl, tp, g_Sessions[sessIdx].name);
if(InpShowTradeLabels)
DrawTradeLabel(dir, entry, sl, tp);
return true;
}
bool IsLadderEnabledForSession(int sessIdx)
{
string name = g_Sessions[sessIdx].name;
if(name == "DailyOpen") return InpLadderOnDailyOpen;
if(name == "Tokyo") return InpLadderOnTokyo;
if(name == "London") return InpLadderOnLondon;
if(name == "NY") return InpLadderOnNY;
if(name == "NYORB") return InpLadderOnNYORB;
return true;
}
bool TrySessionLadderEntry(int sessIdx,
double c1,
double h_cur,
double l_cur,
double ask,
double bid,
double tol)
{
if(!InpUseSessionLevelLadder) return false;
if(!IsLadderEnabledForSession(sessIdx)) return false;
if(!g_Sessions[sessIdx].enabled || !g_Sessions[sessIdx].orbComplete) return false;
if(!g_Sessions[sessIdx].inBreakout) return false;
if(HasActiveExposure()) return false;
if(g_Sessions[sessIdx].retestFiredThisBar) return false;
if(InpBreakoutExpireBars > 0 && g_Sessions[sessIdx].breakoutBarsAgo >= InpBreakoutExpireBars) return false;
if(InpLadderMaxTradesPerSession > 0
&& g_Sessions[sessIdx].ladderTradesThisSession >= InpLadderMaxTradesPerSession) return false;
double orbHigh = g_Sessions[sessIdx].orbHigh;
double orbLow = g_Sessions[sessIdx].orbLow;
double orbMid = SessionMid(g_Sessions[sessIdx]);
if(g_Sessions[sessIdx].breakoutDir > 0)
{
if(!g_Sessions[sessIdx].ladderBullLowDone
&& c1 > orbLow && l_cur <= orbLow && ask >= orbLow - tol)
{
if(OpenLadderTrade(sessIdx, 1, orbLow, orbMid, "LOW_TO_MID"))
{
g_Sessions[sessIdx].ladderBullLowDone = true;
g_Sessions[sessIdx].retestFiredThisBar = true;
return true;
}
}
if(g_Sessions[sessIdx].ladderBullLowDone
&& !g_Sessions[sessIdx].ladderBullMidDone
&& c1 > orbMid && l_cur <= orbMid && ask >= orbMid - tol)
{
if(OpenLadderTrade(sessIdx, 1, orbMid, orbHigh, "MID_TO_HIGH"))
{
g_Sessions[sessIdx].ladderBullMidDone = true;
g_Sessions[sessIdx].retestFiredThisBar = true;
return true;
}
}
}
else if(g_Sessions[sessIdx].breakoutDir < 0)
{
if(!g_Sessions[sessIdx].ladderBearHighDone
&& c1 < orbHigh && h_cur >= orbHigh && bid <= orbHigh + tol)
{
if(OpenLadderTrade(sessIdx, -1, orbHigh, orbMid, "HIGH_TO_MID"))
{
g_Sessions[sessIdx].ladderBearHighDone = true;
g_Sessions[sessIdx].retestFiredThisBar = true;
return true;
}
}
if(g_Sessions[sessIdx].ladderBearHighDone
&& !g_Sessions[sessIdx].ladderBearMidDone
&& c1 < orbMid && h_cur >= orbMid && bid <= orbMid + tol)
{
if(OpenLadderTrade(sessIdx, -1, orbMid, orbLow, "MID_TO_LOW"))
{
g_Sessions[sessIdx].ladderBearMidDone = true;
g_Sessions[sessIdx].retestFiredThisBar = true;
return true;
}
}
}
return false;
}
//+------------------------------------------------------------------+
//| Cancel all pending orders |
//+------------------------------------------------------------------+
@@ -1080,70 +1298,34 @@ void CheckRetestEntriesTick()
if(g_Sessions[s].breakoutDir == 1) // Bullish
{
// Standard: prev close above orbHigh, wick dipped to orbHigh
if(c1 > orbHigh && l_cur <= orbHigh)
// Continuation retest: bullish breakout remains active and wick retests level.
// Use live ask filter for confirmation instead of flipping direction intrabar.
if(l_cur <= orbHigh && ask >= orbHigh - tol)
{
orbLevel = orbHigh;
// ask still above orbHigh → support held → BUY
// ask dropped below orbHigh → sweep failed → SELL, flip to bearish
if(ask >= orbHigh - tol)
dir = 1;
else
{
dir = -1;
g_Sessions[s].breakoutDir = -1;
g_Sessions[s].breakoutEntryTaken = false;
g_GlobalBiasDir = -1; // tick sweep below orbHigh — day bias flips bearish
PrintFormat("NANDR EA: [%s] Tick sweep reversal below orbHigh %.2f → SELL [DayBias=%d]",
g_Sessions[s].name, orbHigh, g_GlobalBiasDir);
}
dir = 1;
}
// Mid retest: prev close above mid, wick dipped to mid
else if(c1 > orbMid && l_cur <= orbMid)
// Mid retest continuation.
else if(l_cur <= orbMid && ask >= orbMid - tol)
{
orbLevel = orbMid;
if(ask >= orbMid - tol)
dir = 1;
else
{
dir = -1;
g_Sessions[s].breakoutDir = -1;
g_Sessions[s].breakoutEntryTaken = false;
}
dir = 1;
}
}
else if(g_Sessions[s].breakoutDir == -1) // Bearish
{
// Standard: prev close below orbLow, wick came back up to orbLow
if(c1 < orbLow && h_cur >= orbLow)
// Continuation retest: bearish breakout remains active and wick retests level.
// Use live bid filter for confirmation instead of flipping direction intrabar.
if(h_cur >= orbLow && bid <= orbLow + tol)
{
orbLevel = orbLow;
// bid still below orbLow → resistance held → SELL (continuation)
// bid moved above orbLow → sweep reversal → BUY, flip to bullish
if(bid <= orbLow + tol)
dir = -1;
else
{
dir = 1;
g_Sessions[s].breakoutDir = 1;
g_Sessions[s].breakoutEntryTaken = false;
g_GlobalBiasDir = 1; // tick sweep above orbLow — day bias flips bullish
PrintFormat("NANDR EA: [%s] Tick sweep reversal above orbLow %.2f → BUY [DayBias=%d]",
g_Sessions[s].name, orbLow, g_GlobalBiasDir);
}
dir = -1;
}
// Mid retest: prev close below mid, wick came back up to mid
else if(c1 < orbMid && h_cur >= orbMid)
// Mid retest continuation.
else if(h_cur >= orbMid && bid <= orbMid + tol)
{
orbLevel = orbMid;
if(bid <= orbMid + tol)
dir = -1;
else
{
dir = 1;
g_Sessions[s].breakoutDir = 1;
g_Sessions[s].breakoutEntryTaken = false;
}
dir = -1;
}
}
@@ -1160,6 +1342,17 @@ void CheckRetestEntriesTick()
}
}
// ORB retest has priority. Ladder logic runs as fallback only when no active
// exposure exists after ORB checks in this tick.
if(InpUseSessionLevelLadder && !HasActiveExposure())
{
for(int s = 0; s < g_SessionCount; s++)
{
if(TrySessionLadderEntry(s, c1, h_cur, l_cur, ask, bid, tol))
break;
}
}
if(!InpUseOBRetestEntry) return;
// ── Bearish OB Retest → SELL at wick touch ───────────────────────
@@ -1264,6 +1457,7 @@ void CheckEntrySignals()
g_Sessions[s].breakoutDir = 0;
g_Sessions[s].inRetest = false;
g_Sessions[s].breakoutEntryTaken = false;
ResetLadderProgress(s);
continue;
}
+16
View File
@@ -24,6 +24,14 @@ A MetaTrader 5 Expert Advisor for scalping **XAUUSD (Gold/USD)** on **M15** (als
Entry is taken when the ORB retest aligns with a nearby Order Block (optional — configurable).
### Session Level Ladder (New)
- After a confirmed session breakout direction, the EA can trade between that session's ORB levels:
- Bullish ladder: `Low -> Mid`, then `Mid -> High`
- Bearish ladder: `High -> Mid`, then `Mid -> Low`
- Confirmation for ladder entries uses wick retest plus bar-close context.
- Priority is ORB retest first, with ladder as fallback.
- Ladder TP is always the next session level. SL is user-selectable (fixed pips or source-level buffer).
---
## Timeframe Scaling
@@ -91,6 +99,14 @@ All sessions are **disabled by default** — enable the ones you want to trade:
| `InpEntryMode` | `Market` | Market order on retest close, or Limit at ORB level |
| `InpUseStrictFilter` | `true` | Strict N-bar breakout filter (recommended) |
### Session Level Ladder Settings
| Parameter | Default | Description |
|-----------|---------|-------------|
| `InpUseSessionLevelLadder` | `true` | Enable level-to-level ladder entries after breakout direction confirmation |
| `InpLadderSLMode` | `Fixed Pips` | Ladder SL mode: fixed pips or buffer beyond source level |
| `InpLadderSLBufferPips` | `5.0` | Buffer size when ladder SL mode is level-buffer |
| `InpLadderMaxTradesPerSession` | `2` | Max ladder trades per session (`0` = unlimited) |
### Lot Size & Risk
| Parameter | Default | Description |
|-----------|---------|-------------|