diff --git a/NANDR_ORB_OB_EA.mq5 b/NANDR_ORB_OB_EA.mq5 index 0836615..0fc134e 100644 --- a/NANDR_ORB_OB_EA.mq5 +++ b/NANDR_ORB_OB_EA.mq5 @@ -5,7 +5,7 @@ //| Based on: ORB-All-Sessions.pine + LuxAlgo Order Block Detector | //+------------------------------------------------------------------+ #property copyright "NANDR" -#property version "1.393" +#property version "1.394" #property strict #include @@ -1009,13 +1009,101 @@ int CountPendingOrders() } //+------------------------------------------------------------------+ -//| Single-active-entry policy | +//| Session-scoped active exposure | //+------------------------------------------------------------------+ bool HasActiveExposure() { 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) { return (sess.orbHigh + sess.orbLow) / 2.0; @@ -1046,13 +1134,9 @@ void ResetLadderDirection(int sessIdx, int dir) 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++) { - if(g_Sessions[s].inBreakout) + if(g_Sessions[s].inBreakout && !HasActiveExposureForSession(s)) 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_TodayTrades >= InpMaxTradesPerDay) return false; - if(HasActiveExposure()) return false; + if(HasActiveExposureForSession(sessIdx)) return false; int ladderCap = GetEffectiveLadderCap(sessIdx); if(ladderCap > 0 && g_Sessions[sessIdx].ladderTradesThisSession >= ladderCap) return false; @@ -1184,7 +1268,7 @@ bool TrySessionLadderEntry(int sessIdx, 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(HasActiveExposureForSession(sessIdx)) return false; if(g_Sessions[sessIdx].retestFiredThisBar) return false; if(InpBreakoutExpireBars > 0 && g_Sessions[sessIdx].breakoutBarsAgo >= InpBreakoutExpireBars) return false; 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_TodayTrades >= InpMaxTradesPerDay) return; if(g_Sessions[sessIdx].tradesThisSession >= InpMaxRetestsPerSession) return; - if(HasActiveExposure()) return; + if(HasActiveExposureForSession(sessIdx)) return; if(g_Sessions[sessIdx].breakoutEntryTaken) return; 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 // 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). - // 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", (dir > 0 ? "BUY" : "SELL"), lots, entry, sl, tp, g_Sessions[sessIdx].name); if(InpShowTradeLabels) @@ -1351,6 +1435,7 @@ void CheckRetestEntriesTick() double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK); double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID); double tol = PipsToPrice(1.0); + string obSessionName = ResolveSessionNameForUtcTime(TimeCurrent() - g_ServerUtcOffsetSec); // ── ORB Session Retest Entries ────────────────────────────────── if(InpWaitForRetest) @@ -1361,8 +1446,8 @@ void CheckRetestEntriesTick() if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue; if(!g_Sessions[s].inBreakout) continue; - // --- Gate 2: no active exposure (one trade at a time) --- - if(HasActiveExposure()) continue; + // --- Gate 2: no active exposure for this session --- + if(HasActiveExposureForSession(s)) continue; // --- Gate 3: entry already taken and not yet released --- if(g_Sessions[s].breakoutEntryTaken) continue; @@ -1417,7 +1502,7 @@ void CheckRetestEntriesTick() } // ── 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++) { @@ -1435,9 +1520,11 @@ void CheckRetestEntriesTick() for(int i = 0; i < g_BearOBCount; i++) { if(!g_BearOBs[i].active || g_BearOBs[i].traded) continue; - if(HasActiveExposure()) break; if(g_TodayTrades >= InpMaxTradesPerDay) break; + if(obSessionName == "") continue; + if(HasActiveExposureForSession(obSessionName)) continue; + double obBottom = g_BearOBs[i].bottom; double obTop2 = g_BearOBs[i].top; @@ -1453,7 +1540,7 @@ void CheckRetestEntriesTick() double lots = CalcLotSize(slPips); 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) { g_TodayTrades++; @@ -1474,9 +1561,11 @@ void CheckRetestEntriesTick() for(int i = 0; i < g_BullOBCount; i++) { if(!g_BullOBs[i].active || g_BullOBs[i].traded) continue; - if(HasActiveExposure()) break; if(g_TodayTrades >= InpMaxTradesPerDay) break; + if(obSessionName == "") continue; + if(HasActiveExposureForSession(obSessionName)) continue; + double obTop3 = g_BullOBs[i].top; double obBottom2 = g_BullOBs[i].bottom; @@ -1492,7 +1581,7 @@ void CheckRetestEntriesTick() double lots = CalcLotSize(slPips); 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) { g_TodayTrades++; diff --git a/README.md b/README.md index 7b235be..13c3e3c 100644 --- a/README.md +++ b/README.md @@ -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). +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) - After a confirmed session breakout direction, the EA can trade between that session's ORB levels: - Bullish ladder: `Low -> Mid`, then `Mid -> High`