Files
NANDR_BOT/NANDR_ORB_OB_EA.mq5
T

1881 lines
70 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.35"
#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
};
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)
};
//+------------------------------------------------------------------+
//| 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 (00:00 UTC)
input bool InpUseTokyoSession = true; // Tokyo Session (00:00 UTC)
input bool InpUseLondonSession = true; // London Session (07:00 UTC)
input bool InpUseNYSession = true; // NY Session (12:00 UTC)
input bool InpUseNYOrbSession = true; // NY ORB Session (13:30 UTC)
// --- 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 = true; // OB Retest Entry (enter on OB boundary retest)
input int InpMaxRetestsPerSession = 3; // Max Retest Entries Per Session (per breakout)
input bool InpUseSessionBiasFilter = true; // Session Bias Filter: block signals against day bias
// --- 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_SL_MODE InpSLMode = SL_OB_BOUNDARY; // SL Mode
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
input double InpTPPips = 100.0; // TP Fixed Pips
input double InpRRRatio = 1.0; // Risk:Reward Ratio
// --- 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;
bool inRetest;
int breakoutBarsAgo;
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;
inRetest = false;
breakoutBarsAgo = 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: direction of the first confirmed ORB breakout of the day (0=none, 1=bull, -1=bear).
// When InpUseSessionBiasFilter=true, subsequent session breakouts and all OB entries that
// contradict this bias are suppressed, preventing counter-trend trades.
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;
// ATR handle
int g_ATRHandle = INVALID_HANDLE;
// History tracking for price buffer (need N bars lookback)
#define MAX_BARS_LOOKBACK 20
double g_HighBuf[];
double g_LowBuf[];
double g_CloseBuf[];
double g_VolBuf[];
//+------------------------------------------------------------------+
//| 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 sl = 0;
if(InpSLMode == SL_FIXED_PIPS)
{
sl = (dir > 0) ? entry - PipsToPrice(InpSLPips)
: entry + PipsToPrice(InpSLPips);
}
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 = (dir > 0) ? entry - PipsToPrice(InpSLPips)
: entry + PipsToPrice(InpSLPips);
}
else // ATR
{
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 = (dir > 0) ? entry - PipsToPrice(InpSLPips)
: entry + PipsToPrice(InpSLPips);
}
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));
}
//+------------------------------------------------------------------+
//| Initialize sessions |
//+------------------------------------------------------------------+
void InitSessions()
{
g_SessionCount = 0;
// Daily Open
if(InpUseDailyOpen)
{
g_Sessions[g_SessionCount].name = "DailyOpen";
g_Sessions[g_SessionCount].startHour = 0;
g_Sessions[g_SessionCount].startMin = 0;
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 = 0;
g_Sessions[g_SessionCount].startMin = 0;
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 = 7;
g_Sessions[g_SessionCount].startMin = 0;
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 = 12;
g_Sessions[g_SessionCount].startMin = 0;
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 = 13;
g_Sessions[g_SessionCount].startMin = 30;
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)
{
MqlDateTime dt;
TimeToStruct(barTime, 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].inRetest = false;
g_Sessions[s].breakoutBarsAgo = 0;
g_Sessions[s].tradesThisSession= 0;
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);
}
//+------------------------------------------------------------------+
//| 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 || g_Sessions[s].inRetest) continue;
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);
if(bullBO)
{
// Bias filter: suppress if day bias is already bearish
if(InpUseSessionBiasFilter && g_GlobalBiasDir == -1)
{
PrintFormat("NANDR EA: [%s] Bullish breakout SUPPRESSED — day bias is BEARISH",
g_Sessions[s].name);
continue;
}
g_Sessions[s].breakoutDir = 1;
g_Sessions[s].inBreakout = true;
g_Sessions[s].breakoutBarsAgo= 0;
if(g_GlobalBiasDir == 0) g_GlobalBiasDir = 1; // first breakout of the day sets bias
PrintFormat("NANDR EA: [%s] Bullish ORB breakout at %.2f [DayBias=%d]",
g_Sessions[s].name, g_Sessions[s].orbHigh, g_GlobalBiasDir);
}
else if(bearBO)
{
// Bias filter: suppress if day bias is already bullish
if(InpUseSessionBiasFilter && g_GlobalBiasDir == 1)
{
PrintFormat("NANDR EA: [%s] Bearish breakout SUPPRESSED — day bias is BULLISH",
g_Sessions[s].name);
continue;
}
g_Sessions[s].breakoutDir = -1;
g_Sessions[s].inBreakout = true;
g_Sessions[s].breakoutBarsAgo= 0;
if(g_GlobalBiasDir == 0) g_GlobalBiasDir = -1; // first breakout of the day sets bias
PrintFormat("NANDR EA: [%s] Bearish ORB breakout at %.2f [DayBias=%d]",
g_Sessions[s].name, g_Sessions[s].orbLow, g_GlobalBiasDir);
}
}
}
//+------------------------------------------------------------------+
//| 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;
PrintFormat("NANDR EA: [%s] Bullish sweep reversal at orbHigh %.2f — flipping to BEARISH",
g_Sessions[sessIdx].name, orbHigh);
}
// 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;
}
}
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;
PrintFormat("NANDR EA: [%s] Bearish sweep reversal at orbLow %.2f — flipping to BULLISH",
g_Sessions[sessIdx].name, orbLow);
}
// 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;
}
}
}
//+------------------------------------------------------------------+
//| 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;
}
//+------------------------------------------------------------------+
//| Count open positions by magic |
//+------------------------------------------------------------------+
int CountOpenPositions()
{
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)
count++;
}
return count;
}
//+------------------------------------------------------------------+
//| Count pending orders by magic |
//+------------------------------------------------------------------+
int CountPendingOrders()
{
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)
count++;
}
return count;
}
//+------------------------------------------------------------------+
//| 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(g_TodayTrades >= InpMaxTradesPerDay) return;
if(g_Sessions[sessIdx].tradesThisSession >= InpMaxRetestsPerSession) return;
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) 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;
double sl = CalcSL(dir, entry, obBottom, obTop);
double 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++;
// Do NOT clear inBreakout — the ORB level remains valid for subsequent retests
// until the breakout expires, fails, or the daily reset. This allows the EA to
// take multiple retest entries on the same level (e.g. 18:45 AND 20:45 retests).
// Re-entry is naturally gated by CountOpenPositions() checked at the top of OpenTrade.
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 — fires at the wick, not on bar close |
//| Covers both ORB session retests and OB boundary retests. |
//| Uses live bar[0] high/low + last closed bar[1] for confirmation. |
//| Direction: determined by breakoutDir for ORB, OB type for OBs. |
//| Sweep reversals: detected via ask/bid relative to the level. |
//+------------------------------------------------------------------+
void CheckRetestEntriesTick()
{
if(g_TradingHalted) return;
if(g_TodayTrades >= InpMaxTradesPerDay) return;
double h_cur = iHigh(g_Symbol, PERIOD_CURRENT, 0); // current bar live high so far
double l_cur = iLow(g_Symbol, PERIOD_CURRENT, 0); // current bar live low so far
double c1 = iClose(g_Symbol, PERIOD_CURRENT, 1); // last closed bar close
double open0 = iOpen(g_Symbol, PERIOD_CURRENT, 0); // current bar open
double ask = SymbolInfoDouble(g_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(g_Symbol, SYMBOL_BID);
double tol = PipsToPrice(1.0); // 1-pip tolerance for direction detection
// ── ORB Session Retest Entries ──────────────────────────────────
// Fires at the moment the wick touches the ORB level, not one bar later.
// Direction uses ask/bid to distinguish continuation vs sweep reversal:
// bid <= level + tol → price is AT or below the level → continuation in breakout dir
// ask >= level - tol → price has passed back through the level → sweep reversal, flip dir
if(InpWaitForRetest)
{
for(int s = 0; s < g_SessionCount; s++)
{
if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue;
if(!g_Sessions[s].inBreakout) continue;
if(g_Sessions[s].tradesThisSession >= InpMaxRetestsPerSession) continue;
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) continue;
if(InpBreakoutExpireBars > 0 && g_Sessions[s].breakoutBarsAgo >= InpBreakoutExpireBars) continue;
double orbHigh = g_Sessions[s].orbHigh;
double orbLow = g_Sessions[s].orbLow;
double orbMid = (orbHigh + orbLow) / 2.0;
int dir = 0;
double orbLevel = 0;
if(g_Sessions[s].breakoutDir == 1) // Bullish
{
// Standard: prev close above orbHigh, wick dipped to orbHigh
if(c1 > orbHigh && l_cur <= orbHigh)
{
orbLevel = orbHigh;
// ask still above orbHigh → support held → BUY
// ask dropped below orbHigh → sweep failed → SELL, flip to bearish
if(ask >= orbHigh - tol)
dir = 1;
else
{
dir = -1;
g_Sessions[s].breakoutDir = -1;
PrintFormat("NANDR EA: [%s] Tick sweep reversal below orbHigh %.2f → SELL",
g_Sessions[s].name, orbHigh);
}
}
// Mid retest: prev close above mid, wick dipped to mid
else if(c1 > orbMid && l_cur <= orbMid)
{
orbLevel = orbMid;
if(ask >= orbMid - tol)
dir = 1;
else
{
dir = -1;
g_Sessions[s].breakoutDir = -1;
}
}
}
else if(g_Sessions[s].breakoutDir == -1) // Bearish
{
// Standard: prev close below orbLow, wick came back up to orbLow
if(c1 < orbLow && h_cur >= orbLow)
{
orbLevel = orbLow;
// bid still below orbLow → resistance held → SELL (continuation)
// bid moved above orbLow → sweep reversal → BUY, flip to bullish
if(bid <= orbLow + tol)
dir = -1;
else
{
dir = 1;
g_Sessions[s].breakoutDir = 1;
PrintFormat("NANDR EA: [%s] Tick sweep reversal above orbLow %.2f → BUY",
g_Sessions[s].name, orbLow);
}
}
// Mid retest: prev close below mid, wick came back up to mid
else if(c1 < orbMid && h_cur >= orbMid)
{
orbLevel = orbMid;
if(bid <= orbMid + tol)
dir = -1;
else
{
dir = 1;
g_Sessions[s].breakoutDir = 1;
}
}
}
if(dir == 0) continue;
// One entry per wick touch per bar — prevents 3 duplicate trades on consecutive ticks
if(g_Sessions[s].retestFiredThisBar) continue;
g_Sessions[s].retestFiredThisBar = true;
double obTop = 0, obBottom = 0;
IsOBNearLevel(orbLevel, dir, obTop, obBottom);
if(InpOBRequireConf && obTop == 0) continue;
OpenTrade(dir, s, orbLevel, obTop, obBottom);
}
}
if(!InpUseOBRetestEntry) return;
// ── Bearish OB Retest → SELL at wick touch ───────────────────────
// Suppressed when day bias is bullish (session takes priority).
if(InpUseSessionBiasFilter && g_GlobalBiasDir == 1) return;
for(int i = 0; i < g_BearOBCount; i++)
{
if(!g_BearOBs[i].active || g_BearOBs[i].traded) continue;
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) break;
if(g_TodayTrades >= InpMaxTradesPerDay) break;
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|OBRetest|SELL|%.2f", 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 ────────────────────────
// Suppressed when day bias is bearish (session takes priority).
if(InpUseSessionBiasFilter && g_GlobalBiasDir == -1) return;
for(int i = 0; i < g_BullOBCount; i++)
{
if(!g_BullOBs[i].active || g_BullOBs[i].traded) continue;
if(CountOpenPositions() + CountPendingOrders() >= InpMaxPosPerSession) break;
if(g_TodayTrades >= InpMaxTradesPerDay) break;
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|OBRetest|BUY|%.2f", 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].inRetest = false;
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);
if(closeLots >= minLot)
{
bool partResult = (posDir > 0)
? g_Trade.Sell(closeLots, g_Symbol, 0, 0, 0,
StringFormat("NANDR|PartialClose|%.0fpips", InpBreakevenTrigPips))
: g_Trade.Buy(closeLots, g_Symbol, 0, 0, 0,
StringFormat("NANDR|PartialClose|%.0fpips", InpBreakevenTrigPips));
if(partResult)
{
MarkPartialClosed(ticket);
PrintFormat("NANDR EA: Partial close %.2f lots at %.2f (%.0f pips profit). Ticket=%llu",
closeLots, price, InpBreakevenTrigPips, 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();
MqlDateTime dt, dtLast;
TimeToStruct(now, dt);
TimeToStruct(g_LastDayReset, 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);
}
//+------------------------------------------------------------------+
//| 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;
PrintFormat("NANDR EA: PipSize=%.5f (digits=%d). 100 pips = %.2f price units.",
g_PipSize, digits, PipsToPrice(100));
// 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
g_DayStartEquity = AccountInfoDouble(ACCOUNT_EQUITY);
g_LastDayReset = TimeCurrent();
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;
// Per-tick management
ManageOpenTrades();
CheckRiskLimits();
// Retest entries fire on tick (at wick touch), not on bar close
CheckRetestEntriesTick();
if(!g_IsNewBar) return;
// Daily reset
CheckDailyReset();
if(g_TradingHalted)
{
DrawDashboard();
return;
}
// ORB accumulation
UpdateORBSessions();
// Order Block updates
ScanOrderBlocks();
MitigateOrderBlocks();
// Breakout detection
DetectBreakouts();
// Entry signals (direct breakout / limit orders on new bar)
CheckEntrySignals();
// Update closed trade stats & dashboard
UpdateClosedTrades();
DrawDashboard();
}
//+------------------------------------------------------------------+
//| 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);
}
}
}
}
//+------------------------------------------------------------------+