fix: UpdateRetestState() — bar-close only, state management (sweep flip + invalidation), no trade firing, CheckOBRetestEntries() Merged into CheckRetestEntriesTick(), CheckRetestEntriesTick() called on every tick, before if(!g_IsNewBar) return, The old single condition is replaced by a 3-pattern check covering all valid entry configurations

This commit is contained in:
Naji El Chemaly
2026-06-09 00:09:56 +03:00
parent 5c7409bbe0
commit 38a6ceec69
+152 -86
View File
@@ -5,7 +5,7 @@
//| Based on: ORB-All-Sessions.pine + LuxAlgo Order Block Detector |
//+------------------------------------------------------------------+
#property copyright "NANDR"
#property version "1.32"
#property version "1.33"
#property strict
#include <Trade\Trade.mqh>
@@ -608,15 +608,16 @@ void DetectBreakouts()
}
//+------------------------------------------------------------------+
//| Detect retest for sessions that had a breakout |
//| Update retest state on bar close: sweep reversals + invalidation |
//| Called once per new bar. Does NOT fire any trades. |
//+------------------------------------------------------------------+
bool DetectRetest(int sessIdx, int &dir)
void UpdateRetestState(int sessIdx)
{
if(!g_Sessions[sessIdx].inBreakout) return false;
if(!g_Sessions[sessIdx].inBreakout) return;
// Use bar[1] (just-closed bar) as the retest candidate and bar[2] as context.
double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1); // just-closed bar
double c1 = iClose(g_Symbol, PERIOD_CURRENT, 2); // bar before it
// Use bar[1] (just-closed) and bar[2] for bar-close confirmation.
double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1);
double c1 = iClose(g_Symbol, PERIOD_CURRENT, 2);
double h0 = iHigh(g_Symbol, PERIOD_CURRENT, 1);
double l0 = iLow(g_Symbol, PERIOD_CURRENT, 1);
@@ -624,31 +625,18 @@ bool DetectRetest(int sessIdx, int &dir)
double orbLow = g_Sessions[sessIdx].orbLow;
double orbMid = (orbHigh + orbLow) / 2.0;
// ----------------------------------------------------------------
// BULLISH BREAKOUT (breakoutDir == 1)
// ----------------------------------------------------------------
if(g_Sessions[sessIdx].breakoutDir == 1)
{
// Standard: prev bar above ORB high, current bar wick dips to ORB high, closes back above
bool retestHigh = (c1 > orbHigh) && (l0 <= orbHigh) && (c0 >= orbHigh);
// Mid retest: price dipped to ORB mid, closes back above mid
bool retestMid = (c1 > orbMid) && (l0 <= orbMid) && (c0 > orbMid);
if(retestHigh || retestMid) { dir = 1; return true; }
// Sweep reversal: wick swept BELOW ORB high (back into range), closed between orbMid and orbHigh.
// Sell-side liquidity grab above orbHigh failed → flip to bearish.
// Sweep reversal: bullish breakout failed — bar closed below orbHigh → flip to bearish
bool sweepRev = (c1 > orbHigh) && (l0 < orbHigh) && (c0 < orbHigh) && (c0 >= orbMid);
if(sweepRev)
{
g_Sessions[sessIdx].breakoutDir = -1;
PrintFormat("NANDR EA: [%s] Bullish sweep reversal at orbHigh %.2f — flipping to BEARISH",
g_Sessions[sessIdx].name, orbHigh);
dir = -1;
return true;
}
// Full invalidation: closed below mid
if(c0 < orbMid && c1 > orbMid)
else if(c0 < orbMid && c1 > orbMid)
{
PrintFormat("NANDR EA: [%s] Failed bullish retest (closed below mid %.2f)",
g_Sessions[sessIdx].name, orbMid);
@@ -656,33 +644,18 @@ bool DetectRetest(int sessIdx, int &dir)
g_Sessions[sessIdx].breakoutDir = 0;
}
}
// ----------------------------------------------------------------
// BEARISH BREAKOUT (breakoutDir == -1)
// ----------------------------------------------------------------
else if(g_Sessions[sessIdx].breakoutDir == -1)
{
// Standard: prev bar below ORB low, current bar wick ticks back to ORB low, closes back below
bool retestLow = (c1 < orbLow) && (h0 >= orbLow) && (c0 <= orbLow);
// Mid retest: price bounced back up to ORB mid, closes back below mid
bool retestMid = (c1 < orbMid) && (h0 >= orbMid) && (c0 < orbMid);
if(retestLow || retestMid) { dir = -1; return true; }
// Sweep reversal: wick swept ABOVE ORB low (back into range), closed between orbLow and orbMid.
// Buy-side liquidity sweep below orbLow failed → flip to bullish.
// This is the "liquidity sweep + reversal" pattern (e.g. 18:15 retest after 17:45 breakout).
// After this flip, subsequent mid retests are detected as BUY signals (Entry 2 scenario).
// Sweep reversal: bearish breakout failed — bar closed above orbLow → flip to bullish
bool sweepRev = (c1 < orbLow) && (h0 > orbLow) && (c0 > orbLow) && (c0 < orbHigh);
if(sweepRev)
{
g_Sessions[sessIdx].breakoutDir = 1;
PrintFormat("NANDR EA: [%s] Bearish sweep reversal at orbLow %.2f — flipping to BULLISH",
g_Sessions[sessIdx].name, orbLow);
dir = 1;
return true;
}
// Full invalidation: closed above mid
if(c0 > orbMid && c1 < orbMid)
else if(c0 > orbMid && c1 < orbMid)
{
PrintFormat("NANDR EA: [%s] Failed bearish retest (closed above mid %.2f)",
g_Sessions[sessIdx].name, orbMid);
@@ -690,8 +663,6 @@ bool DetectRetest(int sessIdx, int &dir)
g_Sessions[sessIdx].breakoutDir = 0;
}
}
return false;
}
//+------------------------------------------------------------------+
@@ -980,39 +951,138 @@ void OpenTrade(int dir, int sessIdx, double orbLevel, double obTop, double obBot
}
//+------------------------------------------------------------------+
//| Standalone OB retest entries (Entry 3 type: 18:00 Bear OB low) |
//| Fires when price retests the boundary of an active OB zone and |
//| closes back on the rejection side — independent of ORB sessions.|
//| Tick-based retest entries — fires at the wick, not on bar close |
//| Covers both ORB session retests and OB boundary retests. |
//| Uses live bar[0] high/low + last closed bar[1] for confirmation. |
//| Direction: determined by breakoutDir for ORB, OB type for OBs. |
//| Sweep reversals: detected via ask/bid relative to the level. |
//+------------------------------------------------------------------+
void CheckOBRetestEntries()
void CheckRetestEntriesTick()
{
if(!InpUseOBRetestEntry) return;
if(g_TradingHalted) return;
if(g_TodayTrades >= InpMaxTradesPerDay) return;
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) return;
double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1); // just-closed bar
double c1 = iClose(g_Symbol, PERIOD_CURRENT, 2); // previous closed bar
double h0 = iHigh(g_Symbol, PERIOD_CURRENT, 1);
double l0 = iLow(g_Symbol, PERIOD_CURRENT, 1);
double h_cur = iHigh(g_Symbol, PERIOD_CURRENT, 0); // current bar live high so far
double l_cur = iLow(g_Symbol, PERIOD_CURRENT, 0); // current bar live low so far
double c1 = iClose(g_Symbol, PERIOD_CURRENT, 1); // last closed bar close
double open0 = iOpen(g_Symbol, PERIOD_CURRENT, 0); // current bar open
double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID);
double tol = PipsToPrice(1.0); // 1-pip tolerance for direction detection
// --- Bearish OB retest → SELL ---
// Setup: price was below OB bottom (already rejected), wick retraces back up
// to OB bottom, candle closes back below it → confirmed rejection sell
// ── ORB Session Retest Entries ──────────────────────────────────
// Fires at the moment the wick touches the ORB level, not one bar later.
// Direction uses ask/bid to distinguish continuation vs sweep reversal:
// bid <= level + tol → price is AT or below the level → continuation in breakout dir
// ask >= level - tol → price has passed back through the level → sweep reversal, flip dir
if(InpWaitForRetest)
{
for(int s = 0; s < g_SessionCount; s++)
{
if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue;
if(!g_Sessions[s].inBreakout) continue;
if(g_Sessions[s].tradesThisSession >= InpMaxRetestsPerSession) continue;
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) continue;
if(InpBreakoutExpireBars > 0 && g_Sessions[s].breakoutBarsAgo >= InpBreakoutExpireBars) continue;
double orbHigh = g_Sessions[s].orbHigh;
double orbLow = g_Sessions[s].orbLow;
double orbMid = (orbHigh + orbLow) / 2.0;
int dir = 0;
double orbLevel = 0;
if(g_Sessions[s].breakoutDir == 1) // Bullish
{
// Standard: prev close above orbHigh, wick dipped to orbHigh
if(c1 > orbHigh && l_cur <= orbHigh)
{
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;
PrintFormat("NANDR EA: [%s] Tick sweep reversal below orbHigh %.2f → SELL",
g_Sessions[s].name, orbHigh);
}
}
// Mid retest: prev close above mid, wick dipped to mid
else if(c1 > orbMid && l_cur <= orbMid)
{
orbLevel = orbMid;
if(ask >= orbMid - tol)
dir = 1;
else
{
dir = -1;
g_Sessions[s].breakoutDir = -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)
{
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;
PrintFormat("NANDR EA: [%s] Tick sweep reversal above orbLow %.2f → BUY",
g_Sessions[s].name, orbLow);
}
}
// Mid retest: prev close below mid, wick came back up to mid
else if(c1 < orbMid && h_cur >= orbMid)
{
orbLevel = orbMid;
if(bid <= orbMid + tol)
dir = -1;
else
{
dir = 1;
g_Sessions[s].breakoutDir = 1;
}
}
}
if(dir == 0) continue;
double obTop = 0, obBottom = 0;
IsOBNearLevel(orbLevel, dir, obTop, obBottom);
if(InpOBRequireConf && obTop == 0) continue;
OpenTrade(dir, s, orbLevel, obTop, obBottom);
}
}
if(!InpUseOBRetestEntry) return;
// ── Bearish OB Retest → SELL at wick touch ───────────────────────
// Fires the moment the current bar's live high reaches the OB bottom.
// prev bar[1] (or current open) must have been at/below OB bottom.
for(int i = 0; i < g_BearOBCount; i++)
{
if(!g_BearOBs[i].active || g_BearOBs[i].traded) continue;
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) break;
if(g_TodayTrades >= InpMaxTradesPerDay) break;
double obBottom = g_BearOBs[i].bottom;
double obTop = g_BearOBs[i].top;
double obTop2 = g_BearOBs[i].top;
bool retest = (c1 < obBottom) // prev bar was below OB bottom (breakdown confirmed)
&& (h0 >= obBottom) // this bar's wick touched the OB bottom
&& (c0 < obBottom); // closed back below OB bottom (rejection confirmed)
if(!retest) continue;
bool prevBelow = (c1 <= obBottom || open0 <= obBottom);
bool wickTouched = (h_cur >= obBottom);
if(!prevBelow || !wickTouched) continue;
double entry = SymbolInfoDouble(g_Symbol, SYMBOL_BID);
double sl = NormalizeDouble(obTop + PipsToPrice(2.0),
double entry = bid;
double sl = NormalizeDouble(obTop2 + PipsToPrice(2.0),
(int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
double slPips = PriceToPips(MathAbs(entry - sl));
double tp = CalcTP(-1, entry, sl);
@@ -1025,45 +1095,43 @@ void CheckOBRetestEntries()
g_TodayTrades++;
g_BearOBs[i].traded = true;
PrintFormat("NANDR EA: OB Retest SELL. Entry=%.2f SL=%.2f TP=%.2f OB=[%.2f-%.2f]",
entry, sl, tp, obBottom, obTop);
entry, sl, tp, obBottom, obTop2);
if(InpShowTradeLabels) DrawTradeLabel(-1, entry, sl, tp);
}
else
PrintFormat("NANDR EA: OB Retest SELL failed. Error=%d", GetLastError());
break; // One OB entry per bar
break;
}
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) return;
// --- Bullish OB retest → BUY ---
// Setup: price was above OB top, wick dips into OB top zone, closes back above
// ── Bullish OB Retest → BUY at wick touch ────────────────────────
for(int i = 0; i < g_BullOBCount; i++)
{
if(!g_BullOBs[i].active || g_BullOBs[i].traded) continue;
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) break;
if(g_TodayTrades >= InpMaxTradesPerDay) break;
double obTop = g_BullOBs[i].top;
double obBottom = g_BullOBs[i].bottom;
double obTop3 = g_BullOBs[i].top;
double obBottom2 = g_BullOBs[i].bottom;
bool retest = (c1 > obTop) // prev bar was above OB top (breakout confirmed)
&& (l0 <= obTop) // this bar's wick touched the OB top
&& (c0 > obTop); // closed back above OB top (rejection confirmed)
if(!retest) continue;
bool prevAbove = (c1 >= obTop3 || open0 >= obTop3);
bool wickTouched = (l_cur <= obTop3);
if(!prevAbove || !wickTouched) continue;
double entry = SymbolInfoDouble(g_Symbol, SYMBOL_ASK);
double sl = NormalizeDouble(obBottom - PipsToPrice(2.0),
double entry = ask;
double sl = NormalizeDouble(obBottom2 - PipsToPrice(2.0),
(int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
double slPips = PriceToPips(MathAbs(entry - sl));
double tp = CalcTP(1, entry, sl);
double lots = CalcLotSize(slPips);
bool result = g_Trade.Buy(lots, g_Symbol, 0, sl, tp,
StringFormat("NANDR|OBRetest|BUY|%.2f", obTop));
StringFormat("NANDR|OBRetest|BUY|%.2f", obTop3));
if(result)
{
g_TodayTrades++;
g_BullOBs[i].traded = true;
PrintFormat("NANDR EA: OB Retest BUY. Entry=%.2f SL=%.2f TP=%.2f OB=[%.2f-%.2f]",
entry, sl, tp, obBottom, obTop);
entry, sl, tp, obBottom2, obTop3);
if(InpShowTradeLabels) DrawTradeLabel(1, entry, sl, tp);
}
else
@@ -1128,13 +1196,9 @@ void CheckEntrySignals()
}
else
{
// Retest-based entry: wait for price to pull back to ORB level
int retestDir = 0;
if(!DetectRetest(s, retestDir)) continue;
double obTop = 0, obBottom = 0;
IsOBNearLevel(orbLevel, retestDir, obTop, obBottom);
if(InpOBRequireConf && obTop == 0) continue;
OpenTrade(retestDir, s, orbLevel, obTop, obBottom);
// Retest entries fire tick-based in CheckRetestEntriesTick().
// Here we only update state: sweep reversals and failed retest cancellation.
UpdateRetestState(s);
}
}
}
@@ -1668,6 +1732,9 @@ void OnTick()
ManageOpenTrades();
CheckRiskLimits();
// Retest entries fire on tick (at wick touch), not on bar close
CheckRetestEntriesTick();
if(!g_IsNewBar) return;
// Daily reset
@@ -1689,9 +1756,8 @@ void OnTick()
// Breakout detection
DetectBreakouts();
// Entry signals
// Entry signals (direct breakout / limit orders on new bar)
CheckEntrySignals();
CheckOBRetestEntries();
// Update closed trade stats & dashboard
UpdateClosedTrades();