fix: Implement session-scoped gate
This commit is contained in:
+107
-18
@@ -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++;
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
Reference in New Issue
Block a user