Files
NANDR_BOT/NANDR_ORB_OB_EA.mq5

2524 lines
95 KiB
Plaintext

//+------------------------------------------------------------------+
//| NANDR_ORB_OB_EA.mq5 |
//| ORB + Order Block Scalping EA for XAUUSD |
//| Supports M1 / M5 / M15 (default M15). ORB = 15 real minutes. |
//| Based on: ORB-All-Sessions.pine + LuxAlgo Order Block Detector |
//+------------------------------------------------------------------+
#property copyright "NANDR"
#property version "1.40"
#property strict
#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\OrderInfo.mqh>
//+------------------------------------------------------------------+
//| Enumerations |
//+------------------------------------------------------------------+
enum ENUM_ORB_TF
{
ORB_TF_M1 = 1, // M1 (15 bars = 15min ORB)
ORB_TF_M5 = 5, // M5 (3 bars = 15min ORB)
ORB_TF_M15 = 15 // M15 (1 bar = 15min ORB) [Default]
};
enum ENUM_SL_MODE
{
SL_OB_BOUNDARY, // OB Boundary
SL_FIXED_PIPS, // Fixed Pips
SL_ATR_BASED, // ATR-Based
SL_ORB_OPPOSITE, // ORB Opposite Boundary (buy=ORL, sell=ORH)
SL_ORB_MID // ORB Range Midpoint
};
enum ENUM_TP_MODE
{
TP_FIXED_PIPS, // Fixed Pips
TP_RR_RATIO // Risk:Reward Ratio
};
enum ENUM_LOT_MODE
{
LOT_FIXED, // Fixed Lot Size
LOT_RISK_PERCENT // Risk % of Balance
};
enum ENUM_ENTRY_MODE
{
ENTRY_MARKET, // Market Order (on retest bar close)
ENTRY_LIMIT // Limit Order (at ORB level)
};
enum ENUM_OB_MITIGATION
{
MITIG_WICK, // Wick (high/low penetrates zone)
MITIG_CLOSE // Close (close penetrates zone)
};
enum ENUM_LADDER_SL_MODE
{
LADDER_SL_FIXED_PIPS, // Use InpSLPips
LADDER_SL_LEVEL_BUFFER // Buffer beyond source level
};
// Coupled ORB risk methods (per ORB strategy spec). Each option fixes BOTH the
// stop-loss placement and the reward:risk ratio together.
enum ENUM_ORB_RISK_METHOD
{
ORB_RISK_LEGACY, // Use InpSLMode / InpTPMode / InpRRRatio (manual)
ORB_RISK_BOUNDARY_1_2, // SL at broken range boundary → 1:2 RR
ORB_RISK_MID_1_1_5, // SL at range midpoint (50%) → 1:1.5 RR
ORB_RISK_OPPOSITE_1_1 // SL at opposite range boundary→ 1:1 RR
};
// Daily directional bias control.
enum ENUM_BIAS_MODE
{
BIAS_AUTO, // Auto: first ORB breakout of the day sets the tradeable direction
BIAS_OFF, // Flat: take no trades today (news / high-uncertainty days)
BIAS_LONG_ONLY, // Manual: only long entries allowed
BIAS_SHORT_ONLY // Manual: only short entries allowed
};
//+------------------------------------------------------------------+
//| Input Parameters |
//+------------------------------------------------------------------+
// --- Symbol ---
input group "═══ Symbol Settings ═══"
input string InpSymbolName = "XAUUSD"; // Symbol Name (auto-detected)
// --- Timeframe ---
input group "═══ Timeframe Settings ═══"
input ENUM_ORB_TF InpOrbTimeframe = ORB_TF_M15; // ORB Timeframe (M1/M5/M15)
input int InpBreakoutConfBars = 0; // Breakout Confirm Bars (0=auto)
// --- Sessions ---
input group "═══ Session Settings ═══"
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 (08:00 UTC = 8AM GMT)
input bool InpUseNYSession = true; // NY Session (12:00 UTC)
input bool InpUseNYOrbSession = true; // NY ORB Session (13:30 UTC = 9:30 EDT)
// Session start times are UTC-based (converted to broker server time internally).
input int InpDailyOpenHour = 22; // Daily Open Start Hour (UTC)
input int InpDailyOpenMin = 0; // Daily Open Start Minute
input int InpTokyoHour = 0; // Tokyo Start Hour (UTC)
input int InpTokyoMin = 0; // Tokyo Start Minute
input int InpLondonHour = 8; // London Start Hour (UTC, 8=8AM GMT)
input int InpLondonMin = 0; // London Start Minute
input int InpNYHour = 12; // NY Start Hour (UTC)
input int InpNYMin = 0; // NY Start Minute
input int InpNYOrbHour = 13; // NY ORB Start Hour (UTC)
input int InpNYOrbMin = 30; // NY ORB Start Minute
// --- Order Block Settings ---
input group "═══ Order Block Settings ═══"
input int InpOBPivotLength = 0; // OB Pivot Length (0=auto by TF)
input int InpOBMaxCount = 5; // Max Active OBs per direction
input ENUM_OB_MITIGATION InpOBMitig = MITIG_WICK; // Mitigation Method
input bool InpOBRequireConf = false; // Require OB Confirmation
input double InpOBProximityPips = 50.0; // OB Proximity (pips)
// --- Entry Settings ---
input group "═══ Entry Settings ═══"
input ENUM_ENTRY_MODE InpEntryMode = ENTRY_MARKET; // Entry Mode
input bool InpUseStrictFilter = false; // Use Strict Breakout Filter (false=simple close-cross)
input bool InpWaitForRetest = true; // Wait for ORB Retest Before Entry
input int InpBreakoutExpireBars = 20; // Bars Before Breakout Expires (0=never)
input bool InpUseOBRetestEntry = false; // OB Retest Entry (optional add-on, default OFF)
input int InpMaxRetestsPerSession = 3; // Max Retest Entries Per Session (per breakout)
input ENUM_BIAS_MODE InpDailyBiasMode = BIAS_AUTO; // Daily Bias: AUTO / OFF(no trades) / LONG-only / SHORT-only
input bool InpUseBreakoutVolFilter = false; // Require Volume Spike on Breakout Bar (fakeout filter)
input double InpBreakoutVolMult = 1.5; // Breakout Volume >= X * Average Volume
input int InpBreakoutVolAvgBars = 20; // Bars to Average Volume Over
// --- Session Level Ladder ---
input group "═══ Session Level Ladder ═══"
input bool InpUseSessionLevelLadder = false; // Enable Session Level Ladder Entries
input bool InpLadderOnDailyOpen = true; // Ladder on Daily Open session
input bool InpLadderOnTokyo = true; // Ladder on Tokyo session
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)
// --- Lot Size & Risk ---
input group "═══ Lot Size & Risk ═══"
input ENUM_LOT_MODE InpLotMode = LOT_RISK_PERCENT; // Lot Mode
input double InpFixedLotSize = 0.05; // Fixed Lot Size
input double InpRiskPercent = 0.05; // Risk % per Trade
// --- Stop Loss ---
input group "═══ Stop Loss ═══"
input ENUM_ORB_RISK_METHOD InpOrbRiskMethod = ORB_RISK_BOUNDARY_1_2; // ORB Risk Method (sets SL + RR together)
input ENUM_SL_MODE InpSLMode = SL_OB_BOUNDARY; // SL Mode (used only when Risk Method = LEGACY)
input double InpSLPips = 100.0; // SL Fixed Pips
input double InpATRMultiplier = 1.5; // ATR Multiplier (for SL_ATR)
input int InpATRPeriod = 14; // ATR Period
// --- Take Profit ---
input group "═══ Take Profit ═══"
input ENUM_TP_MODE InpTPMode = TP_RR_RATIO; // TP Mode (used only when Risk Method = LEGACY)
input double InpTPPips = 100.0; // TP Fixed Pips
input double InpRRRatio = 2.0; // Risk:Reward Ratio (LEGACY mode)
// --- Trade Management ---
input group "═══ Trade Management ═══"
input double InpBreakevenTrigPips = 50.0; // Breakeven Trigger (pips)
input double InpBreakevenOffPips = 2.0; // Breakeven Offset (pips)
input double InpPartialCloseRatio = 0.5; // Partial Close Ratio at BE (0=disabled, 0.5=half)
input double InpTrailingStopPips = 0.0; // Trailing Stop Pips (0=off)
input int InpMaxTradesPerDay = 8; // Max Trades per Day
input int InpMaxPosPerSession = 2; // Max Positions per Session
// --- Risk Management ---
input group "═══ Risk Management ═══"
input double InpMaxDailyLossUSD = 150.0; // Max Daily Loss USD (0=off)
input double InpMaxDailyLossPct = 5.0; // Max Daily Loss % (0=off)
// --- Display ---
input group "═══ Display Settings ═══"
input bool InpShowDashboard = true; // Show Dashboard
input bool InpShowORBLines = true; // Show ORB Lines
input bool InpShowOBZones = true; // Show OB Zones
input bool InpShowTradeLabels = true; // Show Trade Labels
// --- Magic ---
input int InpMagicNumber = 202601; // Magic Number
//+------------------------------------------------------------------+
//| Session structure |
//+------------------------------------------------------------------+
struct SSession
{
string name;
int startHour;
int startMin;
bool enabled;
double orbHigh;
double orbLow;
int orbBarCount;
bool orbComplete;
datetime orbStartTime;
int breakoutDir; // 0=none, 1=bull, -1=bear
bool inBreakout;
int breakoutBarsAgo;
bool breakoutEntryTaken; // one entry max per breakout confirmation leg
bool ladderBullLowDone; // Bull ladder: low -> mid done
bool ladderBullMidDone; // Bull ladder: mid -> high done
bool ladderBearHighDone; // Bear ladder: high -> mid done
bool ladderBearMidDone; // Bear ladder: mid -> low done
int ladderTradesThisSession;
int tradesThisSession;
bool retestFiredThisBar; // blocks duplicate entries on consecutive ticks within same bar
void Reset()
{
orbHigh = 0;
orbLow = DBL_MAX;
orbBarCount = 0;
orbComplete = false;
orbStartTime = 0;
breakoutDir = 0;
inBreakout = false;
breakoutBarsAgo = 0;
breakoutEntryTaken = false;
ladderBullLowDone = false;
ladderBullMidDone = false;
ladderBearHighDone = false;
ladderBearMidDone = false;
ladderTradesThisSession = 0;
tradesThisSession = 0;
retestFiredThisBar = false;
}
};
//+------------------------------------------------------------------+
//| Order Block structure |
//+------------------------------------------------------------------+
struct SOrderBlock
{
double top;
double bottom;
double mid;
datetime time;
bool active;
bool traded; // Prevents re-entering the same OB zone twice in one day
string objName;
};
//+------------------------------------------------------------------+
//| Global variables |
//+------------------------------------------------------------------+
CTrade g_Trade;
CPositionInfo g_Position;
COrderInfo g_Order;
string g_Symbol;
int g_OrbBarsNeeded;
int g_BreakoutConfBars;
int g_OBPivotLength;
double g_PipSize;
double g_PointSize;
// Sessions array (max 5)
SSession g_Sessions[5];
int g_SessionCount = 0;
// Order blocks
SOrderBlock g_BullOBs[];
SOrderBlock g_BearOBs[];
int g_BullOBCount = 0;
int g_BearOBCount = 0;
// Day bias: in AUTO mode this holds the direction of the first confirmed ORB
// breakout of the day (0=none, 1=bull, -1=bear). In manual bias modes it is unused.
// Breakout DETECTION is never suppressed (levels/state keep updating); entries are
// gated by IsDirectionAllowed() so the bias only filters trade execution.
int g_GlobalBiasDir = 0;
// Tracks tickets that have already had partial close executed so it only fires once per position
#define MAX_PARTIAL_TRACKED 20
ulong g_PartialClosedTickets[MAX_PARTIAL_TRACKED];
int g_PartialClosedCount = 0;
bool IsPartialClosed(ulong ticket)
{
for(int i = 0; i < g_PartialClosedCount; i++)
if(g_PartialClosedTickets[i] == ticket) return true;
return false;
}
void MarkPartialClosed(ulong ticket)
{
if(g_PartialClosedCount < MAX_PARTIAL_TRACKED)
{
g_PartialClosedTickets[g_PartialClosedCount] = ticket;
g_PartialClosedCount++;
}
}
// Daily stats
datetime g_LastDayReset = 0;
int g_TodayTrades = 0;
int g_TodayWins = 0;
int g_TodayLosses = 0;
double g_TodayPnL = 0;
double g_DayStartEquity = 0;
bool g_TradingHalted = false;
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;
//+------------------------------------------------------------------+
//| Utility: resolve actual symbol |
//+------------------------------------------------------------------+
string ResolveSymbol(const string base)
{
// Exact match first
if(SymbolInfoDouble(base, SYMBOL_BID) > 0)
return base;
// Scan all symbols for XAUUSD variants
int total = SymbolsTotal(false);
for(int i = 0; i < total; i++)
{
string sym = SymbolName(i, false);
if(StringFind(sym, "XAUUSD") >= 0 || StringFind(sym, "GOLD") >= 0 || StringFind(sym, "XAU") >= 0)
{
if(SymbolInfoDouble(sym, SYMBOL_BID) > 0)
{
PrintFormat("NANDR EA: Symbol '%s' not found. Using '%s' instead.", base, sym);
return sym;
}
}
}
Print("NANDR EA: WARNING — could not resolve symbol '", base, "'. Using as-is.");
return base;
}
//+------------------------------------------------------------------+
//| Utility: pip value |
//+------------------------------------------------------------------+
double PipsToPrice(double pips)
{
return pips * g_PipSize;
}
double PriceToPips(double price)
{
return price / g_PipSize;
}
//+------------------------------------------------------------------+
//| Utility: normalize lot size |
//+------------------------------------------------------------------+
double NormalizeLot(double lots)
{
double step = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_STEP);
double minL = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_MIN);
double maxL = SymbolInfoDouble(g_Symbol, SYMBOL_VOLUME_MAX);
lots = MathFloor(lots / step) * step;
lots = MathMax(lots, minL);
lots = MathMin(lots, maxL);
return NormalizeDouble(lots, 2);
}
//+------------------------------------------------------------------+
//| Calculate lot size from risk |
//+------------------------------------------------------------------+
double CalcLotSize(double slPips)
{
if(InpLotMode == LOT_FIXED)
return NormalizeLot(InpFixedLotSize);
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskUSD = balance * InpRiskPercent / 100.0;
double tickVal = SymbolInfoDouble(g_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(g_Symbol, SYMBOL_TRADE_TICK_SIZE);
double pipValue = tickVal * (g_PipSize / tickSize);
if(pipValue <= 0 || slPips <= 0)
return NormalizeLot(InpFixedLotSize);
double lots = riskUSD / (slPips * pipValue);
// Safety cap: never risk more than 5% of balance per trade regardless of settings.
// This prevents runaway lot sizes caused by an incorrect SL distance (e.g. pip size bug,
// misconfigured pips, or SL that is too tight).
double maxRiskUSD = balance * 5.0 / 100.0;
double maxSafeLots = maxRiskUSD / (slPips * pipValue);
if(lots > maxSafeLots)
{
PrintFormat("NANDR EA: WARNING — Computed lots %.2f exceeds 5%% risk cap. "
"Clamped to %.2f. Check SL pips (%.1f) and pip size (%.5f).",
lots, maxSafeLots, slPips, g_PipSize);
lots = maxSafeLots;
}
return NormalizeLot(lots);
}
//+------------------------------------------------------------------+
//| Calculate SL price |
//+------------------------------------------------------------------+
double CalcSL(int dir, double entry, double obBottom, double obTop, double orbHigh, double orbLow)
{
double sl = 0;
double fixedFallback = (dir > 0) ? entry - PipsToPrice(InpSLPips)
: entry + PipsToPrice(InpSLPips);
if(InpSLMode == SL_FIXED_PIPS)
{
sl = fixedFallback;
}
else if(InpSLMode == SL_OB_BOUNDARY)
{
if(dir > 0 && obBottom > 0)
sl = obBottom - PipsToPrice(2.0); // 2 pip buffer below OB bottom
else if(dir < 0 && obTop > 0)
sl = obTop + PipsToPrice(2.0); // 2 pip buffer above OB top
else
sl = fixedFallback;
}
else if(InpSLMode == SL_ORB_OPPOSITE)
{
// Buy → SL at ORB Low; Sell → SL at ORB High (2 pip buffer beyond).
double raw = (dir > 0) ? orbLow - PipsToPrice(2.0)
: orbHigh + PipsToPrice(2.0);
bool valid = (dir > 0) ? (raw < entry) : (raw > entry);
sl = valid ? raw : fixedFallback;
}
else if(InpSLMode == SL_ORB_MID)
{
// SL at the midpoint of the opening range.
double mid = (orbHigh + orbLow) / 2.0;
bool valid = (dir > 0) ? (mid < entry) : (mid > entry);
sl = valid ? mid : fixedFallback;
}
else // SL_ATR_BASED
{
double atr[1];
if(CopyBuffer(g_ATRHandle, 0, 0, 1, atr) > 0)
sl = (dir > 0) ? entry - atr[0] * InpATRMultiplier
: entry + atr[0] * InpATRMultiplier;
else
sl = fixedFallback;
}
return NormalizeDouble(sl, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
}
//+------------------------------------------------------------------+
//| Calculate TP price |
//+------------------------------------------------------------------+
double CalcTP(int dir, double entry, double sl)
{
double tp = 0;
double slDist = MathAbs(entry - sl);
if(InpTPMode == TP_FIXED_PIPS)
{
tp = (dir > 0) ? entry + PipsToPrice(InpTPPips)
: entry - PipsToPrice(InpTPPips);
}
else // RR ratio
{
tp = (dir > 0) ? entry + slDist * InpRRRatio
: entry - slDist * InpRRRatio;
}
return NormalizeDouble(tp, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
}
//+------------------------------------------------------------------+
//| Coupled ORB risk: derive SL and TP together from the opening |
//| range, per the selected risk method. |
//| BOUNDARY → SL at broken boundary (long=ORH, short=ORL), 1:2 |
//| MID → SL at range midpoint, 1:1.5 |
//| OPPOSITE → SL at opposite boundary (long=ORL, short=ORH), 1:1 |
//| Returns false when the method is LEGACY or the geometry is |
//| invalid (caller then falls back to CalcSL/CalcTP). |
//+------------------------------------------------------------------+
bool CalcOrbRisk(int dir, double entry, double orbHigh, double orbLow,
double &slOut, double &tpOut)
{
if(InpOrbRiskMethod == ORB_RISK_LEGACY) return false;
if(orbHigh <= 0 || orbLow >= DBL_MAX || orbHigh <= orbLow) return false;
double buf = PipsToPrice(2.0);
double mid = (orbHigh + orbLow) / 2.0;
double slLevel = 0.0;
double rr = 0.0;
switch(InpOrbRiskMethod)
{
case ORB_RISK_BOUNDARY_1_2:
slLevel = (dir > 0) ? orbHigh - buf : orbLow + buf;
rr = 2.0;
break;
case ORB_RISK_MID_1_1_5:
slLevel = mid;
rr = 1.5;
break;
case ORB_RISK_OPPOSITE_1_1:
slLevel = (dir > 0) ? orbLow - buf : orbHigh + buf;
rr = 1.0;
break;
default:
return false;
}
// SL must sit on the correct side of entry.
if((dir > 0 && slLevel >= entry) || (dir < 0 && slLevel <= entry))
return false;
double slDist = MathAbs(entry - slLevel);
if(slDist <= 0) return false;
double tpLevel = (dir > 0) ? entry + slDist * rr : entry - slDist * rr;
int digits = (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS);
slOut = NormalizeDouble(slLevel, digits);
tpOut = NormalizeDouble(tpLevel, digits);
return true;
}
//+------------------------------------------------------------------+
//| Daily bias gate: is a trade in this direction allowed today? |
//+------------------------------------------------------------------+
bool IsDirectionAllowed(int dir)
{
switch(InpDailyBiasMode)
{
case BIAS_OFF: return false; // skip the day entirely
case BIAS_LONG_ONLY: return (dir > 0);
case BIAS_SHORT_ONLY: return (dir < 0);
case BIAS_AUTO:
default:
// First breakout of the day establishes the direction; afterwards only
// trades aligned with that direction are permitted.
if(g_GlobalBiasDir == 0) return true;
return (dir == g_GlobalBiasDir);
}
}
//+------------------------------------------------------------------+
//| Initialize sessions |
//+------------------------------------------------------------------+
void InitSessions()
{
g_SessionCount = 0;
// Daily Open
if(InpUseDailyOpen)
{
g_Sessions[g_SessionCount].name = "DailyOpen";
g_Sessions[g_SessionCount].startHour = InpDailyOpenHour;
g_Sessions[g_SessionCount].startMin = InpDailyOpenMin;
g_Sessions[g_SessionCount].enabled = true;
g_Sessions[g_SessionCount].Reset();
g_SessionCount++;
}
// Tokyo
if(InpUseTokyoSession)
{
g_Sessions[g_SessionCount].name = "Tokyo";
g_Sessions[g_SessionCount].startHour = InpTokyoHour;
g_Sessions[g_SessionCount].startMin = InpTokyoMin;
g_Sessions[g_SessionCount].enabled = true;
g_Sessions[g_SessionCount].Reset();
g_SessionCount++;
}
// London
if(InpUseLondonSession)
{
g_Sessions[g_SessionCount].name = "London";
g_Sessions[g_SessionCount].startHour = InpLondonHour;
g_Sessions[g_SessionCount].startMin = InpLondonMin;
g_Sessions[g_SessionCount].enabled = true;
g_Sessions[g_SessionCount].Reset();
g_SessionCount++;
}
// NY
if(InpUseNYSession)
{
g_Sessions[g_SessionCount].name = "NY";
g_Sessions[g_SessionCount].startHour = InpNYHour;
g_Sessions[g_SessionCount].startMin = InpNYMin;
g_Sessions[g_SessionCount].enabled = true;
g_Sessions[g_SessionCount].Reset();
g_SessionCount++;
}
// NY ORB (stock market open)
if(InpUseNYOrbSession)
{
g_Sessions[g_SessionCount].name = "NYORB";
g_Sessions[g_SessionCount].startHour = InpNYOrbHour;
g_Sessions[g_SessionCount].startMin = InpNYOrbMin;
g_Sessions[g_SessionCount].enabled = true;
g_Sessions[g_SessionCount].Reset();
g_SessionCount++;
}
}
//+------------------------------------------------------------------+
//| Check if current bar is the session open bar |
//+------------------------------------------------------------------+
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(utcBarTime, dt);
return (dt.hour == sess.startHour && dt.min == sess.startMin);
}
//+------------------------------------------------------------------+
//| Check if current bar is within session ORB accumulation window |
//+------------------------------------------------------------------+
bool IsWithinOrbWindow(const SSession &sess, datetime barTime)
{
if(sess.orbStartTime == 0) return false;
int elapsed = (int)(barTime - sess.orbStartTime);
return elapsed < g_OrbBarsNeeded * (int)InpOrbTimeframe * 60;
}
//+------------------------------------------------------------------+
//| Update ORB for all sessions on new bar |
//| IMPORTANT: always reads bar[1] (the just-CLOSED candle) so the |
//| true high/low is captured. bar[0] at new-bar time = open only. |
//+------------------------------------------------------------------+
void UpdateORBSessions()
{
datetime bar1Time = iTime(g_Symbol, PERIOD_CURRENT, 1);
double bar1High = iHigh(g_Symbol, PERIOD_CURRENT, 1);
double bar1Low = iLow(g_Symbol, PERIOD_CURRENT, 1);
// New bar: clear per-bar retest flag so the next wick can trigger a fresh entry
for(int s = 0; s < g_SessionCount; s++)
g_Sessions[s].retestFiredThisBar = false;
for(int s = 0; s < g_SessionCount; s++)
{
if(!g_Sessions[s].enabled) continue;
// Session open bar just closed: bar[1] time matches session start
if(IsSessionOpenBar(g_Sessions[s], bar1Time))
{
g_Sessions[s].orbHigh = bar1High;
g_Sessions[s].orbLow = bar1Low;
g_Sessions[s].orbBarCount = 1;
g_Sessions[s].orbStartTime = bar1Time;
g_Sessions[s].orbComplete = (g_OrbBarsNeeded == 1);
g_Sessions[s].breakoutDir = 0;
g_Sessions[s].inBreakout = false;
g_Sessions[s].breakoutBarsAgo = 0;
g_Sessions[s].tradesThisSession= 0;
ResetLadderProgress(s);
if(g_Sessions[s].orbComplete)
PrintFormat("NANDR EA: [%s] ORB complete. High=%.2f Low=%.2f",
g_Sessions[s].name, bar1High, bar1Low);
if(InpShowORBLines)
DrawORBLines(s);
continue;
}
// Still accumulating (M5 or M1): append bar[1] closed data to range
if(!g_Sessions[s].orbComplete && g_Sessions[s].orbBarCount > 0
&& IsWithinOrbWindow(g_Sessions[s], bar1Time))
{
g_Sessions[s].orbHigh = MathMax(g_Sessions[s].orbHigh, bar1High);
g_Sessions[s].orbLow = MathMin(g_Sessions[s].orbLow, bar1Low);
g_Sessions[s].orbBarCount++;
if(g_Sessions[s].orbBarCount >= g_OrbBarsNeeded)
{
g_Sessions[s].orbComplete = true;
PrintFormat("NANDR EA: [%s] ORB complete. High=%.2f Low=%.2f",
g_Sessions[s].name, g_Sessions[s].orbHigh, g_Sessions[s].orbLow);
}
if(InpShowORBLines)
DrawORBLines(s);
}
// Track breakout bar age (only while breakout is active)
// NOTE: incremented here before DetectBreakouts, so newly-detected
// breakouts start at 0 here but will be seen as 0 in CheckEntrySignals
// because DetectBreakouts resets it to 0 AFTER this runs.
// The entry guard uses <=1 to handle both cases safely.
if(g_Sessions[s].inBreakout)
g_Sessions[s].breakoutBarsAgo++;
}
}
//+------------------------------------------------------------------+
//| Strict breakout filter (mirrors Pine filteredHighCrossBO/Low) |
//+------------------------------------------------------------------+
bool CheckStrictBreakout(int dir, double orbLevel)
{
int N = g_BreakoutConfBars;
if(N < 2) N = 2;
// Need at least N+3 closed bars (all indices shifted +1 vs simple version)
int bars = Bars(g_Symbol, PERIOD_CURRENT);
if(bars < N + 3) return false;
if(dir > 0) // bullish
{
if(iLow(g_Symbol, PERIOD_CURRENT, N + 2) >= orbLevel) return false;
for(int i = 2; i <= N + 1; i++)
{
if(iLow(g_Symbol, PERIOD_CURRENT, i) <= orbLevel) return false;
if(iClose(g_Symbol, PERIOD_CURRENT, i) <= orbLevel) return false;
}
if(iClose(g_Symbol, PERIOD_CURRENT, 1) <= iLow(g_Symbol, PERIOD_CURRENT, 2)) return false;
if(iLow(g_Symbol, PERIOD_CURRENT, 1) <= orbLevel) return false;
return true;
}
else // bearish
{
if(iHigh(g_Symbol, PERIOD_CURRENT, N + 2) <= orbLevel) return false;
for(int i = 2; i <= N + 1; i++)
{
if(iHigh(g_Symbol, PERIOD_CURRENT, i) >= orbLevel) return false;
if(iClose(g_Symbol, PERIOD_CURRENT, i) >= orbLevel) return false;
}
if(iClose(g_Symbol, PERIOD_CURRENT, 1) >= iHigh(g_Symbol, PERIOD_CURRENT, 2)) return false;
if(iHigh(g_Symbol, PERIOD_CURRENT, 1) >= orbLevel) return false;
return true;
}
}
//+------------------------------------------------------------------+
//| Simple breakout: uses bar[1] (just-closed) vs bar[2] |
//| Matches Pine Script bar-close evaluation — never checks an |
//| in-progress candle so no premature signals at bar open. |
//+------------------------------------------------------------------+
bool CheckSimpleBreakout(int dir, double orbLevel)
{
double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1); // just-closed bar
double c1 = iClose(g_Symbol, PERIOD_CURRENT, 2); // bar before it
if(dir > 0) return (c1 <= orbLevel && c0 > orbLevel);
else return (c1 >= orbLevel && c0 < orbLevel);
}
//+------------------------------------------------------------------+
//| Volume-spike confirmation for the breakout bar (bar[1]) |
//| Optional fakeout filter: requires the just-closed breakout bar's |
//| tick volume to exceed a multiple of the recent average volume. |
//| Returns true (pass) when the filter is disabled or data is thin. |
//+------------------------------------------------------------------+
bool IsBreakoutVolumeConfirmed()
{
if(!InpUseBreakoutVolFilter) return true;
int n = InpBreakoutVolAvgBars;
if(n < 1) n = 1;
int bars = Bars(g_Symbol, PERIOD_CURRENT);
if(bars < n + 2) return true; // not enough history — don't block
double sum = 0;
for(int i = 2; i <= n + 1; i++)
sum += (double)iVolume(g_Symbol, PERIOD_CURRENT, i);
double avg = sum / n;
if(avg <= 0) return true;
double breakoutVol = (double)iVolume(g_Symbol, PERIOD_CURRENT, 1);
return (breakoutVol >= avg * InpBreakoutVolMult);
}
//+------------------------------------------------------------------+
//| Detect breakout for all sessions |
//+------------------------------------------------------------------+
void DetectBreakouts()
{
for(int s = 0; s < g_SessionCount; s++)
{
if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue;
if(g_Sessions[s].orbHigh <= 0 || g_Sessions[s].orbLow >= DBL_MAX) continue;
if(g_Sessions[s].inBreakout) 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);
bool bearBO = InpUseStrictFilter
? 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);
}
}
// Optional volume-spike confirmation (fakeout filter). Applies to whichever
// direction is breaking out; a weak-volume breakout bar is rejected.
if((bullBO || bearBO) && !IsBreakoutVolumeConfirmed())
{
PrintFormat("NANDR EA: [%s] Breakout rejected — volume on breakout bar below %.2fx average.",
g_Sessions[s].name, InpBreakoutVolMult);
bullBO = false;
bearBO = false;
}
if(bullBO)
{
g_Sessions[s].breakoutDir = 1;
g_Sessions[s].inBreakout = true;
g_Sessions[s].breakoutBarsAgo= 0;
g_Sessions[s].breakoutEntryTaken = false;
ResetLadderDirection(s, 1);
// In AUTO bias mode, the first breakout of the day sets the tradeable direction.
if(InpDailyBiasMode == BIAS_AUTO && g_GlobalBiasDir == 0) g_GlobalBiasDir = 1;
PrintFormat("NANDR EA: [%s] Bullish ORB breakout at %.2f [DayBias=%d]",
g_Sessions[s].name, g_Sessions[s].orbHigh, g_GlobalBiasDir);
}
else if(bearBO)
{
g_Sessions[s].breakoutDir = -1;
g_Sessions[s].inBreakout = true;
g_Sessions[s].breakoutBarsAgo= 0;
g_Sessions[s].breakoutEntryTaken = false;
ResetLadderDirection(s, -1);
// In AUTO bias mode, the first breakout of the day sets the tradeable direction.
if(InpDailyBiasMode == BIAS_AUTO && g_GlobalBiasDir == 0) g_GlobalBiasDir = -1;
PrintFormat("NANDR EA: [%s] Bearish ORB breakout at %.2f [DayBias=%d]",
g_Sessions[s].name, g_Sessions[s].orbLow, g_GlobalBiasDir);
}
}
}
//+------------------------------------------------------------------+
//| Update retest state on bar close: sweep reversals + invalidation |
//| Called once per new bar. Does NOT fire any trades. |
//+------------------------------------------------------------------+
void UpdateRetestState(int sessIdx)
{
if(!g_Sessions[sessIdx].inBreakout) return;
// Use bar[1] (just-closed) and bar[2] for bar-close confirmation.
double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1);
double c1 = iClose(g_Symbol, PERIOD_CURRENT, 2);
double h0 = iHigh(g_Symbol, PERIOD_CURRENT, 1);
double l0 = iLow(g_Symbol, PERIOD_CURRENT, 1);
double orbHigh = g_Sessions[sessIdx].orbHigh;
double orbLow = g_Sessions[sessIdx].orbLow;
double orbMid = (orbHigh + orbLow) / 2.0;
if(g_Sessions[sessIdx].breakoutDir == 1)
{
// Sweep reversal: bullish breakout failed — bar closed below orbHigh → flip to bearish
bool sweepRev = (c1 > orbHigh) && (l0 < orbHigh) && (c0 < orbHigh) && (c0 >= orbMid);
if(sweepRev)
{
g_Sessions[sessIdx].breakoutDir = -1;
g_Sessions[sessIdx].breakoutEntryTaken = false;
ResetLadderDirection(sessIdx, -1);
if(InpDailyBiasMode == BIAS_AUTO) g_GlobalBiasDir = -1; // swept & closed below → bias bearish
PrintFormat("NANDR EA: [%s] Bullish sweep reversal at orbHigh %.2f — flipping to BEARISH [DayBias=%d]",
g_Sessions[sessIdx].name, orbHigh, g_GlobalBiasDir);
}
// Full invalidation: closed below mid
else if(c0 < orbMid && c1 > orbMid)
{
PrintFormat("NANDR EA: [%s] Failed bullish retest (closed below mid %.2f)",
g_Sessions[sessIdx].name, orbMid);
g_Sessions[sessIdx].inBreakout = false;
g_Sessions[sessIdx].breakoutDir = 0;
g_Sessions[sessIdx].breakoutEntryTaken = false;
ResetLadderProgress(sessIdx);
}
}
else if(g_Sessions[sessIdx].breakoutDir == -1)
{
// Sweep reversal: bearish breakout failed — bar closed above orbLow → flip to bullish
bool sweepRev = (c1 < orbLow) && (h0 > orbLow) && (c0 > orbLow) && (c0 < orbHigh);
if(sweepRev)
{
g_Sessions[sessIdx].breakoutDir = 1;
g_Sessions[sessIdx].breakoutEntryTaken = false;
ResetLadderDirection(sessIdx, 1);
if(InpDailyBiasMode == BIAS_AUTO) g_GlobalBiasDir = 1; // swept & closed above → bias bullish
PrintFormat("NANDR EA: [%s] Bearish sweep reversal at orbLow %.2f — flipping to BULLISH [DayBias=%d]",
g_Sessions[sessIdx].name, orbLow, g_GlobalBiasDir);
}
// Full invalidation: closed above mid
else if(c0 > orbMid && c1 < orbMid)
{
PrintFormat("NANDR EA: [%s] Failed bearish retest (closed above mid %.2f)",
g_Sessions[sessIdx].name, orbMid);
g_Sessions[sessIdx].inBreakout = false;
g_Sessions[sessIdx].breakoutDir = 0;
g_Sessions[sessIdx].breakoutEntryTaken = false;
ResetLadderProgress(sessIdx);
}
}
}
//+------------------------------------------------------------------+
//| Order Block: scan for new OBs on new bar |
//+------------------------------------------------------------------+
void ScanOrderBlocks()
{
int length = g_OBPivotLength;
int barsNeeded = length * 2 + 2;
if(Bars(g_Symbol, PERIOD_CURRENT) < barsNeeded) return;
// Check for volume pivot at bar[length]
// Volume at bar[length] must be highest in [0..2*length]
long pivotVol = iVolume(g_Symbol, PERIOD_CURRENT, length);
bool isVolPivot = true;
for(int i = 0; i < length * 2 + 1; i++)
{
if(i == length) continue;
if(iVolume(g_Symbol, PERIOD_CURRENT, i) >= pivotVol)
{
isVolPivot = false;
break;
}
}
if(!isVolPivot) return;
// Market structure: os
// os=1 (bullish): low[length] is lowest low in the window
// os=0 (bearish): high[length] is highest high in the window
double pivotHigh = iHigh(g_Symbol, PERIOD_CURRENT, length);
double pivotLow = iLow(g_Symbol, PERIOD_CURRENT, length);
double highestHigh = pivotHigh;
double lowestLow = pivotLow;
for(int i = 0; i < length * 2 + 1; i++)
{
highestHigh = MathMax(highestHigh, iHigh(g_Symbol, PERIOD_CURRENT, i));
lowestLow = MathMin(lowestLow, iLow(g_Symbol, PERIOD_CURRENT, i));
}
int os = -1; // unknown
if(pivotHigh >= highestHigh) os = 0; // bearish structure
else if(pivotLow <= lowestLow) os = 1; // bullish structure
if(os < 0) return;
datetime obTime = iTime(g_Symbol, PERIOD_CURRENT, length);
double hl2 = (pivotHigh + pivotLow) / 2.0;
if(os == 1) // Bullish OB: zone [low[length], hl2]
{
if(g_BullOBCount < InpOBMaxCount)
{
// Check not duplicate
for(int i = 0; i < g_BullOBCount; i++)
if(g_BullOBs[i].time == obTime) return;
g_BullOBs[g_BullOBCount].top = hl2;
g_BullOBs[g_BullOBCount].bottom = pivotLow;
g_BullOBs[g_BullOBCount].mid = (hl2 + pivotLow) / 2.0;
g_BullOBs[g_BullOBCount].time = obTime;
g_BullOBs[g_BullOBCount].active = true;
g_BullOBs[g_BullOBCount].traded = false;
g_BullOBs[g_BullOBCount].objName= "NANDR_BullOB_" + IntegerToString(obTime);
g_BullOBCount++;
if(InpShowOBZones) DrawOBZone(g_BullOBCount - 1, true);
PrintFormat("NANDR EA: Bullish OB detected. Zone [%.2f - %.2f]", pivotLow, hl2);
}
}
else // Bearish OB: zone [hl2, high[length]]
{
if(g_BearOBCount < InpOBMaxCount)
{
for(int i = 0; i < g_BearOBCount; i++)
if(g_BearOBs[i].time == obTime) return;
g_BearOBs[g_BearOBCount].top = pivotHigh;
g_BearOBs[g_BearOBCount].bottom = hl2;
g_BearOBs[g_BearOBCount].mid = (pivotHigh + hl2) / 2.0;
g_BearOBs[g_BearOBCount].time = obTime;
g_BearOBs[g_BearOBCount].active = true;
g_BearOBs[g_BearOBCount].traded = false;
g_BearOBs[g_BearOBCount].objName= "NANDR_BearOB_" + IntegerToString(obTime);
g_BearOBCount++;
if(InpShowOBZones) DrawOBZone(g_BearOBCount - 1, false);
PrintFormat("NANDR EA: Bearish OB detected. Zone [%.2f - %.2f]", hl2, pivotHigh);
}
}
}
//+------------------------------------------------------------------+
//| Remove mitigated OBs (Wick or Close method) |
//+------------------------------------------------------------------+
void MitigateOrderBlocks()
{
double c0 = iClose(g_Symbol, PERIOD_CURRENT, 0);
double l0 = iLow(g_Symbol, PERIOD_CURRENT, 0);
double h0 = iHigh(g_Symbol, PERIOD_CURRENT, 0);
// Bullish OBs: mitigated if price goes below bottom
for(int i = g_BullOBCount - 1; i >= 0; i--)
{
if(!g_BullOBs[i].active) continue;
double target = (InpOBMitig == MITIG_WICK) ? l0 : c0;
if(target < g_BullOBs[i].bottom)
{
g_BullOBs[i].active = false;
if(InpShowOBZones) RemoveOBZone(g_BullOBs[i].objName);
// Shift array left
for(int j = i; j < g_BullOBCount - 1; j++)
g_BullOBs[j] = g_BullOBs[j+1];
g_BullOBCount--;
}
}
// Bearish OBs: mitigated if price goes above top
for(int i = g_BearOBCount - 1; i >= 0; i--)
{
if(!g_BearOBs[i].active) continue;
double target = (InpOBMitig == MITIG_WICK) ? h0 : c0;
if(target > g_BearOBs[i].top)
{
g_BearOBs[i].active = false;
if(InpShowOBZones) RemoveOBZone(g_BearOBs[i].objName);
for(int j = i; j < g_BearOBCount - 1; j++)
g_BearOBs[j] = g_BearOBs[j+1];
g_BearOBCount--;
}
}
}
//+------------------------------------------------------------------+
//| Find nearest OB to a given price and direction |
//+------------------------------------------------------------------+
bool IsOBNearLevel(double price, int dir, double &obTop, double &obBottom)
{
double proxPrice = PipsToPrice(InpOBProximityPips);
if(dir > 0) // Look for bullish OB near or below entry
{
double nearest = DBL_MAX;
int bestIdx = -1;
for(int i = 0; i < g_BullOBCount; i++)
{
if(!g_BullOBs[i].active) continue;
double dist = MathAbs(price - g_BullOBs[i].top);
if(dist <= proxPrice && dist < nearest)
{
nearest = dist;
bestIdx = i;
}
}
if(bestIdx >= 0)
{
obTop = g_BullOBs[bestIdx].top;
obBottom = g_BullOBs[bestIdx].bottom;
return true;
}
}
else // Look for bearish OB near or above entry
{
double nearest = DBL_MAX;
int bestIdx = -1;
for(int i = 0; i < g_BearOBCount; i++)
{
if(!g_BearOBs[i].active) continue;
double dist = MathAbs(price - g_BearOBs[i].bottom);
if(dist <= proxPrice && dist < nearest)
{
nearest = dist;
bestIdx = i;
}
}
if(bestIdx >= 0)
{
obTop = g_BearOBs[bestIdx].top;
obBottom = g_BearOBs[bestIdx].bottom;
return true;
}
}
obTop = obBottom = 0;
return false;
}
//+------------------------------------------------------------------+
//| Session-scoped active exposure |
//+------------------------------------------------------------------+
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;
}
void ResetLadderProgress(int sessIdx)
{
g_Sessions[sessIdx].ladderBullLowDone = false;
g_Sessions[sessIdx].ladderBullMidDone = false;
g_Sessions[sessIdx].ladderBearHighDone = false;
g_Sessions[sessIdx].ladderBearMidDone = false;
g_Sessions[sessIdx].ladderTradesThisSession = 0;
}
void ResetLadderDirection(int sessIdx, int dir)
{
if(dir > 0)
{
g_Sessions[sessIdx].ladderBullLowDone = false;
g_Sessions[sessIdx].ladderBullMidDone = false;
}
else
{
g_Sessions[sessIdx].ladderBearHighDone = false;
g_Sessions[sessIdx].ladderBearMidDone = false;
}
}
void RefreshBreakoutEntryLocks()
{
for(int s = 0; s < g_SessionCount; s++)
{
if(g_Sessions[s].inBreakout && !HasActiveExposureForSession(s))
g_Sessions[s].breakoutEntryTaken = false;
}
}
bool OpenLadderTrade(int sessIdx, int dir, double srcLevel, double dstLevel, const string stepTag)
{
if(g_TradingHalted) return false;
if(g_TodayTrades >= InpMaxTradesPerDay) return false;
if(HasActiveExposureForSession(sessIdx)) return false;
int ladderCap = GetEffectiveLadderCap(sessIdx);
if(ladderCap > 0 && g_Sessions[sessIdx].ladderTradesThisSession >= ladderCap)
return false;
double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID);
double entry = (dir > 0) ? ask : bid;
double tp = NormalizeDouble(dstLevel, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
double sl = 0.0;
if(InpLadderSLMode == LADDER_SL_LEVEL_BUFFER)
{
sl = (dir > 0) ? srcLevel - PipsToPrice(InpLadderSLBufferPips)
: srcLevel + PipsToPrice(InpLadderSLBufferPips);
}
else
{
sl = (dir > 0) ? entry - PipsToPrice(InpSLPips)
: entry + PipsToPrice(InpSLPips);
}
sl = NormalizeDouble(sl, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
if((dir > 0 && (tp <= entry || sl >= entry))
|| (dir < 0 && (tp >= entry || sl <= entry)))
return false;
double slPips = PriceToPips(MathAbs(entry - sl));
if(slPips <= 0) return false;
double lots = CalcLotSize(slPips);
string comment = StringFormat("NANDR|Ladder|%s|%s|%s",
g_Sessions[sessIdx].name,
(dir > 0 ? "BUY" : "SELL"),
stepTag);
bool result = (dir > 0)
? g_Trade.Buy(lots, g_Symbol, 0, sl, tp, comment)
: g_Trade.Sell(lots, g_Symbol, 0, sl, tp, comment);
if(!result)
{
PrintFormat("NANDR EA: Ladder trade failed. Step=%s Session=%s Error=%d",
stepTag, g_Sessions[sessIdx].name, GetLastError());
return false;
}
g_TodayTrades++;
g_Sessions[sessIdx].tradesThisSession++;
g_Sessions[sessIdx].ladderTradesThisSession++;
g_Sessions[sessIdx].breakoutEntryTaken = true;
PrintFormat("NANDR EA: Ladder trade opened. Step=%s Dir=%s Lots=%.2f Entry=%.2f SL=%.2f TP=%.2f Session=%s",
stepTag, (dir > 0 ? "BUY" : "SELL"), lots, entry, sl, tp, g_Sessions[sessIdx].name);
if(InpShowTradeLabels)
DrawTradeLabel(dir, entry, sl, tp);
return true;
}
bool IsLadderEnabledForSession(int sessIdx)
{
string name = g_Sessions[sessIdx].name;
if(name == "DailyOpen") return InpLadderOnDailyOpen;
if(name == "Tokyo") return InpLadderOnTokyo;
if(name == "London") return InpLadderOnLondon;
if(name == "NY") return InpLadderOnNY;
if(name == "NYORB") return InpLadderOnNYORB;
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,
double l_cur,
double ask,
double bid,
double tol)
{
if(!InpUseSessionLevelLadder) return false;
if(!IsLadderEnabledForSession(sessIdx)) return false;
if(!g_Sessions[sessIdx].enabled || !g_Sessions[sessIdx].orbComplete) return false;
if(!g_Sessions[sessIdx].inBreakout) return false;
if(!IsDirectionAllowed(g_Sessions[sessIdx].breakoutDir)) 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);
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;
double orbMid = SessionMid(g_Sessions[sessIdx]);
if(g_Sessions[sessIdx].breakoutDir > 0)
{
if(!g_Sessions[sessIdx].ladderBullLowDone
&& c1 > orbLow && l_cur <= orbLow && ask >= orbLow - tol)
{
if(OpenLadderTrade(sessIdx, 1, orbLow, orbMid, "LOW_TO_MID"))
{
g_Sessions[sessIdx].ladderBullLowDone = true;
g_Sessions[sessIdx].retestFiredThisBar = true;
return true;
}
}
if(g_Sessions[sessIdx].ladderBullLowDone
&& !g_Sessions[sessIdx].ladderBullMidDone
&& c1 > orbMid && l_cur <= orbMid && ask >= orbMid - tol)
{
if(OpenLadderTrade(sessIdx, 1, orbMid, orbHigh, "MID_TO_HIGH"))
{
g_Sessions[sessIdx].ladderBullMidDone = true;
g_Sessions[sessIdx].retestFiredThisBar = true;
return true;
}
}
}
else if(g_Sessions[sessIdx].breakoutDir < 0)
{
if(!g_Sessions[sessIdx].ladderBearHighDone
&& c1 < orbHigh && h_cur >= orbHigh && bid <= orbHigh + tol)
{
if(OpenLadderTrade(sessIdx, -1, orbHigh, orbMid, "HIGH_TO_MID"))
{
g_Sessions[sessIdx].ladderBearHighDone = true;
g_Sessions[sessIdx].retestFiredThisBar = true;
return true;
}
}
if(g_Sessions[sessIdx].ladderBearHighDone
&& !g_Sessions[sessIdx].ladderBearMidDone
&& c1 < orbMid && h_cur >= orbMid && bid <= orbMid + tol)
{
if(OpenLadderTrade(sessIdx, -1, orbMid, orbLow, "MID_TO_LOW"))
{
g_Sessions[sessIdx].ladderBearMidDone = true;
g_Sessions[sessIdx].retestFiredThisBar = true;
return true;
}
}
}
return false;
}
//+------------------------------------------------------------------+
//| Cancel all pending orders |
//+------------------------------------------------------------------+
void CancelAllPending()
{
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(g_Order.SelectByIndex(i))
if(g_Order.Magic() == InpMagicNumber && g_Order.Symbol() == g_Symbol)
g_Trade.OrderDelete(g_Order.Ticket());
}
}
//+------------------------------------------------------------------+
//| Open trade |
//+------------------------------------------------------------------+
void OpenTrade(int dir, int sessIdx, double orbLevel, double obTop, double obBottom)
{
if(g_TradingHalted) return;
if(!IsDirectionAllowed(dir)) return;
if(g_TodayTrades >= InpMaxTradesPerDay) return;
if(g_Sessions[sessIdx].tradesThisSession >= InpMaxRetestsPerSession) return;
if(HasActiveExposureForSession(sessIdx)) return;
if(g_Sessions[sessIdx].breakoutEntryTaken) return;
double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID);
double entry = (dir > 0) ? ask : bid;
// For limit orders, entry is at the ORB level
if(InpEntryMode == ENTRY_LIMIT)
entry = orbLevel;
// Risk: prefer the coupled ORB risk method (SL+TP from the range); fall back
// to the legacy SL/TP inputs when the method is LEGACY or the geometry fails.
double sl, tp;
if(!CalcOrbRisk(dir, entry, g_Sessions[sessIdx].orbHigh, g_Sessions[sessIdx].orbLow, sl, tp))
{
sl = CalcSL(dir, entry, obBottom, obTop,
g_Sessions[sessIdx].orbHigh, g_Sessions[sessIdx].orbLow);
tp = CalcTP(dir, entry, sl);
}
double slPips = PriceToPips(MathAbs(entry - sl));
double lots = CalcLotSize(slPips);
string comment = StringFormat("NANDR|%s|%s", g_Sessions[sessIdx].name, (dir > 0 ? "BUY" : "SELL"));
bool result = false;
if(InpEntryMode == ENTRY_MARKET)
{
if(dir > 0)
result = g_Trade.Buy(lots, g_Symbol, 0, sl, tp, comment);
else
result = g_Trade.Sell(lots, g_Symbol, 0, sl, tp, comment);
}
else // Limit
{
ENUM_ORDER_TYPE otype = (dir > 0) ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_SELL_LIMIT;
result = g_Trade.OrderOpen(g_Symbol, otype, lots, orbLevel, orbLevel, sl, tp, ORDER_TIME_DAY, 0, comment);
}
if(result)
{
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).
// 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)
DrawTradeLabel(dir, entry, sl, tp);
}
else
{
PrintFormat("NANDR EA: OrderSend failed. Error=%d Session=%s",
GetLastError(), g_Sessions[sessIdx].name);
}
}
//+------------------------------------------------------------------+
//| 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);
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);
string obSessionName = ResolveSessionNameForUtcTime(TimeCurrent() - g_ServerUtcOffsetSec);
// ── ORB Session Retest Entries ──────────────────────────────────
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;
// --- 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;
// --- 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;
// --- 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 breakout: price broke above orbHigh
{
// Retest: wick comes back down to touch orbHigh
if(l_cur <= orbHigh)
{
orbLevel = orbHigh;
dir = 1;
}
}
else if(g_Sessions[s].breakoutDir == -1) // Bearish breakout: price broke below orbLow
{
// Retest: wick comes back up to touch orbLow
if(h_cur >= orbLow)
{
orbLevel = orbLow;
dir = -1;
}
}
if(dir == 0) continue;
g_Sessions[s].retestFiredThisBar = true;
double obTop = 0, obBottom = 0;
IsOBNearLevel(orbLevel, dir, obTop, obBottom);
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);
}
}
// ── Session Level Ladder (fallback, only when no ORB entry taken this tick) ──
if(InpUseSessionLevelLadder)
{
for(int s = 0; s < g_SessionCount; s++)
{
if(TrySessionLadderEntry(s, c1, h_cur, l_cur, ask, bid, tol))
break;
}
}
if(!InpUseOBRetestEntry) return;
// ── Bearish OB Retest → SELL at wick touch ───────────────────────
// Gated by the daily bias (only short entries when bias permits).
if(IsDirectionAllowed(-1))
for(int i = 0; i < g_BearOBCount; i++)
{
if(!g_BearOBs[i].active || g_BearOBs[i].traded) continue;
if(g_TodayTrades >= InpMaxTradesPerDay) break;
if(obSessionName == "") continue;
if(HasActiveExposureForSession(obSessionName)) continue;
double obBottom = g_BearOBs[i].bottom;
double obTop2 = g_BearOBs[i].top;
bool prevBelow = (c1 <= obBottom || open0 <= obBottom);
bool wickTouched = (h_cur >= obBottom);
if(!prevBelow || !wickTouched) continue;
double entry = bid;
double sl = NormalizeDouble(obTop2 + PipsToPrice(2.0),
(int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
double slPips = PriceToPips(MathAbs(entry - sl));
double tp = CalcTP(-1, entry, sl);
double lots = CalcLotSize(slPips);
bool result = g_Trade.Sell(lots, g_Symbol, 0, sl, tp,
StringFormat("NANDR|%s|OBRetest|SELL|%.2f", obSessionName, obBottom));
if(result)
{
g_TodayTrades++;
g_BearOBs[i].traded = true;
PrintFormat("NANDR EA: OB Retest SELL. Entry=%.2f SL=%.2f TP=%.2f OB=[%.2f-%.2f]",
entry, sl, tp, obBottom, obTop2);
if(InpShowTradeLabels) DrawTradeLabel(-1, entry, sl, tp);
}
else
PrintFormat("NANDR EA: OB Retest SELL failed. Error=%d", GetLastError());
break;
}
// ── Bullish OB Retest → BUY at wick touch ────────────────────────
// Gated by the daily bias (only long entries when bias permits).
if(IsDirectionAllowed(1))
for(int i = 0; i < g_BullOBCount; i++)
{
if(!g_BullOBs[i].active || g_BullOBs[i].traded) continue;
if(g_TodayTrades >= InpMaxTradesPerDay) break;
if(obSessionName == "") continue;
if(HasActiveExposureForSession(obSessionName)) continue;
double obTop3 = g_BullOBs[i].top;
double obBottom2 = g_BullOBs[i].bottom;
bool prevAbove = (c1 >= obTop3 || open0 >= obTop3);
bool wickTouched = (l_cur <= obTop3);
if(!prevAbove || !wickTouched) continue;
double entry = ask;
double sl = NormalizeDouble(obBottom2 - PipsToPrice(2.0),
(int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
double slPips = PriceToPips(MathAbs(entry - sl));
double tp = CalcTP(1, entry, sl);
double lots = CalcLotSize(slPips);
bool result = g_Trade.Buy(lots, g_Symbol, 0, sl, tp,
StringFormat("NANDR|%s|OBRetest|BUY|%.2f", obSessionName, obTop3));
if(result)
{
g_TodayTrades++;
g_BullOBs[i].traded = true;
PrintFormat("NANDR EA: OB Retest BUY. Entry=%.2f SL=%.2f TP=%.2f OB=[%.2f-%.2f]",
entry, sl, tp, obBottom2, obTop3);
if(InpShowTradeLabels) DrawTradeLabel(1, entry, sl, tp);
}
else
PrintFormat("NANDR EA: OB Retest BUY failed. Error=%d", GetLastError());
break;
}
}
//+------------------------------------------------------------------+
//| Check all sessions for entry signals |
//+------------------------------------------------------------------+
void CheckEntrySignals()
{
for(int s = 0; s < g_SessionCount; s++)
{
if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue;
// Guard: allow up to InpMaxRetestsPerSession retest entries on the same breakout level.
// Previously used InpMaxPosPerSession (=1) which permanently blocked the second retest.
if(g_Sessions[s].tradesThisSession >= InpMaxRetestsPerSession) continue;
if(!g_Sessions[s].inBreakout) continue;
// --- Breakout expiry ---
if(InpBreakoutExpireBars > 0
&& g_Sessions[s].breakoutBarsAgo >= InpBreakoutExpireBars)
{
PrintFormat("NANDR EA: [%s] Breakout expired after %d bars. Resetting.",
g_Sessions[s].name, g_Sessions[s].breakoutBarsAgo);
g_Sessions[s].inBreakout = false;
g_Sessions[s].breakoutDir = 0;
g_Sessions[s].breakoutEntryTaken = false;
ResetLadderProgress(s);
continue;
}
int dir = g_Sessions[s].breakoutDir;
double orbLevel = (dir > 0) ? g_Sessions[s].orbHigh : g_Sessions[s].orbLow;
// --- LIMIT mode: place order immediately on breakout bar ---
if(InpEntryMode == ENTRY_LIMIT)
{
// Place once — on the bar breakout is first detected (age 0 or 1 due to update order)
if(g_Sessions[s].breakoutBarsAgo <= 1)
{
double obTop = 0, obBottom = 0;
IsOBNearLevel(orbLevel, dir, obTop, obBottom);
if(InpOBRequireConf && obTop == 0) continue;
OpenTrade(dir, s, orbLevel, obTop, obBottom);
}
continue;
}
// --- MARKET mode ---
if(!InpWaitForRetest)
{
// Direct entry on the bar the breakout is confirmed (age 0 or 1)
if(g_Sessions[s].breakoutBarsAgo <= 1)
{
double obTop = 0, obBottom = 0;
IsOBNearLevel(orbLevel, dir, obTop, obBottom);
if(InpOBRequireConf && obTop == 0) continue;
OpenTrade(dir, s, orbLevel, obTop, obBottom);
}
}
else
{
// Retest entries fire tick-based in CheckRetestEntriesTick().
// Here we only update state: sweep reversals and failed retest cancellation.
UpdateRetestState(s);
}
}
}
//+------------------------------------------------------------------+
//| Manage open positions: breakeven + trailing stop |
//+------------------------------------------------------------------+
void ManageOpenTrades()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(!g_Position.SelectByIndex(i)) continue;
if(g_Position.Magic() != InpMagicNumber) continue;
if(g_Position.Symbol() != g_Symbol) continue;
double entry = g_Position.PriceOpen();
double sl = g_Position.StopLoss();
double currentSL= sl;
double price = g_Position.PriceCurrent();
ulong ticket = g_Position.Ticket();
int posDir = (g_Position.PositionType() == POSITION_TYPE_BUY) ? 1 : -1;
// Breakeven + Partial Close
if(InpBreakevenTrigPips > 0)
{
double trigDist = PipsToPrice(InpBreakevenTrigPips);
double beDist = PipsToPrice(InpBreakevenOffPips);
double beLevel = (posDir > 0) ? entry + beDist : entry - beDist;
bool triggered = (posDir > 0)
? (price >= entry + trigDist && sl < beLevel)
: (price <= entry - trigDist && (sl > beLevel || sl == 0));
if(triggered)
{
// Step 1: partial close — fire once per position
if(InpPartialCloseRatio > 0 && !IsPartialClosed(ticket))
{
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)
{
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)
{
// 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",
GetLastError(), ticket);
}
}
// Step 2: move SL to breakeven
currentSL = NormalizeDouble(beLevel, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
}
}
// Trailing stop
if(InpTrailingStopPips > 0)
{
double trailDist = PipsToPrice(InpTrailingStopPips);
if(posDir > 0)
{
double newSL = price - trailDist;
if(newSL > currentSL) currentSL = NormalizeDouble(newSL, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
}
else
{
double newSL = price + trailDist;
if(currentSL == 0 || newSL < currentSL)
currentSL = NormalizeDouble(newSL, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS));
}
}
if(currentSL != sl && currentSL > 0)
g_Trade.PositionModify(ticket, currentSL, g_Position.TakeProfit());
}
}
//+------------------------------------------------------------------+
//| Daily reset logic |
//+------------------------------------------------------------------+
void CheckDailyReset()
{
datetime now = TimeCurrent();
// The trading day is anchored to the 22:00 UTC Daily Open session. Shifting
// UTC time forward by 2h moves that anchor onto a UTC-midnight boundary, so
// comparing calendar days resets exactly at 22:00 UTC regardless of the
// broker server timezone. This prevents a server-midnight reset from
// truncating the Daily Open session only a couple of hours after it starts.
datetime tradingNow = (now - g_ServerUtcOffsetSec) + 2 * 3600;
datetime tradingLast = (g_LastDayReset - g_ServerUtcOffsetSec) + 2 * 3600;
MqlDateTime dt, dtLast;
TimeToStruct(tradingNow, dt);
TimeToStruct(tradingLast, dtLast);
if(dt.day != dtLast.day || dt.mon != dtLast.mon || dt.year != dtLast.year)
{
g_LastDayReset = now;
g_TodayTrades = 0;
g_TodayWins = 0;
g_TodayLosses = 0;
g_TodayPnL = 0;
g_DayStartEquity = AccountInfoDouble(ACCOUNT_EQUITY);
g_TradingHalted = false;
g_GlobalBiasDir = 0; // clear day bias — first session breakout will re-establish it
g_PartialClosedCount = 0; // clear partial close tracker for the new day
// Reset all session ORB data for the new day
for(int s = 0; s < g_SessionCount; s++)
g_Sessions[s].Reset();
// Reset OB traded flags so the same zones can re-trigger on the new day
for(int i = 0; i < g_BullOBCount; i++) g_BullOBs[i].traded = false;
for(int i = 0; i < g_BearOBCount; i++) g_BearOBs[i].traded = false;
Print("NANDR EA: Daily reset. Equity=", g_DayStartEquity);
}
}
//+------------------------------------------------------------------+
//| Check risk limits |
//+------------------------------------------------------------------+
void CheckRiskLimits()
{
if(g_TradingHalted) return;
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
bool halt = false;
if(InpMaxDailyLossUSD > 0 && (g_DayStartEquity - equity) >= InpMaxDailyLossUSD)
{
PrintFormat("NANDR EA: Max daily loss USD reached (%.2f). Trading halted.", InpMaxDailyLossUSD);
halt = true;
}
if(InpMaxDailyLossPct > 0 && g_DayStartEquity > 0)
{
double lossPct = (g_DayStartEquity - equity) / g_DayStartEquity * 100.0;
if(lossPct >= InpMaxDailyLossPct)
{
PrintFormat("NANDR EA: Max daily loss %% reached (%.2f%%). Trading halted.", InpMaxDailyLossPct);
halt = true;
}
}
if(halt)
{
g_TradingHalted = true;
CancelAllPending();
}
}
//+------------------------------------------------------------------+
//| Update PnL stats from closed trades |
//+------------------------------------------------------------------+
void UpdateClosedTrades()
{
// Scan deal history for today
datetime dayStart = iTime(g_Symbol, PERIOD_D1, 0);
HistorySelect(dayStart, TimeCurrent());
int deals = HistoryDealsTotal();
double newPnL = 0;
int newWins = 0, newLosses = 0;
for(int i = 0; i < deals; i++)
{
ulong ticket = HistoryDealGetTicket(i);
if(HistoryDealGetInteger(ticket, DEAL_MAGIC) != InpMagicNumber) continue;
if(HistoryDealGetString(ticket, DEAL_SYMBOL) != g_Symbol) continue;
if(HistoryDealGetInteger(ticket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue;
double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT)
+ HistoryDealGetDouble(ticket, DEAL_SWAP)
+ HistoryDealGetDouble(ticket, DEAL_COMMISSION);
newPnL += profit;
if(profit >= 0) newWins++;
else newLosses++;
}
g_TodayPnL = newPnL;
g_TodayWins = newWins;
g_TodayLosses = newLosses;
}
//+------------------------------------------------------------------+
//| Draw ORB lines for a session |
//+------------------------------------------------------------------+
void DrawORBLines(int sessIdx)
{
if(sessIdx < 0 || sessIdx >= g_SessionCount) return;
color lineColor = clrWhite;
if(g_Sessions[sessIdx].name == "Tokyo") lineColor = clrDodgerBlue;
else if(g_Sessions[sessIdx].name == "London") lineColor = clrTomato;
else if(g_Sessions[sessIdx].name == "NY"
|| g_Sessions[sessIdx].name == "NYORB") lineColor = clrGold;
else if(g_Sessions[sessIdx].name == "DailyOpen") lineColor = clrSilver;
string prefix = "NANDR_ORB_" + g_Sessions[sessIdx].name + "_";
// High line
string hName = prefix + "High";
ObjectDelete(0, hName);
if(g_Sessions[sessIdx].orbHigh > 0)
{
ObjectCreate(0, hName, OBJ_HLINE, 0, 0, g_Sessions[sessIdx].orbHigh);
ObjectSetInteger(0, hName, OBJPROP_COLOR, lineColor);
ObjectSetInteger(0, hName, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, hName, OBJPROP_WIDTH, 1);
ObjectSetString(0, hName, OBJPROP_TOOLTIP,
g_Sessions[sessIdx].name + " ORB High: " +
DoubleToString(g_Sessions[sessIdx].orbHigh, 2));
}
// Low line
string lName = prefix + "Low";
ObjectDelete(0, lName);
if(g_Sessions[sessIdx].orbLow < DBL_MAX && g_Sessions[sessIdx].orbLow > 0)
{
ObjectCreate(0, lName, OBJ_HLINE, 0, 0, g_Sessions[sessIdx].orbLow);
ObjectSetInteger(0, lName, OBJPROP_COLOR, lineColor);
ObjectSetInteger(0, lName, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, lName, OBJPROP_WIDTH, 1);
ObjectSetString(0, lName, OBJPROP_TOOLTIP,
g_Sessions[sessIdx].name + " ORB Low: " +
DoubleToString(g_Sessions[sessIdx].orbLow, 2));
}
// Mid line (dotted)
if(g_Sessions[sessIdx].orbHigh > 0
&& g_Sessions[sessIdx].orbLow < DBL_MAX && g_Sessions[sessIdx].orbLow > 0)
{
string mName = prefix + "Mid";
ObjectDelete(0, mName);
double mid = (g_Sessions[sessIdx].orbHigh + g_Sessions[sessIdx].orbLow) / 2.0;
ObjectCreate(0, mName, OBJ_HLINE, 0, 0, mid);
ObjectSetInteger(0, mName, OBJPROP_COLOR, lineColor);
ObjectSetInteger(0, mName, OBJPROP_STYLE, STYLE_DOT);
ObjectSetInteger(0, mName, OBJPROP_WIDTH, 1);
}
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Draw OB zone rectangle |
//+------------------------------------------------------------------+
void DrawOBZone(int idx, bool isBull)
{
SOrderBlock ob;
if(isBull) ob = g_BullOBs[idx];
else ob = g_BearOBs[idx];
if(!ob.active) return;
color bgColor = isBull ? (color)ColorToARGB(clrGreen, 40) : (color)ColorToARGB(clrRed, 40);
color borderColor = isBull ? clrGreen : clrRed;
datetime t1 = ob.time;
datetime t2 = ob.time + PeriodSeconds(PERIOD_CURRENT) * 200; // extend right
ObjectDelete(0, ob.objName);
ObjectCreate(0, ob.objName, OBJ_RECTANGLE, 0, t1, ob.top, t2, ob.bottom);
ObjectSetInteger(0, ob.objName, OBJPROP_COLOR, borderColor);
ObjectSetInteger(0, ob.objName, OBJPROP_BGCOLOR, isBull ? clrGreen : clrRed);
ObjectSetInteger(0, ob.objName, OBJPROP_FILL, true);
ObjectSetInteger(0, ob.objName, OBJPROP_BACK, true);
ObjectSetInteger(0, ob.objName, OBJPROP_STYLE, STYLE_SOLID);
ObjectSetInteger(0, ob.objName, OBJPROP_WIDTH, 1);
ObjectSetString(0, ob.objName, OBJPROP_TOOLTIP,
(isBull ? "Bull OB" : "Bear OB") + " [" +
DoubleToString(ob.bottom, 2) + " - " + DoubleToString(ob.top, 2) + "]");
// Mid line
string midName = ob.objName + "_mid";
ObjectDelete(0, midName);
ObjectCreate(0, midName, OBJ_TREND, 0, t1, ob.mid, t2, ob.mid);
ObjectSetInteger(0, midName, OBJPROP_COLOR, isBull ? clrLimeGreen : clrOrangeRed);
ObjectSetInteger(0, midName, OBJPROP_STYLE, STYLE_DASH);
ObjectSetInteger(0, midName, OBJPROP_RAY_RIGHT, true);
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Remove OB zone objects |
//+------------------------------------------------------------------+
void RemoveOBZone(const string name)
{
ObjectDelete(0, name);
ObjectDelete(0, name + "_mid");
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Draw trade entry label |
//+------------------------------------------------------------------+
void DrawTradeLabel(int dir, double entry, double sl, double tp)
{
string name = "NANDR_Trade_" + IntegerToString(TimeCurrent());
datetime t = iTime(g_Symbol, PERIOD_CURRENT, 0);
ObjectCreate(0, name, OBJ_ARROW, 0, t, entry);
ObjectSetInteger(0, name, OBJPROP_ARROWCODE, (dir > 0) ? 233 : 234);
ObjectSetInteger(0, name, OBJPROP_COLOR, (dir > 0) ? clrLime : clrRed);
ObjectSetInteger(0, name, OBJPROP_WIDTH, 2);
// SL line
string slName = name + "_SL";
ObjectCreate(0, slName, OBJ_HLINE, 0, t, sl);
ObjectSetInteger(0, slName, OBJPROP_COLOR, clrRed);
ObjectSetInteger(0, slName, OBJPROP_STYLE, STYLE_DASH);
// TP line
string tpName = name + "_TP";
ObjectCreate(0, tpName, OBJ_HLINE, 0, t, tp);
ObjectSetInteger(0, tpName, OBJPROP_COLOR, clrLime);
ObjectSetInteger(0, tpName, OBJPROP_STYLE, STYLE_DASH);
ChartRedraw(0);
}
// Dashboard helper — module-level state set by DrawDashboard before each call
string g_DBPrefix = "NANDR_DB_";
int g_DBX = 15;
int g_DBY = 30;
int g_DBDY = 18;
int g_DBLine = 0;
void DBLine(string txt, color col, int fontSize = 9)
{
string name = g_DBPrefix + IntegerToString(g_DBLine);
ObjectDelete(0, name);
ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, g_DBX);
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, g_DBY + g_DBLine * g_DBDY);
ObjectSetInteger(0, name, OBJPROP_COLOR, col);
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
ObjectSetString(0, name, OBJPROP_TEXT, txt);
g_DBLine++;
}
//+------------------------------------------------------------------+
//| Draw dashboard |
//+------------------------------------------------------------------+
void DrawDashboard()
{
if(!InpShowDashboard) return;
color title = clrGold;
color normal = clrSilver;
color good = clrLimeGreen;
color bad = clrTomato;
g_DBPrefix = "NANDR_DB_";
g_DBX = 15;
g_DBY = 30;
g_DBDY = 18;
g_DBLine = 0;
DBLine("== NANDR ORB+OB EA ==", title, 10);
DBLine("-------------------------", normal);
DBLine(StringFormat("Symbol : %s TF: %s", g_Symbol, EnumToString(Period())), normal);
DBLine(StringFormat("OrbTF : M%d BarsNeeded: %d", (int)InpOrbTimeframe, g_OrbBarsNeeded), normal);
DBLine("-------------------------", normal);
// Sessions
for(int s = 0; s < g_SessionCount; s++)
{
string status;
if(g_Sessions[s].orbComplete)
status = "ARMED";
else if(g_Sessions[s].orbBarCount > 0)
status = "ACCUM";
else
status = "WAIT ";
if(g_Sessions[s].inBreakout)
status = (g_Sessions[s].breakoutDir > 0 ? "BULL+" : "BEAR-");
color sc = g_Sessions[s].orbComplete ? good : normal;
if(g_TradingHalted) sc = bad;
DBLine(StringFormat("%-8s H:%.2f L:%.2f [%s]",
g_Sessions[s].name,
g_Sessions[s].orbHigh > 0 ? g_Sessions[s].orbHigh : 0,
g_Sessions[s].orbLow < DBL_MAX ? g_Sessions[s].orbLow : 0,
status), sc);
}
DBLine("-------------------------", normal);
DBLine(StringFormat("BullOBs: %d BearOBs: %d", g_BullOBCount, g_BearOBCount), normal);
DBLine("-------------------------", normal);
// Daily stats
double wr = (g_TodayWins + g_TodayLosses > 0)
? (double)g_TodayWins / (g_TodayWins + g_TodayLosses) * 100.0 : 0;
DBLine(StringFormat("Trades : %d / %d", g_TodayTrades, InpMaxTradesPerDay), normal);
DBLine(StringFormat("W/L : %d / %d WR: %.0f%%", g_TodayWins, g_TodayLosses, wr),
g_TodayWins >= g_TodayLosses ? good : normal);
DBLine(StringFormat("Today PnL: %+.2f USD", g_TodayPnL),
g_TodayPnL >= 0 ? good : bad);
DBLine(StringFormat("Equity : %.2f", AccountInfoDouble(ACCOUNT_EQUITY)), normal);
if(g_TradingHalted)
DBLine("! TRADING HALTED - Daily limit", bad, 10);
else if(g_SessionCount == 0)
DBLine("! No sessions enabled", bad);
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Remove all EA objects from chart |
//+------------------------------------------------------------------+
void RemoveAllObjects()
{
int total = ObjectsTotal(0, 0, -1);
for(int i = total - 1; i >= 0; i--)
{
string name = ObjectName(0, i, 0, -1);
if(StringFind(name, "NANDR_") >= 0)
ObjectDelete(0, name);
}
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| Reconstruct ORB levels from historical bars on startup |
//| Handles mid-day EA restarts — finds today's session open bars |
//| in price history and restores High/Low so sessions show ARMED. |
//+------------------------------------------------------------------+
void ReconstructORBFromHistory()
{
MqlDateTime dtNow;
TimeToStruct(TimeCurrent(), dtNow);
for(int s = 0; s < g_SessionCount; s++)
{
if(!g_Sessions[s].enabled || g_Sessions[s].orbComplete) continue;
// Build this session's open datetime for today
// Session start hours are UTC; convert to server time so iBarShift/iTime
// (which operate in server time) locate the correct opening bar.
MqlDateTime dtSess = dtNow;
dtSess.hour = g_Sessions[s].startHour;
dtSess.min = g_Sessions[s].startMin;
dtSess.sec = 0;
datetime sessOpenTime = StructToTime(dtSess) + g_ServerUtcOffsetSec;
// Only reconstruct if the opening bar has fully closed
long barSecs = (long)g_OrbBarsNeeded * (int)InpOrbTimeframe * 60;
if(TimeCurrent() < sessOpenTime + barSecs) continue;
// Find the bar index for this session open time
int barIdx = iBarShift(g_Symbol, PERIOD_CURRENT, sessOpenTime, false);
if(barIdx < 1) continue; // bar[0] is the live bar
// Verify exact time match (session bar must exist in chart history)
if(iTime(g_Symbol, PERIOD_CURRENT, barIdx) != sessOpenTime) continue;
// Accumulate ORB range across all required bars (M1: 15 bars, M5: 3, M15: 1)
double h = iHigh(g_Symbol, PERIOD_CURRENT, barIdx);
double l = iLow(g_Symbol, PERIOD_CURRENT, barIdx);
for(int b = 1; b < g_OrbBarsNeeded; b++)
{
int idx = barIdx - b;
if(idx < 1) break;
h = MathMax(h, iHigh(g_Symbol, PERIOD_CURRENT, idx));
l = MathMin(l, iLow(g_Symbol, PERIOD_CURRENT, idx));
}
g_Sessions[s].orbHigh = h;
g_Sessions[s].orbLow = l;
g_Sessions[s].orbBarCount = g_OrbBarsNeeded;
g_Sessions[s].orbStartTime = sessOpenTime;
g_Sessions[s].orbComplete = true;
PrintFormat("NANDR EA: [%s] ORB reconstructed from history. High=%.2f Low=%.2f",
g_Sessions[s].name, h, l);
if(InpShowORBLines) DrawORBLines(s);
}
}
//+------------------------------------------------------------------+
//| Sync daily state from deal history on startup/restart |
//| Restores g_DayStartEquity so the daily loss limit persists across|
//| EA restarts, and syncs g_TodayTrades to prevent double-counting. |
//+------------------------------------------------------------------+
void SyncDailyState()
{
datetime dayStart = iTime(g_Symbol, PERIOD_D1, 0);
if(dayStart == 0) return;
HistorySelect(dayStart, TimeCurrent());
int deals = HistoryDealsTotal();
double closedPnL = 0;
int tradeOpens = 0;
for(int i = 0; i < deals; i++)
{
ulong ticket = HistoryDealGetTicket(i);
if(HistoryDealGetInteger(ticket, DEAL_MAGIC) != InpMagicNumber) continue;
if(HistoryDealGetString(ticket, DEAL_SYMBOL) != g_Symbol) continue;
long entry = HistoryDealGetInteger(ticket, DEAL_ENTRY);
if(entry == DEAL_ENTRY_IN)
tradeOpens++;
if(entry == DEAL_ENTRY_OUT)
closedPnL += HistoryDealGetDouble(ticket, DEAL_PROFIT)
+ HistoryDealGetDouble(ticket, DEAL_SWAP)
+ HistoryDealGetDouble(ticket, DEAL_COMMISSION);
}
// Reconstruct day-start equity: current balance minus today's already-realized P&L.
// This ensures the daily loss limit check works correctly after a mid-day restart.
double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE);
g_DayStartEquity = currentBalance - closedPnL;
g_TodayTrades = tradeOpens;
PrintFormat("NANDR EA: Daily state synced. DayStartEquity=%.2f TodayTrades=%d ClosedPnL=%.2f",
g_DayStartEquity, g_TodayTrades, closedPnL);
}
//+------------------------------------------------------------------+
//| OnInit |
//+------------------------------------------------------------------+
int OnInit()
{
// Resolve symbol
g_Symbol = ResolveSymbol(InpSymbolName);
// Validate timeframe
ENUM_TIMEFRAMES chartTF = Period();
int chartMins = (int)(PeriodSeconds(chartTF) / 60);
if(chartMins != (int)InpOrbTimeframe)
{
PrintFormat("NANDR EA: WARNING — Chart TF is M%d but ORB Timeframe input is M%d. "
"Attach EA to M%d chart for best results.",
chartMins, (int)InpOrbTimeframe, (int)InpOrbTimeframe);
}
// Bars needed for 15min ORB at current TF
switch(InpOrbTimeframe)
{
case ORB_TF_M1: g_OrbBarsNeeded = 15; break;
case ORB_TF_M5: g_OrbBarsNeeded = 3; break;
case ORB_TF_M15: g_OrbBarsNeeded = 1; break;
default: g_OrbBarsNeeded = 1; break;
}
// Breakout confirmation bars
if(InpBreakoutConfBars > 0)
g_BreakoutConfBars = InpBreakoutConfBars;
else
{
switch(InpOrbTimeframe)
{
case ORB_TF_M1: g_BreakoutConfBars = 5; break;
case ORB_TF_M5: g_BreakoutConfBars = 3; break;
case ORB_TF_M15: g_BreakoutConfBars = 2; break;
default: g_BreakoutConfBars = 2; break;
}
}
// OB pivot length
if(InpOBPivotLength > 0)
g_OBPivotLength = InpOBPivotLength;
else
{
switch(InpOrbTimeframe)
{
case ORB_TF_M1: g_OBPivotLength = 10; break;
default: g_OBPivotLength = 5; break;
}
}
// Pip size: 1 pip = 10 points for all standard decimal counts (2, 3, 5).
// digits==2 (XAUUSD): point=0.01 → pip=0.10 → 100 pips = $10 price distance
// digits==3 (USDJPY): point=0.001 → pip=0.01
// digits==5 (EURUSD): point=0.00001 → pip=0.0001
// BUG FIX: digits==2 previously used g_PointSize directly (0.01) making 100 pips = $1.00,
// causing a near-zero SL and massively oversized lot on XAUUSD.
double tickSize = SymbolInfoDouble(g_Symbol, SYMBOL_TRADE_TICK_SIZE);
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);
ArrayResize(g_BearOBs, InpOBMaxCount);
g_BullOBCount = 0;
g_BearOBCount = 0;
// ATR indicator
g_ATRHandle = iATR(g_Symbol, PERIOD_CURRENT, InpATRPeriod);
if(g_ATRHandle == INVALID_HANDLE)
Print("NANDR EA: WARNING — could not create ATR indicator handle.");
// Trade object
g_Trade.SetExpertMagicNumber(InpMagicNumber);
g_Trade.SetDeviationInPoints(20);
g_Trade.SetTypeFilling(ORDER_FILLING_IOC);
// Init sessions
InitSessions();
if(g_SessionCount == 0)
Print("NANDR EA: WARNING — No sessions enabled. EA will not trade. Enable at least one session.");
// Daily init — set conservative defaults; SyncDailyState() will correct them from history
g_DayStartEquity = AccountInfoDouble(ACCOUNT_EQUITY);
g_LastDayReset = TimeCurrent();
// Sync trade count and day-start equity from today's deal history
// (handles mid-day restarts so daily loss limits carry over correctly)
SyncDailyState();
// Restore ORB levels from historical bars (handles mid-day restarts
// where the session opening bars have already closed before EA loaded)
ReconstructORBFromHistory();
PrintFormat("NANDR EA: Initialized. Symbol=%s OrbBarsNeeded=%d BreakoutConf=%d OBPivot=%d Sessions=%d",
g_Symbol, g_OrbBarsNeeded, g_BreakoutConfBars, g_OBPivotLength, g_SessionCount);
DrawDashboard();
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| OnDeinit |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
RemoveAllObjects();
if(g_ATRHandle != INVALID_HANDLE)
IndicatorRelease(g_ATRHandle);
Comment("");
}
//+------------------------------------------------------------------+
//| Returns true if current server time is Saturday or Sunday |
//+------------------------------------------------------------------+
bool IsWeekend()
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
return (dt.day_of_week == 0 || dt.day_of_week == 6); // 0=Sunday, 6=Saturday
}
//+------------------------------------------------------------------+
//| OnTick |
//+------------------------------------------------------------------+
void OnTick()
{
// Silence all processing during weekend market closure
if(IsWeekend()) return;
// New bar detection
datetime barTime = iTime(g_Symbol, PERIOD_CURRENT, 0);
g_IsNewBar = (barTime != g_LastBarTime);
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
CheckRetestEntriesTick();
if(!g_IsNewBar) return;
// 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).
UpdateORBSessions();
ScanOrderBlocks();
MitigateOrderBlocks();
DetectBreakouts();
UpdateClosedTrades();
DrawDashboard();
// Skip trade execution only when halted — state tracking continues above
if(g_TradingHalted)
return;
// Entry signals (direct breakout / limit orders on new bar)
CheckEntrySignals();
}
//+------------------------------------------------------------------+
//| OnTradeTransaction — handle position close events |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
{
ulong ticket = trans.deal;
if(HistoryDealSelect(ticket))
{
if(HistoryDealGetInteger(ticket, DEAL_MAGIC) == InpMagicNumber &&
HistoryDealGetString(ticket, DEAL_SYMBOL) == g_Symbol &&
HistoryDealGetInteger(ticket, DEAL_ENTRY) == DEAL_ENTRY_OUT)
{
double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT)
+ HistoryDealGetDouble(ticket, DEAL_SWAP)
+ HistoryDealGetDouble(ticket, DEAL_COMMISSION);
g_TotalPnL += profit;
PrintFormat("NANDR EA: Trade closed. Profit=%.2f TotalPnL=%.2f", profit, g_TotalPnL);
}
}
}
}
//+------------------------------------------------------------------+