//+------------------------------------------------------------------+
//|                                     SimplePullback_WIN_GlobalOpt |
//|                                                  Samuel Ferreira |
//|                                        samuelsf2014@yahoo.com.br |
//+------------------------------------------------------------------+
#property copyright "Samuel Ferreira"
#property link      "samuelsf2014@yahoo.com.br"
#property version   "1.21"
#property strict

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input string optimizationTag = "SIMPLEPULLBACK_WIN_INITIAL_WFE";
input bool   saveOptimizationPasses = true;
input string csvExportPrefix = "SimplePullback_WIN_GlobalOpt";
input int    dummyOptimizationPass = 0;

enum ENUM_CORE_TIMEFRAME_INDEX
  {
   CORE_TF_M1  = 0,                                                       // 1-minute timeframe.
   CORE_TF_M2  = 1,                                                       // 2-minute timeframe.
   CORE_TF_M3  = 2,                                                       // 3-minute timeframe.
   CORE_TF_M5  = 3,                                                       // 5-minute timeframe.
   CORE_TF_M10 = 4,                                                       // 10-minute timeframe.
   CORE_TF_M15 = 5                                                        // 15-minute timeframe.
  };

//==================================================================
// @group G0_EXECUTION
// @subgroup G0A_FIXED_BASICS
//==================================================================
// @fixed
input bool enableStatusLogs = false;                                      // Enables optional logs in the Experts tab.
// @fixed
input ulong  buyMagic = 41001;                                            // Magic number for buy positions.
// @fixed
input ulong  sellMagic = 41002;                                           // Magic number for sell positions.
// @fixed
input double lots = 1.0;                                                  // Fixed position size.
// @fixed
input bool   allowHedge = false;                                          // Allows hedge/external exposure when true.

// @fixed
input int minTradesOnTester = 300;
// @fixed
input int referenceTradesOnTester = 900;
// @fixed
input double pfMinOnTester = 1.04;
// @fixed
input double ddRelMaxOnTester = 30.0;

//==================================================================
// @group G3_RISK
// @subgroup G3A_INITIAL_STOP
//==================================================================
// @wfe
// @opt 40 10 120
input int stopLossTicks = 110;                                            // Initial stop in symbol ticks; 0 disables initial SL.

//==================================================================
// @group G4_MANAGEMENT
// @subgroup G4A_FIXED_TAKE_PROFIT
//==================================================================
// @fixed
input bool   useFixedTakeProfit = false;                                  // Enables optional fixed take profit in R.
// @fixed
input double fixedTakeProfitRR = 2.00;                                    // Fixed TP multiple; 2.00 means 2R.

//==================================================================
// @group G4_MANAGEMENT
// @subgroup G4B_BREAKEVEN
//==================================================================
// @wfe1
// @opt false true
input bool   useBreakEven = false;                                        // Enables optional break-even protection.
// @wfe2
// @opt 0.80 0.20 1.80
input double breakEvenActivationRR = 1.40;                                // Profit in R required to activate break-even.
// @wfe3
// @opt 0.00 0.10 0.40
input double breakEvenLockRR = 0.20;                                      // Profit in R locked after break-even activation.

//==================================================================
// @group G4_MANAGEMENT
// @subgroup G4C_TRAILING
//==================================================================
// @fixed
input bool   useTrailingStop = true;                                      // Enables optional trailing stop management.
// @wfe1
// @opt 1.00 0.20 2.60
input double trailingActivationRR = 1.40;                                 // Profit in R required to activate trailing.
// @wfe2
// @opt 0.80 0.20 2.60
input double trailingDistanceRR = 1.00;                                   // Trailing distance from executable price in R.

//--- The regime timeframe defines the broader directional context.
//--- The retracement timeframe detects the pullback and the return trigger.
//==================================================================
// @group G1_STRUCTURE
// @subgroup G1A_FIXED_TIMEFRAMES
//==================================================================
// @fixed
input ENUM_CORE_TIMEFRAME_INDEX regimeTimeframeIndex = CORE_TF_M15;        // Timeframe used to define direction.
// @fixed
input ENUM_CORE_TIMEFRAME_INDEX retracementTimeframeIndex = CORE_TF_M1;   // Timeframe used for pullback and trigger.

//==================================================================
// @group G1_STRUCTURE
// @subgroup G1B_CORE_EMAS
//==================================================================
// @wfe1
// @opt 50 5 105
input int regimeEMAPeriod = 85;                                           // EMA period used for the regime filter.
// @wfe2
// @opt 15 5 50
input int retracementEMAPeriod = 40;                                      // EMA period used for pullback and trigger.

//--- The ATR filter rejects weak EMA crosses that remain too close to the average.
//==================================================================
// @group G2_ATR_TRIGGER
// @subgroup G2A_EMA_ATR_DISTANCE
//==================================================================
// @fixed
input bool   useEntryDistanceFromEMAByATR = true;                         // Enables ATR distance confirmation after the EMA cross.
// @wfe1
// @opt 8 2 20
input int    entryATRPeriod = 14;                                         // ATR period used by the entry confirmation filter.
// @wfe2
// @opt 0.60 0.10 1.40
input double entryDistanceFromEMAAtr = 1.00;                              // Minimum close distance from EMA in ATR units.

//==================================================================
// @group G5_WINDOW
// @subgroup G5A_DAYTRADE
//==================================================================
// @fixed
input bool useDaytradeWindow = true;                                      // Enables the intraday trading window.
// @fixed
input int daytradeStartMinute = 540;                                      // 09:00 = 540.
// @opt 960 30 1050
input int lastEntryMinute = 1050;                                         // 17:30 = 1050.
// @fixed
input int forcedCloseMinute = 1050;                                       // 17:30 = 1050.
// @fixed
input bool forceCloseAtEndOfDay = true;                                   // Closes EA positions at the end of the day.

//+------------------------------------------------------------------+
//| State                                                            |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES g_regimeTimeframe = PERIOD_M15;                          // Converted regime timeframe.
ENUM_TIMEFRAMES g_retracementTimeframe = PERIOD_M1;                       // Converted retracement timeframe.

int g_regimeEMAHandle = INVALID_HANDLE;                                   // Regime EMA indicator handle.
int g_retracementEMAHandle = INVALID_HANDLE;                              // Retracement EMA indicator handle.
int g_retracementATRHandle = INVALID_HANDLE;                              // Retracement ATR indicator handle.

double g_regimeEMA[];                                                     // Regime EMA buffer.
double g_retracementEMA[];                                                // Retracement EMA buffer.
double g_retracementATR[];                                                // Retracement ATR buffer.

MqlRates g_regimeRates[];                                                 // Regime timeframe candles.
MqlRates g_retracementRates[];                                            // Retracement timeframe candles.
MqlTick g_tick;                                                           // Last received symbol tick.

double g_bid = 0.0;                                                       // Current Bid.
double g_ask = 0.0;                                                       // Current Ask.
double g_tickSize = 0.0;                                                  // Symbol tick size used for price normalization.

ulong g_deviation = 1;                                                    // Maximum order deviation in points.

datetime g_lastRetracementBarTime = 0;                                    // Last processed retracement candle.
datetime g_lastEntryAttemptBarTime = 0;                                   // Prevents repeated entries on the same candle.

bool g_buyRegimeActive = false;                                           // Current buy regime state.
bool g_sellRegimeActive = false;                                          // Current sell regime state.

bool g_buyRetracementActive = false;                                      // Armed buy pullback state.
bool g_sellRetracementActive = false;                                     // Armed sell pullback state.

bool g_closingBuy = false;                                                // Prevents repeated buy close attempts.
bool g_closingSell = false;                                               // Prevents repeated sell close attempts.

#define OPT_FRAME_ID 220260
int g_frameCsvHandle = INVALID_HANDLE;                                     // CSV handle used by optimizer frames.
bool g_frameCsvHeaderDone = false;                                         // Header flag for optimizer CSV export.

//+------------------------------------------------------------------+
//| Timeframe conversion                                             |
//+------------------------------------------------------------------+
//--- The optimizer works with a small integer enum.
//--- This function converts the selected enum value into a real MT5 timeframe.
//--- Keeping this layer separated makes the article easier to follow and avoids
//--- scattering timeframe conversion rules through the entry logic.
ENUM_TIMEFRAMES TimeframeFromIndex(const ENUM_CORE_TIMEFRAME_INDEX index)
  {
   switch(index)
     {
      case CORE_TF_M1:
         return PERIOD_M1;
      case CORE_TF_M2:
         return PERIOD_M2;
      case CORE_TF_M3:
         return PERIOD_M3;
      case CORE_TF_M5:
         return PERIOD_M5;
      case CORE_TF_M10:
         return PERIOD_M10;
      case CORE_TF_M15:
         return PERIOD_M15;
     }

   return PERIOD_M5;
  }

//+------------------------------------------------------------------+
//| Volume precision                                                 |
//+------------------------------------------------------------------+
//--- Brokers can use different volume steps.
//--- This helper detects how many decimal places are needed to represent
//--- the current symbol volume step before sending an order.
int VolumeDigits()
  {
   double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   int digits = 0;

   while(step > 0.0 && step < 1.0 && digits < 8)
     {
      step *= 10.0;
      digits++;
     }

   return digits;
  }

//+------------------------------------------------------------------+
//| Volume normalization                                             |
//+------------------------------------------------------------------+
//--- The requested lot size must respect the symbol minimum, maximum, and step.
//--- Normalizing volume before OrderSend avoids avoidable broker rejections and
//--- keeps the execution block focused on trade logic instead of broker details.
double NormalizeVolume(const double volume)
  {
   double minVolume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxVolume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);

   if(step <= 0.0)
      step = (minVolume > 0.0 ? minVolume : 1.0);

   double normalized = MathFloor(volume / step + 1e-9) * step;
   normalized = MathMax(minVolume, MathMin(maxVolume, normalized));

   return NormalizeDouble(normalized, VolumeDigits());
  }

//+------------------------------------------------------------------+
//| Price normalization                                              |
//+------------------------------------------------------------------+
//--- All generated prices are normalized to the symbol number of digits.
//--- This is the final formatting layer before a price is sent to the server.
double NormalizePriceDigits(const double price)
  {
   return NormalizeDouble(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
  }

//+------------------------------------------------------------------+
//| Price normalization down to tick                                 |
//+------------------------------------------------------------------+
//--- Used for buy SL and sell TP values.
double NormalizePriceDownToTick(const double price)
  {
   if(g_tickSize <= 0.0)
      return NormalizePriceDigits(price);

   return NormalizePriceDigits(MathFloor(price / g_tickSize) * g_tickSize);
  }

//+------------------------------------------------------------------+
//| Price normalization up to tick                                   |
//+------------------------------------------------------------------+
//--- Used for sell SL and buy TP values.
double NormalizePriceUpToTick(const double price)
  {
   if(g_tickSize <= 0.0)
      return NormalizePriceDigits(price);

   return NormalizePriceDigits(MathCeil(price / g_tickSize) * g_tickSize);
  }

//+------------------------------------------------------------------+
//| Broker stop distance                                             |
//+------------------------------------------------------------------+
//--- SYMBOL_TRADE_STOPS_LEVEL is given in points, not in ticks.
//--- The EA converts the broker minimum distance into price units and adds
//--- one tick as a safety buffer to reduce invalid-stop rejections at exact limits.
double BrokerMinimumStopDistance()
  {
   int stopsLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);

   double minimumDistance = 0.0;

   if(stopsLevel > 0)
      minimumDistance = (double)stopsLevel * _Point;

//--- A one-tick buffer helps avoid invalid-stop errors at exact broker limits.
   return minimumDistance + g_tickSize;
  }

//+------------------------------------------------------------------+
//| Configured risk distance                                         |
//+------------------------------------------------------------------+
//--- This is the price distance used as the model's 1R.
//--- The stop is configured in symbol ticks, then converted into price distance.
//--- Fixed TP, break-even and trailing all use this same reference, which keeps
//--- the whole management layer proportional to the initial risk.
double ConfiguredInitialRiskDistance()
  {
   if(stopLossTicks <= 0 || g_tickSize <= 0.0)
      return 0.0;

   return (double)stopLossTicks * g_tickSize;
  }

//+------------------------------------------------------------------+
//| Initial stop loss price                                          |
//+------------------------------------------------------------------+
//--- The initial stop is built from stopLossTicks, using the symbol tick size.
//--- Buy stops are placed below the entry price and rounded down to a valid tick.
//--- Sell stops are placed above the entry price and rounded up to a valid tick.
//--- Before returning the final SL, the function also checks the broker minimum
//--- stop distance, because a mathematically correct stop can still be rejected
//--- if it is too close to Bid or Ask.
double InitialStopLossPrice(const ENUM_ORDER_TYPE orderType,
                            const double entryPrice)
  {
   double requestedDistance = ConfiguredInitialRiskDistance();

   if(requestedDistance <= 0.0 || entryPrice <= 0.0)
      return 0.0;

   double brokerDistance = BrokerMinimumStopDistance();

   if(orderType == ORDER_TYPE_BUY)
     {
      double stopPrice = entryPrice - requestedDistance;
      double highestValidStop = g_bid - brokerDistance;

      if(stopPrice > highestValidStop)
         stopPrice = highestValidStop;

      return NormalizePriceDownToTick(stopPrice);
     }

   if(orderType == ORDER_TYPE_SELL)
     {
      double stopPrice = entryPrice + requestedDistance;
      double lowestValidStop = g_ask + brokerDistance;

      if(stopPrice < lowestValidStop)
         stopPrice = lowestValidStop;

      return NormalizePriceUpToTick(stopPrice);
     }

   return 0.0;
  }

//+------------------------------------------------------------------+
//| Initial take profit price                                        |
//+------------------------------------------------------------------+
//--- Fixed take profit is optional.
//--- When enabled, the target is not calculated in points or ATR.
//--- It is projected as fixedTakeProfitRR times the configured 1R distance,
//--- keeping the TP proportional to the same risk used by the stop.
double InitialTakeProfitPrice(const ENUM_ORDER_TYPE orderType,
                              const double entryPrice)
  {
   if(!useFixedTakeProfit)
      return 0.0;

   double riskDistance = ConfiguredInitialRiskDistance();

   if(entryPrice <= 0.0 || riskDistance <= 0.0)
      return 0.0;

   if(orderType == ORDER_TYPE_BUY)
      return NormalizePriceUpToTick(entryPrice + fixedTakeProfitRR * riskDistance);

   if(orderType == ORDER_TYPE_SELL)
      return NormalizePriceDownToTick(entryPrice - fixedTakeProfitRR * riskDistance);

   return 0.0;
  }

//+------------------------------------------------------------------+
//| Initial take profit validation                                   |
//+------------------------------------------------------------------+
//--- The initial TP must also respect the broker minimum stop distance.
//--- This validation is separated from price calculation so the article can show
//--- that computing a target and validating it are two different steps.
bool InitialTakeProfitIsValid(const ENUM_ORDER_TYPE orderType,
                              const double takeProfitPrice)
  {
   if(!useFixedTakeProfit)
      return true;

   if(takeProfitPrice <= 0.0)
      return false;

   double brokerDistance = BrokerMinimumStopDistance();

   if(orderType == ORDER_TYPE_BUY)
      return (takeProfitPrice > g_ask + brokerDistance);

   if(orderType == ORDER_TYPE_SELL)
      return (takeProfitPrice < g_bid - brokerDistance);

   return false;
  }

//+------------------------------------------------------------------+
//| Minutes of day                                                   |
//+------------------------------------------------------------------+
//--- Used to control the day trade window.
int MinutesOfDay(const datetime when)
  {
   MqlDateTime timeStruct;
   TimeToStruct(when, timeStruct);

   return timeStruct.hour * 60 + timeStruct.min;
  }

//+------------------------------------------------------------------+
//| Entry time filter                                                |
//+------------------------------------------------------------------+
//--- The day trade window affects only new entries.
//--- Existing positions can still be managed by stop, BE, trailing or forced close.
bool EntryTimeAllowed(const datetime when)
  {
   if(!useDaytradeWindow)
      return true;

   int minute = MinutesOfDay(when);
   return (minute >= daytradeStartMinute && minute < lastEntryMinute);
  }

//+------------------------------------------------------------------+
//| End-of-day close filter                                          |
//+------------------------------------------------------------------+
//--- This protects the strategy from carrying trades overnight.
bool MustForceCloseEndOfDay(const datetime when)
  {
   return (useDaytradeWindow &&
           forceCloseAtEndOfDay &&
           MinutesOfDay(when) >= forcedCloseMinute);
  }

//+------------------------------------------------------------------+
//| Direction name                                                   |
//+------------------------------------------------------------------+
string DirectionName(const bool isBuy)
  {
   return (isBuy ? "BUY" : "SELL");
  }

//+------------------------------------------------------------------+
//| Status log writer                                                |
//+------------------------------------------------------------------+
void LogEvent(const string eventName, const string details = "")
  {
   if(!enableStatusLogs)
      return;

   string line = StringFormat("[OneEMA_CORE][%s][%s]", _Symbol, eventName);

   if(StringLen(details) > 0)
      line += " | " + details;

   Print(line);
  }

//+------------------------------------------------------------------+
//| Parameter validation                                             |
//+------------------------------------------------------------------+
//--- This validation prevents invalid optimization passes before the tester starts.
//--- RR-based management needs a positive stopLossTicks value, because without an
//--- initial risk distance there is no valid 1R reference for TP, BE or trailing.
bool ParametersAreValid()
  {
   if(regimeEMAPeriod <= 0)
      return false;

   if(retracementEMAPeriod <= 0)
      return false;

   if(lots <= 0.0)
      return false;

   if(stopLossTicks < 0)
      return false;

   if((useFixedTakeProfit || useBreakEven || useTrailingStop) &&
      stopLossTicks <= 0)
      return false;

   if(useFixedTakeProfit && fixedTakeProfitRR <= 0.0)
      return false;

   if(useBreakEven)
     {
      if(breakEvenActivationRR <= 0.0)
         return false;

      if(breakEvenLockRR < 0.0)
         return false;

      if(breakEvenLockRR >= breakEvenActivationRR)
         return false;
     }

   if(useTrailingStop)
     {
      if(trailingActivationRR <= 0.0)
         return false;

      if(trailingDistanceRR <= 0.0)
         return false;
     }

   if(entryATRPeriod <= 0)
      return false;

   if(entryDistanceFromEMAAtr < 0.0)
      return false;

   if(daytradeStartMinute < 0 ||
      lastEntryMinute <= daytradeStartMinute ||
      forcedCloseMinute < lastEntryMinute ||
      forcedCloseMinute > 1440)
      return false;

   return true;
  }

//+------------------------------------------------------------------+
//| Magic number validation                                          |
//+------------------------------------------------------------------+
bool IsEAMagic(const ulong magic)
  {
   return (magic == buyMagic || magic == sellMagic);
  }

//+------------------------------------------------------------------+
//| Position selection by magic                                      |
//+------------------------------------------------------------------+
bool SelectPositionByMagic(const ulong magic,
                           const ENUM_POSITION_TYPE type,
                           ulong &ticket)
  {
   ticket = 0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong candidate = PositionGetTicket(i);

      if(candidate == 0 || !PositionSelectByTicket(candidate))
         continue;

      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;

      if((ulong)PositionGetInteger(POSITION_MAGIC) != magic)
         continue;

      if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != type)
         continue;

      ticket = candidate;
      return true;
     }

   return false;
  }

//+------------------------------------------------------------------+
//| EA exposure check                                                |
//+------------------------------------------------------------------+
bool HasEAExposure()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);

      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
         IsEAMagic((ulong)PositionGetInteger(POSITION_MAGIC)))
         return true;
     }

   return false;
  }

//+------------------------------------------------------------------+
//| Symbol exposure check                                            |
//+------------------------------------------------------------------+
//--- This is used when hedging is disabled.
bool HasAnyPositionOnSymbol()
  {
   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);

      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(PositionGetString(POSITION_SYMBOL) == _Symbol)
         return true;
     }

   return false;
  }

//+------------------------------------------------------------------+
//| Buy position check                                               |
//+------------------------------------------------------------------+
bool HasBuyPosition()
  {
   ulong ticket = 0;
   return SelectPositionByMagic(buyMagic, POSITION_TYPE_BUY, ticket);
  }

//+------------------------------------------------------------------+
//| Sell position check                                              |
//+------------------------------------------------------------------+
bool HasSellPosition()
  {
   ulong ticket = 0;
   return SelectPositionByMagic(sellMagic, POSITION_TYPE_SELL, ticket);
  }

//+------------------------------------------------------------------+
//| Margin validation                                                |
//+------------------------------------------------------------------+
bool HasEnoughMargin(const ENUM_ORDER_TYPE orderType,
                     const double volume,
                     const double price)
  {
   double margin = 0.0;

   if(volume <= 0.0 ||
      price <= 0.0 ||
      !OrderCalcMargin(orderType, _Symbol, volume, price, margin))
      return false;

   return (AccountInfoDouble(ACCOUNT_MARGIN_FREE) >= margin);
  }

//+------------------------------------------------------------------+
//| Market order sender                                              |
//+------------------------------------------------------------------+
//--- This function is responsible only for sending the final order request.
//--- Entry logic, SL calculation, TP calculation and margin checks are performed
//--- before this point. This separation keeps execution code simple and reusable.
bool SendMarketOrder(const ENUM_ORDER_TYPE orderType,
                     const double volume,
                     const ulong magic,
                     const double stopLossPrice,
                     const double takeProfitPrice)
  {
   double normalizedVolume = NormalizeVolume(volume);

   if(normalizedVolume <= 0.0)
      return false;

   MqlTradeRequest request;
   MqlTradeResult result;

   ZeroMemory(request);
   ZeroMemory(result);

   request.action = TRADE_ACTION_DEAL;
   request.symbol = _Symbol;
   request.magic = magic;
   request.volume = normalizedVolume;
   request.deviation = g_deviation;
   request.type = orderType;
   request.type_filling = ORDER_FILLING_FOK;
   request.price = (orderType == ORDER_TYPE_BUY ? g_ask : g_bid);
   request.sl = stopLossPrice;
   request.tp = takeProfitPrice;

   ResetLastError();

   if(!OrderSend(request, result))
     {
      LogEvent("ORDER_SEND_FAILED",
               StringFormat("error=%d price=%.2f sl=%.2f tp=%.2f bid=%.2f ask=%.2f",
                            GetLastError(),
                            request.price,
                            request.sl,
                            request.tp,
                            g_bid,
                            g_ask));
      return false;
     }

   bool accepted = (result.retcode == TRADE_RETCODE_DONE ||
                    result.retcode == TRADE_RETCODE_DONE_PARTIAL ||
                    result.retcode == TRADE_RETCODE_PLACED);

   if(!accepted)
      LogEvent("ORDER_REJECTED",
               StringFormat("retcode=%d price=%.2f sl=%.2f tp=%.2f bid=%.2f ask=%.2f tick_size=%.5f broker_stop_distance=%.2f",
                            result.retcode,
                            request.price,
                            request.sl,
                            request.tp,
                            g_bid,
                            g_ask,
                            g_tickSize,
                            BrokerMinimumStopDistance()));

   return accepted;
  }

//+------------------------------------------------------------------+
//| Position protection modification                                 |
//+------------------------------------------------------------------+
//--- Break-even and trailing do not open or close positions directly.
//--- They only update the SL/TP protection of an existing position.
//--- Keeping this function separate makes both management rules share one
//--- execution path and avoids duplicating TRADE_ACTION_SLTP code.
bool ModifyPositionProtection(const ulong ticket,
                              const double stopLossPrice,
                              const double takeProfitPrice,
                              const string reason)
  {
   if(ticket == 0 || !PositionSelectByTicket(ticket))
      return false;

   MqlTradeRequest request;
   MqlTradeResult result;

   ZeroMemory(request);
   ZeroMemory(result);

   request.action = TRADE_ACTION_SLTP;
   request.position = ticket;
   request.symbol = _Symbol;
   request.magic = (ulong)PositionGetInteger(POSITION_MAGIC);
   request.sl = stopLossPrice;
   request.tp = takeProfitPrice;

   ResetLastError();

   if(!OrderSend(request, result))
     {
      LogEvent("PROTECTION_MODIFY_FAILED",
               StringFormat("reason=%s error=%d sl=%.2f tp=%.2f",
                            reason,
                            GetLastError(),
                            request.sl,
                            request.tp));
      return false;
     }

   bool accepted = (result.retcode == TRADE_RETCODE_DONE ||
                    result.retcode == TRADE_RETCODE_DONE_PARTIAL ||
                    result.retcode == TRADE_RETCODE_PLACED);

   if(!accepted)
      LogEvent("PROTECTION_MODIFY_REJECTED",
               StringFormat("reason=%s retcode=%d sl=%.2f tp=%.2f",
                            reason,
                            result.retcode,
                            request.sl,
                            request.tp));

   return accepted;
  }

//+------------------------------------------------------------------+
//| Stop loss improvement check                                      |
//+------------------------------------------------------------------+
//--- Managed stops must never increase the risk of an open trade.
//--- For buy positions, protection improves only when the SL moves upward.
//--- For sell positions, protection improves only when the SL moves downward.
//--- A half-tick buffer avoids repeated modifications caused by rounding noise.
bool StopLossImprovesProtection(const ENUM_POSITION_TYPE positionType,
                                const double currentStopLoss,
                                const double candidateStopLoss)
  {
   if(candidateStopLoss <= 0.0)
      return false;

   if(currentStopLoss <= 0.0)
      return true;

   if(positionType == POSITION_TYPE_BUY)
      return (candidateStopLoss > currentStopLoss + g_tickSize * 0.5);

   if(positionType == POSITION_TYPE_SELL)
      return (candidateStopLoss < currentStopLoss - g_tickSize * 0.5);

   return false;
  }

//+------------------------------------------------------------------+
//| Managed stop broker clamp                                        |
//+------------------------------------------------------------------+
//--- A candidate stop can be logically correct but still too close to market price.
//--- This function clamps the managed stop to the closest valid broker distance,
//--- then normalizes it to a valid tick on the correct side of Bid or Ask.
double ClampManagedStopToBrokerDistance(const ENUM_POSITION_TYPE positionType,
                                        const double candidateStopLoss)
  {
   if(candidateStopLoss <= 0.0)
      return 0.0;

   double brokerDistance = BrokerMinimumStopDistance();

   if(positionType == POSITION_TYPE_BUY)
     {
      double highestValidStop = g_bid - brokerDistance;
      double adjustedStop = MathMin(candidateStopLoss, highestValidStop);

      return NormalizePriceDownToTick(adjustedStop);
     }

   if(positionType == POSITION_TYPE_SELL)
     {
      double lowestValidStop = g_ask + brokerDistance;
      double adjustedStop = MathMax(candidateStopLoss, lowestValidStop);

      return NormalizePriceUpToTick(adjustedStop);
     }

   return 0.0;
  }

//+------------------------------------------------------------------+
//| Current position RR                                              |
//+------------------------------------------------------------------+
//--- This converts open profit into R-multiples.
//--- Buy positions use Bid because Bid is the executable exit price for buys.
//--- Sell positions use Ask because Ask is the executable exit price for sells.
double CurrentPositionRR(const ENUM_POSITION_TYPE positionType,
                         const double openPrice)
  {
   double riskDistance = ConfiguredInitialRiskDistance();

   if(openPrice <= 0.0 || riskDistance <= 0.0)
      return 0.0;

   if(positionType == POSITION_TYPE_BUY)
      return (g_bid - openPrice) / riskDistance;

   if(positionType == POSITION_TYPE_SELL)
      return (openPrice - g_ask) / riskDistance;

   return 0.0;
  }

//+------------------------------------------------------------------+
//| Break-even stop candidate                                        |
//+------------------------------------------------------------------+
//--- Break-even is built as a candidate stop, not applied immediately.
//--- First the position must reach breakEvenActivationRR.
//--- Then the stop is moved to the entry price plus or minus breakEvenLockRR.
//--- The candidate still needs to pass the improvement and broker-distance checks.
double BreakEvenStopCandidate(const ENUM_POSITION_TYPE positionType,
                              const double openPrice)
  {
   double riskDistance = ConfiguredInitialRiskDistance();

   if(!useBreakEven || openPrice <= 0.0 || riskDistance <= 0.0)
      return 0.0;

   double currentRR = CurrentPositionRR(positionType, openPrice);

   if(currentRR < breakEvenActivationRR)
      return 0.0;

   if(positionType == POSITION_TYPE_BUY)
      return NormalizePriceDownToTick(openPrice + breakEvenLockRR * riskDistance);

   if(positionType == POSITION_TYPE_SELL)
      return NormalizePriceUpToTick(openPrice - breakEvenLockRR * riskDistance);

   return 0.0;
  }

//+------------------------------------------------------------------+
//| Trailing stop candidate                                          |
//+------------------------------------------------------------------+
//--- Trailing stop is also built as a candidate stop.
//--- The activation check is performed in ManagePositionByRR.
//--- This function only calculates where the trailing stop should be placed
//--- from the current executable price using trailingDistanceRR.
double TrailingStopCandidate(const ENUM_POSITION_TYPE positionType)
  {
   double riskDistance = ConfiguredInitialRiskDistance();

   if(!useTrailingStop || riskDistance <= 0.0)
      return 0.0;

   if(positionType == POSITION_TYPE_BUY)
      return NormalizePriceDownToTick(g_bid - trailingDistanceRR * riskDistance);

   if(positionType == POSITION_TYPE_SELL)
      return NormalizePriceUpToTick(g_ask + trailingDistanceRR * riskDistance);

   return 0.0;
  }

//+------------------------------------------------------------------+
//| Single-position RR management                                    |
//+------------------------------------------------------------------+
//--- This function compares all active RR-based protection candidates.
//--- Break-even and trailing are not applied blindly: each one proposes a stop.
//--- The EA keeps only the best candidate that improves the current SL, clamps it
//--- to broker rules, and modifies the position only if the final value is valid.
void ManagePositionByRR(const ulong ticket)
  {
   if(!PositionSelectByTicket(ticket))
      return;

   ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);

   if(positionType != POSITION_TYPE_BUY &&
      positionType != POSITION_TYPE_SELL)
      return;

   double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
   double currentStopLoss = PositionGetDouble(POSITION_SL);
   double currentTakeProfit = PositionGetDouble(POSITION_TP);
   double currentRR = CurrentPositionRR(positionType, openPrice);

   if(currentRR <= 0.0)
      return;

   double bestStopLoss = currentStopLoss;

//--- Break-even proposes the first protective stop candidate.
//--- It is used only if it improves the current protection.
   double breakEvenStop = BreakEvenStopCandidate(positionType, openPrice);

   if(StopLossImprovesProtection(positionType, bestStopLoss, breakEvenStop))
      bestStopLoss = breakEvenStop;

//--- Trailing proposes a second candidate after its activation RR is reached.
//--- If both BE and trailing are active, the EA keeps the most protective value.
   if(useTrailingStop && currentRR >= trailingActivationRR)
     {
      double trailingStop = TrailingStopCandidate(positionType);

      if(StopLossImprovesProtection(positionType, bestStopLoss, trailingStop))
         bestStopLoss = trailingStop;
     }

//--- If no candidate improves the current SL, there is nothing to modify.
   if(!StopLossImprovesProtection(positionType, currentStopLoss, bestStopLoss))
      return;

//--- The selected candidate must still respect broker distance rules.
   double adjustedStopLoss = ClampManagedStopToBrokerDistance(positionType, bestStopLoss);

   if(!StopLossImprovesProtection(positionType, currentStopLoss, adjustedStopLoss))
      return;

   ModifyPositionProtection(ticket,
                            adjustedStopLoss,
                            currentTakeProfit,
                            "rr_management");

   LogEvent("RR_MANAGEMENT_UPDATED",
            StringFormat("ticket=%I64u rr=%.2f old_sl=%.2f new_sl=%.2f tp=%.2f",
                         ticket,
                         currentRR,
                         currentStopLoss,
                         adjustedStopLoss,
                         currentTakeProfit));
  }

//+------------------------------------------------------------------+
//| RR position management                                           |
//+------------------------------------------------------------------+
//--- Entry signals are closed-candle based, but management must be tick-based.
//--- Once a position is open, BE and trailing depend on executable Bid/Ask prices,
//--- so this function is called on every tick and processes all EA positions.
void ManageRRBasedPositionManagement()
  {
   if(!useBreakEven && !useTrailingStop)
      return;

   if(ConfiguredInitialRiskDistance() <= 0.0)
      return;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
     {
      ulong ticket = PositionGetTicket(i);

      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;

      if(!IsEAMagic((ulong)PositionGetInteger(POSITION_MAGIC)))
         continue;

      ManagePositionByRR(ticket);
     }
  }

//+------------------------------------------------------------------+
//| Close position by ticket                                         |
//+------------------------------------------------------------------+
bool ClosePositionByTicket(const ulong ticket)
  {
   if(ticket == 0 || !PositionSelectByTicket(ticket))
      return false;

   ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
   double volume = PositionGetDouble(POSITION_VOLUME);
   ulong magic = (ulong)PositionGetInteger(POSITION_MAGIC);

   MqlTradeRequest request;
   MqlTradeResult result;

   ZeroMemory(request);
   ZeroMemory(result);

   request.action = TRADE_ACTION_DEAL;
   request.position = ticket;
   request.symbol = _Symbol;
   request.volume = volume;
   request.magic = magic;
   request.deviation = g_deviation;
   request.type_filling = ORDER_FILLING_FOK;

   if(positionType == POSITION_TYPE_BUY)
     {
      request.type = ORDER_TYPE_SELL;
      request.price = g_bid;
     }
   else
     {
      request.type = ORDER_TYPE_BUY;
      request.price = g_ask;
     }

   if(!OrderSend(request, result))
      return false;

   return (result.retcode == TRADE_RETCODE_DONE ||
           result.retcode == TRADE_RETCODE_DONE_PARTIAL ||
           result.retcode == TRADE_RETCODE_PLACED);
  }

//+------------------------------------------------------------------+
//| Close buy position                                               |
//+------------------------------------------------------------------+
void CloseBuyPosition(const string reason)
  {
   if(g_closingBuy)
      return;

   g_closingBuy = true;

   ulong ticket = 0;

   if(SelectPositionByMagic(buyMagic, POSITION_TYPE_BUY, ticket))
      ClosePositionByTicket(ticket);

   LogEvent("BUY_POSITION_CLOSED", "reason=" + reason);

   g_closingBuy = false;
  }

//+------------------------------------------------------------------+
//| Close sell position                                              |
//+------------------------------------------------------------------+
void CloseSellPosition(const string reason)
  {
   if(g_closingSell)
      return;

   g_closingSell = true;

   ulong ticket = 0;

   if(SelectPositionByMagic(sellMagic, POSITION_TYPE_SELL, ticket))
      ClosePositionByTicket(ticket);

   LogEvent("SELL_POSITION_CLOSED", "reason=" + reason);

   g_closingSell = false;
  }

//+------------------------------------------------------------------+
//| Force end-of-day close                                           |
//+------------------------------------------------------------------+
void ForceCloseAllPositions()
  {
   bool hadExposure = HasEAExposure();

   if(HasBuyPosition())
      CloseBuyPosition("end_of_day");

   if(HasSellPosition())
      CloseSellPosition("end_of_day");

   if(hadExposure)
      LogEvent("FORCED_DAYTRADE_CLOSE");
  }

//+------------------------------------------------------------------+
//| Buy regime                                                       |
//+------------------------------------------------------------------+
//--- The regime timeframe defines the broader directional context.
//--- A buy regime is active only when the last fully closed regime candle
//--- closes above the regime EMA. The current forming candle is ignored.
bool BuyRegime()
  {
   if(ArraySize(g_regimeRates) <= 1 || ArraySize(g_regimeEMA) <= 1)
      return false;

   return (g_regimeRates[1].close > g_regimeEMA[1]);
  }

//+------------------------------------------------------------------+
//| Sell regime                                                      |
//+------------------------------------------------------------------+
//--- A sell regime is active only when the last fully closed regime candle
//--- closes below the regime EMA. Using closed candles keeps regime detection
//--- stable and avoids changing direction intrabar.
bool SellRegime()
  {
   if(ArraySize(g_regimeRates) <= 1 || ArraySize(g_regimeEMA) <= 1)
      return false;

   return (g_regimeRates[1].close < g_regimeEMA[1]);
  }

//+------------------------------------------------------------------+
//| Clear buy retracement                                            |
//+------------------------------------------------------------------+
void ClearBuyRetracement()
  {
   g_buyRetracementActive = false;
  }

//+------------------------------------------------------------------+
//| Clear sell retracement                                           |
//+------------------------------------------------------------------+
void ClearSellRetracement()
  {
   g_sellRetracementActive = false;
  }

//+------------------------------------------------------------------+
//| Clear all retracements                                           |
//+------------------------------------------------------------------+
void ClearRetracements()
  {
   ClearBuyRetracement();
   ClearSellRetracement();
  }

//+------------------------------------------------------------------+
//| Regime update                                                    |
//+------------------------------------------------------------------+
//--- The regime state is the first layer of the pullback model.
//--- When the regime changes, all armed pullbacks are cleared because they were
//--- created under a different directional context and should not be reused.
void UpdateRegime()
  {
   bool buy = BuyRegime();
   bool sell = SellRegime();

   if(buy)
     {
      if(!g_buyRegimeActive)
        {
         LogEvent("BUY_REGIME_ACTIVE",
                  StringFormat("tf=%d candle=%s close=%.2f ema=%.2f",
                               (int)g_regimeTimeframe,
                               TimeToString(g_regimeRates[1].time, TIME_DATE | TIME_MINUTES),
                               g_regimeRates[1].close,
                               g_regimeEMA[1]));

         ClearRetracements();
        }

      g_buyRegimeActive = true;
      g_sellRegimeActive = false;
      return;
     }

   if(sell)
     {
      if(!g_sellRegimeActive)
        {
         LogEvent("SELL_REGIME_ACTIVE",
                  StringFormat("tf=%d candle=%s close=%.2f ema=%.2f",
                               (int)g_regimeTimeframe,
                               TimeToString(g_regimeRates[1].time, TIME_DATE | TIME_MINUTES),
                               g_regimeRates[1].close,
                               g_regimeEMA[1]));

         ClearRetracements();
        }

      g_buyRegimeActive = false;
      g_sellRegimeActive = true;
      return;
     }

   g_buyRegimeActive = false;
   g_sellRegimeActive = false;
   ClearRetracements();
  }

//+------------------------------------------------------------------+
//| Regime invalidation exit                                         |
//+------------------------------------------------------------------+
//--- Regime invalidation is the structural exit of the model.
//--- Buy positions are closed if the broader context turns bearish.
//--- Sell positions are closed if the broader context turns bullish.
void ManageExitByRegime()
  {
   if(HasBuyPosition() && SellRegime())
     {
      CloseBuyPosition("regime_reversed_to_sell");
      ClearRetracements();
     }

   if(HasSellPosition() && BuyRegime())
     {
      CloseSellPosition("regime_reversed_to_buy");
      ClearRetracements();
     }
  }

//+------------------------------------------------------------------+
//| Buy retracement candle                                           |
//+------------------------------------------------------------------+
//--- In a buy regime, the pullback is armed when the retracement candle closes
//--- at or below the retracement EMA. This means price corrected back into the
//--- average before attempting a bullish return trigger.
bool BuyRetracementCandle()
  {
   if(ArraySize(g_retracementRates) <= 1 || ArraySize(g_retracementEMA) <= 1)
      return false;

   return (g_retracementRates[1].close <= g_retracementEMA[1]);
  }

//+------------------------------------------------------------------+
//| Sell retracement candle                                          |
//+------------------------------------------------------------------+
//--- In a sell regime, the pullback is armed when the retracement candle closes
//--- at or above the retracement EMA. This means price corrected back into the
//--- average before attempting a bearish return trigger.
bool SellRetracementCandle()
  {
   if(ArraySize(g_retracementRates) <= 1 || ArraySize(g_retracementEMA) <= 1)
      return false;

   return (g_retracementRates[1].close >= g_retracementEMA[1]);
  }

//+------------------------------------------------------------------+
//| Bullish EMA return cross                                         |
//+------------------------------------------------------------------+
//--- This is the buy trigger.
//--- The previous closed candle must be below or equal to the EMA, showing that
//--- the pullback was still present. The latest closed candle must close above
//--- the EMA, confirming the first return in the regime direction.
bool CrossedUpRetracementEMA()
  {
   if(ArraySize(g_retracementRates) <= 2 || ArraySize(g_retracementEMA) <= 2)
      return false;

   bool previousClosedBelowOrEqual = (g_retracementRates[2].close <= g_retracementEMA[2]);
   bool currentClosedAbove = (g_retracementRates[1].close > g_retracementEMA[1]);

   return (previousClosedBelowOrEqual && currentClosedAbove);
  }

//+------------------------------------------------------------------+
//| Bearish EMA return cross                                         |
//+------------------------------------------------------------------+
//--- This is the sell trigger.
//--- The previous closed candle must be above or equal to the EMA, showing that
//--- the pullback was still present. The latest closed candle must close below
//--- the EMA, confirming the first return in the regime direction.
bool CrossedDownRetracementEMA()
  {
   if(ArraySize(g_retracementRates) <= 2 || ArraySize(g_retracementEMA) <= 2)
      return false;

   bool previousClosedAboveOrEqual = (g_retracementRates[2].close >= g_retracementEMA[2]);
   bool currentClosedBelow = (g_retracementRates[1].close < g_retracementEMA[1]);

   return (previousClosedAboveOrEqual && currentClosedBelow);
  }

//+------------------------------------------------------------------+
//| ATR distance confirmation                                        |
//+------------------------------------------------------------------+
//--- The EMA cross alone can be too weak when price closes only a few points
//--- beyond the average. This filter requires the signal close to be at least
//--- entryDistanceFromEMAAtr * ATR away from the retracement EMA.
//--- It keeps the original EMA-cross trigger, but rejects weak returns.
bool EntryDistanceFromEMAByATRIsValid(const bool isBuy)
  {
   if(!useEntryDistanceFromEMAByATR)
      return true;

   if(entryDistanceFromEMAAtr <= 0.0)
      return true;

   if(ArraySize(g_retracementRates) <= 1 ||
      ArraySize(g_retracementEMA) <= 1 ||
      ArraySize(g_retracementATR) <= 1)
      return false;

   double atr = g_retracementATR[1];

   if(atr <= 0.0)
      return false;

   double requiredDistance = entryDistanceFromEMAAtr * atr;
   double signalClose = g_retracementRates[1].close;
   double ema = g_retracementEMA[1];

   if(isBuy)
     {
      bool valid = (signalClose >= ema + requiredDistance);

      if(!valid)
         LogEvent("BUY_ATR_EMA_DISTANCE_BLOCKED",
                  StringFormat("close=%.2f ema=%.2f atr=%.2f required=%.2f",
                               signalClose,
                               ema,
                               atr,
                               requiredDistance));

      return valid;
     }

   bool valid = (signalClose <= ema - requiredDistance);

   if(!valid)
      LogEvent("SELL_ATR_EMA_DISTANCE_BLOCKED",
               StringFormat("close=%.2f ema=%.2f atr=%.2f required=%.2f",
                            signalClose,
                            ema,
                            atr,
                            requiredDistance));

   return valid;
  }

//+------------------------------------------------------------------+
//| New position permission                                          |
//+------------------------------------------------------------------+
//--- Entry permission is checked immediately before opening a trade.
//--- It prevents duplicate EA exposure, respects the optional hedge rule, and
//--- blocks new entries outside the intraday entry window.
bool CanOpenNewPosition()
  {
   if(HasEAExposure())
      return false;

   if(!allowHedge && HasAnyPositionOnSymbol())
      return false;

   if(!EntryTimeAllowed(g_tick.time))
      return false;

   return true;
  }

//+------------------------------------------------------------------+
//| Open trade                                                       |
//+------------------------------------------------------------------+
//--- This function is called only after regime, retracement, trigger, and ATR
//--- confirmation have already passed. It builds the executable order request:
//--- volume, entry price, initial SL in ticks, optional TP in RR, margin check,
//--- and final market order submission.
bool OpenTrade(const bool isBuy)
  {
   datetime currentBar = g_retracementRates[0].time;

   if(g_lastEntryAttemptBarTime == currentBar)
      return false;

   g_lastEntryAttemptBarTime = currentBar;

   if(!CanOpenNewPosition())
      return false;

   double volume = NormalizeVolume(lots);
   double entryPrice = (isBuy ? g_ask : g_bid);
   ENUM_ORDER_TYPE orderType = (isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);

//--- The stop is calculated first because it defines the initial 1R distance.
//--- The optional fixed TP uses that same configured risk reference.
   double stopLossPrice = InitialStopLossPrice(orderType, entryPrice);
   double takeProfitPrice = InitialTakeProfitPrice(orderType, entryPrice);

   if(useFixedTakeProfit && !InitialTakeProfitIsValid(orderType, takeProfitPrice))
     {
      LogEvent("ENTRY_BLOCKED_INVALID_TP",
               StringFormat("direction=%s entry=%.2f tp=%.2f rr=%.2f",
                            DirectionName(isBuy),
                            entryPrice,
                            takeProfitPrice,
                            fixedTakeProfitRR));
      return false;
     }

   if(volume <= 0.0 || !HasEnoughMargin(orderType, volume, entryPrice))
     {
      LogEvent("ENTRY_BLOCKED_MARGIN", "direction=" + DirectionName(isBuy));
      return false;
     }

   bool sent = SendMarketOrder(orderType,
                               volume,
                               isBuy ? buyMagic : sellMagic,
                               stopLossPrice,
                               takeProfitPrice);

   if(!sent)
      return false;

   if(isBuy)
     {
      LogEvent("BUY_ENTRY_CORE",
               StringFormat("retracement_tf=%d signal_candle=%s entry=%.2f retracement_ema=%.2f sl=%.2f tp=%.2f",
                            (int)g_retracementTimeframe,
                            TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                            entryPrice,
                            g_retracementEMA[1],
                            stopLossPrice,
                            takeProfitPrice));

      ClearBuyRetracement();
     }
   else
     {
      LogEvent("SELL_ENTRY_CORE",
               StringFormat("retracement_tf=%d signal_candle=%s entry=%.2f retracement_ema=%.2f sl=%.2f tp=%.2f",
                            (int)g_retracementTimeframe,
                            TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                            entryPrice,
                            g_retracementEMA[1],
                            stopLossPrice,
                            takeProfitPrice));

      ClearSellRetracement();
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Retracement and trigger update                                   |
//+------------------------------------------------------------------+
//--- This is the core entry sequence of the strategy.
//--- First, the active regime decides which direction is allowed.
//--- Second, a pullback candle arms the setup.
//--- Third, a closed EMA return cross confirms the trigger.
//--- Finally, the ATR distance filter can reject weak crosses before OpenTrade.
void UpdateRetracementAndTrigger()
  {
   if(!CanOpenNewPosition())
      return;

   if(g_buyRegimeActive)
     {
      //--- In a buy regime, sell pullbacks are invalid and must be cleared.
      ClearSellRetracement();

      //--- The first closed candle back to or below the retracement EMA arms the buy setup.
      if(!g_buyRetracementActive && BuyRetracementCandle())
        {
         g_buyRetracementActive = true;
         LogEvent("BUY_RETRACEMENT_ARMED",
                  StringFormat("tf=%d candle=%s close=%.2f ema=%.2f",
                               (int)g_retracementTimeframe,
                               TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                               g_retracementRates[1].close,
                               g_retracementEMA[1]));
        }

      //--- After the pullback is armed, the EA waits for a closed bullish return cross.
      if(g_buyRetracementActive && CrossedUpRetracementEMA())
        {
         //--- The ATR distance filter rejects weak crosses before the order is sent.
         if(!EntryDistanceFromEMAByATRIsValid(true))
            return;

         LogEvent("BUY_TRIGGER_RETRACEMENT_EMA_CROSS",
                  StringFormat("tf=%d candle=%s close=%.2f ema=%.2f atr=%.2f",
                               (int)g_retracementTimeframe,
                               TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                               g_retracementRates[1].close,
                               g_retracementEMA[1],
                               g_retracementATR[1]));

         OpenTrade(true);
        }

      return;
     }

   if(g_sellRegimeActive)
     {
      //--- In a sell regime, buy pullbacks are invalid and must be cleared.
      ClearBuyRetracement();

      //--- The first closed candle back to or above the retracement EMA arms the sell setup.
      if(!g_sellRetracementActive && SellRetracementCandle())
        {
         g_sellRetracementActive = true;
         LogEvent("SELL_RETRACEMENT_ARMED",
                  StringFormat("tf=%d candle=%s close=%.2f ema=%.2f",
                               (int)g_retracementTimeframe,
                               TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                               g_retracementRates[1].close,
                               g_retracementEMA[1]));
        }

      //--- After the pullback is armed, the EA waits for a closed bearish return cross.
      if(g_sellRetracementActive && CrossedDownRetracementEMA())
        {
         //--- The ATR distance filter rejects weak crosses before the order is sent.
         if(!EntryDistanceFromEMAByATRIsValid(false))
            return;

         LogEvent("SELL_TRIGGER_RETRACEMENT_EMA_CROSS",
                  StringFormat("tf=%d candle=%s close=%.2f ema=%.2f atr=%.2f",
                               (int)g_retracementTimeframe,
                               TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                               g_retracementRates[1].close,
                               g_retracementEMA[1],
                               g_retracementATR[1]));

         OpenTrade(false);
        }

      return;
     }

   ClearRetracements();
  }

//+------------------------------------------------------------------+
//| Market data update                                               |
//+------------------------------------------------------------------+
//--- This function refreshes all candles and indicator buffers used by the EA.
//--- Signals use shift 1, the last fully closed candle. Shift 0 is still copied
//--- because it is needed to detect when a new retracement bar begins.
bool UpdateMarketData()
  {
   int regimeBars = MathMax(120, regimeEMAPeriod + 20);
   int retracementLookback = MathMax(retracementEMAPeriod, entryATRPeriod);
   int retracementBars = MathMax(120, retracementLookback + 20);

   if(CopyRates(_Symbol, g_regimeTimeframe, 0, regimeBars, g_regimeRates) < regimeBars)
      return false;

   if(CopyBuffer(g_regimeEMAHandle, 0, 0, regimeBars, g_regimeEMA) < regimeBars)
      return false;

   if(CopyRates(_Symbol, g_retracementTimeframe, 0, retracementBars, g_retracementRates) < retracementBars)
      return false;

   if(CopyBuffer(g_retracementEMAHandle, 0, 0, retracementBars, g_retracementEMA) < retracementBars)
      return false;

   if(CopyBuffer(g_retracementATRHandle, 0, 0, retracementBars, g_retracementATR) < retracementBars)
      return false;

   return (ArraySize(g_regimeRates) > 2 &&
           ArraySize(g_retracementRates) > 2 &&
           ArraySize(g_regimeEMA) > 2 &&
           ArraySize(g_retracementEMA) > 2 &&
           ArraySize(g_retracementATR) > 2);
  }

//+------------------------------------------------------------------+
//| Optimizer value clamp                                            |
//+------------------------------------------------------------------+
double ClampOptimizerValue(const double value,
                           const double minimum,
                           const double maximum)
  {
   return MathMax(minimum, MathMin(maximum, value));
  }

//+------------------------------------------------------------------+
//| Optimization frames                                              |
//+------------------------------------------------------------------+
bool IsOptimizationPassSavingEnabled()
  {
   return saveOptimizationPasses;
  }

string OptimizationFramesCsvPrefix()
  {
   return (StringLen(csvExportPrefix) > 0 ? csvExportPrefix : "SimplePullback_WIN_GlobalOpt");
  }

void SaveOptimizationPass(const double score,
                          const double profit,
                          const double trades,
                          const double pf,
                          const double ddRel,
                          const double payoff,
                          const double recovery,
                          const double sharpe)
  {
   if(!IsOptimizationPassSavingEnabled())
      return;

   double data[8];
   data[0] = score;
   data[1] = profit;
   data[2] = trades;
   data[3] = pf;
   data[4] = ddRel;
   data[5] = payoff;
   data[6] = recovery;
   data[7] = sharpe;

   FrameAdd(optimizationTag, OPT_FRAME_ID, score, data);
  }

string FrameParameterName(const string parameter)
  {
   int pos = StringFind(parameter, "=");
   return (pos < 0 ? parameter : StringSubstr(parameter, 0, pos));
  }

string FrameParameterValue(const string parameter)
  {
   int pos = StringFind(parameter, "=");
   return (pos < 0 ? parameter : StringSubstr(parameter, pos + 1));
  }

bool OpenOptimizationFramesCsv()
  {
   if(g_frameCsvHandle != INVALID_HANDLE)
      return true;

   string fileName = OptimizationFramesCsvPrefix() + "_" + optimizationTag + "_frames.csv";
   g_frameCsvHandle = FileOpen(fileName, FILE_WRITE | FILE_CSV | FILE_ANSI | FILE_SHARE_READ, ';');

   if(g_frameCsvHandle == INVALID_HANDLE)
      return false;

   g_frameCsvHeaderDone = false;
   return true;
  }

void ProcessOptimizationFrames()
  {
   if(!IsOptimizationPassSavingEnabled() || !OpenOptimizationFramesCsv())
      return;

   ulong pass = 0;
   string name = "";
   long frameId = 0;
   double value = 0.0;
   double data[];

   while(FrameNext(pass, name, frameId, value, data))
     {
      if(frameId != OPT_FRAME_ID)
         continue;

      string parameters[];
      uint count = 0;

      if(!FrameInputs(pass, parameters, count))
         continue;

      if(!g_frameCsvHeaderDone)
        {
         string header = "tag;pass;score;profit;trades;pf;ddRel;payoff;recovery;sharpe";

         for(uint i = 0; i < count; i++)
            header += ";" + FrameParameterName(parameters[i]);

         FileWriteString(g_frameCsvHandle, header + "\r\n");
         g_frameCsvHeaderDone = true;
        }

      string line = name + ";" + IntegerToString((long)pass);

      for(int i = 0; i < 8; i++)
         line += ";" + DoubleToString(ArraySize(data) > i ? data[i] : 0.0, 8);

      for(uint i = 0; i < count; i++)
         line += ";" + FrameParameterValue(parameters[i]);

      FileWriteString(g_frameCsvHandle, line + "\r\n");
     }

   FileFlush(g_frameCsvHandle);
  }

int OnTesterInit()
  {
   if(IsOptimizationPassSavingEnabled())
      OpenOptimizationFramesCsv();

   return INIT_SUCCEEDED;
  }

void OnTesterPass()
  {
   ProcessOptimizationFrames();
  }

void OnTesterDeinit()
  {
   ProcessOptimizationFrames();

   if(g_frameCsvHandle != INVALID_HANDLE)
     {
      FileClose(g_frameCsvHandle);
      g_frameCsvHandle = INVALID_HANDLE;
     }
  }

double OnTester()
  {
   if(!ParametersAreValid())
      return -1000000000.0;

   double profit = TesterStatistics(STAT_PROFIT);
   double trades = TesterStatistics(STAT_TRADES);
   double pf = TesterStatistics(STAT_PROFIT_FACTOR);
   double ddRel = TesterStatistics(STAT_EQUITY_DDREL_PERCENT);
   double recovery = TesterStatistics(STAT_RECOVERY_FACTOR);
   double payoff = TesterStatistics(STAT_EXPECTED_PAYOFF);
   double sharpe = TesterStatistics(STAT_SHARPE_RATIO);

   if(trades <= 0.0)
     {
      SaveOptimizationPass(-1000000.0, profit, trades, pf, ddRel, payoff, recovery, sharpe);
      return -1000000.0;
     }

   if(profit <= 0.0)
     {
      double scoreLoss = -MathMax(MathAbs(profit), 1.0) * (1.0 + ddRel / 25.0);
      SaveOptimizationPass(scoreLoss, profit, trades, pf, ddRel, payoff, recovery, sharpe);
      return scoreLoss;
     }

   double minTrades = MathMax(1.0, (double)minTradesOnTester);
   double tradesRef = MathMax(minTrades, (double)referenceTradesOnTester);
   double tradeRatio = ClampOptimizerValue(trades / tradesRef, 0.0, 1.0);
   double tradeScore = 0.45 + 0.55 * MathSqrt(tradeRatio);

   if(trades < minTrades)
      tradeScore *= 0.40 + 0.60 * MathSqrt(ClampOptimizerValue(trades / minTrades, 0.0, 1.0));

   double cappedPf = MathMin(pf, 5.0);
   double pfRef = MathMax(pfMinOnTester + 0.15, 1.55);
   double pfScore = 0.80 + 0.20 * ClampOptimizerValue((cappedPf - 1.0) / MathMax(0.01, pfRef - 1.0), 0.0, 1.0);

   if(pf < pfMinOnTester)
      pfScore *= MathMax(0.10, MathPow(MathMax(0.01, pf / MathMax(1.0, pfMinOnTester)), 3.0));

   double ddPenalty = 1.0 / (1.0 + MathPow(ddRel / 18.0, 1.60));

   if(ddRel > ddRelMaxOnTester)
      ddPenalty *= MathMax(0.10, ddRelMaxOnTester / MathMax(ddRel, 0.01));

   double payoffScore = 0.96 + 0.04 * ClampOptimizerValue(payoff / MathMax(1.0, profit / minTrades), 0.0, 1.0);
   double recoveryScore = 0.94 + 0.06 * ClampOptimizerValue(recovery / 3.0, 0.0, 1.0);
   double sharpeScore = 0.95 + 0.05 * ClampOptimizerValue(MathMax(sharpe, 0.0) / 2.0, 0.0, 1.0);
   double score = profit * tradeScore * pfScore * ddPenalty * payoffScore * recoveryScore * sharpeScore;

   SaveOptimizationPass(score, profit, trades, pf, ddRel, payoff, recovery, sharpe);
   return score;
  }

//+------------------------------------------------------------------+
//| New retracement bar                                              |
//+------------------------------------------------------------------+
//--- Entry evaluation is allowed only once per new retracement bar.
//--- Because arrays are series-based, when a new bar appears, shift 1 becomes
//--- the candle that just closed and can safely be used as the signal candle.
bool NewRetracementBar()
  {
   if(ArraySize(g_retracementRates) <= 0)
      return false;

   datetime currentBarTime = g_retracementRates[0].time;

   if(currentBarTime == g_lastRetracementBarTime)
      return false;

   g_lastRetracementBarTime = currentBarTime;
   return true;
  }

//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
//--- Initialization validates the parameters, converts timeframe inputs, reads
//--- the symbol tick size, creates EMA/ATR handles, and prepares all buffers as
//--- series arrays so shift 1 always points to the last closed candle.
int OnInit()
  {
   if(!ParametersAreValid())
     {
      Print("Invalid parameters in SimplePullback_RR_Management.");
      return INIT_PARAMETERS_INCORRECT;
     }

   g_regimeTimeframe = TimeframeFromIndex(regimeTimeframeIndex);
   g_retracementTimeframe = TimeframeFromIndex(retracementTimeframeIndex);

   g_tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);

   if(g_tickSize <= 0.0)
      g_tickSize = _Point;

   g_regimeEMAHandle = iMA(_Symbol,
                           g_regimeTimeframe,
                           regimeEMAPeriod,
                           0,
                           MODE_EMA,
                           PRICE_CLOSE);

   g_retracementEMAHandle = iMA(_Symbol,
                                g_retracementTimeframe,
                                retracementEMAPeriod,
                                0,
                                MODE_EMA,
                                PRICE_CLOSE);

   g_retracementATRHandle = iATR(_Symbol,
                                 g_retracementTimeframe,
                                 entryATRPeriod);

   if(g_regimeEMAHandle == INVALID_HANDLE ||
      g_retracementEMAHandle == INVALID_HANDLE ||
      g_retracementATRHandle == INVALID_HANDLE)
      return INIT_FAILED;

   ArraySetAsSeries(g_regimeEMA, true);
   ArraySetAsSeries(g_retracementEMA, true);
   ArraySetAsSeries(g_retracementATR, true);
   ArraySetAsSeries(g_regimeRates, true);
   ArraySetAsSeries(g_retracementRates, true);

   Print("EA initialized: SimplePullback_RR_Management | Regime TF=",
         (int)g_regimeTimeframe,
         " | Retracement TF=",
         (int)g_retracementTimeframe,
         " | Stop ticks=",
         stopLossTicks,
         " | Fixed TP=",
         (useFixedTakeProfit ? "true" : "false"),
         " | BE=",
         (useBreakEven ? "true" : "false"),
         " | Trailing=",
         (useTrailingStop ? "true" : "false"),
         " | ATR EMA distance filter=",
         (useEntryDistanceFromEMAByATR ? "true" : "false"));

   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Deinitialization                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(g_regimeEMAHandle != INVALID_HANDLE)
      IndicatorRelease(g_regimeEMAHandle);

   if(g_retracementEMAHandle != INVALID_HANDLE)
      IndicatorRelease(g_retracementEMAHandle);

   if(g_retracementATRHandle != INVALID_HANDLE)
      IndicatorRelease(g_retracementATRHandle);
  }

//+------------------------------------------------------------------+
//| Main tick loop                                                   |
//+------------------------------------------------------------------+
//--- OnTick separates execution into two different speeds.
//--- Position management is tick-based because stops, BE and trailing depend on
//--- executable Bid/Ask prices. Entry logic is closed-bar based and runs only
//--- after NewRetracementBar confirms that a signal candle has closed.
void OnTick()
  {
   if(!SymbolInfoTick(_Symbol, g_tick))
      return;

   g_bid = g_tick.bid;
   g_ask = g_tick.ask;

   if(g_bid <= 0.0 || g_ask <= 0.0)
      return;

   if(!UpdateMarketData())
      return;

//--- End-of-day protection.
   if(MustForceCloseEndOfDay(g_tick.time))
     {
      ForceCloseAllPositions();
      ClearRetracements();
      return;
     }

//--- Regime and exit are updated on every tick,
//--- but they only use closed-bar values.
   UpdateRegime();
   ManageExitByRegime();

//--- RR-based management is tick-based after the position is open.
//--- Entry signals remain closed-bar only.
   ManageRRBasedPositionManagement();

//--- Entry logic is evaluated only on a new retracement timeframe bar.
//--- This guarantees that the signal candle is closed.
   if(!NewRetracementBar())
      return;

   UpdateRegime();
   ManageExitByRegime();
   ManageRRBasedPositionManagement();

   if(!EntryTimeAllowed(g_tick.time))
      return;

   UpdateRetracementAndTrigger();
  }

//+------------------------------------------------------------------+
//| Trade transaction handler                                        |
//+------------------------------------------------------------------+
//--- When an EA position is closed, old pullback states are cleared.
//--- This prevents the next trade from reusing a setup that belonged to a
//--- previous position context.
void OnTradeTransaction(const MqlTradeTransaction &transaction,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
  {
   if(transaction.type != TRADE_TRANSACTION_DEAL_ADD ||
      transaction.deal == 0)
      return;

   if(!HistoryDealSelect(transaction.deal))
      return;

   if(HistoryDealGetString(transaction.deal, DEAL_SYMBOL) != _Symbol)
      return;

   ulong magic = (ulong)HistoryDealGetInteger(transaction.deal, DEAL_MAGIC);

   if(!IsEAMagic(magic))
      return;

   ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(transaction.deal, DEAL_ENTRY);

   bool isExit = (entry == DEAL_ENTRY_OUT ||
                  entry == DEAL_ENTRY_INOUT ||
                  entry == DEAL_ENTRY_OUT_BY);

   if(!isExit)
      return;

   if(!HasEAExposure())
     {
      ClearRetracements();
      LogEvent("TRADE_CLOSED_CONTEXT_RESET");
     }
  }
//+------------------------------------------------------------------+
