//+------------------------------------------------------------------+
//|                               Automated Breakout From Logger.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                     https://www.mql5.com/en/users/johnhlomohang/ |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com/en/users/johnhlomohang/"
#property version   "1.00"

#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_RANGE_MODE
  {
   RANGE_SESSION      = 0,  // Session breakout
   RANGE_CUSTOM_TIME  = 1   // Time-based breakout
  };

enum ENUM_LOG_SESSION
  {
   SESSION_SYDNEY     = 0,  // Sydney
   SESSION_TOKYO      = 1,  // Tokyo (Asian range)
   SESSION_LONDON     = 2,  // London
   SESSION_NEWYORK    = 3   // New York
  };

enum ENUM_SL_MODE
  {
   SL_WIDE            = 0,  // Wide boundary stop (evidence base case)
   SL_AGGRESSIVE      = 1   // Aggressive stop (documented alternative)
  };

enum ENUM_TIME_POLICY
  {
   TIME_OFF           = 0,  // Ignore breakout timing entirely
   TIME_WEIGHT        = 1,  // Timing scales position size only (soft)
   TIME_GATE          = 2   // Timing blocks the late zone (hard)
  };

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "=== Range definition (must match the logger) ==="
input ENUM_TIMEFRAMES  InpTF              = PERIOD_M15;     // Working timeframe
input ENUM_RANGE_MODE  InpRangeMode       = RANGE_SESSION;  // Range mode
input ENUM_LOG_SESSION InpSession         = SESSION_TOKYO;  // Session
input string           InpTimeStart       = "02:00";        // Start HH:MM (time mode)
input string           InpTimeEnd         = "10:00";        // End HH:MM (time mode)
input int              InpServerOffsetHr  = 0;              // Hours to shift session presets
input int              InpMinRangeBars    = 8;              // Minimum bars for a valid range
input int              InpATRPeriod       = 14;             // ATR period

input group "=== Entry gates (Stage 10, unchanged by stress test) ==="
input double           InpDistPrevMaxPts  = 50.0;           // Gate: distance to prior-day level <= this
input double           InpDistPrevExclPts = 400.0;          // Hard exclusion: distance > this
input double           InpBodyRatioMin    = 0.80;           // Gate: breakout candle body ratio >=
input double           InpBufferATR       = 0.05;           // Breakout buffer beyond edge (x ATR)
input int              InpBreakWindowBar  = 48;             // Bars after range to hunt breakouts
input int              InpMaxTradesPerDay = 1;              // Max entries per range

input group "=== Experimental penetration filter (off; not a Phase 10 rule) ==="
input bool             InpUseDistATR      = false;          // Enable experimental filter (off in all reported tests)
input double           InpDistATRConfirm  = 0.50;           // Size upgrade x1.20 at or above (x ATR)
input double           InpDistATRVeto     = 0.25;           // HARD reject at or below (x ATR)

input group "=== Timing preference (Phase 9, soft by default) ==="
input ENUM_TIME_POLICY InpTimePolicy      = TIME_WEIGHT;    // How breakout timing is applied
input int              InpAnchorBarsFrom  = 3;              // Anchor zone start (bars after range)
input int              InpAnchorBarsTo    = 6;              // Anchor zone end
input double           InpMultImmediate   = 0.60;           // Size multiplier, <= 2 bars
input double           InpMultAnchor      = 1.00;           // Size multiplier, anchor zone
input double           InpMultContested   = 0.40;           // Size multiplier, 7-14 bars
input double           InpMultLate        = 0.60;           // Size multiplier, 15+ bars

input group "=== Stop loss ==="
input ENUM_SL_MODE     InpSLMode          = SL_WIDE;        // Stop loss mode
input double           InpSL_R_Up         = 0.60;           // UP stop, in R below the boundary
input double           InpSL_R_Down       = 1.50;           // DOWN stop, in R above the boundary
input double           InpSL_R_Aggressive = 0.50;           // Aggressive mode stop, in R
input double           InpSLFloorATR      = 0.50;           // Minimum stop distance (x ATR)
input double           InpSLSpreadMult    = 3.0;            // Minimum stop distance (x spread)

input group "=== Take profit and exits ==="
input double           InpTP_R_Up         = 0.85;           // UP target, in R from breakout price
input double           InpTP_R_Down       = 0.30;           // DOWN target, in R from breakout price
input double           InpDeadManR        = 0.25;           // Dead-man: close back inside by this R
input int              InpTrailStartBars  = 16;             // Start trailing after this many bars
input double           InpTrailATR        = 1.50;           // Trail distance (x ATR)
input int              InpMaxBarsInTrade  = 32;             // Hard exit after this many bars
input string           InpSessionEndExit  = "21:45";        // Hard session-end close HH:MM

input group "=== Risk and execution ==="
input double           InpRiskPercent     = 0.50;           // Risk per trade (% of balance)
input double           InpDownRiskMult    = 0.60;           // DOWN size multiplier (risk asymmetry)
input double           InpFixedLot        = 0.0;            // Fixed lot (0 = use risk %)
input bool             InpSkipBelowMinLot = false;          // Skip if risk lot < broker minimum (false = use minimum and log)
input int              InpSlippage        = 20;             // Deviation in points
input long             InpMagic           = 20260924;       // Magic number
input bool             InpVerbose         = true;           // Print decisions to the journal

//+------------------------------------------------------------------+
//| Structures                                                       |
//+------------------------------------------------------------------+
struct RangeInfo
  {
   bool     valid;
   datetime startTime;
   datetime endTime;
   double   high;
   double   low;
   double   size;
   int      bars;
   double   atr;
   double   prevDayHigh;
   double   prevDayLow;
   int      barsSinceEnd;
   int      trades;
   bool     brokeUp;
   bool     brokeDown;
  };

struct PendingEntry
  {
   bool     active;
   int      dir;          // +1 up, -1 down
   double   brkPrice;     // close of the breakout bar (research anchor)
   double   boundary;     // range edge that was broken
   double   rUnit;        // range width in price
   double   atr;
   double   sl;
   double   tp;
   double   sizeMult;
   int      barsAfterRange;
  };

struct TradeState
  {
   bool     active;
   int      dir;
   double   boundary;
   double   rUnit;
   double   atr;
   double   brkPrice;
   int      barsInTrade;
   datetime openBar;
  };

//+------------------------------------------------------------------+
//| Globals                                                          |
//+------------------------------------------------------------------+
CTrade        g_trade;
CPositionInfo g_pos;

RangeInfo    g_range;
PendingEntry g_pending;
TradeState   g_state;

int      g_atrHandle   = INVALID_HANDLE;
datetime g_lastBarTime = 0;
bool     g_prevInWin   = false;
int      g_winStartMin = 0;
int      g_winEndMin   = 0;
int      g_exitMin     = 0;
string   g_winName     = "";

//--- run statistics
int    g_entries = 0, g_wins = 0, g_losses = 0;
int    g_exitBroker = 0, g_exitDeadMan = 0, g_exitTime = 0, g_exitSession = 0;
int    g_rejDist = 0, g_rejExcl = 0, g_rejBody = 0, g_rejConfirm = 0, g_rejTime = 0;
int    g_rejTP = 0, g_minLotRaised = 0, g_minLotSkipped = 0;

//+------------------------------------------------------------------+
//| Small helpers                                                    |
//+------------------------------------------------------------------+
double Pts(const double priceDiff)
  {
   return (_Point > 0.0) ? priceDiff / _Point : 0.0;
  }

string TS(const datetime t)
  {
   return TimeToString(t, TIME_DATE | TIME_MINUTES);
  }

int MinuteOfDay(const datetime t)
  {
   MqlDateTime dt;
   TimeToStruct(t, dt);
   return dt.hour * 60 + dt.min;
  }

int WrapMin(const int m)
  {
   return ((m % 1440) + 1440) % 1440;
  }

int ParseHHMM(const string s)
  {
   string parts[];
   if(StringSplit(s, ':', parts) != 2)
      return -1;
   int h = (int)StringToInteger(parts[0]);
   int m = (int)StringToInteger(parts[1]);
   if(h < 0 || h > 23 || m < 0 || m > 59)
      return -1;
   return h * 60 + m;
  }

bool InWindow(const int m)
  {
   if(g_winStartMin == g_winEndMin)
      return false;
   if(g_winStartMin < g_winEndMin)
      return (m >= g_winStartMin && m < g_winEndMin);
   return (m >= g_winStartMin || m < g_winEndMin);
  }

bool GetBar(const int shift, MqlRates &r)
  {
   MqlRates tmp[];
   if(CopyRates(_Symbol, InpTF, shift, 1, tmp) != 1)
      return false;
   r = tmp[0];
   return true;
  }

double AtrAt(const int shift)
  {
   double buf[];
   if(CopyBuffer(g_atrHandle, 0, shift, 1, buf) != 1)
      return 0.0;
   return buf[0];
  }

bool HasPosition()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
      if(g_pos.SelectByIndex(i))
         if(g_pos.Symbol() == _Symbol && g_pos.Magic() == InpMagic)
            return true;
   return false;
  }

bool SelectOurPosition()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
      if(g_pos.SelectByIndex(i))
         if(g_pos.Symbol() == _Symbol && g_pos.Magic() == InpMagic)
            return true;
   return false;
  }

//+------------------------------------------------------------------+
//| Window resolution                                                |
//+------------------------------------------------------------------+
bool ResolveWindow()
  {
   if(InpRangeMode == RANGE_CUSTOM_TIME)
     {
      int s = ParseHHMM(InpTimeStart);
      int e = ParseHHMM(InpTimeEnd);
      if(s < 0 || e < 0 || s == e)
        {
         Print("Invalid time window. Use HH:MM and make start differ from end.");
         return false;
        }
      g_winStartMin = s;
      g_winEndMin   = e;
      g_winName     = "CUSTOM";
      return true;
     }

   int sh = 0, eh = 0;
   switch(InpSession)
     {
      case SESSION_SYDNEY:  sh = 23; eh =  6; g_winName = "SYDNEY";  break;
      case SESSION_TOKYO:   sh =  2; eh = 10; g_winName = "TOKYO";   break;
      case SESSION_LONDON:  sh = 10; eh = 15; g_winName = "LONDON";  break;
      case SESSION_NEWYORK: sh = 15; eh = 21; g_winName = "NEWYORK"; break;
     }
   g_winStartMin = WrapMin((sh + InpServerOffsetHr) * 60);
   g_winEndMin   = WrapMin((eh + InpServerOffsetHr) * 60);
   return true;
  }

//+------------------------------------------------------------------+
//| Range lifecycle                                                  |
//+------------------------------------------------------------------+
void StartRange(const MqlRates &b)
  {
   ZeroMemory(g_range);
   g_range.startTime = b.time;
   g_range.endTime   = b.time;
   g_range.high      = b.high;
   g_range.low       = b.low;
   g_range.bars      = 1;
  }

void UpdateRange(const MqlRates &b)
  {
   if(b.high > g_range.high)
      g_range.high = b.high;
   if(b.low  < g_range.low)
      g_range.low  = b.low;
   g_range.endTime = b.time;
   g_range.bars++;
  }

void FreezeRange(const int shift)
  {
   g_range.size = g_range.high - g_range.low;
   g_range.atr  = AtrAt(shift);

   MqlRates d1[];
   if(CopyRates(_Symbol, PERIOD_D1, 1, 1, d1) == 1)
     {
      g_range.prevDayHigh = d1[0].high;
      g_range.prevDayLow  = d1[0].low;
     }

   g_range.barsSinceEnd = 0;
   g_range.trades       = 0;
   g_range.brokeUp      = false;
   g_range.brokeDown    = false;

   g_range.valid = (g_range.bars >= InpMinRangeBars &&
                    g_range.size > 0.0 &&
                    g_range.atr  > 0.0 &&
                    g_range.prevDayHigh > g_range.prevDayLow);

   if(g_range.valid && InpVerbose)
      PrintFormat("Range %s -> %s | high %s low %s | width %.1f pts | %.2f ATR",
                  TS(g_range.startTime), TS(g_range.endTime),
                  DoubleToString(g_range.high, _Digits),
                  DoubleToString(g_range.low, _Digits),
                  Pts(g_range.size), g_range.size / g_range.atr);
  }

//+------------------------------------------------------------------+
//| Timing preference — Phase 9 evidence as a size multiplier        |
//+------------------------------------------------------------------+
double TimingMultiplier(const int barsAfter, bool &blocked)
  {
   blocked = false;
   if(InpTimePolicy == TIME_OFF)
      return 1.0;

   bool contested = (barsAfter >= 7 && barsAfter <= 14);

   if(InpTimePolicy == TIME_GATE && contested)
     {
      blocked = true;
      return 0.0;
     }

   if(barsAfter <= 2)
      return InpMultImmediate;
   if(barsAfter >= InpAnchorBarsFrom && barsAfter <= InpAnchorBarsTo)
      return InpMultAnchor;
   if(contested)
      return InpMultContested;
   return InpMultLate;
  }

//+------------------------------------------------------------------+
//| Entry evaluation — every gate traces to a Part 1 finding         |
//+------------------------------------------------------------------+
bool EvaluateEntry(const int dir, const MqlRates &b, PendingEntry &out)
  {
   ZeroMemory(out);

   double boundary = (dir > 0) ? g_range.high : g_range.low;

   //--- Gate 1: location versus prior-day structure, measured at the range edge.
   //    Positive = headroom still exists before the prior-day level.
   double distPrevPts = (dir > 0)
                        ? Pts(g_range.prevDayHigh - g_range.high)
                        : Pts(g_range.low - g_range.prevDayLow);

   if(distPrevPts > InpDistPrevExclPts)
     {
      g_rejExcl++;
      if(InpVerbose)
         PrintFormat("Reject %s at %s: distance %.0f pts exceeds exclusion zone",
                     (dir > 0 ? "UP" : "DOWN"), TS(b.time), distPrevPts);
      return false;
     }

   if(distPrevPts > InpDistPrevMaxPts)
     {
      g_rejDist++;
      if(InpVerbose)
         PrintFormat("Reject %s at %s: distance %.0f pts outside entry zone (<= %.0f)",
                     (dir > 0 ? "UP" : "DOWN"), TS(b.time), distPrevPts, InpDistPrevMaxPts);
      return false;
     }

   //--- Gate 2: breakout candle decisiveness. Body, not size.
   double candle = b.high - b.low;
   if(candle <= 0.0)
      return false;
   double bodyRatio = MathAbs(b.close - b.open) / candle;

   if(bodyRatio < InpBodyRatioMin)
     {
      g_rejBody++;
      if(InpVerbose)
         PrintFormat("Reject %s at %s: body ratio %.2f below %.2f",
                     (dir > 0 ? "UP" : "DOWN"), TS(b.time), bodyRatio, InpBodyRatioMin);
      return false;
     }

   //--- Experimental hard filter: penetration depth. Off in every reported test.
   //    The veto is a hard reject, not the Phase 10 preference weight.
   double brkDist    = (dir > 0) ? (b.close - g_range.high) : (g_range.low - b.close);
   double brkDistATR = brkDist / g_range.atr;

   if(InpUseDistATR && brkDistATR <= InpDistATRVeto)
     {
      g_rejConfirm++;
      if(InpVerbose)
         PrintFormat("Reject %s at %s: penetration %.2f ATR at or below veto level",
                     (dir > 0 ? "UP" : "DOWN"), TS(b.time), brkDistATR);
      return false;
     }

   //--- Timing preference
   bool blocked = false;
   double timeMult = TimingMultiplier(g_range.barsSinceEnd, blocked);
   if(blocked)
     {
      g_rejTime++;
      if(InpVerbose)
         PrintFormat("Reject %s at %s: contested timing zone (%d bars after range)",
                     (dir > 0 ? "UP" : "DOWN"), TS(b.time), g_range.barsSinceEnd);
      return false;
     }

   //--- Size multipliers: timing preference, direction risk asymmetry,
   //    and the optional confirmation as an upgrade rather than a gate.
   double sizeMult = timeMult;
   if(dir < 0)
      sizeMult *= InpDownRiskMult;
   if(InpUseDistATR && brkDistATR >= InpDistATRConfirm)
      sizeMult *= 1.20;

   //--- Stop loss: anchored on the boundary, wide enough to survive a holding retest.
   double slR = (InpSLMode == SL_AGGRESSIVE)
                ? InpSL_R_Aggressive
                : ((dir > 0) ? InpSL_R_Up : InpSL_R_Down);

   double slDist = slR * g_range.size;

   //--- Floors: volatility floor and spread floor. Execution safety, not evidence.
   double spreadPrice = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * _Point;
   double floorATR    = InpSLFloorATR * g_range.atr;
   double floorSpread = InpSLSpreadMult * spreadPrice;
   double stopsLevel  = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point;

   slDist = MathMax(slDist, floorATR);
   slDist = MathMax(slDist, floorSpread);
   slDist = MathMax(slDist, stopsLevel * 1.5);

   //--- Take profit: anchored on the breakout price, direction-specific R multiple.
   double tpR    = (dir > 0) ? InpTP_R_Up : InpTP_R_Down;
   double tpDist = tpR * g_range.size;
   tpDist = MathMax(tpDist, stopsLevel * 1.5);

   out.active         = true;
   out.dir            = dir;
   out.brkPrice       = b.close;
   out.boundary       = boundary;
   out.rUnit          = g_range.size;
   out.atr            = g_range.atr;
   out.sizeMult       = sizeMult;
   out.barsAfterRange = g_range.barsSinceEnd;
   out.sl             = (dir > 0) ? (boundary - slDist) : (boundary + slDist);
   out.tp             = (dir > 0) ? (b.close + tpDist)  : (b.close - tpDist);

   if(InpVerbose)
      PrintFormat("Accept %s at %s | dist %.0f pts | body %.2f | pen %.2f ATR | %d bars after | size x%.2f",
                  (dir > 0 ? "UP" : "DOWN"), TS(b.time),
                  distPrevPts, bodyRatio, brkDistATR, g_range.barsSinceEnd, sizeMult);

   return true;
  }

//+------------------------------------------------------------------+
//| Position sizing                                                  |
//+------------------------------------------------------------------+
double CalcLot(const double slDistance, const double sizeMult)
  {
   double minLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);

   if(InpFixedLot > 0.0)
      return MathMin(MathMax(InpFixedLot, minLot), maxLot);

   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
   double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   if(tickValue <= 0.0 || tickSize <= 0.0 || slDistance <= 0.0)
      return minLot;

   double riskMoney  = AccountInfoDouble(ACCOUNT_BALANCE) * (InpRiskPercent / 100.0) * sizeMult;
   double lossPerLot = (slDistance / tickSize) * tickValue;
   if(lossPerLot <= 0.0)
      return minLot;

   double rawLot = riskMoney / lossPerLot;
   double lot    = MathFloor(rawLot / lotStep) * lotStep;

   //--- Broker minimum: on wide ranges or small multipliers the risk-based
   //    lot can fall below it, and the minimum then risks more than planned.
   if(lot < minLot)
     {
      double plannedPct = InpRiskPercent * sizeMult;
      double actualPct  = 100.0 * minLot * lossPerLot / AccountInfoDouble(ACCOUNT_BALANCE);
      if(InpSkipBelowMinLot)
        {
         g_minLotSkipped++;
         if(InpVerbose)
            PrintFormat("Skip: risk lot %.4f below minimum %.2f (min lot would risk %.2f%% vs %.2f%% planned)",
                        rawLot, minLot, actualPct, plannedPct);
         return 0.0;
        }
      g_minLotRaised++;
      if(InpVerbose)
         PrintFormat("Min lot used: risk lot %.4f raised to %.2f | actual risk %.2f%% vs %.2f%% planned",
                     rawLot, minLot, actualPct, plannedPct);
      lot = minLot;
     }
   return MathMin(lot, maxLot);
  }

//+------------------------------------------------------------------+
//| Order execution — placed on the bar AFTER the breakout closes    |
//+------------------------------------------------------------------+
void ExecutePending()
  {
   if(!g_pending.active)
      return;

   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double price = (g_pending.dir > 0) ? ask : bid;

   //--- Sanity: the market must not already be at or beyond the stop.
   bool invalid = (g_pending.dir > 0) ? (price <= g_pending.sl) : (price >= g_pending.sl);
   if(invalid)
     {
      if(InpVerbose)
         Print("Pending entry abandoned: price already beyond the planned stop.");
      g_pending.active = false;
      return;
     }

   //--- Sanity: the target must still lie ahead of the market, beyond the
   //    broker's minimum stop distance. Otherwise the signal counts as missed.
   double stopsLevel = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point;
   bool tpInvalid = (g_pending.dir > 0) ? (g_pending.tp <= price + stopsLevel)
                                        : (g_pending.tp >= price - stopsLevel);
   if(tpInvalid)
     {
      g_rejTP++;
      if(InpVerbose)
         Print("Pending entry abandoned: price already at or through the planned target.");
      g_pending.active = false;
      return;
     }

   double slDistance = MathAbs(price - g_pending.sl);
   double lot = CalcLot(slDistance, g_pending.sizeMult);
   if(lot <= 0.0)
     {
      g_pending.active = false;
      return;
     }

   bool ok = (g_pending.dir > 0)
             ? g_trade.Buy(lot, _Symbol, 0.0, g_pending.sl, g_pending.tp, "RB-Evidence")
             : g_trade.Sell(lot, _Symbol, 0.0, g_pending.sl, g_pending.tp, "RB-Evidence");

   if(ok)
     {
      g_entries++;
      g_range.trades++;

      ZeroMemory(g_state);
      g_state.active      = true;
      g_state.dir         = g_pending.dir;
      g_state.boundary    = g_pending.boundary;
      g_state.rUnit       = g_pending.rUnit;
      g_state.atr         = g_pending.atr;
      g_state.brkPrice    = g_pending.brkPrice;
      g_state.barsInTrade = 0;
      g_state.openBar     = iTime(_Symbol, InpTF, 0);

      if(InpVerbose)
         PrintFormat("ENTRY %s %.2f lots at %s | SL %s | TP %s",
                     (g_pending.dir > 0 ? "BUY" : "SELL"), lot,
                     DoubleToString(price, _Digits),
                     DoubleToString(g_pending.sl, _Digits),
                     DoubleToString(g_pending.tp, _Digits));
     }
   else
      PrintFormat("Order failed: retcode %d (%s)", g_trade.ResultRetcode(),
                  g_trade.ResultRetcodeDescription());

   g_pending.active = false;
  }

//+------------------------------------------------------------------+
//| Position management                                              |
//+------------------------------------------------------------------+
void CloseOurPosition(const string reason)
  {
   if(!SelectOurPosition())
      return;

   double profit = g_pos.Profit() + g_pos.Swap() + g_pos.Commission();
   if(g_trade.PositionClose(_Symbol, InpSlippage))
     {
      if(profit >= 0.0)
         g_wins++;
      else
         g_losses++;

      if(InpVerbose)
         PrintFormat("EXIT (%s) | result %.2f", reason, profit);
     }
   g_state.active = false;
  }

void ManagePosition(const MqlRates &b)
  {
   if(!SelectOurPosition())
     {
      //--- position closed by SL or TP since the last bar
      if(g_state.active)
        {
         g_state.active = false;
         g_exitBroker++;  // SL or TP; the two are not distinguished here
        }
      return;
     }

   if(!g_state.active)
      return;

   g_state.barsInTrade++;
   int k = g_state.barsInTrade;

   //--- 1. Dead-man exit: the FALSE signature is a close back inside the range.
   double insideLevel = (g_state.dir > 0)
                        ? g_state.boundary - InpDeadManR * g_state.rUnit
                        : g_state.boundary + InpDeadManR * g_state.rUnit;

   bool backInside = (g_state.dir > 0) ? (b.close < insideLevel) : (b.close > insideLevel);
   if(backInside)
     {
      g_exitDeadMan++;
      CloseOurPosition("dead-man: closed back inside range");
      return;
     }

   //--- 2. Session-end close. The live equivalent of the dataset's censoring.
   int mod = MinuteOfDay(b.time);
   if(g_exitMin >= 0 && mod >= g_exitMin)
     {
      g_exitSession++;
      CloseOurPosition("session end");
      return;
     }

   //--- 3. Hard bar limit.
   if(k >= InpMaxBarsInTrade)
     {
      g_exitTime++;
      CloseOurPosition("max bars in trade");
      return;
     }

   //--- 4. Trail only after winner latency has elapsed (Phase 3: winners need ~14 bars).
   if(k >= InpTrailStartBars)
     {
      double trailDist = InpTrailATR * g_state.atr;
      double curSL     = g_pos.StopLoss();
      double curTP     = g_pos.TakeProfit();
      double newSL     = (g_state.dir > 0) ? (b.close - trailDist) : (b.close + trailDist);

      bool improve = (g_state.dir > 0) ? (newSL > curSL) : (newSL < curSL);

      //--- never trail past the current market, and respect the broker stop distance
      double stopsLevel = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point;
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      bool   safe = (g_state.dir > 0) ? (newSL < bid - stopsLevel) : (newSL > ask + stopsLevel);

      if(improve && safe)
        {
         if(g_trade.PositionModify(_Symbol, NormalizeDouble(newSL, _Digits), curTP))
            if(InpVerbose)
               PrintFormat("Trail | bar %d | SL -> %s", k, DoubleToString(newSL, _Digits));
        }
     }
  }

//+------------------------------------------------------------------+
//| Per-bar logic                                                    |
//+------------------------------------------------------------------+
void ProcessBar(const MqlRates &b, const int shift)
  {
   //--- 1. Manage what is already open, using the bar that just closed.
   ManagePosition(b);

   //--- 2. Fire any entry decided on the previous bar's close.
   if(g_pending.active && !HasPosition())
      ExecutePending();
   else
      g_pending.active = false;

   //--- 3. Window edge detection.
   int  mod   = MinuteOfDay(b.time);
   bool inWin = InWindow(mod);

   if(inWin && !g_prevInWin)
     {
      StartRange(b);
      g_prevInWin = true;
      return;
     }

   if(inWin && g_prevInWin)
     {
      UpdateRange(b);
      g_prevInWin = true;
      return;
     }

   if(!inWin && g_prevInWin)
      FreezeRange(shift);

   g_prevInWin = false;

   if(!g_range.valid)
      return;

   g_range.barsSinceEnd++;

   //--- 4. Hunt for a breakout worth taking.
   if(g_range.barsSinceEnd > InpBreakWindowBar)
      return;
   if(g_range.trades >= InpMaxTradesPerDay)
      return;
   if(HasPosition() || g_pending.active)
      return;

   double buffer  = InpBufferATR * g_range.atr;
   double upLevel = g_range.high + buffer;
   double dnLevel = g_range.low  - buffer;

   int dir = 0;
   if(!g_range.brokeUp && b.close > upLevel)
      dir = 1;
   else if(!g_range.brokeDown && b.close < dnLevel)
      dir = -1;

   if(dir == 0)
      return;

   if(dir > 0)
      g_range.brokeUp = true;
   else
      g_range.brokeDown = true;

   PendingEntry pe;
   if(EvaluateEntry(dir, b, pe))
      g_pending = pe;
  }

//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(!ResolveWindow())
      return INIT_PARAMETERS_INCORRECT;

   g_exitMin = ParseHHMM(InpSessionEndExit);

   if(InpAnchorBarsFrom > InpAnchorBarsTo || InpTrailStartBars > InpMaxBarsInTrade)
     {
      Print("Check the anchor zone and the trail/max-bar relationship.");
      return INIT_PARAMETERS_INCORRECT;
     }

   g_atrHandle = iATR(_Symbol, InpTF, InpATRPeriod);
   if(g_atrHandle == INVALID_HANDLE)
     {
      Print("Failed to create the ATR handle.");
      return INIT_FAILED;
     }

   g_trade.SetExpertMagicNumber(InpMagic);
   g_trade.SetDeviationInPoints(InpSlippage);
   g_trade.SetTypeFillingBySymbol(_Symbol);
   g_trade.LogLevel(LOG_LEVEL_ERRORS);

   ZeroMemory(g_range);
   ZeroMemory(g_pending);
   ZeroMemory(g_state);

   g_lastBarTime = iTime(_Symbol, InpTF, 0);
   g_prevInWin   = false;

   PrintFormat("RangeBreakoutEvidence started | %s window %02d:%02d-%02d:%02d | gates: dist<=%.0f pts, body>=%.2f",
               g_winName, g_winStartMin / 60, g_winStartMin % 60,
               g_winEndMin / 60, g_winEndMin % 60,
               InpDistPrevMaxPts, InpBodyRatioMin);

   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Deinitialization                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(g_atrHandle != INVALID_HANDLE)
      IndicatorRelease(g_atrHandle);

   Print("---------------- Range Breakout EA summary ----------------");
   PrintFormat("Entries: %d | closed by us: wins %d, losses %d", g_entries, g_wins, g_losses);
   PrintFormat("Exits   | dead-man %d | session end %d | bar limit %d | SL-TP combined %d",
               g_exitDeadMan, g_exitSession, g_exitTime, g_exitBroker);
   PrintFormat("Rejects | distance %d | exclusion %d | body %d | confirm %d | timing %d",
               g_rejDist, g_rejExcl, g_rejBody, g_rejConfirm, g_rejTime);
   PrintFormat("Execution | target already reached %d | min lot raised %d | min lot skipped %d",
               g_rejTP, g_minLotRaised, g_minLotSkipped);
   Print("-----------------------------------------------------------");
  }

//+------------------------------------------------------------------+
//| Tick handler — all decisions happen on closed bars               |
//+------------------------------------------------------------------+
void OnTick()
  {
   datetime t = iTime(_Symbol, InpTF, 0);
   if(t == 0 || t == g_lastBarTime)
      return;
   g_lastBarTime = t;

   MqlRates b;
   if(!GetBar(1, b))
      return;

   ProcessBar(b, 1);
  }
//+------------------------------------------------------------------+