commit 6e36b9bd47b5a95150b6f4c0e24b04cd93084d1c Author: Naji El Chemaly Date: Tue Jun 2 06:39:15 2026 +0300 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7248a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# MetaTrader 5 compiled files +*.ex5 +*.ex4 + +# MetaEditor build artifacts +*.log + +# OS +Thumbs.db +Desktop.ini +.DS_Store + +# VS Code +.vscode/ + +# External files +*.pine diff --git a/NANDR_ORB_OB_EA.mq5 b/NANDR_ORB_OB_EA.mq5 new file mode 100644 index 0000000..af8691e --- /dev/null +++ b/NANDR_ORB_OB_EA.mq5 @@ -0,0 +1,1475 @@ +//+------------------------------------------------------------------+ +//| 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.00" +#property strict + +#include +#include +#include + +//+------------------------------------------------------------------+ +//| 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 = false; // Daily Open Session (00:00 UTC) +input bool InpUseTokyoSession = false; // Tokyo Session (00:00 UTC) +input bool InpUseLondonSession = false; // London Session (07:00 UTC) +input bool InpUseNYSession = false; // NY Session (12:00 UTC) +input bool InpUseNYOrbSession = false; // 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 = true; // Use Strict Breakout Filter + +// --- Lot Size & Risk --- +input group "═══ Lot Size & Risk ═══" +input ENUM_LOT_MODE InpLotMode = LOT_RISK_PERCENT; // Lot Mode +input double InpFixedLotSize = 0.01; // Fixed Lot Size +input double InpRiskPercent = 1.0; // Risk % per Trade + +// --- Stop Loss --- +input group "═══ Stop Loss ═══" +input ENUM_SL_MODE InpSLMode = SL_OB_BOUNDARY; // SL Mode +input double InpSLPips = 20.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 = 40.0; // TP Fixed Pips +input double InpRRRatio = 2.0; // Risk:Reward Ratio + +// --- Trade Management --- +input group "═══ Trade Management ═══" +input double InpBreakevenTrigPips = 15.0; // Breakeven Trigger (pips) +input double InpBreakevenOffPips = 2.0; // Breakeven Offset (pips) +input double InpTrailingStopPips = 0.0; // Trailing Stop Pips (0=off) +input int InpMaxTradesPerDay = 3; // Max Trades per Day +input int InpMaxPosPerSession = 1; // Max Positions per Session + +// --- Risk Management --- +input group "═══ Risk Management ═══" +input double InpMaxDailyLossUSD = 0.0; // Max Daily Loss USD (0=off) +input double InpMaxDailyLossPct = 0.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; + + void Reset() + { + orbHigh = 0; + orbLow = DBL_MAX; + orbBarCount = 0; + orbComplete = false; + orbStartTime = 0; + breakoutDir = 0; + inBreakout = false; + inRetest = false; + breakoutBarsAgo= 0; + tradesThisSession = 0; + } +}; + +//+------------------------------------------------------------------+ +//| Order Block structure | +//+------------------------------------------------------------------+ +struct SOrderBlock +{ + double top; + double bottom; + double mid; + datetime time; + bool active; + 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; + +// 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); + 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 | +//+------------------------------------------------------------------+ +void UpdateORBSessions() +{ + datetime barTime = iTime(g_Symbol, PERIOD_CURRENT, 0); + double barHigh = iHigh(g_Symbol, PERIOD_CURRENT, 0); + double barLow = iLow(g_Symbol, PERIOD_CURRENT, 0); + + MqlDateTime dt; + TimeToStruct(barTime, dt); + + for(int s = 0; s < g_SessionCount; s++) + { + SSession *sess = &g_Sessions[s]; + if(!sess.enabled) continue; + + // Session open bar starts accumulation + if(IsSessionOpenBar(*sess, barTime)) + { + // Reset session state for new day's ORB + sess.orbHigh = barHigh; + sess.orbLow = barLow; + sess.orbBarCount = 1; + sess.orbStartTime= barTime; + sess.orbComplete = (g_OrbBarsNeeded == 1); // M15: complete immediately + sess.breakoutDir = 0; + sess.inBreakout = false; + sess.inRetest = false; + sess.breakoutBarsAgo = 0; + sess.tradesThisSession = 0; + + if(InpShowORBLines) + DrawORBLines(s); + continue; + } + + // Still accumulating (M5 or M1) + if(!sess.orbComplete && sess.orbBarCount > 0 && IsWithinOrbWindow(*sess, barTime)) + { + sess.orbHigh = MathMax(sess.orbHigh, barHigh); + sess.orbLow = MathMin(sess.orbLow, barLow); + sess.orbBarCount++; + if(sess.orbBarCount >= g_OrbBarsNeeded) + { + sess.orbComplete = true; + PrintFormat("NANDR EA: [%s] ORB complete. High=%.2f Low=%.2f", sess.name, sess.orbHigh, sess.orbLow); + } + if(InpShowORBLines) + DrawORBLines(s); + } + + // Track breakout bar age + if(sess.inBreakout) + sess.breakoutBarsAgo++; + } +} + +//+------------------------------------------------------------------+ +//| Strict breakout filter (mirrors Pine filteredHighCrossBO/Low) | +//+------------------------------------------------------------------+ +bool CheckStrictBreakout(int dir, double orbLevel) +{ + int N = g_BreakoutConfBars; // number of confirmation bars (already shifted) + if(N < 2) N = 2; + + // Need at least N+1 bars of history + int bars = Bars(g_Symbol, PERIOD_CURRENT); + if(bars < N + 2) return false; + + if(dir > 0) // bullish + { + // low[N] < orbLevel (pre-breakout penetration from below) + // Then all N bars: low > orbLevel AND close > orbLevel + // Current bar: close > low[1] AND low > orbLevel + if(iLow(g_Symbol, PERIOD_CURRENT, N + 1) >= orbLevel) return false; + for(int i = 1; i <= N; 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, 0) <= iLow(g_Symbol, PERIOD_CURRENT, 1)) return false; + if(iLow(g_Symbol, PERIOD_CURRENT, 0) <= orbLevel) return false; + return true; + } + else // bearish + { + if(iHigh(g_Symbol, PERIOD_CURRENT, N + 1) <= orbLevel) return false; + for(int i = 1; i <= N; 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, 0) >= iHigh(g_Symbol, PERIOD_CURRENT, 1)) return false; + if(iHigh(g_Symbol, PERIOD_CURRENT, 0) >= orbLevel) return false; + return true; + } +} + +//+------------------------------------------------------------------+ +//| Simple breakout: close crosses ORB level | +//+------------------------------------------------------------------+ +bool CheckSimpleBreakout(int dir, double orbLevel) +{ + double c0 = iClose(g_Symbol, PERIOD_CURRENT, 0); + double c1 = iClose(g_Symbol, PERIOD_CURRENT, 1); + 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++) + { + SSession *sess = &g_Sessions[s]; + if(!sess.enabled || !sess.orbComplete) continue; + if(sess.orbHigh <= 0 || sess.orbLow >= DBL_MAX) continue; + if(sess.inBreakout || sess.inRetest) continue; // already tracking one + + bool bullBO = InpUseStrictFilter + ? CheckStrictBreakout( 1, sess.orbHigh) + : CheckSimpleBreakout( 1, sess.orbHigh); + bool bearBO = InpUseStrictFilter + ? CheckStrictBreakout(-1, sess.orbLow) + : CheckSimpleBreakout(-1, sess.orbLow); + + if(bullBO) + { + sess.breakoutDir = 1; + sess.inBreakout = true; + sess.breakoutBarsAgo = 0; + PrintFormat("NANDR EA: [%s] Bullish ORB breakout at %.2f", sess.name, sess.orbHigh); + } + else if(bearBO) + { + sess.breakoutDir = -1; + sess.inBreakout = true; + sess.breakoutBarsAgo = 0; + PrintFormat("NANDR EA: [%s] Bearish ORB breakout at %.2f", sess.name, sess.orbLow); + } + } +} + +//+------------------------------------------------------------------+ +//| Detect retest for sessions that had a breakout | +//+------------------------------------------------------------------+ +bool DetectRetest(int sessIdx, int &dir) +{ + SSession *sess = &g_Sessions[sessIdx]; + if(!sess.inBreakout) return false; + + double c0 = iClose(g_Symbol, PERIOD_CURRENT, 0); + double c1 = iClose(g_Symbol, PERIOD_CURRENT, 1); + double h0 = iHigh(g_Symbol, PERIOD_CURRENT, 0); + double l0 = iLow(g_Symbol, PERIOD_CURRENT, 0); + + // Bullish retest: previous bar was above ORB, this bar touched it and closed back above + if(sess.breakoutDir == 1) + { + bool retest = (c1 > sess.orbHigh) && (l0 <= sess.orbHigh) && (c0 >= sess.orbHigh); + if(retest) { dir = 1; return true; } + } + // Bearish retest: previous bar was below ORB, this bar touched it and closed back below + else if(sess.breakoutDir == -1) + { + bool retest = (c1 < sess.orbLow) && (h0 >= sess.orbLow) && (c0 <= sess.orbLow); + if(retest) { dir = -1; return true; } + } + + // Failed retest + if(sess.breakoutDir == 1 && c0 < sess.orbHigh && c1 > sess.orbHigh) + { + PrintFormat("NANDR EA: [%s] Failed bullish retest", sess.name); + sess.inBreakout = false; sess.breakoutDir = 0; + } + else if(sess.breakoutDir == -1 && c0 > sess.orbLow && c1 < sess.orbLow) + { + PrintFormat("NANDR EA: [%s] Failed bearish retest", sess.name); + sess.inBreakout = false; sess.breakoutDir = 0; + } + + return false; +} + +//+------------------------------------------------------------------+ +//| 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] + double 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].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].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 >= InpMaxPosPerSession) 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++; + g_Sessions[sessIdx].inBreakout = false; + g_Sessions[sessIdx].inRetest = false; + 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); + } +} + +//+------------------------------------------------------------------+ +//| Check all sessions for entry signals | +//+------------------------------------------------------------------+ +void CheckEntrySignals() +{ + for(int s = 0; s < g_SessionCount; s++) + { + SSession *sess = &g_Sessions[s]; + if(!sess.enabled || !sess.orbComplete) continue; + if(sess.tradesThisSession >= InpMaxPosPerSession) continue; + + int dir = 0; + if(!DetectRetest(s, dir)) continue; + + double obTop = 0, obBottom = 0; + bool obFound = IsOBNearLevel( + (dir > 0) ? sess.orbHigh : sess.orbLow, + dir, obTop, obBottom); + + // If OB required and not found, skip + if(InpOBRequireConf && !obFound) continue; + + double orbLevel = (dir > 0) ? sess.orbHigh : sess.orbLow; + OpenTrade(dir, s, orbLevel, obTop, obBottom); + } +} + +//+------------------------------------------------------------------+ +//| 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(); + long ticket = g_Position.Ticket(); + int posDir = (g_Position.PositionType() == POSITION_TYPE_BUY) ? 1 : -1; + + // Breakeven + if(InpBreakevenTrigPips > 0) + { + double trigDist = PipsToPrice(InpBreakevenTrigPips); + double beDist = PipsToPrice(InpBreakevenOffPips); + double beLevel = (posDir > 0) ? entry + beDist : entry - beDist; + + if(posDir > 0 && price >= entry + trigDist && sl < beLevel) + currentSL = NormalizeDouble(beLevel, (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS)); + else if(posDir < 0 && price <= entry - trigDist && (sl > beLevel || sl == 0)) + 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; + + // Reset all session ORB data for the new day + for(int s = 0; s < g_SessionCount; s++) + g_Sessions[s].Reset(); + + 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; + SSession *sess = &g_Sessions[sessIdx]; + + color lineColor = clrWhite; + if(sess.name == "Tokyo") lineColor = clrDodgerBlue; + else if(sess.name == "London") lineColor = clrTomato; + else if(sess.name == "NY" || sess.name == "NYORB") lineColor = clrGold; + else if(sess.name == "DailyOpen") lineColor = clrSilver; + + string prefix = "NANDR_ORB_" + sess.name + "_"; + + // High line + string hName = prefix + "High"; + ObjectDelete(0, hName); + if(sess.orbHigh > 0) + { + ObjectCreate(0, hName, OBJ_HLINE, 0, 0, sess.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, sess.name + " ORB High: " + DoubleToString(sess.orbHigh, 2)); + } + + // Low line + string lName = prefix + "Low"; + ObjectDelete(0, lName); + if(sess.orbLow < DBL_MAX && sess.orbLow > 0) + { + ObjectCreate(0, lName, OBJ_HLINE, 0, 0, sess.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, sess.name + " ORB Low: " + DoubleToString(sess.orbLow, 2)); + } + + // Mid line (dotted) + if(sess.orbHigh > 0 && sess.orbLow < DBL_MAX && sess.orbLow > 0) + { + string mName = prefix + "Mid"; + ObjectDelete(0, mName); + double mid = (sess.orbHigh + sess.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 = isBull ? &g_BullOBs[idx] : &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); +} + +//+------------------------------------------------------------------+ +//| Draw dashboard | +//+------------------------------------------------------------------+ +void DrawDashboard() +{ + if(!InpShowDashboard) return; + + string prefix = "NANDR_DB_"; + int x = 15; + int y = 30; + int dy = 18; + int line = 0; + color title = clrGold; + color normal = clrSilver; + color good = clrLimeGreen; + color bad = clrTomato; + + auto DrawText = [&](string txt, color col, int fontSize = 9) + { + string name = prefix + IntegerToString(line); + ObjectDelete(0, name); + ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER); + ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x); + ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y + line * dy); + ObjectSetInteger(0, name, OBJPROP_COLOR, col); + ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize); + ObjectSetString(0, name, OBJPROP_FONT, "Consolas"); + ObjectSetString(0, name, OBJPROP_TEXT, txt); + line++; + }; + + DrawText("▌ NANDR ORB+OB EA", title, 10); + DrawText("─────────────────────────", normal); + DrawText(StringFormat("Symbol : %s TF: %s", g_Symbol, EnumToString(Period())), normal); + DrawText(StringFormat("OrbTF : M%d BarsNeeded: %d", (int)InpOrbTimeframe, g_OrbBarsNeeded), normal); + DrawText("─────────────────────────", normal); + + // Sessions + for(int s = 0; s < g_SessionCount; s++) + { + SSession *sess = &g_Sessions[s]; + string status = sess.orbComplete ? "ARMED" : (sess.orbBarCount > 0 ? "ACCUM" : "WAIT "); + if(sess.inBreakout) status = (sess.breakoutDir > 0 ? "BULL↑" : "BEAR↓"); + color sc = sess.orbComplete ? good : normal; + if(g_TradingHalted) sc = bad; + + DrawText(StringFormat("%-8s H:%.2f L:%.2f [%s]", + sess.name, sess.orbHigh > 0 ? sess.orbHigh : 0, + sess.orbLow < DBL_MAX ? sess.orbLow : 0, status), sc); + } + + DrawText("─────────────────────────", normal); + + // OB counts + DrawText(StringFormat("BullOBs: %d BearOBs: %d", g_BullOBCount, g_BearOBCount), normal); + + DrawText("─────────────────────────", normal); + + // Daily stats + double wr = (g_TodayWins + g_TodayLosses > 0) + ? (double)g_TodayWins / (g_TodayWins + g_TodayLosses) * 100.0 : 0; + DrawText(StringFormat("Trades : %d / %d", g_TodayTrades, InpMaxTradesPerDay), normal); + DrawText(StringFormat("W/L : %d / %d WR: %.0f%%", g_TodayWins, g_TodayLosses, wr), + g_TodayWins >= g_TodayLosses ? good : normal); + DrawText(StringFormat("Today PnL: %+.2f USD", g_TodayPnL), + g_TodayPnL >= 0 ? good : bad); + DrawText(StringFormat("Equity : %.2f", AccountInfoDouble(ACCOUNT_EQUITY)), normal); + + if(g_TradingHalted) + DrawText("⚠ TRADING HALTED — Daily limit", bad, 10); + else if(g_SessionCount == 0) + DrawText("⚠ 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 (for XAUUSD: 1 pip = 0.1, i.e. 5-digit broker) + double tickSize = SymbolInfoDouble(g_Symbol, SYMBOL_TRADE_TICK_SIZE); + int digits = (int)SymbolInfoInteger(g_Symbol, SYMBOL_DIGITS); + g_PointSize = SymbolInfoDouble(g_Symbol, SYMBOL_POINT); + // For XAUUSD with 2 decimal places, 1 pip = 0.01 (SYMBOL_POINT) + // For XAUUSD with 3 decimal places, 1 pip = 0.01 (10 points) + g_PipSize = (digits == 3 || digits == 5) ? g_PointSize * 10 : g_PointSize; + + // 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(""); +} + +//+------------------------------------------------------------------+ +//| OnTick | +//+------------------------------------------------------------------+ +void OnTick() +{ + // 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(); + + 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 + 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); + } + } + } +} +//+------------------------------------------------------------------+