diff --git a/NANDR_ORB_OB_EA.mq5 b/NANDR_ORB_OB_EA.mq5 index 13e21e0..964d710 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.37" +#property version "1.38" #property strict #include @@ -68,7 +68,7 @@ input int InpBreakoutConfBars = 0; // Breakout Confirm Bars // --- Sessions --- input group "═══ Session Settings ═══" -input bool InpUseDailyOpen = true; // Daily Open Session (00:00 UTC) +input bool InpUseDailyOpen = true; // Daily Open Session (22:00 UTC, 01:00 Kuwait) input bool InpUseTokyoSession = true; // Tokyo Session (00:00 UTC) input bool InpUseLondonSession = true; // London Session (07:00 UTC) input bool InpUseNYSession = true; // NY Session (12:00 UTC) @@ -155,6 +155,7 @@ struct SSession bool inBreakout; bool inRetest; int breakoutBarsAgo; + bool breakoutEntryTaken; // one entry max per breakout confirmation leg int tradesThisSession; bool retestFiredThisBar; // blocks duplicate entries on consecutive ticks within same bar @@ -170,6 +171,7 @@ struct SSession inBreakout = false; inRetest = false; breakoutBarsAgo = 0; + breakoutEntryTaken = false; tradesThisSession = 0; retestFiredThisBar = false; } @@ -252,6 +254,7 @@ double g_TotalPnL = 0; // Bar tracking datetime g_LastBarTime = 0; bool g_IsNewBar = false; +int g_ServerUtcOffsetSec = 0; // ATR handle int g_ATRHandle = INVALID_HANDLE; @@ -323,7 +326,7 @@ double NormalizeLot(double lots) double CalcLotSize(double slPips) { if(InpLotMode == LOT_FIXED) - return NormalizeLot(InpFixedLotSize); + return NormalizeLot(InpFixedLotSize); double balance = AccountInfoDouble(ACCOUNT_BALANCE); double riskUSD = balance * InpRiskPercent / 100.0; @@ -417,7 +420,7 @@ void InitSessions() if(InpUseDailyOpen) { g_Sessions[g_SessionCount].name = "DailyOpen"; - g_Sessions[g_SessionCount].startHour = 0; + g_Sessions[g_SessionCount].startHour = 22; g_Sessions[g_SessionCount].startMin = 0; g_Sessions[g_SessionCount].enabled = true; g_Sessions[g_SessionCount].Reset(); @@ -470,8 +473,10 @@ void InitSessions() //+------------------------------------------------------------------+ bool IsSessionOpenBar(const SSession &sess, datetime barTime) { + // Session inputs are UTC-based; convert server bar time to UTC before matching. + datetime utcBarTime = barTime - g_ServerUtcOffsetSec; MqlDateTime dt; - TimeToStruct(barTime, dt); + TimeToStruct(utcBarTime, dt); return (dt.hour == sess.startHour && dt.min == sess.startMin); } @@ -634,6 +639,7 @@ void DetectBreakouts() g_Sessions[s].breakoutDir = 1; g_Sessions[s].inBreakout = true; g_Sessions[s].breakoutBarsAgo= 0; + g_Sessions[s].breakoutEntryTaken = false; if(g_GlobalBiasDir == 0) g_GlobalBiasDir = 1; // first breakout of the day sets bias PrintFormat("NANDR EA: [%s] Bullish ORB breakout at %.2f [DayBias=%d]", g_Sessions[s].name, g_Sessions[s].orbHigh, g_GlobalBiasDir); @@ -650,6 +656,7 @@ void DetectBreakouts() g_Sessions[s].breakoutDir = -1; g_Sessions[s].inBreakout = true; g_Sessions[s].breakoutBarsAgo= 0; + g_Sessions[s].breakoutEntryTaken = false; if(g_GlobalBiasDir == 0) g_GlobalBiasDir = -1; // first breakout of the day sets bias PrintFormat("NANDR EA: [%s] Bearish ORB breakout at %.2f [DayBias=%d]", g_Sessions[s].name, g_Sessions[s].orbLow, g_GlobalBiasDir); @@ -682,6 +689,7 @@ void UpdateRetestState(int sessIdx) if(sweepRev) { g_Sessions[sessIdx].breakoutDir = -1; + g_Sessions[sessIdx].breakoutEntryTaken = false; g_GlobalBiasDir = -1; // price swept the level and closed below — day bias now bearish PrintFormat("NANDR EA: [%s] Bullish sweep reversal at orbHigh %.2f — flipping to BEARISH [DayBias=%d]", g_Sessions[sessIdx].name, orbHigh, g_GlobalBiasDir); @@ -693,6 +701,7 @@ void UpdateRetestState(int sessIdx) g_Sessions[sessIdx].name, orbMid); g_Sessions[sessIdx].inBreakout = false; g_Sessions[sessIdx].breakoutDir = 0; + g_Sessions[sessIdx].breakoutEntryTaken = false; } } else if(g_Sessions[sessIdx].breakoutDir == -1) @@ -702,6 +711,7 @@ void UpdateRetestState(int sessIdx) if(sweepRev) { g_Sessions[sessIdx].breakoutDir = 1; + g_Sessions[sessIdx].breakoutEntryTaken = false; g_GlobalBiasDir = 1; // price swept the level and closed above — day bias now bullish PrintFormat("NANDR EA: [%s] Bearish sweep reversal at orbLow %.2f — flipping to BULLISH [DayBias=%d]", g_Sessions[sessIdx].name, orbLow, g_GlobalBiasDir); @@ -713,6 +723,7 @@ void UpdateRetestState(int sessIdx) g_Sessions[sessIdx].name, orbMid); g_Sessions[sessIdx].inBreakout = false; g_Sessions[sessIdx].breakoutDir = 0; + g_Sessions[sessIdx].breakoutEntryTaken = false; } } } @@ -930,6 +941,27 @@ int CountPendingOrders() return count; } +//+------------------------------------------------------------------+ +//| Single-active-entry policy | +//+------------------------------------------------------------------+ +bool HasActiveExposure() +{ + return (CountOpenPositions() + CountPendingOrders()) > 0; +} + +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) + g_Sessions[s].breakoutEntryTaken = false; + } +} + //+------------------------------------------------------------------+ //| Cancel all pending orders | //+------------------------------------------------------------------+ @@ -951,7 +983,8 @@ 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(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) return; + if(HasActiveExposure()) return; + if(g_Sessions[sessIdx].breakoutEntryTaken) return; double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK); double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID); @@ -986,6 +1019,7 @@ void OpenTrade(int dir, int sessIdx, double orbLevel, double obTop, double obBot { g_TodayTrades++; g_Sessions[sessIdx].tradesThisSession++; + g_Sessions[sessIdx].breakoutEntryTaken = true; // 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). @@ -1033,8 +1067,9 @@ void CheckRetestEntriesTick() { 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; - if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) continue; + if(HasActiveExposure()) continue; if(InpBreakoutExpireBars > 0 && g_Sessions[s].breakoutBarsAgo >= InpBreakoutExpireBars) continue; double orbHigh = g_Sessions[s].orbHigh; @@ -1057,6 +1092,7 @@ void CheckRetestEntriesTick() { dir = -1; g_Sessions[s].breakoutDir = -1; + g_Sessions[s].breakoutEntryTaken = false; g_GlobalBiasDir = -1; // tick sweep below orbHigh — day bias flips bearish PrintFormat("NANDR EA: [%s] Tick sweep reversal below orbHigh %.2f → SELL [DayBias=%d]", g_Sessions[s].name, orbHigh, g_GlobalBiasDir); @@ -1072,6 +1108,7 @@ void CheckRetestEntriesTick() { dir = -1; g_Sessions[s].breakoutDir = -1; + g_Sessions[s].breakoutEntryTaken = false; } } } @@ -1089,6 +1126,7 @@ void CheckRetestEntriesTick() { dir = 1; g_Sessions[s].breakoutDir = 1; + g_Sessions[s].breakoutEntryTaken = false; g_GlobalBiasDir = 1; // tick sweep above orbLow — day bias flips bullish PrintFormat("NANDR EA: [%s] Tick sweep reversal above orbLow %.2f → BUY [DayBias=%d]", g_Sessions[s].name, orbLow, g_GlobalBiasDir); @@ -1104,6 +1142,7 @@ void CheckRetestEntriesTick() { dir = 1; g_Sessions[s].breakoutDir = 1; + g_Sessions[s].breakoutEntryTaken = false; } } } @@ -1130,7 +1169,7 @@ void CheckRetestEntriesTick() for(int i = 0; i < g_BearOBCount; i++) { if(!g_BearOBs[i].active || g_BearOBs[i].traded) continue; - if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) break; + if(HasActiveExposure()) break; if(g_TodayTrades >= InpMaxTradesPerDay) break; double obBottom = g_BearOBs[i].bottom; @@ -1169,7 +1208,7 @@ void CheckRetestEntriesTick() for(int i = 0; i < g_BullOBCount; i++) { if(!g_BullOBs[i].active || g_BullOBs[i].traded) continue; - if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) break; + if(HasActiveExposure()) break; if(g_TodayTrades >= InpMaxTradesPerDay) break; double obTop3 = g_BullOBs[i].top; @@ -1224,6 +1263,7 @@ void CheckEntrySignals() g_Sessions[s].inBreakout = false; g_Sessions[s].breakoutDir = 0; g_Sessions[s].inRetest = false; + g_Sessions[s].breakoutEntryTaken = false; continue; } @@ -1301,21 +1341,38 @@ void ManageOpenTrades() double fullLots = g_Position.Volume(); double closeLots = NormalizeLot(fullLots * InpPartialCloseRatio); double minLot = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_MIN); + double stepLot = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_STEP); // Ensure we leave at least minLot remaining so the position stays open double remaining = NormalizeLot(fullLots - closeLots); if(closeLots >= minLot && remaining >= minLot) { - // PositionClosePartial reduces the existing position by closeLots WITHOUT - // opening a new opposite trade. This is correct for both netting and hedging - // accounts. Using g_Trade.Sell/Buy here would open a new hedge position - // instead of reducing the original one — that was the previous bug. - g_Trade.SetComment(StringFormat("NANDR|PartialClose|%.0fpips", InpBreakevenTrigPips)); + PrintFormat("NANDR EA: Partial close trigger. Ticket=%llu Full=%.2f Ratio=%.2f Close=%.2f Remaining=%.2f Step=%.2f Min=%.2f", + ticket, fullLots, InpPartialCloseRatio, closeLots, remaining, stepLot, minLot); + ResetLastError(); bool partResult = g_Trade.PositionClosePartial(ticket, closeLots); if(partResult) { - MarkPartialClosed(ticket); - PrintFormat("NANDR EA: Partial close %.2f lots at %.2f (%.0f pips profit). Ticket=%llu", - closeLots, price, InpBreakevenTrigPips, ticket); + // Verify the same position volume actually decreased. + if(PositionSelectByTicket(ticket)) + { + double afterLots = PositionGetDouble(POSITION_VOLUME); + if(afterLots < fullLots) + { + MarkPartialClosed(ticket); + PrintFormat("NANDR EA: Partial close %.2f lots at %.2f (%.0f pips profit). Ticket=%llu Remaining=%.2f", + closeLots, price, InpBreakevenTrigPips, ticket, afterLots); + } + else + { + PrintFormat("NANDR EA: Partial close returned true but volume did not reduce. Ticket=%llu Before=%.2f After=%.2f", + ticket, fullLots, afterLots); + } + } + else + { + // If ticket no longer exists it may have been fully closed; do not mark partial as successful. + PrintFormat("NANDR EA: Partial close check could not reselect ticket %llu after close call.", ticket); + } } else PrintFormat("NANDR EA: Partial close failed. Error=%d Ticket=%llu", @@ -1848,8 +1905,11 @@ int OnInit() int digits = (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS); g_PointSize = SymbolInfoDouble(g_Symbol, SYMBOL_POINT); g_PipSize = (digits == 2 || digits == 3 || digits == 5) ? g_PointSize * 10 : g_PointSize; + g_ServerUtcOffsetSec = (int)(TimeCurrent() - TimeGMT()); PrintFormat("NANDR EA: PipSize=%.5f (digits=%d). 100 pips = %.2f price units.", g_PipSize, digits, PipsToPrice(100)); + PrintFormat("NANDR EA: Server UTC offset = %+d seconds (%+.1f hours)", + g_ServerUtcOffsetSec, g_ServerUtcOffsetSec / 3600.0); // Initialize OB arrays ArrayResize(g_BullOBs, InpOBMaxCount); @@ -1926,8 +1986,13 @@ void OnTick() if(g_IsNewBar) g_LastBarTime = barTime; + // Reset day/session state before any tick-based entry logic runs. + if(g_IsNewBar) + CheckDailyReset(); + // Per-tick management ManageOpenTrades(); + RefreshBreakoutEntryLocks(); CheckRiskLimits(); // Retest entries fire on tick (at wick touch), not on bar close @@ -1935,9 +2000,6 @@ void OnTick() if(!g_IsNewBar) return; - // Daily reset - CheckDailyReset(); - // Always update ORB and OB state regardless of halt status. // This keeps session levels current for display and ensures breakouts are detected // as soon as trading resumes (e.g. next day after a daily-loss halt). diff --git a/README.md b/README.md index 082b70f..e373aee 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ All sessions are **disabled by default** — enable the ones you want to trade: | Session | UTC Start | Best for XAUUSD | |---------|-----------|-----------------| -| Daily Open | 00:00 | Asian range setup | +| Daily Open | 22:00 | Asian range setup | | Tokyo | 00:00 | Low volume, range-bound | | London | 07:00 | High volatility ✓ | | NY | 12:00 | High volatility ✓ |