fix: Implement session-scoped gate

This commit is contained in:
Naji El Chemaly
2026-06-11 02:02:22 +03:00
parent de24c99af4
commit b5e62249eb
2 changed files with 109 additions and 18 deletions
+107 -18
View File
@@ -5,7 +5,7 @@
//| Based on: ORB-All-Sessions.pine + LuxAlgo Order Block Detector | //| Based on: ORB-All-Sessions.pine + LuxAlgo Order Block Detector |
//+------------------------------------------------------------------+ //+------------------------------------------------------------------+
#property copyright "NANDR" #property copyright "NANDR"
#property version "1.393" #property version "1.394"
#property strict #property strict
#include <Trade\Trade.mqh> #include <Trade\Trade.mqh>
@@ -1009,13 +1009,101 @@ int CountPendingOrders()
} }
//+------------------------------------------------------------------+ //+------------------------------------------------------------------+
//| Single-active-entry policy | //| Session-scoped active exposure |
//+------------------------------------------------------------------+ //+------------------------------------------------------------------+
bool HasActiveExposure() bool HasActiveExposure()
{ {
return (CountOpenPositions() + CountPendingOrders()) > 0; return (CountOpenPositions() + CountPendingOrders()) > 0;
} }
bool CommentHasSessionTag(const string comment, const string sessionName)
{
if(comment == "" || sessionName == "")
return false;
return (StringFind(comment, "|" + sessionName + "|") >= 0);
}
string ResolveSessionNameForUtcTime(datetime utcTime)
{
MqlDateTime t;
TimeToStruct(utcTime, t);
int nowMin = t.hour * 60 + t.min;
int bestIdx = -1;
int bestStart = -1;
for(int s = 0; s < g_SessionCount; s++)
{
if(!g_Sessions[s].enabled)
continue;
int startMin = g_Sessions[s].startHour * 60 + g_Sessions[s].startMin;
if(startMin <= nowMin && startMin >= bestStart)
{
bestStart = startMin;
bestIdx = s;
}
}
if(bestIdx >= 0)
return g_Sessions[bestIdx].name;
for(int s = 0; s < g_SessionCount; s++)
{
if(g_Sessions[s].enabled)
return g_Sessions[s].name;
}
return "";
}
int CountOpenPositionsForSession(const string sessionName)
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(g_Position.SelectByIndex(i))
{
if(g_Position.Magic() == InpMagicNumber && g_Position.Symbol() == g_Symbol)
{
string comment = PositionGetString(POSITION_COMMENT);
if(CommentHasSessionTag(comment, sessionName))
count++;
}
}
}
return count;
}
int CountPendingOrdersForSession(const string sessionName)
{
int count = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(g_Order.SelectByIndex(i))
{
if(g_Order.Magic() == InpMagicNumber && g_Order.Symbol() == g_Symbol)
{
string comment = OrderGetString(ORDER_COMMENT);
if(CommentHasSessionTag(comment, sessionName))
count++;
}
}
}
return count;
}
bool HasActiveExposureForSession(const string sessionName)
{
return (CountOpenPositionsForSession(sessionName) + CountPendingOrdersForSession(sessionName)) > 0;
}
bool HasActiveExposureForSession(int sessIdx)
{
if(sessIdx < 0 || sessIdx >= g_SessionCount)
return false;
return HasActiveExposureForSession(g_Sessions[sessIdx].name);
}
double SessionMid(const SSession &sess) double SessionMid(const SSession &sess)
{ {
return (sess.orbHigh + sess.orbLow) / 2.0; return (sess.orbHigh + sess.orbLow) / 2.0;
@@ -1046,13 +1134,9 @@ void ResetLadderDirection(int sessIdx, int dir)
void RefreshBreakoutEntryLocks() void RefreshBreakoutEntryLocks()
{ {
// Allow a new entry from the same breakout leg only after all managed
// positions/pending orders are gone (one ACTIVE entry at any time).
if(HasActiveExposure()) return;
for(int s = 0; s < g_SessionCount; s++) for(int s = 0; s < g_SessionCount; s++)
{ {
if(g_Sessions[s].inBreakout) if(g_Sessions[s].inBreakout && !HasActiveExposureForSession(s))
g_Sessions[s].breakoutEntryTaken = false; g_Sessions[s].breakoutEntryTaken = false;
} }
} }
@@ -1061,7 +1145,7 @@ bool OpenLadderTrade(int sessIdx, int dir, double srcLevel, double dstLevel, con
{ {
if(g_TradingHalted) return false; if(g_TradingHalted) return false;
if(g_TodayTrades >= InpMaxTradesPerDay) return false; if(g_TodayTrades >= InpMaxTradesPerDay) return false;
if(HasActiveExposure()) return false; if(HasActiveExposureForSession(sessIdx)) return false;
int ladderCap = GetEffectiveLadderCap(sessIdx); int ladderCap = GetEffectiveLadderCap(sessIdx);
if(ladderCap > 0 && g_Sessions[sessIdx].ladderTradesThisSession >= ladderCap) if(ladderCap > 0 && g_Sessions[sessIdx].ladderTradesThisSession >= ladderCap)
return false; return false;
@@ -1184,7 +1268,7 @@ bool TrySessionLadderEntry(int sessIdx,
if(!IsLadderEnabledForSession(sessIdx)) return false; if(!IsLadderEnabledForSession(sessIdx)) return false;
if(!g_Sessions[sessIdx].enabled || !g_Sessions[sessIdx].orbComplete) return false; if(!g_Sessions[sessIdx].enabled || !g_Sessions[sessIdx].orbComplete) return false;
if(!g_Sessions[sessIdx].inBreakout) return false; if(!g_Sessions[sessIdx].inBreakout) return false;
if(HasActiveExposure()) return false; if(HasActiveExposureForSession(sessIdx)) return false;
if(g_Sessions[sessIdx].retestFiredThisBar) return false; if(g_Sessions[sessIdx].retestFiredThisBar) return false;
if(InpBreakoutExpireBars > 0 && g_Sessions[sessIdx].breakoutBarsAgo >= InpBreakoutExpireBars) return false; if(InpBreakoutExpireBars > 0 && g_Sessions[sessIdx].breakoutBarsAgo >= InpBreakoutExpireBars) return false;
int ladderCap = GetEffectiveLadderCap(sessIdx); int ladderCap = GetEffectiveLadderCap(sessIdx);
@@ -1278,7 +1362,7 @@ void OpenTrade(int dir, int sessIdx, double orbLevel, double obTop, double obBot
if(g_TradingHalted) return; if(g_TradingHalted) return;
if(g_TodayTrades >= InpMaxTradesPerDay) return; if(g_TodayTrades >= InpMaxTradesPerDay) return;
if(g_Sessions[sessIdx].tradesThisSession >= InpMaxRetestsPerSession) return; if(g_Sessions[sessIdx].tradesThisSession >= InpMaxRetestsPerSession) return;
if(HasActiveExposure()) return; if(HasActiveExposureForSession(sessIdx)) return;
if(g_Sessions[sessIdx].breakoutEntryTaken) return; if(g_Sessions[sessIdx].breakoutEntryTaken) return;
double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK); double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK);
@@ -1318,7 +1402,7 @@ void OpenTrade(int dir, int sessIdx, double orbLevel, double obTop, double obBot
// Do NOT clear inBreakout — the ORB level remains valid for subsequent retests // Do NOT clear inBreakout — the ORB level remains valid for subsequent retests
// until the breakout expires, fails, or the daily reset. This allows the EA to // until the breakout expires, fails, or the daily reset. This allows the EA to
// take multiple retest entries on the same level (e.g. 18:45 AND 20:45 retests). // take multiple retest entries on the same level (e.g. 18:45 AND 20:45 retests).
// Re-entry is naturally gated by CountOpenPositions() checked at the top of OpenTrade. // Re-entry is naturally gated by session-scoped exposure checks at the top of OpenTrade.
PrintFormat("NANDR EA: Trade opened. Dir=%s Lots=%.2f Entry=%.2f SL=%.2f TP=%.2f Session=%s", PrintFormat("NANDR EA: Trade opened. Dir=%s Lots=%.2f Entry=%.2f SL=%.2f TP=%.2f Session=%s",
(dir > 0 ? "BUY" : "SELL"), lots, entry, sl, tp, g_Sessions[sessIdx].name); (dir > 0 ? "BUY" : "SELL"), lots, entry, sl, tp, g_Sessions[sessIdx].name);
if(InpShowTradeLabels) if(InpShowTradeLabels)
@@ -1351,6 +1435,7 @@ void CheckRetestEntriesTick()
double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK); double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID); double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID);
double tol = PipsToPrice(1.0); double tol = PipsToPrice(1.0);
string obSessionName = ResolveSessionNameForUtcTime(TimeCurrent() - g_ServerUtcOffsetSec);
// ── ORB Session Retest Entries ────────────────────────────────── // ── ORB Session Retest Entries ──────────────────────────────────
if(InpWaitForRetest) if(InpWaitForRetest)
@@ -1361,8 +1446,8 @@ void CheckRetestEntriesTick()
if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue; if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue;
if(!g_Sessions[s].inBreakout) continue; if(!g_Sessions[s].inBreakout) continue;
// --- Gate 2: no active exposure (one trade at a time) --- // --- Gate 2: no active exposure for this session ---
if(HasActiveExposure()) continue; if(HasActiveExposureForSession(s)) continue;
// --- Gate 3: entry already taken and not yet released --- // --- Gate 3: entry already taken and not yet released ---
if(g_Sessions[s].breakoutEntryTaken) continue; if(g_Sessions[s].breakoutEntryTaken) continue;
@@ -1417,7 +1502,7 @@ void CheckRetestEntriesTick()
} }
// ── Session Level Ladder (fallback, only when no ORB entry taken this tick) ── // ── Session Level Ladder (fallback, only when no ORB entry taken this tick) ──
if(InpUseSessionLevelLadder && !HasActiveExposure()) if(InpUseSessionLevelLadder)
{ {
for(int s = 0; s < g_SessionCount; s++) for(int s = 0; s < g_SessionCount; s++)
{ {
@@ -1435,9 +1520,11 @@ void CheckRetestEntriesTick()
for(int i = 0; i < g_BearOBCount; i++) for(int i = 0; i < g_BearOBCount; i++)
{ {
if(!g_BearOBs[i].active || g_BearOBs[i].traded) continue; if(!g_BearOBs[i].active || g_BearOBs[i].traded) continue;
if(HasActiveExposure()) break;
if(g_TodayTrades >= InpMaxTradesPerDay) break; if(g_TodayTrades >= InpMaxTradesPerDay) break;
if(obSessionName == "") continue;
if(HasActiveExposureForSession(obSessionName)) continue;
double obBottom = g_BearOBs[i].bottom; double obBottom = g_BearOBs[i].bottom;
double obTop2 = g_BearOBs[i].top; double obTop2 = g_BearOBs[i].top;
@@ -1453,7 +1540,7 @@ void CheckRetestEntriesTick()
double lots = CalcLotSize(slPips); double lots = CalcLotSize(slPips);
bool result = g_Trade.Sell(lots, g_Symbol, 0, sl, tp, bool result = g_Trade.Sell(lots, g_Symbol, 0, sl, tp,
StringFormat("NANDR|OBRetest|SELL|%.2f", obBottom)); StringFormat("NANDR|%s|OBRetest|SELL|%.2f", obSessionName, obBottom));
if(result) if(result)
{ {
g_TodayTrades++; g_TodayTrades++;
@@ -1474,9 +1561,11 @@ void CheckRetestEntriesTick()
for(int i = 0; i < g_BullOBCount; i++) for(int i = 0; i < g_BullOBCount; i++)
{ {
if(!g_BullOBs[i].active || g_BullOBs[i].traded) continue; if(!g_BullOBs[i].active || g_BullOBs[i].traded) continue;
if(HasActiveExposure()) break;
if(g_TodayTrades >= InpMaxTradesPerDay) break; if(g_TodayTrades >= InpMaxTradesPerDay) break;
if(obSessionName == "") continue;
if(HasActiveExposureForSession(obSessionName)) continue;
double obTop3 = g_BullOBs[i].top; double obTop3 = g_BullOBs[i].top;
double obBottom2 = g_BullOBs[i].bottom; double obBottom2 = g_BullOBs[i].bottom;
@@ -1492,7 +1581,7 @@ void CheckRetestEntriesTick()
double lots = CalcLotSize(slPips); double lots = CalcLotSize(slPips);
bool result = g_Trade.Buy(lots, g_Symbol, 0, sl, tp, bool result = g_Trade.Buy(lots, g_Symbol, 0, sl, tp,
StringFormat("NANDR|OBRetest|BUY|%.2f", obTop3)); StringFormat("NANDR|%s|OBRetest|BUY|%.2f", obSessionName, obTop3));
if(result) if(result)
{ {
g_TodayTrades++; g_TodayTrades++;
+2
View File
@@ -24,6 +24,8 @@ 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). Entry is taken when the ORB retest aligns with a nearby Order Block (optional — configurable).
Active exposure is session-scoped: one open trade is allowed per session, and different sessions may each hold one trade on the same day.
### Session Level Ladder (New) ### Session Level Ladder (New)
- After a confirmed session breakout direction, the EA can trade between that session's ORB levels: - After a confirmed session breakout direction, the EA can trade between that session's ORB levels:
- Bullish ladder: `Low -> Mid`, then `Mid -> High` - Bullish ladder: `Low -> Mid`, then `Mid -> High`