Files
mql-trading-bots/OrdersEA_Smart_Grid_BO.mq5
T
garfield 90f97606ea v5.8: self-heal gridPlaced from real broker state at OnInit
Fixes the duplicate-grid bug found 2026-08-11: gridPlaced (and the
rest of SaveGridState's GlobalVariable-backed state) never reaches
disk in this environment -- confirmed no .gvr file exists anywhere
under the MT5 data dir. Every full container restart mid-cycle wiped
it back to false while a grid was still resting live on the broker,
and OnTick would place a brand new duplicate grid on top of the old,
uncancelled one. Found 189 stacked pending orders account-wide before
cleanup (CancelAllPendingNow.mq5), one symbol alone had 29 spanning
24+ hours of normal cycles -- this has likely been happening on every
watchdog restart for weeks, not just from that day's testing.

Adds HasLiveGridPresence(): checks actual OrdersTotal()/PositionsTotal()
for this symbol+magic. At OnInit, if gridPlaced reads false but real
orders or positions already exist, trust reality over the flag and
recover gridPlaced=true -- so a restart mid-cycle can no longer cause
a duplicate placement, regardless of whether GlobalVariable persistence
ever gets fixed underneath. Deployed and recompiled live (all 14 EAs
confirmed reinitialized as v5.8); not yet validated against a live
container-restart cycle given the risk of disrupting the account
further right after cleanup -- logic mirrors the existing broker-state
query pattern already used in OnTick's monitor branch.
2026-08-11 15:44:41 -04:00

1747 lines
66 KiB
Plaintext
Executable File

//+------------------------------------------------------------------+
//| 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 <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>
#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();
}
}
//+------------------------------------------------------------------+