diff --git a/NANDR_ORB_OB_EA.mq5 b/NANDR_ORB_OB_EA.mq5 index 8267ee3..0836615 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.39" +#property version "1.393" #property strict #include @@ -106,6 +106,14 @@ input bool InpLadderOnTokyo = true; // Ladder on Tokyo ses 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 bool InpUseLadderATRVolCeiling = false; // Use ATR Volatility Ceiling for Ladder Entries +input double InpLadderATRMaxPips = 200.0; // Max ATR (pips) allowed for Ladder Entries +input bool InpUseLadderSessionCaps = false; // Use Per-Session Ladder Trade Caps +input int InpLadderCapDailyOpen = 2; // Daily Open Ladder Cap (0=unlimited) +input int InpLadderCapTokyo = 2; // Tokyo Ladder Cap (0=unlimited) +input int InpLadderCapLondon = 2; // London Ladder Cap (0=unlimited) +input int InpLadderCapNY = 2; // NY Ladder Cap (0=unlimited) +input int InpLadderCapNYORB = 2; // NY ORB Ladder Cap (0=unlimited) 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) @@ -653,6 +661,10 @@ void DetectBreakouts() if(g_Sessions[s].orbHigh <= 0 || g_Sessions[s].orbLow >= DBL_MAX) continue; if(g_Sessions[s].inBreakout || g_Sessions[s].inRetest) continue; + double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1); // just-closed bar + bool outsideBull = (c0 > g_Sessions[s].orbHigh); + bool outsideBear = (c0 < g_Sessions[s].orbLow); + bool bullBO = InpUseStrictFilter ? CheckStrictBreakout( 1, g_Sessions[s].orbHigh) : CheckSimpleBreakout( 1, g_Sessions[s].orbHigh); @@ -660,6 +672,24 @@ void DetectBreakouts() ? CheckStrictBreakout(-1, g_Sessions[s].orbLow) : CheckSimpleBreakout(-1, g_Sessions[s].orbLow); + // Fallback arming for simple mode: if the exact close-cross event was missed, + // still arm breakout while price is already closed beyond the ORB boundary. + if(!InpUseStrictFilter) + { + if(!bullBO && outsideBull) + { + bullBO = true; + PrintFormat("NANDR EA: [%s] Bullish breakout armed by outside-range fallback. Close=%.2f ORBHigh=%.2f", + g_Sessions[s].name, c0, g_Sessions[s].orbHigh); + } + if(!bearBO && outsideBear) + { + bearBO = true; + PrintFormat("NANDR EA: [%s] Bearish breakout armed by outside-range fallback. Close=%.2f ORBLow=%.2f", + g_Sessions[s].name, c0, g_Sessions[s].orbLow); + } + } + if(bullBO) { // Bias filter note: do not suppress ORB breakout detection. @@ -1032,8 +1062,8 @@ 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(InpLadderMaxTradesPerSession > 0 - && g_Sessions[sessIdx].ladderTradesThisSession >= InpLadderMaxTradesPerSession) + int ladderCap = GetEffectiveLadderCap(sessIdx); + if(ladderCap > 0 && g_Sessions[sessIdx].ladderTradesThisSession >= ladderCap) return false; double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK); @@ -1102,6 +1132,46 @@ bool IsLadderEnabledForSession(int sessIdx) return true; } +int GetLadderSessionCap(int sessIdx) +{ + if(!InpUseLadderSessionCaps) + return 0; + + string name = g_Sessions[sessIdx].name; + if(name == "DailyOpen") return InpLadderCapDailyOpen; + if(name == "Tokyo") return InpLadderCapTokyo; + if(name == "London") return InpLadderCapLondon; + if(name == "NY") return InpLadderCapNY; + if(name == "NYORB") return InpLadderCapNYORB; + return 0; +} + +int GetEffectiveLadderCap(int sessIdx) +{ + int globalCap = InpLadderMaxTradesPerSession; + int sessionCap = GetLadderSessionCap(sessIdx); + + if(globalCap <= 0) return sessionCap; + if(sessionCap <= 0) return globalCap; + return MathMin(globalCap, sessionCap); +} + +bool IsLadderVolatilityTooHigh(double &atrPips) +{ + atrPips = 0.0; + if(!InpUseLadderATRVolCeiling) + return false; + if(g_ATRHandle == INVALID_HANDLE) + return false; + + double atr[1]; + if(CopyBuffer(g_ATRHandle, 0, 0, 1, atr) <= 0) + return false; + + atrPips = PriceToPips(atr[0]); + return (atrPips > InpLadderATRMaxPips); +} + bool TrySessionLadderEntry(int sessIdx, double c1, double h_cur, @@ -1117,8 +1187,17 @@ bool TrySessionLadderEntry(int sessIdx, 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; + int ladderCap = GetEffectiveLadderCap(sessIdx); + if(ladderCap > 0 && g_Sessions[sessIdx].ladderTradesThisSession >= ladderCap) + return false; + + double atrPips = 0.0; + if(IsLadderVolatilityTooHigh(atrPips)) + { + PrintFormat("NANDR EA: Ladder skipped [%s] - ATR %.1f pips exceeds ceiling %.1f pips.", + g_Sessions[sessIdx].name, atrPips, InpLadderATRMaxPips); + return false; + } double orbHigh = g_Sessions[sessIdx].orbHigh; double orbLow = g_Sessions[sessIdx].orbLow; @@ -1253,95 +1332,91 @@ void OpenTrade(int dir, int sessIdx, double orbLevel, double obTop, double obBot } //+------------------------------------------------------------------+ -//| 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. | +//| Tick-based retest entries | +//| Core ORB strategy: | +//| 1. Session ORB complete | +//| 2. Breakout confirmed (close crossed ORB level) | +//| 3. Wick of current bar touches the broken level | +//| 4. Enter in breakout direction | //+------------------------------------------------------------------+ void CheckRetestEntriesTick() { if(g_TradingHalted) return; if(g_TodayTrades >= InpMaxTradesPerDay) return; - 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 + double h_cur = iHigh(g_Symbol, PERIOD_CURRENT, 0); + double l_cur = iLow(g_Symbol, PERIOD_CURRENT, 0); + double c1 = iClose(g_Symbol, PERIOD_CURRENT, 1); + double open0 = iOpen(g_Symbol, PERIOD_CURRENT, 0); + double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK); + double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID); + double tol = PipsToPrice(1.0); // ── 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++) { + // --- Gate 1: session must have a live confirmed breakout --- if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue; if(!g_Sessions[s].inBreakout) continue; - if(g_Sessions[s].breakoutEntryTaken) continue; - if(g_Sessions[s].tradesThisSession >= InpMaxRetestsPerSession) continue; + + // --- Gate 2: no active exposure (one trade at a time) --- if(HasActiveExposure()) continue; + + // --- Gate 3: entry already taken and not yet released --- + if(g_Sessions[s].breakoutEntryTaken) continue; + + // --- Gate 4: session trade count limit --- + if(g_Sessions[s].tradesThisSession >= InpMaxRetestsPerSession) continue; + + // --- Gate 5: breakout expiry --- 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; + // --- Gate 6: one entry per bar to prevent duplicate ticks --- + if(g_Sessions[s].retestFiredThisBar) continue; + + double orbHigh = g_Sessions[s].orbHigh; + double orbLow = g_Sessions[s].orbLow; + int dir = 0; double orbLevel = 0; - if(g_Sessions[s].breakoutDir == 1) // Bullish + if(g_Sessions[s].breakoutDir == 1) // Bullish breakout: price broke above orbHigh { - // Continuation retest: breakout direction is already confirmed. - // Trigger on wick-touch of the session level. + // Retest: wick comes back down to touch orbHigh if(l_cur <= orbHigh) { orbLevel = orbHigh; dir = 1; } - // Mid retest continuation. - else if(l_cur <= orbMid) - { - orbLevel = orbMid; - dir = 1; - } } - else if(g_Sessions[s].breakoutDir == -1) // Bearish + else if(g_Sessions[s].breakoutDir == -1) // Bearish breakout: price broke below orbLow { - // Continuation retest: breakout direction is already confirmed. - // Trigger on wick-touch of the session level. + // Retest: wick comes back up to touch orbLow if(h_cur >= orbLow) { orbLevel = orbLow; dir = -1; } - // Mid retest continuation. - else if(h_cur >= orbMid) - { - orbLevel = orbMid; - dir = -1; - } } if(dir == 0) continue; - // One entry per wick touch per bar — prevents 3 duplicate trades on consecutive ticks - if(g_Sessions[s].retestFiredThisBar) continue; g_Sessions[s].retestFiredThisBar = true; double obTop = 0, obBottom = 0; IsOBNearLevel(orbLevel, dir, obTop, obBottom); - if(InpOBRequireConf && obTop == 0) continue; + if(InpOBRequireConf && obTop == 0) + { + PrintFormat("NANDR EA: [%s] Retest at %.2f blocked — OB confirmation required but no OB nearby.", + g_Sessions[s].name, orbLevel); + continue; + } OpenTrade(dir, s, orbLevel, obTop, obBottom); } } - // ORB retest has priority. Ladder logic runs as fallback only when no active - // exposure exists after ORB checks in this tick. + // ── Session Level Ladder (fallback, only when no ORB entry taken this tick) ── if(InpUseSessionLevelLadder && !HasActiveExposure()) { for(int s = 0; s < g_SessionCount; s++) diff --git a/README.md b/README.md index c45a657..7b235be 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,22 @@ All sessions are **disabled by default** — enable the ones you want to trade: | Parameter | Default | Description | |-----------|---------|-------------| | `InpUseSessionLevelLadder` | `true` | Enable level-to-level ladder entries after breakout direction confirmation | +| `InpLadderOnDailyOpen` | `true` | Enable ladder entries on Daily Open session | +| `InpLadderOnTokyo` | `true` | Enable ladder entries on Tokyo session | +| `InpLadderOnLondon` | `true` | Enable ladder entries on London session | +| `InpLadderOnNY` | `true` | Enable ladder entries on NY session | +| `InpLadderOnNYORB` | `true` | Enable ladder entries on NY ORB session | +| `InpUseLadderATRVolCeiling` | `false` | Enable ATR volatility ceiling filter for ladder entries | +| `InpLadderATRMaxPips` | `200.0` | Skip ladder entries when ATR exceeds this pips threshold | +| `InpUseLadderSessionCaps` | `false` | Enable per-session ladder trade caps | +| `InpLadderCapDailyOpen` | `2` | Daily Open ladder cap (`0` = unlimited) | +| `InpLadderCapTokyo` | `2` | Tokyo ladder cap (`0` = unlimited) | +| `InpLadderCapLondon` | `2` | London ladder cap (`0` = unlimited) | +| `InpLadderCapNY` | `2` | NY ladder cap (`0` = unlimited) | +| `InpLadderCapNYORB` | `2` | NY ORB ladder cap (`0` = unlimited) | | `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) | +| `InpLadderMaxTradesPerSession` | `2` | Global ladder cap per session (`0` = unlimited); combined with per-session cap using the stricter limit | ### Lot Size & Risk | Parameter | Default | Description |