//+------------------------------------------------------------------+
//|                                   OneEMA_LooseCore_GlobalOpt.mq5 |
//|                                                  Samuel Ferreira |
//|                                        samuelsf2014@yahoo.com.br |
//+------------------------------------------------------------------+
#property copyright "Samuel Ferreira"
#property link      "samuelsf2014@yahoo.com.br"
#property version   "1.20"
#property strict

//+------------------------------------------------------------------+
//| Optimizer integration                                             |
//+------------------------------------------------------------------+
input string optimizationTag = "ONEEMA_LOOSECORE_GLOBAL";                // Tag used to identify optimizer exports.
input bool   saveOptimizationPasses = true;                               // Writes optimization passes to CSV frames.
input string csvExportPrefix = "OneEMA_LooseCore_GlobalOpt";              // Prefix used by optimization CSV files.
input int    dummyOptimizationPass = 0;                                   // Reserved input for optimizer compatibility.

//+------------------------------------------------------------------+
//| Selectable timeframe enumeration.                                |
//| Integer enum values are converted into real MetaTrader 5         |
//| timeframes, keeping timeframe testing simple and readable.       |
//+------------------------------------------------------------------+
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.
  };

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

input int minTradesOnTester = 300;                                        // Minimum trade count used by the custom tester score.
input int referenceTradesOnTester = 900;                                  // Reference trade count used by the custom tester score.
input double pfMinOnTester = 1.04;                                        // Minimum profit factor used by the custom tester score.
input double ddRelMaxOnTester = 30.0;                                     // Maximum relative drawdown used by the custom tester score.

//+------------------------------------------------------------------+
//| Main degrees of freedom.                                         |
//| Timeframes, EMAs, and optional trend-strength filters.           |
//| All new filters are disabled by default.                         |
//+------------------------------------------------------------------+
//--- Timeframes are intentionally controlled by the JSON execution profile.
input ENUM_CORE_TIMEFRAME_INDEX regimeTimeframeIndex = CORE_TF_M15;        // Timeframe used to define direction.
input ENUM_CORE_TIMEFRAME_INDEX retracementTimeframeIndex = CORE_TF_M1;    // Timeframe used for pullback and trigger.

//==================================================================
// @group G1_STRUCTURE
// @fixed subgroup=G1A_CORE_EMAS group=G1_STRUCTURE regimeTimeframeIndex=CORE_TF_M15 retracementTimeframeIndex=CORE_TF_M1
// @subgroup G1A_CORE_EMAS
//==================================================================
// @wfe1
// @opt 50 5 105
input int regimeEMAPeriod = 84;                                           // EMA period used for the regime filter.
// @wfe2
// @opt 15 5 50
input int retracementEMAPeriod = 25;                                      // EMA period used for pullback and trigger.

//--- Optional trend-strength filters. They are disabled by default so the EA
//--- keeps the original OneEMA_LooseCore behavior unless explicitly enabled.
//==================================================================
// @group G1_STRUCTURE
// @subgroup G1B_REGIME_SLOPE
//==================================================================
// @fixed
input int regimeATRPeriod = 14;                                           // ATR period used by regime filters and slope normalization.

// @wfe1
// @opt false true
input bool useRegimeSlopeFilter = false;                                  // Enables the normalized regime EMA slope filter.
// @wfe2
// @opt 3 1 7
input int regimeSlopeBars = 5;                                            // Closed regime candles used to calculate EMA slope.
// @wfe3
// @opt 0.00 0.05 0.15
input double regimeSlopeMinAtr = 0.10;                                    // Minimum regime EMA slope expressed in ATR units.

//--- Optional regime price distance filter.
//--- Buy regime requires the closed regime candle to be above the regime EMA by
//--- at least regimePriceDistanceMinAtr * regime ATR.
//--- Sell regime uses the symmetrical rule below the regime EMA.
//--- Set regimePriceDistanceMaxAtr to 0 to disable the maximum-distance cap.
//==================================================================
// @group G1_STRUCTURE
// @fixed subgroup=G1C_REGIME_ATR_MIN_MAX group=G1_STRUCTURE regimeATRPeriod=14
// @subgroup G1C_REGIME_ATR_MIN_MAX
//==================================================================
// @wfe1
// @opt false true
input bool useRegimePriceDistanceFromEMAByATR = false;                    // Enables the regime price-distance channel.
// @wfe2
// @opt 0.00 0.10 0.30
input double regimePriceDistanceMinAtr = 0.10;                            // Minimum price distance from regime EMA in ATR units.
// @wfe3
// @opt 0.00 1.00 4.00
input double regimePriceDistanceMaxAtr = 0.00;                            // Maximum price distance in ATR units; 0 disables the cap.

//==================================================================
// @group G2_RETRACEMENT_TRIGGER
// @subgroup G2A_RETRACEMENT_SLOPE
//==================================================================
// @wfe1
// @opt false true
input bool useRetracementSlopeFilter = false;                             // Enables the normalized retracement EMA slope filter.
// @wfe2
// @opt 2 1 5
input int retracementSlopeBars = 3;                                       // Closed retracement candles used to calculate EMA slope.
// @wfe3
// @opt -0.05 0.05 0.15
input double retracementSlopeMinAtr = 0.00;                               // Minimum retracement EMA slope expressed in ATR units.

//--- Optional entry filter based on ATR distance from the retracement EMA.
//--- When enabled, buy entries require the signal close to be above the EMA
//--- by at least entryDistanceFromEMAAtr * ATR.
//--- Sell entries require the signal close to be below the EMA by the same logic.
//==================================================================
// @group G2_ATR_TRIGGER
// @subgroup G2B_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.

//--- Optional loosening of the ATR distance trigger.
//--- If the EMA return cross happens but the ATR distance is still too small,
//--- the setup can wait a few retracement-timeframe candles for the distance
//--- confirmation instead
//--- of dying immediately on the cross candle.
//==================================================================
// @group G2_ATR_TRIGGER
// @subgroup G2C_LOOSE_ATR_WINDOW
//==================================================================
// @wfe1
// @opt false true
input bool useLooseATRTriggerWindow = false;                              // Allows ATR confirmation during a retracement-candle window.
// @wfe2
// @opt 2 1 6
input int looseATRTriggerWindowRetracementCandles = 3;                    // Number of retracement candles available for confirmation.

//==================================================================
// @group G3_RISK
// @subgroup G3A_INITIAL_STOP
//==================================================================
//--- Initial stop loss distance measured in symbol ticks, not in points.
//--- Example: if tick size is 5.0 and stopLossTicks is 100,
//--- the requested stop distance will be 500 price units.
//--- If this distance is below the broker minimum stop level,
//--- the EA automatically moves the SL to the nearest valid level.
//--- Set to 0 to disable the initial stop loss.
// @wfe
// @opt 40 10 120
input int stopLossTicks = 80;                                             // Initial stop in symbol ticks; 0 disables initial SL.

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

//==================================================================
// @group G4_MANAGEMENT
// @subgroup G4A_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.00;                                      // Profit in R locked after break-even activation.

//==================================================================
// @group G4_MANAGEMENT
// @subgroup G4B_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.80;                                   // Trailing distance from executable price in R.

//+------------------------------------------------------------------+
//| Day trade window settings.                                       |
//| Entries are allowed only inside the window, and positions are    |
//| force-closed at the end of the day.                              |
//+------------------------------------------------------------------+
input bool useDaytradeWindow = true;                                      // Enables the intraday trading window.

input int daytradeStartMinute = 540;                                      // 09:00 = 540.

input int lastEntryMinute = 1020;                                         // Last minute in which a new entry is allowed.

input int forcedCloseMinute = 1050;                                       // 17:30 = 1050.

input bool forceCloseAtEndOfDay = true;                                   // Closes EA positions at the end of the day.

//+------------------------------------------------------------------+
//| Global EA state.                                                 |
//| Internal variables, indicator handles, buffers, and control      |
//| flags.                                                           |
//+------------------------------------------------------------------+

ENUM_TIMEFRAMES g_regimeTimeframe = PERIOD_M5;
ENUM_TIMEFRAMES g_retracementTimeframe = PERIOD_M1;

int g_regimeEMAHandle = INVALID_HANDLE;
int g_retracementEMAHandle = INVALID_HANDLE;
int g_regimeATRHandle = INVALID_HANDLE;
int g_retracementATRHandle = INVALID_HANDLE;

double g_regimeEMA[];
double g_retracementEMA[];
double g_regimeATR[];
double g_retracementATR[];

MqlRates g_regimeRates[];
MqlRates g_retracementRates[];
MqlTick g_tick;

double g_bid = 0.0;
double g_ask = 0.0;

//--- Symbol tick size used to convert stopLossTicks into a price distance.
double g_tickSize = 0.0;

ulong g_deviation = 1;

datetime g_lastRetracementBarTime = 0;
datetime g_lastEntryAttemptBarTime = 0;
datetime g_lastBlockedLogCandleTime = 0;

bool g_buyRegimeActive = false;
bool g_sellRegimeActive = false;

bool g_buyRetracementActive = false;
bool g_sellRetracementActive = false;
datetime g_buyLooseATRTriggerStartRetracementBar = 0;
datetime g_sellLooseATRTriggerStartRetracementBar = 0;
bool g_buyLooseATRExpiredWaitRetracementReset = false;
bool g_sellLooseATRExpiredWaitRetracementReset = false;

bool g_closingBuy = false;
bool g_closingSell = false;

#define OPT_FRAME_ID 220260
int g_frameCsvHandle = INVALID_HANDLE;
bool g_frameCsvHeaderDone = false;

//+------------------------------------------------------------------+
//| Converts the timeframe index into a real MT5 timeframe.          |
//| This keeps timeframe selection simple and readable.              |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Detects how many decimal places are needed for volume.           |
//| This helps normalize lot size according to the symbol step.      |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Normalizes volume to the symbol minimum, maximum, and step.      |
//+------------------------------------------------------------------+
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());
  }

//+------------------------------------------------------------------+
//| Normalizes a price to the symbol digits.                         |
//+------------------------------------------------------------------+
double NormalizePriceDigits(const double price)
  {
   return NormalizeDouble(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
  }

//+------------------------------------------------------------------+
//| Rounds a price down to the nearest valid tick.                    |
//| Used for buy stop losses, which must stay below the market.       |
//+------------------------------------------------------------------+
double NormalizePriceDownToTick(const double price)
  {
   if(g_tickSize <= 0.0)
      return NormalizePriceDigits(price);

   return NormalizePriceDigits(MathFloor(price / g_tickSize) * g_tickSize);
  }

//+------------------------------------------------------------------+
//| Rounds a price up to the nearest valid tick.                      |
//| Used for sell stop losses, which must stay above the market.      |
//+------------------------------------------------------------------+
double NormalizePriceUpToTick(const double price)
  {
   if(g_tickSize <= 0.0)
      return NormalizePriceDigits(price);

   return NormalizePriceDigits(MathCeil(price / g_tickSize) * g_tickSize);
  }

//+------------------------------------------------------------------+
//| Returns the broker minimum stop distance in price units.          |
//| SYMBOL_TRADE_STOPS_LEVEL is given in points, not in ticks.        |
//+------------------------------------------------------------------+
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 avoids invalid stop errors caused by exact limits.
//--- It also guarantees that the SL stays on the correct side of Bid/Ask
//--- even when the broker reports zero as the minimum stop level.
   return minimumDistance + g_tickSize;
  }

//+------------------------------------------------------------------+
//| Returns the configured 1R distance in price units.               |
//+------------------------------------------------------------------+
double ConfiguredInitialRiskDistance()
  {
   if(stopLossTicks <= 0 || g_tickSize <= 0.0)
      return 0.0;

   return (double)stopLossTicks * g_tickSize;
  }

//+------------------------------------------------------------------+
//| Calculates the initial stop loss price.                          |
//| Converts stopLossTicks into a valid stop-loss price.             |
//| The final SL also respects the broker minimum stop level.        |
//+------------------------------------------------------------------+
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;

//--- For buy orders, the SL must be below the current Bid by at least
//--- the broker minimum stop distance.
      if(brokerDistance > 0.0)
        {
         double highestValidStop = g_bid - brokerDistance;

         if(stopPrice > highestValidStop)
            stopPrice = highestValidStop;
        }

      return NormalizePriceDownToTick(stopPrice);
     }

   if(orderType == ORDER_TYPE_SELL)
     {
      double stopPrice = entryPrice + requestedDistance;

//--- For sell orders, the SL must be above the current Ask by at least
//--- the broker minimum stop distance.
      if(brokerDistance > 0.0)
        {
         double lowestValidStop = g_ask + brokerDistance;

         if(stopPrice < lowestValidStop)
            stopPrice = lowestValidStop;
        }

      return NormalizePriceUpToTick(stopPrice);
     }

   return 0.0;
  }

//+------------------------------------------------------------------+
//| Calculates the optional fixed take profit price.                 |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Validates the optional fixed take profit against broker rules.   |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Converts a datetime value into minutes since midnight.           |
//| Used to control the day trade window.                            |
//+------------------------------------------------------------------+
int MinutesOfDay(const datetime when)
  {
   MqlDateTime timeStruct;
   TimeToStruct(when, timeStruct);

   return timeStruct.hour * 60 + timeStruct.min;
  }

//+------------------------------------------------------------------+
//| Checks whether new entries are allowed at the current time.      |
//+------------------------------------------------------------------+
bool EntryTimeAllowed(const datetime when)
  {
   if(!useDaytradeWindow)
      return true;

   int minute = MinutesOfDay(when);
   return (minute >= daytradeStartMinute && minute < lastEntryMinute);
  }

//+------------------------------------------------------------------+
//| Checks whether the EA must force close all positions.            |
//| This protects the strategy from carrying trades overnight.       |
//+------------------------------------------------------------------+
bool MustForceCloseEndOfDay(const datetime when)
  {
   return (useDaytradeWindow &&
           forceCloseAtEndOfDay &&
           MinutesOfDay(when) >= forcedCloseMinute);
  }

//+------------------------------------------------------------------+
//| Converts trade direction into text for optional logs.            |
//+------------------------------------------------------------------+
string DirectionName(const bool isBuy)
  {
   return (isBuy ? "BUY" : "SELL");
  }

//+------------------------------------------------------------------+
//| Writes optional structured logs when status logs are enabled.    |
//+------------------------------------------------------------------+
void LogEvent(const string eventName, const string details = "")
  {
   if(!enableStatusLogs)
      return;

   string line = StringFormat("[OneEMA_LooseCore][%s][%s]", _Symbol, eventName);

   if(StringLen(details) > 0)
      line += " | " + details;

   Print(line);
  }

//+------------------------------------------------------------------+
//| Returns the closed candle used to throttle blocked-entry logs.    |
//+------------------------------------------------------------------+
datetime CurrentBlockedLogCandleTime()
  {
   if(ArraySize(g_retracementRates) > 1 && g_retracementRates[1].time > 0)
      return g_retracementRates[1].time;

   if(ArraySize(g_regimeRates) > 1 && g_regimeRates[1].time > 0)
      return g_regimeRates[1].time;

   return 0;
  }

//+------------------------------------------------------------------+
//| Writes at most one blocked-entry message per closed signal candle.|
//+------------------------------------------------------------------+
void LogBlockedEventOncePerCandle(const string eventName, const string details = "")
  {
   if(!enableStatusLogs)
      return;

   datetime candleTime = CurrentBlockedLogCandleTime();

   if(candleTime > 0)
     {
      if(g_lastBlockedLogCandleTime == candleTime)
         return;

      g_lastBlockedLogCandleTime = candleTime;
     }

   LogEvent(eventName, details);
  }

//+------------------------------------------------------------------+
//| Validates the basic input parameters before initialization.      |
//+------------------------------------------------------------------+
bool ParametersAreValid()
  {
   if(regimeEMAPeriod <= 0)
      return false;

   if(retracementEMAPeriod <= 0)
      return false;

   if(regimeATRPeriod <= 0)
      return false;

   if(regimeSlopeBars < 1)
      return false;

   if(regimeSlopeMinAtr < 0.0)
      return false;

   if(retracementSlopeBars < 1)
      return false;

   if(regimePriceDistanceMinAtr < 0.0)
      return false;

   if(regimePriceDistanceMaxAtr < 0.0)
      return false;

   if(regimePriceDistanceMaxAtr > 0.0 &&
      regimePriceDistanceMaxAtr < regimePriceDistanceMinAtr)
      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(useLooseATRTriggerWindow && looseATRTriggerWindowRetracementCandles < 1)
      return false;

   if(daytradeStartMinute < 0 ||
      lastEntryMinute <= daytradeStartMinute ||
      forcedCloseMinute < lastEntryMinute ||
      forcedCloseMinute > 1440)
      return false;

   return true;
  }

//+------------------------------------------------------------------+
//| Checks whether a magic number belongs to this EA.                |
//+------------------------------------------------------------------+
bool IsEAMagic(const ulong magic)
  {
   return (magic == buyMagic || magic == sellMagic);
  }

//+------------------------------------------------------------------+
//| Finds an open position by magic number and direction.            |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Checks whether this EA has any open position on this symbol.     |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Checks whether any position exists on the current symbol.        |
//| 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;
  }

//+------------------------------------------------------------------+
//| Checks whether this EA has an open buy position.                 |
//+------------------------------------------------------------------+
bool HasBuyPosition()
  {
   ulong ticket = 0;
   return SelectPositionByMagic(buyMagic, POSITION_TYPE_BUY, ticket);
  }

//+------------------------------------------------------------------+
//| Checks whether this EA has an open sell position.                |
//+------------------------------------------------------------------+
bool HasSellPosition()
  {
   ulong ticket = 0;
   return SelectPositionByMagic(sellMagic, POSITION_TYPE_SELL, ticket);
  }

//+------------------------------------------------------------------+
//| Checks whether free margin is enough for the requested order.    |
//+------------------------------------------------------------------+
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);
  }

//+------------------------------------------------------------------+
//| Sends a market order with optional initial stop and take profit. |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Modifies SL/TP protection for an existing position.              |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Checks whether a candidate SL improves current protection.       |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Clamps a managed SL to the closest broker-valid price.           |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Converts current open profit into R multiples.                   |
//+------------------------------------------------------------------+
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.                                      |
//+------------------------------------------------------------------+
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.                                        |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Applies BE/trailing management to one position.                  |
//+------------------------------------------------------------------+
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;
   double breakEvenStop = BreakEvenStopCandidate(positionType, openPrice);

   if(StopLossImprovesProtection(positionType, bestStopLoss, breakEvenStop))
      bestStopLoss = breakEvenStop;

   if(useTrailingStop && currentRR >= trailingActivationRR)
     {
      double trailingStop = TrailingStopCandidate(positionType);

      if(StopLossImprovesProtection(positionType, bestStopLoss, trailingStop))
         bestStopLoss = trailingStop;
     }

   if(!StopLossImprovesProtection(positionType, currentStopLoss, bestStopLoss))
      return;

   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));
  }

//+------------------------------------------------------------------+
//| Processes RR-based management on every tick.                     |
//+------------------------------------------------------------------+
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);
     }
  }

//+------------------------------------------------------------------+
//| Closes an existing 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);
  }

//+------------------------------------------------------------------+
//| Closes the EA buy position if it exists.                         |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Closes the EA sell position if it exists.                        |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Forces all EA positions to close at the end of the day.          |
//+------------------------------------------------------------------+
void ForceCloseAllPositions()
  {
   bool hadExposure = HasEAExposure();

   if(HasBuyPosition())
      CloseBuyPosition("end_of_day");

   if(HasSellPosition())
      CloseSellPosition("end_of_day");

   if(hadExposure)
      LogEvent("FORCED_DAYTRADE_CLOSE");
  }

//+------------------------------------------------------------------+
//| Optional regime EMA slope filter normalized by regime ATR.       |
//+------------------------------------------------------------------+
bool RegimeSlopeFilterIsValid(const bool isBuy)
  {
   if(!useRegimeSlopeFilter)
      return true;

   int slopeShift = 1 + regimeSlopeBars;

   if(ArraySize(g_regimeEMA) <= slopeShift || ArraySize(g_regimeATR) <= 1)
      return false;

   double atr = g_regimeATR[1];

   if(atr <= 0.0)
      return false;

   double slope = g_regimeEMA[1] - g_regimeEMA[slopeShift];
   double requiredSlope = regimeSlopeMinAtr * atr;
   bool valid = false;

   if(isBuy)
      valid = (slope >= requiredSlope);
   else
      valid = (slope <= -requiredSlope);

   if(!valid)
      LogBlockedEventOncePerCandle((isBuy ? "BUY_REGIME_SLOPE_BLOCKED" : "SELL_REGIME_SLOPE_BLOCKED"),
                                   StringFormat("bars=%d slope=%.2f atr=%.2f required_atr=%.2f required_price=%.2f",
                                                regimeSlopeBars,
                                                slope,
                                                atr,
                                                regimeSlopeMinAtr,
                                                requiredSlope));

   return valid;
  }

//+------------------------------------------------------------------+
//| Optional regime price distance filter normalized by regime ATR.  |
//+------------------------------------------------------------------+
bool RegimePriceDistanceFromEMAByATRIsValid(const bool isBuy)
  {
   if(!useRegimePriceDistanceFromEMAByATR)
      return true;

   if(ArraySize(g_regimeRates) <= 1 ||
      ArraySize(g_regimeEMA) <= 1 ||
      ArraySize(g_regimeATR) <= 1)
      return false;

   double atr = g_regimeATR[1];

   if(atr <= 0.0)
      return false;

   double close = g_regimeRates[1].close;
   double ema = g_regimeEMA[1];
   double distance = (isBuy ? close - ema : ema - close);
   double minDistance = regimePriceDistanceMinAtr * atr;
   double maxDistance = regimePriceDistanceMaxAtr * atr;
   bool valid = (distance >= minDistance);

   if(valid && regimePriceDistanceMaxAtr > 0.0)
      valid = (distance <= maxDistance);

   if(!valid)
      LogBlockedEventOncePerCandle((isBuy ? "BUY_REGIME_PRICE_ATR_BLOCKED" : "SELL_REGIME_PRICE_ATR_BLOCKED"),
                                   StringFormat("close=%.2f ema=%.2f atr=%.2f distance=%.2f min_atr=%.2f max_atr=%.2f",
                                                close,
                                                ema,
                                                atr,
                                                distance,
                                                regimePriceDistanceMinAtr,
                                                regimePriceDistanceMaxAtr));

   return valid;
  }

//+------------------------------------------------------------------+
//| Checks whether the buy regime is active.                         |
//| The EA uses only the last fully closed candle on the regime TF.  |
//| A buy regime exists when the candle closes above the regime EMA. |
//+------------------------------------------------------------------+
bool BuyRegime()
  {
   if(ArraySize(g_regimeRates) <= 1 || ArraySize(g_regimeEMA) <= 1)
      return false;

   if(g_regimeRates[1].close <= g_regimeEMA[1])
      return false;

   return (RegimeSlopeFilterIsValid(true) &&
           RegimePriceDistanceFromEMAByATRIsValid(true));
  }

//+------------------------------------------------------------------+
//| Checks whether the sell regime is active.                        |
//| The EA uses only the last fully closed candle on the regime TF.  |
//| A sell regime exists when the candle closes below the regime EMA.|
//+------------------------------------------------------------------+
bool SellRegime()
  {
   if(ArraySize(g_regimeRates) <= 1 || ArraySize(g_regimeEMA) <= 1)
      return false;

   if(g_regimeRates[1].close >= g_regimeEMA[1])
      return false;

   return (RegimeSlopeFilterIsValid(false) &&
           RegimePriceDistanceFromEMAByATRIsValid(false));
  }

//+------------------------------------------------------------------+
//| Clears the buy retracement state.                                |
//+------------------------------------------------------------------+
void ClearBuyRetracement()
  {
   g_buyRetracementActive = false;
   g_buyLooseATRTriggerStartRetracementBar = 0;
   g_buyLooseATRExpiredWaitRetracementReset = false;
  }

//+------------------------------------------------------------------+
//| Clears the sell retracement state.                               |
//+------------------------------------------------------------------+
void ClearSellRetracement()
  {
   g_sellRetracementActive = false;
   g_sellLooseATRTriggerStartRetracementBar = 0;
   g_sellLooseATRExpiredWaitRetracementReset = false;
  }

//+------------------------------------------------------------------+
//| Clears both buy and sell retracement states.                     |
//+------------------------------------------------------------------+
void ClearRetracements()
  {
   ClearBuyRetracement();
   ClearSellRetracement();
  }

//+------------------------------------------------------------------+
//| Updates the active regime state using closed candles only.       |
//| When the regime changes, old retracement states are cleared      |
//| so the EA does not reuse signals from a previous context.        |
//+------------------------------------------------------------------+
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();
  }

//+------------------------------------------------------------------+
//| Manages exits by regime invalidation.                            |
//| Buy positions are closed when the regime turns bearish.          |
//| Sell positions are closed when the regime turns bullish.         |
//+------------------------------------------------------------------+
void ManageExitByRegime()
  {
   if(HasBuyPosition() && SellRegime())
     {
      CloseBuyPosition("regime_reversed_to_sell");
      ClearRetracements();
     }

   if(HasSellPosition() && BuyRegime())
     {
      CloseSellPosition("regime_reversed_to_buy");
      ClearRetracements();
     }
  }

//+------------------------------------------------------------------+
//| Detects a buy retracement candle.                                |
//| In a buy regime, retracement is identified when the closed price |
//| is below or equal to the retracement EMA.                        |
//+------------------------------------------------------------------+
bool BuyRetracementCandle()
  {
   if(ArraySize(g_retracementRates) <= 1 || ArraySize(g_retracementEMA) <= 1)
      return false;

   return (g_retracementRates[1].close <= g_retracementEMA[1]);
  }

//+------------------------------------------------------------------+
//| Detects a sell retracement candle.                               |
//| In a sell regime, retracement is identified when the closed      |
//| price is above or equal to the retracement EMA.                  |
//+------------------------------------------------------------------+
bool SellRetracementCandle()
  {
   if(ArraySize(g_retracementRates) <= 1 || ArraySize(g_retracementEMA) <= 1)
      return false;

   return (g_retracementRates[1].close >= g_retracementEMA[1]);
  }

//+------------------------------------------------------------------+
//| Detects a bullish cross back through the retracement EMA.        |
//| The previous closed candle must be below or equal to the EMA,    |
//| while the latest closed candle must close above it.              |
//+------------------------------------------------------------------+
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);
  }

//+------------------------------------------------------------------+
//| Detects a bearish cross back through the retracement EMA.        |
//| The previous closed candle must be above or equal to the EMA,    |
//| while the latest closed candle must close below it.              |
//+------------------------------------------------------------------+
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);
  }

//+------------------------------------------------------------------+
//| Checks the ATR distance from the retracement EMA.                |
//| This optional filter confirms that the signal candle closed far  |
//| enough beyond the EMA before the EA opens a market position.     |
//+------------------------------------------------------------------+
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)
          LogBlockedEventOncePerCandle("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)
      LogBlockedEventOncePerCandle("SELL_ATR_EMA_DISTANCE_BLOCKED",
                                   StringFormat("close=%.2f ema=%.2f atr=%.2f required=%.2f",
                                                signalClose,
                                                ema,
                                                atr,
                                                requiredDistance));

   return valid;
  }

//+------------------------------------------------------------------+
//| Optional retracement EMA slope filter normalized by entry ATR.   |
//+------------------------------------------------------------------+
bool RetracementSlopeFilterIsValid(const bool isBuy)
  {
   if(!useRetracementSlopeFilter)
      return true;

   int slopeShift = 1 + retracementSlopeBars;

   if(ArraySize(g_retracementEMA) <= slopeShift || ArraySize(g_retracementATR) <= 1)
      return false;

   double atr = g_retracementATR[1];

   if(atr <= 0.0)
      return false;

   double slope = g_retracementEMA[1] - g_retracementEMA[slopeShift];
   double requiredSlope = retracementSlopeMinAtr * atr;
   bool valid = false;

   if(isBuy)
      valid = (slope >= requiredSlope);
   else
      valid = (slope <= -requiredSlope);

   if(!valid)
      LogBlockedEventOncePerCandle((isBuy ? "BUY_RETRACEMENT_SLOPE_BLOCKED" : "SELL_RETRACEMENT_SLOPE_BLOCKED"),
                                   StringFormat("bars=%d slope=%.2f atr=%.2f required_atr=%.2f required_price=%.2f",
                                                retracementSlopeBars,
                                                slope,
                                                atr,
                                                retracementSlopeMinAtr,
                                                requiredSlope));

   return valid;
  }

//+------------------------------------------------------------------+
//| Starts the relaxed ATR trigger window on the retracement TF.     |
//+------------------------------------------------------------------+
void ArmLooseATRTriggerWindow(const bool isBuy)
  {
   datetime currentRetracementBar = iTime(_Symbol, g_retracementTimeframe, 0);

   if(currentRetracementBar <= 0)
      currentRetracementBar = TimeCurrent();

   if(isBuy)
      g_buyLooseATRTriggerStartRetracementBar = currentRetracementBar;
   else
      g_sellLooseATRTriggerStartRetracementBar = currentRetracementBar;
  }

//+------------------------------------------------------------------+
//| Checks whether the relaxed ATR trigger window is active.         |
//+------------------------------------------------------------------+
bool LooseATRTriggerWindowActive(const bool isBuy)
  {
   datetime start = (isBuy ? g_buyLooseATRTriggerStartRetracementBar : g_sellLooseATRTriggerStartRetracementBar);

   if(!useLooseATRTriggerWindow || start <= 0)
      return false;

   int shift = iBarShift(_Symbol, g_retracementTimeframe, start, false);

   if(shift < 0)
      return false;

   return (shift <= looseATRTriggerWindowRetracementCandles);
  }

//+------------------------------------------------------------------+
//| Checks whether the relaxed ATR trigger window has expired.       |
//+------------------------------------------------------------------+
bool LooseATRTriggerWindowExpired(const bool isBuy)
  {
   datetime start = (isBuy ? g_buyLooseATRTriggerStartRetracementBar : g_sellLooseATRTriggerStartRetracementBar);

   if(!useLooseATRTriggerWindow || start <= 0)
      return false;

   int shift = iBarShift(_Symbol, g_retracementTimeframe, start, false);

   return (shift < 0 || shift > looseATRTriggerWindowRetracementCandles);
  }

//+------------------------------------------------------------------+
//| Expires the relaxed ATR trigger and waits for a fresh pullback.  |
//+------------------------------------------------------------------+
void ExpireLooseATRTriggerWindow(const bool isBuy)
  {
   if(isBuy)
     {
      ClearBuyRetracement();
      g_buyLooseATRExpiredWaitRetracementReset = true;
      return;
     }

   ClearSellRetracement();
   g_sellLooseATRExpiredWaitRetracementReset = true;
  }

//+------------------------------------------------------------------+
//| Prevents immediate rearming after a relaxed ATR window expires.  |
//+------------------------------------------------------------------+
bool LooseATRExpiredWaitResetIsActive(const bool isBuy)
  {
   if(isBuy)
     {
      if(!g_buyLooseATRExpiredWaitRetracementReset)
         return false;

      if(BuyRetracementCandle())
         return true;

      g_buyLooseATRExpiredWaitRetracementReset = false;
      return false;
     }

   if(!g_sellLooseATRExpiredWaitRetracementReset)
      return false;

   if(SellRetracementCandle())
      return true;

   g_sellLooseATRExpiredWaitRetracementReset = false;
   return false;
  }

//+------------------------------------------------------------------+
//| Checks whether the EA can open a new position.                   |
//+------------------------------------------------------------------+
bool CanOpenNewPosition()
  {
   if(HasEAExposure())
      return false;

   if(!allowHedge && HasAnyPositionOnSymbol())
      return false;

   if(!EntryTimeAllowed(g_tick.time))
      return false;

   return true;
  }

//+------------------------------------------------------------------+
//| Opens a new market position after the EMA cross trigger.         |
//| The initial stop uses tick size and broker distance validation.  |
//+------------------------------------------------------------------+
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);
   double stopLossPrice = InitialStopLossPrice(orderType, entryPrice);
   double takeProfitPrice = InitialTakeProfitPrice(orderType, entryPrice);

   if(useFixedTakeProfit && !InitialTakeProfitIsValid(orderType, takeProfitPrice))
     {
      LogBlockedEventOncePerCandle("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))
     {
      LogBlockedEventOncePerCandle("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;
  }

//+------------------------------------------------------------------+
//| Updates retracement state and checks the EMA cross trigger.      |
//| The function uses only closed candles from the retracement TF.   |
//| The loose ATR window can keep a failed ATR-distance cross alive. |
//+------------------------------------------------------------------+
void UpdateRetracementAndTrigger()
  {
   if(!CanOpenNewPosition())
      return;

   if(g_buyRegimeActive)
     {
      ClearSellRetracement();

      if(LooseATRExpiredWaitResetIsActive(true))
         return;

      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]));
        }

      if(g_buyRetracementActive && LooseATRTriggerWindowExpired(true))
         {
           LogEvent("BUY_LOOSE_ATR_TRIGGER_WINDOW_EXPIRED",
                    StringFormat("window_retracement_candles=%d", looseATRTriggerWindowRetracementCandles));
          ExpireLooseATRTriggerWindow(true);
          return;
         }

      if(g_buyRetracementActive && LooseATRTriggerWindowActive(true))
         {
          if(EntryDistanceFromEMAByATRIsValid(true) &&
             RetracementSlopeFilterIsValid(true))
            {
             LogEvent("BUY_TRIGGER_LOOSE_ATR_CONFIRMED",
                      StringFormat("tf=%d candle=%s close=%.2f ema=%.2f atr=%.2f window_retracement_candles=%d",
                                  (int)g_retracementTimeframe,
                                  TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                                  g_retracementRates[1].close,
                                  g_retracementEMA[1],
                                  g_retracementATR[1],
                                  looseATRTriggerWindowRetracementCandles));

             OpenTrade(true);
            }

          return;
        }

      if(g_buyRetracementActive && CrossedUpRetracementEMA())
        {
         if(!EntryDistanceFromEMAByATRIsValid(true))
           {
            if(useLooseATRTriggerWindow)
              {
               ArmLooseATRTriggerWindow(true);
               LogEvent("BUY_LOOSE_ATR_TRIGGER_WINDOW_ARMED",
                        StringFormat("tf=%d candle=%s close=%.2f ema=%.2f atr=%.2f window_retracement_candles=%d",
                                     (int)g_retracementTimeframe,
                                     TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                                     g_retracementRates[1].close,
                                     g_retracementEMA[1],
                                     g_retracementATR[1],
                                     looseATRTriggerWindowRetracementCandles));
              }

             return;
            }

          if(!RetracementSlopeFilterIsValid(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)
     {
      ClearBuyRetracement();

      if(LooseATRExpiredWaitResetIsActive(false))
         return;

      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]));
        }

      if(g_sellRetracementActive && LooseATRTriggerWindowExpired(false))
         {
           LogEvent("SELL_LOOSE_ATR_TRIGGER_WINDOW_EXPIRED",
                    StringFormat("window_retracement_candles=%d", looseATRTriggerWindowRetracementCandles));
          ExpireLooseATRTriggerWindow(false);
          return;
         }

      if(g_sellRetracementActive && LooseATRTriggerWindowActive(false))
         {
          if(EntryDistanceFromEMAByATRIsValid(false) &&
             RetracementSlopeFilterIsValid(false))
            {
             LogEvent("SELL_TRIGGER_LOOSE_ATR_CONFIRMED",
                      StringFormat("tf=%d candle=%s close=%.2f ema=%.2f atr=%.2f window_retracement_candles=%d",
                                  (int)g_retracementTimeframe,
                                  TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                                  g_retracementRates[1].close,
                                  g_retracementEMA[1],
                                  g_retracementATR[1],
                                  looseATRTriggerWindowRetracementCandles));

             OpenTrade(false);
            }

          return;
        }

      if(g_sellRetracementActive && CrossedDownRetracementEMA())
        {
         if(!EntryDistanceFromEMAByATRIsValid(false))
           {
            if(useLooseATRTriggerWindow)
              {
               ArmLooseATRTriggerWindow(false);
               LogEvent("SELL_LOOSE_ATR_TRIGGER_WINDOW_ARMED",
                        StringFormat("tf=%d candle=%s close=%.2f ema=%.2f atr=%.2f window_retracement_candles=%d",
                                     (int)g_retracementTimeframe,
                                     TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                                     g_retracementRates[1].close,
                                     g_retracementEMA[1],
                                     g_retracementATR[1],
                                     looseATRTriggerWindowRetracementCandles));
              }

             return;
            }

          if(!RetracementSlopeFilterIsValid(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();
  }

//+------------------------------------------------------------------+
//| Updates candles and EMA buffers.                                 |
//| Signals use shift 1, the last fully closed candle.               |
//+------------------------------------------------------------------+
bool UpdateMarketData()
  {
   int regimeLookback = MathMax(regimeEMAPeriod, regimeATRPeriod);
   regimeLookback = MathMax(regimeLookback, regimeSlopeBars + 2);
   int regimeBars = MathMax(120, regimeLookback + 20);

   int retracementLookback = MathMax(retracementEMAPeriod, entryATRPeriod);
   retracementLookback = MathMax(retracementLookback, retracementSlopeBars + 2);
   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_regimeATRHandle, 0, 0, regimeBars, g_regimeATR) < regimeBars)
      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_regimeATR) > 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 : "OneEMA_LooseCore_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;
  }

//+------------------------------------------------------------------+
//| Detects a new closed-bar cycle on the retracement timeframe.     |
//| Entries are evaluated only after the signal candle is closed.    |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| Lifecycle: initializes the EA, indicators, timeframes, and       |
//| buffers.                                                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(!ParametersAreValid())
     {
      Print("Invalid parameters in OneEMA_LooseCore.");
      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_regimeATRHandle = iATR(_Symbol,
                            g_regimeTimeframe,
                            regimeATRPeriod);

   g_retracementATRHandle = iATR(_Symbol,
                                  g_retracementTimeframe,
                                  entryATRPeriod);

   if(g_regimeEMAHandle == INVALID_HANDLE ||
       g_retracementEMAHandle == INVALID_HANDLE ||
       g_regimeATRHandle == INVALID_HANDLE ||
       g_retracementATRHandle == INVALID_HANDLE)
      return INIT_FAILED;

   ArraySetAsSeries(g_regimeEMA, true);
   ArraySetAsSeries(g_retracementEMA, true);
   ArraySetAsSeries(g_regimeATR, true);
   ArraySetAsSeries(g_retracementATR, true);
   ArraySetAsSeries(g_regimeRates, true);
   ArraySetAsSeries(g_retracementRates, true);

   Print("EA initialized: OneEMA_LooseCore | 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"),
          " | Loose ATR trigger window=",
          (useLooseATRTriggerWindow ? "true" : "false"),
          " | Window retracement candles=",
          looseATRTriggerWindowRetracementCandles,
          " | Regime slope filter=",
          (useRegimeSlopeFilter ? "true" : "false"),
          " | Retracement slope filter=",
          (useRetracementSlopeFilter ? "true" : "false"),
          " | Regime price ATR filter=",
          (useRegimePriceDistanceFromEMAByATR ? "true" : "false"));

   return INIT_SUCCEEDED;
  }

//+------------------------------------------------------------------+
//| Lifecycle: releases indicator handles when the EA is removed.    |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(g_regimeEMAHandle != INVALID_HANDLE)
      IndicatorRelease(g_regimeEMAHandle);

   if(g_retracementEMAHandle != INVALID_HANDLE)
      IndicatorRelease(g_retracementEMAHandle);

   if(g_regimeATRHandle != INVALID_HANDLE)
      IndicatorRelease(g_regimeATRHandle);

   if(g_retracementATRHandle != INVALID_HANDLE)
      IndicatorRelease(g_retracementATRHandle);
  }

//+------------------------------------------------------------------+
//| Lifecycle: main EA execution loop.                               |
//| Updates data, handles day trade protection, manages exits, and   |
//| evaluates entries only on closed retracement candles.            |
//+------------------------------------------------------------------+
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();
   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();
  }

//+------------------------------------------------------------------+
//| Lifecycle: resets context after an EA trade is closed.           |
//+------------------------------------------------------------------+
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");
     }
  }
//+------------------------------------------------------------------+
