//+------------------------------------------------------------------+ //| 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.20" #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 = false; // Use Strict Breakout Filter (false=simple close-cross) input bool InpWaitForRetest = false; // Wait for ORB Retest Before Entry input int InpBreakoutExpireBars = 8; // Bars Before Breakout Expires (0=never) // --- 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 | //| 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); 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) { g_Sessions[s].breakoutDir = 1; g_Sessions[s].inBreakout = true; g_Sessions[s].breakoutBarsAgo= 0; PrintFormat("NANDR EA: [%s] Bullish ORB breakout at %.2f", g_Sessions[s].name, g_Sessions[s].orbHigh); } else if(bearBO) { g_Sessions[s].breakoutDir = -1; g_Sessions[s].inBreakout = true; g_Sessions[s].breakoutBarsAgo= 0; PrintFormat("NANDR EA: [%s] Bearish ORB breakout at %.2f", g_Sessions[s].name, g_Sessions[s].orbLow); } } } //+------------------------------------------------------------------+ //| Detect retest for sessions that had a breakout | //+------------------------------------------------------------------+ bool DetectRetest(int sessIdx, int &dir) { if(!g_Sessions[sessIdx].inBreakout) return false; // Use bar[1] (just-closed bar) as the retest candidate and bar[2] as context. // This mirrors Pine Script: decisions only on completed candles, never on bar[0] // which is only the opening price at new-bar time. double c0 = iClose(g_Symbol, PERIOD_CURRENT, 1); // just-closed bar double c1 = iClose(g_Symbol, PERIOD_CURRENT, 2); // bar before it double h0 = iHigh(g_Symbol, PERIOD_CURRENT, 1); double l0 = iLow(g_Symbol, PERIOD_CURRENT, 1); // Bullish retest: bar[2] confirmed above ORB high, bar[1] dipped to ORB and closed back above if(g_Sessions[sessIdx].breakoutDir == 1) { bool retest = (c1 > g_Sessions[sessIdx].orbHigh) && (l0 <= g_Sessions[sessIdx].orbHigh) && (c0 >= g_Sessions[sessIdx].orbHigh); if(retest) { dir = 1; return true; } } // Bearish retest: bar[2] confirmed below ORB low, bar[1] ticked back to ORB and closed back below else if(g_Sessions[sessIdx].breakoutDir == -1) { bool retest = (c1 < g_Sessions[sessIdx].orbLow) && (h0 >= g_Sessions[sessIdx].orbLow) && (c0 <= g_Sessions[sessIdx].orbLow); if(retest) { dir = -1; return true; } } // Failed retest — only on a COMPLETED bar, never on bar open if(g_Sessions[sessIdx].breakoutDir == 1 && c0 < g_Sessions[sessIdx].orbHigh && c1 > g_Sessions[sessIdx].orbHigh) { PrintFormat("NANDR EA: [%s] Failed bullish retest", g_Sessions[sessIdx].name); g_Sessions[sessIdx].inBreakout = false; g_Sessions[sessIdx].breakoutDir = 0; } else if(g_Sessions[sessIdx].breakoutDir == -1 && c0 > g_Sessions[sessIdx].orbLow && c1 < g_Sessions[sessIdx].orbLow) { PrintFormat("NANDR EA: [%s] Failed bearish retest", g_Sessions[sessIdx].name); g_Sessions[sessIdx].inBreakout = false; g_Sessions[sessIdx].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] 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].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++) { if(!g_Sessions[s].enabled || !g_Sessions[s].orbComplete) continue; if(g_Sessions[s].tradesThisSession >= InpMaxPosPerSession) 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-based entry: wait for price to pull back to ORB level int retestDir = 0; if(!DetectRetest(s, retestDir)) continue; double obTop = 0, obBottom = 0; IsOBNearLevel(orbLevel, retestDir, obTop, obBottom); if(InpOBRequireConf && obTop == 0) continue; OpenTrade(retestDir, 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(); ulong 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; 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 (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); } } } } //+------------------------------------------------------------------+