fix: Opening Day time, Only one ACTIVE entry is allowed at a time (position or pending), Previous-day session state could still trade before reset

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