//+------------------------------------------------------------------+ //| OrdersEA_Smart_Grid_BO.mq5 | //| Copyright 2024, Garfield Heron | //| https://fetcherpay.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, Garfield Heron" #property link "https://fetcherpay.com" #property version "5.8" #include #include #define VERSION "Version 5.8 Smart Grid Breakout BO MT5" #define MAX_TRADES 600 #define MAX_LOG_TRADES 1200 //--- Input Parameters input string Email= "garfield@fetcherpay.com"; input int MagicNum= 333; //--- Smart Grid Settings input string GridSettings = "=== Smart Grid Settings ==="; input bool UseAutoPivots = true; input double InpManualHigh= 0; // Manual HIGH level (0 = use AutoPivots) input double InpManualLow= 0; // Manual LOW level (0 = use AutoPivots) input double Entry= 10; // Grid spacing in points input double TP= 15; // Take-profit in points per level input double Lots=0.01; input int MaxLevels = 10; // Max levels per side (cap; adaptive may place fewer) //--- Range Filter Settings input string FilterSettings = "=== Range Filters ==="; input bool UseRSIFilter = true; input int RSIPeriod = 14; input int RSILower = 35; input int RSIUpper = 65; input bool UseADXFilter = true; input int ADXPeriod = 14; input double ADXMax = 30; // Widened default from 25 input bool UseATRFilter = true; input int ATRPeriod = 14; input double ATRMultiplier = 1.5; //--- Adaptive Filters (4.4) input string AdaptiveSettings = "=== Adaptive Filters ==="; input int InpRelaxFilterAfterDays = 3; // If no trade for N days, relax filters (0=disabled) input double InpRelaxADXFactor = 1.3; // Multiply ADXMax by this when relaxed input double InpRelaxLotFactor = 0.5; // Multiply lots by this when relaxed //--- Adaptive Entry (3.3) input bool InpAdaptiveEntry = false; // Auto-scale grid spacing from ATR input double InpEntryATRFactor = 0.5; // spacing = ATR * factor / MaxLevels (per level) //--- Spread Filter (3.4) input string SpreadSettings = "=== Spread Filter ==="; input int InpMaxSpreadPoints = 30; // 0 = disabled //--- Stop Loss (3.5) input string SLSettings = "=== Stop Loss ==="; input bool InpUseStopLoss = false; // false = NO stop loss (original grid behavior) input int StopLoss = 300; // Only applied if InpUseStopLoss=true //--- Session Filter (4.1) input string SessionSettings = "=== Session Filter ==="; input bool InpUseSessionFilter = false; input int InpSessionStartHour = 1; // Broker time, inclusive input int InpSessionEndHour = 10; // Broker time, exclusive //--- Breakeven (4.2) input string BreakevenSettings = "=== Breakeven ==="; input bool InpUseBreakeven = false; // Move remaining SLs to breakeven after first TP hits input int InpBreakevenBufferPoints = 5; // Spread cushion above entry //--- Trailing Stop (5.1) input string TrailingSettings = "=== Trailing Stop ==="; input bool InpUseTrailingStop = false; // Enable trailing stop on open positions // Renamed from InpTrailStart in 5.4 so all charts pick up the new 500pt // default on recompile (renamed inputs reset to source default; the old // 1000pt start was reachable on metals only — presets set metals back to 1000). input int InpTrailStartPoints = 500; // Points of profit before trail activates input int InpTrailStop = 500; // Trail distance behind price (points) //--- Correlation Cap (4.3) input string CorrelationSettings = "=== Correlation Cap ==="; input int InpMaxLongSymbols = 0; // Max distinct symbols with open longs (0 = unlimited) input int InpMaxShortSymbols = 0; // Max distinct symbols with open shorts (0 = unlimited) //--- Range Drift (4.6) input string DriftSettings = "=== Range Drift ==="; input bool InpRangeDriftEnable = false; input int InpMoveRangeTrigger = 200; // Re-center grid when mid drifts by N points //--- Risk Management input string RiskSettings = "=== Risk Management ==="; input int TRADE_RANGE= 50; input double LongLimit= 0; input double ShortLimit= 0; input string GetOut= "N"; input string OpenNewTrades="Y"; input int TakeProfitLevelPercent= 0; // % equity gain to close cycle (0=disabled) input int TakeProfitLevelDollarAmount= 0; // $ gain to close cycle (0=disabled) input int EquityFactorPercent= 0; input int LotsFactorPercent= 0; input int BaseEquity= 10000; input bool Master= false; input bool DiagnosticModeOn= false; input double InpMaxDailyDrawdown = 5.0; // Max daily EA drawdown % (per-EA) input double InpMaxWeeklyDrawdown = 10.0; // Max weekly EA drawdown % (per-EA, 0=disable) //--- Profit Protection input string ProfitSettings = "=== Profit Protection ==="; input bool InpStopAfterProfit = false; // Stop new grids after profitable cycle input bool InpCycleReport = true; // Emit notification on cycle end //--- Weekend Protection input string WeekendSettings = "=== Weekend Protection ==="; input bool InpCloseBeforeWeekend = true; input int InpWeekendCloseHour = 17; input bool InpCancelPendingBeforeWeekend = true; input int InpMondayStartHour = 7; // Monday: no new grids before this server hour (0 = off) //--- Thursday Wind-Down (5.6): approach the weekend close algorithmically — // stop taking new risk Thursday afternoon and let tight trails walk // positions out at their natural end; the hard close stays as backstop. input string WinddownSettings = "=== Thursday Wind-Down ==="; input bool InpUseWinddown = true; input int InpWinddownStartHour = 19; // Server hour Thursday: no new grids from here (Thu 12:00 ET) input int InpWinddownTrailStart = 200; // Wind-down trail activation (points) input int InpWinddownTrailStop = 150; // Wind-down trail distance (points) //--- Trade Object CTrade trade; CPositionInfo positionInfo; //--- Indicator Handles int RSIHandle = INVALID_HANDLE; int ADXHandle = INVALID_HANDLE; int ATRHandle = INVALID_HANDLE; //--- Pivot Point Variables double PivotP = 0; double PivotR1 = 0; double PivotR2 = 0; double PivotS1 = 0; double PivotS2 = 0; double GridHigh = 0; double GridLow = 0; //--- Stats Variables (1.3 — now actually computed) int longs = 0; int shorts = 0; double longAvgPrice = 0; double longAvgLots = 0; double shortAvgPrice = 0; double shortAvgLots = 0; double longProfit = 0; double shortProfit = 0; //--- State double initEquity; int lotDigits; bool bEnableLongs = false; bool bEnableShorts = false; bool bGetOutOK = false; bool bOpenNewTradesOK = false; bool bGetOutHandled = false; //--- Daily Drawdown (per-EA, 3.1B) double dailyStartEquity = 0; datetime lastEquityReset = 0; double realizedPnLToday = 0; datetime lastRealizedScan = 0; //--- Weekly Drawdown (per-EA) double weeklyStartEquity = 0; datetime lastWeeklyReset = 0; double realizedPnLWeek = 0; datetime lastWeeklyScan = 0; //--- Weekend Protection bool weekendCloseExecuted = false; bool mondayStandDownLogged = false; bool winddownLogged = false; bool winddownPendingsCancelled = false; //--- Master one-shot (1.4) bool masterShutdownDone = false; //--- Pivot recalc (3.8) datetime lastPivotCalcDate = 0; //--- Grid State (3.7 — now persisted) bool gridPlaced = false; double cycleStartEquity = 0; datetime cycleStartTime = 0; bool cycleProfitStop = false; int cyclePartialTPCount = 0; // For breakeven logic datetime lastTradePlacedTime = 0; // For adaptive filters //--- Runtime derived double currentEntryPts = 0; // Resolved at bar open (adaptive or fixed) bool filtersRelaxed = false; // Set by adaptive filter check //+------------------------------------------------------------------+ //| Log helper with symbol prefix (3.6) | //+------------------------------------------------------------------+ void PrintS(string msg) { Print("[", _Symbol, ":", MagicNum, "] ", msg); } void Log(string st) { if(DiagnosticModeOn) PrintS(st); } //+------------------------------------------------------------------+ //| Determine filling mode supported by symbol (1.2) | //+------------------------------------------------------------------+ ENUM_ORDER_TYPE_FILLING GetMarketFilling() { int modes = (int)SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE); if((modes & SYMBOL_FILLING_IOC) != 0) return ORDER_FILLING_IOC; if((modes & SYMBOL_FILLING_FOK) != 0) return ORDER_FILLING_FOK; return ORDER_FILLING_RETURN; } // ORDER_FILLING_RETURN is always valid for pending orders per MT5 docs — // SYMBOL_FILLING_MODE bitmask only exposes FOK/IOC flags (RETURN is implicit). ENUM_ORDER_TYPE_FILLING GetPendingFilling() { return ORDER_FILLING_RETURN; } //+------------------------------------------------------------------+ //| State persistence (3.7) | //+------------------------------------------------------------------+ string GvKey(string suffix) { return "OrdersEA_BO_" + IntegerToString(MagicNum) + "_" + _Symbol + "_" + suffix; } void SaveGridState() { GlobalVariableSet(GvKey("gridPlaced"), gridPlaced ? 1.0 : 0.0); GlobalVariableSet(GvKey("cycleStartEquity"), cycleStartEquity); GlobalVariableSet(GvKey("cycleStartTime"), (double)cycleStartTime); GlobalVariableSet(GvKey("cycleProfitStop"), cycleProfitStop ? 1.0 : 0.0); GlobalVariableSet(GvKey("lastPivotCalcDate"), (double)lastPivotCalcDate); GlobalVariableSet(GvKey("lastTradeTime"), (double)lastTradePlacedTime); GlobalVariableSet(GvKey("dailyStartEquity"), dailyStartEquity); GlobalVariableSet(GvKey("lastEquityReset"), (double)lastEquityReset); GlobalVariableSet(GvKey("weeklyStartEquity"), weeklyStartEquity); GlobalVariableSet(GvKey("lastWeeklyReset"), (double)lastWeeklyReset); } void LoadGridState() { if(GlobalVariableCheck(GvKey("gridPlaced"))) gridPlaced = GlobalVariableGet(GvKey("gridPlaced")) > 0.5; if(GlobalVariableCheck(GvKey("cycleStartEquity"))) cycleStartEquity = GlobalVariableGet(GvKey("cycleStartEquity")); if(GlobalVariableCheck(GvKey("cycleStartTime"))) cycleStartTime = (datetime)GlobalVariableGet(GvKey("cycleStartTime")); if(GlobalVariableCheck(GvKey("cycleProfitStop"))) cycleProfitStop = GlobalVariableGet(GvKey("cycleProfitStop")) > 0.5; if(GlobalVariableCheck(GvKey("lastPivotCalcDate"))) lastPivotCalcDate = (datetime)GlobalVariableGet(GvKey("lastPivotCalcDate")); if(GlobalVariableCheck(GvKey("lastTradeTime"))) lastTradePlacedTime = (datetime)GlobalVariableGet(GvKey("lastTradeTime")); if(GlobalVariableCheck(GvKey("dailyStartEquity"))) dailyStartEquity = GlobalVariableGet(GvKey("dailyStartEquity")); if(GlobalVariableCheck(GvKey("lastEquityReset"))) lastEquityReset = (datetime)GlobalVariableGet(GvKey("lastEquityReset")); if(GlobalVariableCheck(GvKey("weeklyStartEquity"))) weeklyStartEquity = GlobalVariableGet(GvKey("weeklyStartEquity")); if(GlobalVariableCheck(GvKey("lastWeeklyReset"))) lastWeeklyReset = (datetime)GlobalVariableGet(GvKey("lastWeeklyReset")); } //+------------------------------------------------------------------+ //| Today-at-broker / Monday-at-broker | //+------------------------------------------------------------------+ datetime TodayStartBroker() { datetime now = TimeCurrent(); return (datetime)((now / 86400) * 86400); } datetime WeekStartBroker() { datetime today = TodayStartBroker(); MqlDateTime dt; TimeToStruct(today, dt); int offset = (dt.day_of_week == 0) ? 6 : (dt.day_of_week - 1); return (datetime)(today - offset * 86400); } //+------------------------------------------------------------------+ //| Sum deals for this EA in a window (3.1B) | //+------------------------------------------------------------------+ double SumRealizedPnLSince(datetime from) { datetime to = TimeCurrent() + 60; if(!HistorySelect(from, to)) return 0; double total = 0; int deals = HistoryDealsTotal(); for(int i = 0; i < deals; i++) { ulong dealTicket = HistoryDealGetTicket(i); if(dealTicket == 0) continue; if(HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != MagicNum) continue; if(HistoryDealGetString(dealTicket, DEAL_SYMBOL) != _Symbol) continue; long entry = HistoryDealGetInteger(dealTicket, DEAL_ENTRY); if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_INOUT) continue; total += HistoryDealGetDouble(dealTicket, DEAL_PROFIT); total += HistoryDealGetDouble(dealTicket, DEAL_SWAP); total += HistoryDealGetDouble(dealTicket, DEAL_COMMISSION); } return total; } double GetRealizedPnLToday() { if(TimeCurrent() - lastRealizedScan < 60 && realizedPnLToday != 0) return realizedPnLToday; realizedPnLToday = SumRealizedPnLSince(TodayStartBroker()); lastRealizedScan = TimeCurrent(); return realizedPnLToday; } double GetRealizedPnLWeek() { if(TimeCurrent() - lastWeeklyScan < 120 && realizedPnLWeek != 0) return realizedPnLWeek; realizedPnLWeek = SumRealizedPnLSince(WeekStartBroker()); lastWeeklyScan = TimeCurrent(); return realizedPnLWeek; } //+------------------------------------------------------------------+ //| Compute floating P&L for this EA | //+------------------------------------------------------------------+ double GetFloatingPnL() { double total = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; total += PositionGetDouble(POSITION_PROFIT); total += PositionGetDouble(POSITION_SWAP); } return total; } //+------------------------------------------------------------------+ //| Check Drawdown — daily + weekly, per-EA (3.1B) | //+------------------------------------------------------------------+ bool CheckDailyDrawdown() { static bool dailyWarned = false; static bool weeklyWarned = false; // --- Daily rollover --- datetime today = TodayStartBroker(); if(today != lastEquityReset) { dailyStartEquity = AccountInfoDouble(ACCOUNT_EQUITY); lastEquityReset = today; realizedPnLToday = 0; lastRealizedScan = 0; dailyWarned = false; PrintS("Daily equity reset: $" + DoubleToString(dailyStartEquity, 2)); SaveGridState(); } // --- Weekly rollover (Monday 00:00 broker) --- datetime weekStart = WeekStartBroker(); if(weekStart != lastWeeklyReset) { weeklyStartEquity = AccountInfoDouble(ACCOUNT_EQUITY); lastWeeklyReset = weekStart; realizedPnLWeek = 0; lastWeeklyScan = 0; weeklyWarned = false; PrintS("Weekly equity reset: $" + DoubleToString(weeklyStartEquity, 2)); SaveGridState(); } double floating = GetFloatingPnL(); // --- Daily check --- if(InpMaxDailyDrawdown > 0 && dailyStartEquity > 0) { double dailyPnL = GetRealizedPnLToday() + floating; double dailyLimit = dailyStartEquity * InpMaxDailyDrawdown / 100.0; if(dailyPnL <= -dailyLimit) { if(!dailyWarned) { PrintS("⚠️ DAILY DD HIT: $" + DoubleToString(dailyPnL, 2) + " (limit: -$" + DoubleToString(dailyLimit, 2) + ")"); SendNotification("Grid EA " + _Symbol + ": DAILY DD reached ($" + DoubleToString(dailyPnL, 2) + ")"); dailyWarned = true; } return false; } dailyWarned = false; } // --- Weekly check --- if(InpMaxWeeklyDrawdown > 0 && weeklyStartEquity > 0) { double weeklyPnL = GetRealizedPnLWeek() + floating; double weeklyLimit = weeklyStartEquity * InpMaxWeeklyDrawdown / 100.0; if(weeklyPnL <= -weeklyLimit) { if(!weeklyWarned) { PrintS("⚠️ WEEKLY DD HIT: $" + DoubleToString(weeklyPnL, 2) + " (limit: -$" + DoubleToString(weeklyLimit, 2) + ")"); SendNotification("Grid EA " + _Symbol + ": WEEKLY DD reached ($" + DoubleToString(weeklyPnL, 2) + ")"); weeklyWarned = true; } return false; } weeklyWarned = false; } return true; } //+------------------------------------------------------------------+ //| Check Weekend Protection | //+------------------------------------------------------------------+ bool CheckWeekendProtection() { if(!InpCloseBeforeWeekend) return true; MqlDateTime dt; TimeToStruct(TimeCurrent(), dt); if(dt.day_of_week != FRIDAY) { if(weekendCloseExecuted) weekendCloseExecuted = false; return true; } // Once the weekend close has run, stay blocked for the rest of Friday — // no new grids until the next trading day. Keep sweeping in case a // close/cancel failed on the first attempt (both helpers are silent no-ops // when there is nothing left to do). if(weekendCloseExecuted) { CloseAllPositions("Weekend protection retry"); if(InpCancelPendingBeforeWeekend) CancelAllOrders("Weekend protection retry"); return false; } if(dt.hour >= InpWeekendCloseHour) { PrintS("⚠️ WEEKEND CLOSE — closing all positions"); SendNotificationEx("WEEKEND CLOSE", "Closing positions before weekend"); CloseAllPositions("Weekend protection"); if(InpCancelPendingBeforeWeekend) CancelAllOrders("Weekend protection"); weekendCloseExecuted = true; gridPlaced = false; SaveGridState(); return false; } return true; } //+------------------------------------------------------------------+ //| Thursday Wind-Down window (5.6) | //| Server Thursday >= InpWinddownStartHour through the Friday weekend | //| close: no new grids, pendings cancelled, tight trails take over. | //+------------------------------------------------------------------+ bool InWinddown() { if(!InpUseWinddown) return false; MqlDateTime dt; TimeToStruct(TimeCurrent(), dt); if(dt.day_of_week == THURSDAY && dt.hour >= InpWinddownStartHour) return true; if(dt.day_of_week == FRIDAY && dt.hour < InpWeekendCloseHour) return true; return false; } //+------------------------------------------------------------------+ //| Friday shadow metric (5.6) | //| First tick of the new trading week: report what each position | //| flattened at the weekend close would have done through Friday — | //| mark-to-market at Friday's last H1 close plus worst adverse | //| excursion. Pure telemetry: quantifies what the flatten policy | //| costs (or saves) for the weekly analysis. | //+------------------------------------------------------------------+ void ReportFridayShadow() { datetime monday = WeekStartBroker(); datetime friStart = monday - 3*86400 + InpWeekendCloseHour*3600; datetime friEnd = monday - 2*86400; MqlRates bars[]; int n = CopyRates(_Symbol, PERIOD_H1, friStart, friEnd, bars); if(n < 1) return; double friClose = bars[n-1].close; double hi = bars[0].high, lo = bars[0].low; for(int i = 1; i < n; i++) { if(bars[i].high > hi) hi = bars[i].high; if(bars[i].low < lo) lo = bars[i].low; } double tickVal = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); if(tickSize <= 0 || tickVal <= 0) return; if(!HistorySelect(friStart - 300, friStart + 900)) return; for(int i = 0; i < HistoryDealsTotal(); i++) { ulong d = HistoryDealGetTicket(i); if(d == 0) continue; if(HistoryDealGetString(d, DEAL_SYMBOL) != _Symbol) continue; if(HistoryDealGetInteger(d, DEAL_MAGIC) != MagicNum) continue; if((ENUM_DEAL_ENTRY)HistoryDealGetInteger(d, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue; // a SELL deal closes a long position int dir = ((ENUM_DEAL_TYPE)HistoryDealGetInteger(d, DEAL_TYPE) == DEAL_TYPE_SELL) ? 1 : -1; double vol = HistoryDealGetDouble(d, DEAL_VOLUME); double closeP = HistoryDealGetDouble(d, DEAL_PRICE); double mtm = (friClose - closeP) * dir / tickSize * tickVal * vol; double worstP = (dir > 0) ? lo : hi; double worst = (worstP - closeP) * dir / tickSize * tickVal * vol; PrintS("SHADOW " + (dir > 0 ? "long" : "short") + " " + DoubleToString(vol, 2) + " flattened@" + DoubleToString(closeP, _Digits) + " friClose=" + DoubleToString(friClose, _Digits) + " mtm=$" + DoubleToString(mtm, 2) + " worst=$" + DoubleToString(worst, 2)); } } //+------------------------------------------------------------------+ //| Monday Morning Stand-Down (5.3) | //| Sunday-evening ET liquidity is thin and the pivots/ATR are built | //| from Friday's session — skip new grids until the configured | //| Monday server hour. Blocking before the daily pivot recalc also | //| defers the ATR sample to the resume hour instead of the rollover. | //+------------------------------------------------------------------+ bool CheckMondayStandDown() { if(InpMondayStartHour <= 0) return true; MqlDateTime dt; TimeToStruct(TimeCurrent(), dt); if(dt.day_of_week == MONDAY && dt.hour < InpMondayStartHour) { if(!mondayStandDownLogged) { PrintS("⏸ MONDAY STAND-DOWN — no new grids before " + IntegerToString(InpMondayStartHour) + ":00 server"); mondayStandDownLogged = true; } return false; } if(mondayStandDownLogged) { PrintS("Monday stand-down over — normal trading resumed"); mondayStandDownLogged = false; } return true; } //+------------------------------------------------------------------+ //| Session Filter (4.1) | //+------------------------------------------------------------------+ bool CheckSessionFilter() { if(!InpUseSessionFilter) return true; MqlDateTime dt; TimeToStruct(TimeCurrent(), dt); int h = dt.hour; bool inSession; if(InpSessionStartHour <= InpSessionEndHour) inSession = (h >= InpSessionStartHour && h < InpSessionEndHour); else inSession = (h >= InpSessionStartHour || h < InpSessionEndHour); if(!inSession && DiagnosticModeOn) PrintS("Session filter: outside window (" + IntegerToString(h) + "h)"); return inSession; } //+------------------------------------------------------------------+ //| Spread Filter (3.4) | //+------------------------------------------------------------------+ bool CheckSpreadFilter() { if(InpMaxSpreadPoints <= 0) return true; int spread = (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); if(spread > InpMaxSpreadPoints) { if(DiagnosticModeOn) PrintS("Spread filter: " + IntegerToString(spread) + "pts > " + IntegerToString(InpMaxSpreadPoints)); return false; } return true; } //+------------------------------------------------------------------+ //| Correlation Cap (4.3) — count distinct symbols w/ open positions | //+------------------------------------------------------------------+ int CountSymbolsWithOpenPositions(int posType) { string symbols[]; int count = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; if(PositionGetInteger(POSITION_TYPE) != posType) continue; string sym = PositionGetString(POSITION_SYMBOL); bool found = false; for(int j = 0; j < count; j++) if(symbols[j] == sym) { found = true; break; } if(!found) { ArrayResize(symbols, count + 1); symbols[count] = sym; count++; } } return count; } bool CheckCorrelationCap(bool forLong) { int cap = forLong ? InpMaxLongSymbols : InpMaxShortSymbols; if(cap <= 0) return true; int type = forLong ? POSITION_TYPE_BUY : POSITION_TYPE_SELL; int current = CountSymbolsWithOpenPositions(type); // If this symbol already has positions, we're not adding a new distinct symbol bool thisSymbolHasPos = false; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if(PositionGetInteger(POSITION_TYPE) == type) { thisSymbolHasPos = true; break; } } if(thisSymbolHasPos) return true; if(current >= cap) { if(DiagnosticModeOn) PrintS("Correlation cap: " + IntegerToString(current) + "/" + IntegerToString(cap) + (forLong ? " long" : " short") + " symbols open"); return false; } return true; } //+------------------------------------------------------------------+ //| Adaptive Filters (4.4) | //+------------------------------------------------------------------+ void UpdateFilterRelaxation() { filtersRelaxed = false; if(InpRelaxFilterAfterDays <= 0) return; if(lastTradePlacedTime == 0) return; // never traded — don't relax yet long secsIdle = (long)TimeCurrent() - (long)lastTradePlacedTime; if(secsIdle > (long)InpRelaxFilterAfterDays * 86400) filtersRelaxed = true; } double EffectiveADXMax() { return filtersRelaxed ? (ADXMax * InpRelaxADXFactor) : ADXMax; } //+------------------------------------------------------------------+ //| Calculate Pivot Points | //+------------------------------------------------------------------+ void CalculatePivotPoints() { MqlRates rates[1]; int copied = CopyRates(_Symbol, PERIOD_D1, 1, 1, rates); if(copied < 1) { PrintS("Failed to get daily rates for pivot calculation"); return; } double prevHigh = rates[0].high; double prevLow = rates[0].low; double prevClose = rates[0].close; PivotP = (prevHigh + prevLow + prevClose) / 3.0; PivotR1 = (2.0 * PivotP) - prevLow; PivotS1 = (2.0 * PivotP) - prevHigh; PivotR2 = PivotP + (prevHigh - prevLow); PivotS2 = PivotP - (prevHigh - prevLow); if(UseAutoPivots) { double atr = 0; if(ATRHandle != INVALID_HANDLE) { double atrBuf[1]; if(CopyBuffer(ATRHandle, 0, 0, 1, atrBuf) > 0) atr = atrBuf[0]; } if(UseATRFilter && atr > 0) { GridHigh = NormalizeDouble(PivotP + (atr * ATRMultiplier), _Digits); GridLow = NormalizeDouble(PivotP - (atr * ATRMultiplier), _Digits); } else { GridHigh = PivotR1; GridLow = PivotS1; } PrintS("AutoPivots: HIGH=" + DoubleToString(GridHigh, _Digits) + " LOW=" + DoubleToString(GridLow, _Digits) + " ATR=" + DoubleToString(atr, _Digits)); } PrintS("Pivot: P=" + DoubleToString(PivotP, _Digits) + " R1=" + DoubleToString(PivotR1, _Digits) + " S1=" + DoubleToString(PivotS1, _Digits)); lastPivotCalcDate = TodayStartBroker(); SaveGridState(); } //+------------------------------------------------------------------+ //| Resolve Entry spacing at runtime (3.3 adaptive) | //+------------------------------------------------------------------+ double ResolveEntryPoints() { if(!InpAdaptiveEntry) return Entry; if(ATRHandle == INVALID_HANDLE) return Entry; double buf[1]; if(CopyBuffer(ATRHandle, 0, 0, 1, buf) <= 0) return Entry; double atr = buf[0]; double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); if(point <= 0 || atr <= 0) return Entry; double maxLvl = MathMax(1, MaxLevels); double pts = (atr * InpEntryATRFactor) / maxLvl / point; return MathMax(5.0, pts); // floor at 5pts } //+------------------------------------------------------------------+ //| Check if Market is Ranging | //+------------------------------------------------------------------+ bool IsRangingMarket() { bool isRanging = true; double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(UseRSIFilter && RSIHandle != INVALID_HANDLE) { double rsiBuf[1]; if(CopyBuffer(RSIHandle, 0, 0, 1, rsiBuf) > 0) { double rsi = rsiBuf[0]; if(rsi < RSILower || rsi > RSIUpper) { if(DiagnosticModeOn) PrintS("RSI extreme: " + DoubleToString(rsi, 1)); isRanging = false; } } } if(UseADXFilter && ADXHandle != INVALID_HANDLE && isRanging) { double adxBuf[1]; if(CopyBuffer(ADXHandle, 0, 0, 1, adxBuf) > 0) { double adx = adxBuf[0]; double adxCap = EffectiveADXMax(); if(adx > adxCap) { if(DiagnosticModeOn) PrintS("ADX trending: " + DoubleToString(adx, 1) + " > " + DoubleToString(adxCap, 1) + (filtersRelaxed ? " (relaxed)" : "")); isRanging = false; } } } double actualHigh = (InpManualHigh > 0) ? InpManualHigh : GridHigh; double actualLow = (InpManualLow > 0) ? InpManualLow : GridLow; if(currentPrice > actualHigh || currentPrice < actualLow) { if(DiagnosticModeOn) PrintS("Price outside grid range"); isRanging = false; } return isRanging; } //+------------------------------------------------------------------+ //| Hard breakout confirmation — price beyond R2/S2 | //| Reserved for optional entry guard: only place BO orders if price | //| has already cleared the outer pivot (stronger breakout signal). | //+------------------------------------------------------------------+ bool IsHardBreakout() { double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(currentPrice < PivotS2 || currentPrice > PivotR2) { PrintS("Hard breakout: price=" + DoubleToString(currentPrice, _Digits) + " S2=" + DoubleToString(PivotS2, _Digits) + " R2=" + DoubleToString(PivotR2, _Digits)); return true; } return false; } //+------------------------------------------------------------------+ //| Calculate Lot Size | //+------------------------------------------------------------------+ double CalcLots() { double tmp = (AccountInfoDouble(ACCOUNT_EQUITY) - initEquity); double a = EquityFactorPercent; double b = LotsFactorPercent; double lots; if(0 == EquityFactorPercent || 0 == LotsFactorPercent) lots = Lots; else { a = initEquity * a / 100.0; b = b / 100.0; if(tmp > 0) tmp = MathPow(1 + b, (tmp / a)); else if(tmp < 0) tmp = MathPow(1 - b, MathAbs(tmp / a)); else tmp = 1; lots = NormalizeDouble(Lots * tmp, lotDigits); } if(filtersRelaxed) lots *= InpRelaxLotFactor; double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); if(lots < minLot) lots = minLot; if(lots > maxLot) lots = maxLot; if(step > 0) lots = MathFloor(lots / step + 1e-9) * step; return NormalizeDouble(lots, lotDigits); } //+------------------------------------------------------------------+ //| Send Notification | //+------------------------------------------------------------------+ void SendNotificationEx(string title, string subject) { if(MQLInfoInteger(MQL_OPTIMIZATION)) return; double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); string msg = "[" + _Symbol + "] " + title + ": " + subject; msg += " | Px: " + DoubleToString(bid, _Digits); msg += " | L: " + IntegerToString(longs) + " @ " + DoubleToString(longAvgPrice, _Digits); msg += " | S: " + IntegerToString(shorts) + " @ " + DoubleToString(shortAvgPrice, _Digits); msg += " | Eq: " + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2); SendNotification(msg); PrintS(title + ": " + subject); } //+------------------------------------------------------------------+ //| Compute averages for this EA (1.3) | //+------------------------------------------------------------------+ void CalcAvgPrice() { longs = 0; shorts = 0; longAvgPrice = 0; longAvgLots = 0; shortAvgPrice = 0; shortAvgLots = 0; longProfit = 0; shortProfit = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; long ptype = PositionGetInteger(POSITION_TYPE); double vol = PositionGetDouble(POSITION_VOLUME); double open = PositionGetDouble(POSITION_PRICE_OPEN); double profit = PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP); if(ptype == POSITION_TYPE_BUY) { longAvgPrice += open * vol; longAvgLots += vol; longProfit += profit; longs++; } else if(ptype == POSITION_TYPE_SELL) { shortAvgPrice += open * vol; shortAvgLots += vol; shortProfit += profit; shorts++; } } if(longAvgLots > 0) longAvgPrice /= longAvgLots; if(shortAvgLots > 0) shortAvgPrice /= shortAvgLots; } //+------------------------------------------------------------------+ //| Breakeven move (4.2) | //+------------------------------------------------------------------+ void ApplyBreakeven() { if(!InpUseBreakeven) return; if(cyclePartialTPCount <= 0) return; // Nothing filled profitably yet double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); double buffer = InpBreakevenBufferPoints * point; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; long ptype = PositionGetInteger(POSITION_TYPE); double open = PositionGetDouble(POSITION_PRICE_OPEN); double curSL = PositionGetDouble(POSITION_SL); double tp = PositionGetDouble(POSITION_TP); double newSL = (ptype == POSITION_TYPE_BUY) ? (open - buffer) : (open + buffer); newSL = NormalizeDouble(newSL, _Digits); bool alreadyAtBE = (ptype == POSITION_TYPE_BUY) ? (curSL >= open - point) : (curSL <= open + point && curSL > 0); if(alreadyAtBE) continue; if(!trade.PositionModify(ticket, newSL, tp)) PrintS("Breakeven modify failed #" + IntegerToString((int)ticket) + " err=" + IntegerToString((int)trade.ResultRetcode())); else PrintS("Breakeven set #" + IntegerToString((int)ticket) + " SL=" + DoubleToString(newSL, _Digits)); } } //+------------------------------------------------------------------+ //| Trailing Stop (5.1) | //+------------------------------------------------------------------+ void ApplyTrailingStop() { if(!InpUseTrailingStop) return; // Wind-down (5.6): much tighter trail so positions walk themselves out // ahead of the weekend close instead of being chopped at the deadline int effStartPts = InWinddown() ? InpWinddownTrailStart : InpTrailStartPoints; int effDistPts = InWinddown() ? InpWinddownTrailStop : InpTrailStop; if(effStartPts <= 0 || effDistPts <= 0) return; double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double trailStart = effStartPts * point; double trailDist = effDistPts * point; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; long ptype = PositionGetInteger(POSITION_TYPE); double open = PositionGetDouble(POSITION_PRICE_OPEN); double curSL = PositionGetDouble(POSITION_SL); double tp = PositionGetDouble(POSITION_TP); double newSL; if(ptype == POSITION_TYPE_BUY) { if(bid - open < trailStart) continue; // profit threshold not reached newSL = NormalizeDouble(bid - trailDist, _Digits); if(newSL <= curSL) continue; // SL already at or ahead of trail } else if(ptype == POSITION_TYPE_SELL) { if(open - ask < trailStart) continue; newSL = NormalizeDouble(ask + trailDist, _Digits); if(curSL > 0 && newSL >= curSL) continue; // SL already at or ahead of trail } else continue; if(!trade.PositionModify(ticket, newSL, tp)) PrintS("Trail modify failed #" + IntegerToString((int)ticket) + " err=" + IntegerToString((int)trade.ResultRetcode())); else PrintS("Trail SL → " + DoubleToString(newSL, _Digits) + " #" + IntegerToString((int)ticket)); } } //+------------------------------------------------------------------+ //| Count partial TP fills since cycle start | //+------------------------------------------------------------------+ int CountPartialTPsSinceCycle() { if(cycleStartTime == 0) return 0; if(!HistorySelect(cycleStartTime, TimeCurrent() + 60)) return 0; int n = 0; int total = HistoryDealsTotal(); for(int i = 0; i < total; i++) { ulong ticket = HistoryDealGetTicket(i); if(ticket == 0) continue; if(HistoryDealGetInteger(ticket, DEAL_MAGIC) != MagicNum) continue; if(HistoryDealGetString(ticket, DEAL_SYMBOL) != _Symbol) continue; long entry = HistoryDealGetInteger(ticket, DEAL_ENTRY); if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_INOUT) continue; if(HistoryDealGetDouble(ticket, DEAL_PROFIT) > 0) n++; } return n; } //+------------------------------------------------------------------+ //| Profit target check (2.1 + 2.2) | //+------------------------------------------------------------------+ bool CheckProfitTarget() { if(cycleStartEquity <= 0) return false; double gain = AccountInfoDouble(ACCOUNT_EQUITY) - cycleStartEquity; double pctGain = (gain / cycleStartEquity) * 100.0; bool pctHit = (TakeProfitLevelPercent > 0 && pctGain >= TakeProfitLevelPercent); bool dollHit = (TakeProfitLevelDollarAmount > 0 && gain >= TakeProfitLevelDollarAmount); if(pctHit || dollHit) { PrintS("PROFIT TARGET — gain=$" + DoubleToString(gain, 2) + " (" + DoubleToString(pctGain, 2) + "%) closing cycle"); SendNotificationEx("PROFIT TARGET HIT", "Gain $" + DoubleToString(gain, 2) + " (" + DoubleToString(pctGain, 2) + "%)"); CloseAllPositions("Profit target"); CancelAllOrders("Profit target"); EmitCycleReport(true); gridPlaced = false; cycleStartEquity = 0; cycleStartTime = 0; cyclePartialTPCount = 0; SaveGridState(); return true; } return false; } //+------------------------------------------------------------------+ //| Cycle Report (4.5) | //+------------------------------------------------------------------+ void EmitCycleReport(bool targetHit) { if(!InpCycleReport) return; if(cycleStartEquity <= 0) return; double gain = AccountInfoDouble(ACCOUNT_EQUITY) - cycleStartEquity; long dur = (long)TimeCurrent() - (long)cycleStartTime; int hrs = (int)(dur / 3600); int mins = (int)((dur % 3600) / 60); string tag = targetHit ? "TARGET" : "END"; SendNotificationEx("CYCLE " + tag, "P&L: $" + DoubleToString(gain, 2) + " | Dur: " + IntegerToString(hrs) + "h" + IntegerToString(mins) + "m" + " | Partials: " + IntegerToString(cyclePartialTPCount)); } //+------------------------------------------------------------------+ //| Order helpers | //+------------------------------------------------------------------+ int CountPendingOrders(int type) { int count = 0; for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(ticket == 0) continue; if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue; if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue; if(OrderGetInteger(ORDER_TYPE) == type) count++; } return count; } // True if this symbol+magic already has any pending order or open position // on the broker, regardless of what gridPlaced (GlobalVariable-backed, // found 2026-08-11 to never actually reach disk in this environment) says. // Used at OnInit to recover from a restart wiping that flag to false while // a grid is still resting live -- without this check, every restart mid- // cycle placed a brand new duplicate grid on top of the old one (see vault // "2026-08-11 Duplicate Grid Orders" note; found 189 stacked pending // orders account-wide, one symbol alone had 29 spanning 24+ hours). bool HasLiveGridPresence() { for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(ticket == 0) continue; if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue; if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue; return true; } for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; return true; } return false; } void CancelAllOrders(string reason) { int cancelled = 0; for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(ticket == 0) continue; if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue; if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue; if(trade.OrderDelete(ticket)) cancelled++; } if(cancelled > 0) PrintS("Cancelled " + IntegerToString(cancelled) + " orders: " + reason); } void CloseAllPositions(string reason) { int closed = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; if(trade.PositionClose(ticket)) closed++; } if(closed > 0) PrintS("Closed " + IntegerToString(closed) + " positions: " + reason); } //+------------------------------------------------------------------+ //| Close positions by direction (edge cleanup 5.0) | //+------------------------------------------------------------------+ void ClosePositionsBySide(long posType, string reason) { int closed = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; if(PositionGetInteger(POSITION_TYPE) != posType) continue; if(trade.PositionClose(ticket)) closed++; } if(closed > 0) PrintS("Closed " + IntegerToString(closed) + " " + (posType == POSITION_TYPE_BUY ? "BUY" : "SELL") + " positions: " + reason); } //+------------------------------------------------------------------+ //| Place Buy/Sell Stop Orders (Breakout BO) | //+------------------------------------------------------------------+ bool PlaceBuyStop(double priceLevel, int level) { if(level >= MaxLevels) return false; double lots = CalcLots(); double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * point; // BUY STOP must be above current ask by at least stopLevel double minPrice = currentAsk + stopLevel; if(priceLevel < minPrice) { if(DiagnosticModeOn) PrintS("BuyStop " + IntegerToString(level) + " too close — skip"); return false; } double sl = 0; if(InpUseStopLoss && StopLoss > 0) sl = NormalizeDouble(priceLevel - (StopLoss * point), _Digits); double tp = NormalizeDouble(priceLevel + (TP * point), _Digits); if(tp <= priceLevel + stopLevel) tp = NormalizeDouble(priceLevel + stopLevel + (TP * point), _Digits); MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = TRADE_ACTION_PENDING; req.symbol = _Symbol; req.volume = lots; req.price = NormalizeDouble(priceLevel, _Digits); req.sl = sl; req.tp = tp; req.deviation = 10; req.magic = MagicNum; req.comment = "BO Buy " + IntegerToString(level); req.type = ORDER_TYPE_BUY_STOP; req.type_filling = GetPendingFilling(); if(!trade.OrderSend(req, res)) { PrintS("BuyStop error: " + trade.ResultRetcodeDescription() + " (" + IntegerToString((int)res.retcode) + ")"); return false; } if(res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED) { PrintS("BuyStop L" + IntegerToString(level) + " @ " + DoubleToString(req.price, _Digits) + " TP=" + DoubleToString(tp, _Digits) + " #" + IntegerToString((int)res.order)); lastTradePlacedTime = TimeCurrent(); return true; } return false; } bool PlaceSellStop(double priceLevel, int level) { if(level >= MaxLevels) return false; double lots = CalcLots(); double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double stopLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * point; // SELL STOP must be below current bid by at least stopLevel double maxPrice = currentBid - stopLevel; if(priceLevel > maxPrice) { if(DiagnosticModeOn) PrintS("SellStop " + IntegerToString(level) + " too close — skip"); return false; } double sl = 0; if(InpUseStopLoss && StopLoss > 0) sl = NormalizeDouble(priceLevel + (StopLoss * point), _Digits); double tp = NormalizeDouble(priceLevel - (TP * point), _Digits); if(tp >= priceLevel - stopLevel) tp = NormalizeDouble(priceLevel - stopLevel - (TP * point), _Digits); MqlTradeRequest req = {}; MqlTradeResult res = {}; req.action = TRADE_ACTION_PENDING; req.symbol = _Symbol; req.volume = lots; req.price = NormalizeDouble(priceLevel, _Digits); req.sl = sl; req.tp = tp; req.deviation = 10; req.magic = MagicNum; req.comment = "BO Sell " + IntegerToString(level); req.type = ORDER_TYPE_SELL_STOP; req.type_filling = GetPendingFilling(); if(!trade.OrderSend(req, res)) { PrintS("SellStop error: " + trade.ResultRetcodeDescription() + " (" + IntegerToString((int)res.retcode) + ")"); return false; } if(res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED) { PrintS("SellStop L" + IntegerToString(level) + " @ " + DoubleToString(req.price, _Digits) + " TP=" + DoubleToString(tp, _Digits) + " #" + IntegerToString((int)res.order)); lastTradePlacedTime = TimeCurrent(); return true; } return false; } //+------------------------------------------------------------------+ //| Handle GetOut mode | //+------------------------------------------------------------------+ void HandleGetOut() { bool getOutLongs = (GetOut=="L"||GetOut=="l"||GetOut=="A"||GetOut=="a"||GetOut=="X"||GetOut=="x"); bool getOutShorts = (GetOut=="S"||GetOut=="s"||GetOut=="A"||GetOut=="a"||GetOut=="X"||GetOut=="x"); if(!bGetOutHandled) { PrintS("GET OUT (" + GetOut + ") — closing " + (getOutLongs ? "L " : "") + (getOutShorts ? "S" : "")); bGetOutHandled = true; } for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; long ptype = PositionGetInteger(POSITION_TYPE); if(ptype == POSITION_TYPE_BUY && getOutLongs) trade.PositionClose(ticket); if(ptype == POSITION_TYPE_SELL && getOutShorts) trade.PositionClose(ticket); } for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(ticket == 0) continue; if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue; if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue; trade.OrderDelete(ticket); } gridPlaced = false; SaveGridState(); } //+------------------------------------------------------------------+ //| Master global shutdown — one-shot (1.4) | //+------------------------------------------------------------------+ void GlobalShutdown() { int posCount = 0, ordCount = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; posCount++; trade.PositionClose(ticket); } for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(ticket == 0) continue; if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue; ordCount++; trade.OrderDelete(ticket); } PrintS("MASTER SHUTDOWN — closed " + IntegerToString(posCount) + " pos, " + IntegerToString(ordCount) + " orders"); gridPlaced = false; SaveGridState(); } //+------------------------------------------------------------------+ //| Range Drift (4.6) | //+------------------------------------------------------------------+ bool CheckRangeDrift() { if(!InpRangeDriftEnable) return false; if(InpManualHigh > 0 || InpManualLow > 0) return false; // manual mode — don't drift if(GridHigh <= 0 || GridLow <= 0) return false; double price = SymbolInfoDouble(_Symbol, SYMBOL_BID); double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); double mid = (GridHigh + GridLow) / 2.0; double drift = MathAbs(price - mid) / point; if(drift > InpMoveRangeTrigger) { double shift = price - mid; GridHigh = NormalizeDouble(GridHigh + shift, _Digits); GridLow = NormalizeDouble(GridLow + shift, _Digits); PrintS("Range drift — shifted by " + DoubleToString(shift, _Digits) + " new HIGH=" + DoubleToString(GridHigh, _Digits) + " LOW=" + DoubleToString(GridLow, _Digits)); return true; } return false; } //+------------------------------------------------------------------+ //| Expert initialization | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(MagicNum); trade.SetDeviationInPoints(10); trade.SetTypeFilling(GetMarketFilling()); initEquity = (double)BaseEquity; lotDigits = 2; double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); if(minLot > 0) { double l = minLot; lotDigits = 0; while(l < 1 && lotDigits < 6) { l *= 10; lotDigits++; } } if(UseRSIFilter) RSIHandle = iRSI(_Symbol, PERIOD_CURRENT, RSIPeriod, PRICE_CLOSE); if(UseADXFilter) ADXHandle = iADX(_Symbol, PERIOD_CURRENT, ADXPeriod); if(UseATRFilter || InpAdaptiveEntry || UseAutoPivots) ATRHandle = iATR(_Symbol, PERIOD_CURRENT, ATRPeriod); LoadGridState(); // Self-heal (2026-08-11): gridPlaced is GlobalVariable-backed and was // found to never actually reach disk in this environment (no .gvr file // anywhere under the MT5 data dir) — so a full container restart mid- // cycle silently resets it to false while the grid is still resting // live on the broker, and the code below would place a brand new // duplicate grid on top of it. Trust reality over the flag: if this // symbol+magic already has orders or positions out, we already have a // grid, no matter what LoadGridState() just said. if(!gridPlaced && HasLiveGridPresence()) { PrintS("Self-heal: found existing orders/positions on init — recovering gridPlaced=true (see 2026-08-11 duplicate-grid fix)"); gridPlaced = true; } // GridHigh/GridLow are NOT persisted (only lastPivotCalcDate is) — if a // prior process already recalculated today before this restart, the // date-match guard alone would skip CalculatePivotPoints() and leave the // band at its uninitialized 0/0, causing "Invalid grid bounds" until the // next real day rollover (5.7 fix — cost XAUUSD/XAGUSD ~17h on 2026-07-23 // after two restarts landed close together near a day boundary). if(lastPivotCalcDate != TodayStartBroker() || GridHigh <= 0 || GridLow <= 0) CalculatePivotPoints(); bGetOutOK = (GetOut=="L"||GetOut=="l"||GetOut=="A"||GetOut=="a"|| GetOut=="S"||GetOut=="s"||GetOut=="N"||GetOut=="n"|| GetOut=="X"||GetOut=="x"); bOpenNewTradesOK = (OpenNewTrades=="Y"||OpenNewTrades=="y"||OpenNewTrades=="L"||OpenNewTrades=="l"|| OpenNewTrades=="N"||OpenNewTrades=="n"||OpenNewTrades=="S"||OpenNewTrades=="s"); bEnableLongs = (OpenNewTrades=="Y"||OpenNewTrades=="y"||OpenNewTrades=="L"||OpenNewTrades=="l"); bEnableShorts = (OpenNewTrades=="Y"||OpenNewTrades=="y"||OpenNewTrades=="S"||OpenNewTrades=="s"); if(!bGetOutOK) PrintS("WARNING: invalid GetOut value: " + GetOut); if(!bOpenNewTradesOK) PrintS("WARNING: invalid OpenNewTrades value: " + OpenNewTrades); PrintS(VERSION + " initialized — Magic=" + IntegerToString(MagicNum) + " SL=" + (InpUseStopLoss ? "ON(" + IntegerToString(StopLoss) + ")" : "OFF") + " Trail=" + (InpUseTrailingStop ? "ON(start=" + IntegerToString(InpTrailStartPoints) + "/dist=" + IntegerToString(InpTrailStop) + "pts)" : "OFF") + " AdaptiveEntry=" + (InpAdaptiveEntry ? "ON" : "OFF")); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { SaveGridState(); if(RSIHandle != INVALID_HANDLE) IndicatorRelease(RSIHandle); if(ADXHandle != INVALID_HANDLE) IndicatorRelease(ADXHandle); if(ATRHandle != INVALID_HANDLE) IndicatorRelease(ATRHandle); PrintS("Deinitialized (reason=" + IntegerToString(reason) + ")"); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { // --- Master one-shot (1.4) --- if(Master) { if(!masterShutdownDone) { GlobalShutdown(); masterShutdownDone = true; } return; } // --- Hourly heartbeat (5.5) — ABOVE every gate on purpose: it fires on // any tick, so log silence >1h during market hours means the chart feed // is dead (tick starvation), never a blocked/quiet EA. The ea-watchdog // keys off this; without it, a stable placed grid looks identical to a // starved chart and the watchdog restart-loops (seen Jul 15-16). static datetime lastHeartbeat = 0; if(TimeCurrent() - lastHeartbeat >= 3600) { PrintS("hb " + DoubleToString(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits)); lastHeartbeat = TimeCurrent(); } // --- Friday shadow report (5.6) — once per week on the first tick of the // new server week (Sunday evening ET), before any gate can block it --- MqlDateTime shdt; TimeToStruct(TimeCurrent(), shdt); if(shdt.day_of_week == MONDAY) { datetime wk = WeekStartBroker(); if((datetime)GlobalVariableGet(GvKey("shadowWk")) != wk) { GlobalVariableSet(GvKey("shadowWk"), (double)wk); ReportFridayShadow(); } } // --- Per-EA daily drawdown (3.1B) --- if(!CheckDailyDrawdown()) { if(gridPlaced) { CancelAllOrders("Daily drawdown"); gridPlaced = false; SaveGridState(); } return; } // --- Weekend protection --- if(!CheckWeekendProtection()) return; // --- Monday morning stand-down (5.3) — must stay above the pivot // recalc so Monday's pivots/ATR are computed at the resume hour --- if(!CheckMondayStandDown()) return; // --- GetOut mode --- if(GetOut != "N" && GetOut != "n") { HandleGetOut(); return; } else if(bGetOutHandled) bGetOutHandled = false; // --- Pivot recalc (3.8 — reliable, runs once per broker day whenever tick arrives) --- datetime today = TodayStartBroker(); if(today != lastPivotCalcDate) { PrintS("New day — recalc pivots, reset cycle flags"); if(gridPlaced) { CancelAllOrders("End of day"); gridPlaced = false; } if(cycleProfitStop) cycleProfitStop = false; CalculatePivotPoints(); // also updates lastPivotCalcDate & saves } else if(GridHigh <= 0 || GridLow <= 0) { // Self-heal (5.7): same-day, but the band was never actually computed // this process (see OnInit comment above). Recalc only — no // cancel/reset, since this isn't a real day transition. PrintS("Grid bounds invalid on same-day check — self-healing recalc"); CalculatePivotPoints(); } // --- Profit target check runs every tick (cheap) --- if(CheckProfitTarget()) return; // --- Stats + breakeven throttled to every 5s (history scans are expensive) --- static datetime lastStatsTime = 0; if(TimeCurrent() - lastStatsTime >= 5) { CalcAvgPrice(); cyclePartialTPCount = CountPartialTPsSinceCycle(); ApplyBreakeven(); ApplyTrailingStop(); lastStatsTime = TimeCurrent(); } // --- Bar-close gate for expensive checks --- static datetime lastBarTime = 0; datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); if(currentBarTime == lastBarTime) return; lastBarTime = currentBarTime; // --- Thursday wind-down (5.6): no new risk into the shortened week. // Trailing/breakeven keep running in the 5s block above; only new grid // placement (and existing entry pendings) are shut off. if(InWinddown()) { if(!winddownLogged) { PrintS("🌙 WIND-DOWN — no new grids; trail tightened to " + IntegerToString(InpWinddownTrailStart) + "/" + IntegerToString(InpWinddownTrailStop) + "pts until weekend close"); winddownLogged = true; } if(!winddownPendingsCancelled) { CancelAllOrders("Wind-down — no new risk"); winddownPendingsCancelled = true; if(gridPlaced) { gridPlaced = false; SaveGridState(); } } return; } if(winddownLogged || winddownPendingsCancelled) { winddownLogged = false; winddownPendingsCancelled = false; } // --- Update adaptive-filter state each bar --- UpdateFilterRelaxation(); // --- Session + spread filters --- if(!CheckSessionFilter()) return; if(!CheckSpreadFilter()) return; // --- BO: only trade when trending; cancel everything when market returns to range --- if(IsRangingMarket()) { if(gridPlaced) { PrintS("Market returned to range — cancelling BO orders"); CancelAllOrders("Range detected"); gridPlaced = false; SaveGridState(); } // --- BO edge cleanup: close filled BO positions that reversed back into range --- double actualHigh = (InpManualHigh > 0) ? InpManualHigh : GridHigh; double actualLow = (InpManualLow > 0) ? InpManualLow : GridLow; double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(actualHigh > 0) ClosePositionsBySide(POSITION_TYPE_BUY, "BO edge cleanup — reversal below GridHigh"); if(actualLow > 0) ClosePositionsBySide(POSITION_TYPE_SELL, "BO edge cleanup — reversal above GridLow"); return; } // --- Range drift (4.6) — re-center if price drifted --- if(gridPlaced && CheckRangeDrift()) { CancelAllOrders("Range drift"); gridPlaced = false; // will replace below } // --- Grid already placed: monitor fills & decide when to reset --- if(gridPlaced) { int buyLimits = CountPendingOrders(ORDER_TYPE_BUY_STOP); int sellLimits = CountPendingOrders(ORDER_TYPE_SELL_STOP); int buyPos = 0, sellPos = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue; if(PositionGetInteger(POSITION_MAGIC) != MagicNum) continue; if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) buyPos++; if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) sellPos++; } // Cancel remaining BO stops once a direction fills (one-fill-per-side, avoid pyramiding) if(buyPos > 0 && buyLimits > 0) { for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(ticket == 0) continue; if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue; if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue; if(OrderGetInteger(ORDER_TYPE) == ORDER_TYPE_BUY_STOP) trade.OrderDelete(ticket); } } if(sellPos > 0 && sellLimits > 0) { for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(ticket == 0) continue; if(OrderGetString(ORDER_SYMBOL) != _Symbol) continue; if(OrderGetInteger(ORDER_MAGIC) != MagicNum) continue; if(OrderGetInteger(ORDER_TYPE) == ORDER_TYPE_SELL_STOP) trade.OrderDelete(ticket); } } // Cycle closed? if(buyLimits == 0 && sellLimits == 0 && buyPos == 0 && sellPos == 0) { if(InpStopAfterProfit && AccountInfoDouble(ACCOUNT_EQUITY) > cycleStartEquity) { PrintS("Profitable cycle — stop for today"); cycleProfitStop = true; } EmitCycleReport(false); gridPlaced = false; cycleStartEquity = 0; cycleStartTime = 0; cyclePartialTPCount = 0; SaveGridState(); } return; } // --- Block new grids if profit-stop active --- if(cycleProfitStop) { if(DiagnosticModeOn) PrintS("Profit-stop active — no new grid today"); return; } // --- Trade enable checks --- if(OpenNewTrades == "N" || OpenNewTrades == "n") return; if(!bEnableLongs && !bEnableShorts) return; // --- Correlation cap (4.3) --- bool canLong = bEnableLongs && CheckCorrelationCap(true); bool canShort = bEnableShorts && CheckCorrelationCap(false); if(!canLong && !canShort) return; // --- Resolve grid geometry --- double actualHigh = (InpManualHigh > 0) ? InpManualHigh : GridHigh; double actualLow = (InpManualLow > 0) ? InpManualLow : GridLow; if(actualHigh <= actualLow) { PrintS("Invalid grid bounds — skip"); return; } currentEntryPts = ResolveEntryPoints(); double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); double entryPrice = currentEntryPts * point; double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); // --- Derive effective levels from actual range (3.2) --- double span = actualHigh - actualLow; int derivedLevels = (entryPrice > 0) ? (int)(span / entryPrice) : MaxLevels; int effectiveLevels = MathMin(MaxLevels, MathMax(1, derivedLevels)); PrintS("Placing BO orders — px=" + DoubleToString(currentPrice, _Digits) + " [" + DoubleToString(actualLow, _Digits) + "-" + DoubleToString(actualHigh, _Digits) + "]" + " entry=" + DoubleToString(currentEntryPts, 0) + "pts" + " levels=" + IntegerToString(effectiveLevels) + (filtersRelaxed ? " (relaxed)" : "")); int buyCount = 0, sellCount = 0; double noTradeZone = entryPrice * 2; for(int i = 0; i < effectiveLevels; i++) { // BO: buy above resistance, sell below support double buyLevel = actualHigh + ((i + 1) * entryPrice); double sellLevel = actualLow - ((i + 1) * entryPrice); if(canLong && buyLevel > currentPrice + noTradeZone) if(PlaceBuyStop(buyLevel, i)) buyCount++; if(canShort && sellLevel < currentPrice - noTradeZone) if(PlaceSellStop(sellLevel, i)) sellCount++; } PrintS("BO orders placed: " + IntegerToString(buyCount) + " buy, " + IntegerToString(sellCount) + " sell stops"); if(buyCount > 0 || sellCount > 0) { gridPlaced = true; cycleStartEquity = AccountInfoDouble(ACCOUNT_EQUITY); cycleStartTime = TimeCurrent(); cyclePartialTPCount = 0; SaveGridState(); } } //+------------------------------------------------------------------+