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

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input bool enableStatusLogs = false;                                      // Enables optional logs in the Experts tab.

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 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 external symbol exposure when true.

input ENUM_CORE_TIMEFRAME_INDEX regimeTimeframeIndex = CORE_TF_M5;        // Timeframe used to define direction.
input ENUM_CORE_TIMEFRAME_INDEX retracementTimeframeIndex = CORE_TF_M1;   // Timeframe used for pullback and trigger.
input int regimeEMAPeriod = 70;                                           // EMA period used for the regime filter.
input int retracementEMAPeriod = 20;                                      // EMA period used for pullback and trigger.

input bool useDaytradeWindow = true;                                      // Enables the intraday trading window.
input int daytradeStartMinute = 540;                                      // 09:00 = 540.
input int lastEntryMinute = 1050;                                         // 17:30 = 1050.
input int forcedCloseMinute = 1050;                                       // 17:30 = 1050.
input bool forceCloseAtEndOfDay = true;                                   // Closes EA positions at the end of the day.

//+------------------------------------------------------------------+
//| State                                                            |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES g_regimeTimeframe = PERIOD_M5;                            // 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.

double g_regimeEMA[];                                                     // Regime EMA buffer.
double g_retracementEMA[];                                                // Retracement EMA 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.
int g_symbolDigits = 0;                                                   // Price decimals used by the current symbol.

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.

//+------------------------------------------------------------------+
//| Timeframe conversion                                             |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES TimeframeFromIndex(const ENUM_CORE_TIMEFRAME_INDEX index)
  {
//--- Integer inputs are converted into real MT5 timeframes.
   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                                                 |
//+------------------------------------------------------------------+
int VolumeDigits()
  {
   double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);           // Broker volume step.
   int digits = 0;                                                        // Required decimal places.

//--- Count decimal places needed to represent the volume step.
   while(step > 0.0 && step < 1.0 && digits < 8)
     {
      step *= 10.0;
      digits++;
     }

   return digits;
  }

//+------------------------------------------------------------------+
//| Volume normalization                                             |
//+------------------------------------------------------------------+
double NormalizeVolume(const double volume)
  {
   double minVolume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);       // Minimum allowed volume.
   double maxVolume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);       // Maximum allowed volume.
   double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);           // Volume step.

   if(step <= 0.0)
      step = (minVolume > 0.0 ? minVolume : 1.0);

//--- Volume must respect broker minimum, maximum and step.
   double normalized = MathFloor(volume / step + 1e-9) * step;
   normalized = MathMax(minVolume, MathMin(maxVolume, normalized));

   return NormalizeDouble(normalized, VolumeDigits());
  }

//+------------------------------------------------------------------+
//| Price precision                                                  |
//+------------------------------------------------------------------+
int SymbolPriceDigits()
  {
   if(g_symbolDigits > 0)
      return g_symbolDigits;

   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);

   if(digits < 0)
      digits = _Digits;

   return digits;
  }

//+------------------------------------------------------------------+
//| Price normalization                                              |
//+------------------------------------------------------------------+
double NormalizePrice(const double price)
  {
   return NormalizeDouble(price, SymbolPriceDigits());
  }

//+------------------------------------------------------------------+
//| Price formatting                                                 |
//+------------------------------------------------------------------+
string PriceToString(const double price)
  {
   return DoubleToString(price, SymbolPriceDigits());
  }

//+------------------------------------------------------------------+
//| Time filters                                                     |
//+------------------------------------------------------------------+
int MinutesOfDay(const datetime when)
  {
   MqlDateTime timeStruct;
   TimeToStruct(when, timeStruct);

   return timeStruct.hour * 60 + timeStruct.min;
  }

//+------------------------------------------------------------------+
//| Entry window                                                     |
//+------------------------------------------------------------------+
bool EntryTimeAllowed(const datetime when)
  {
   if(!useDaytradeWindow)
      return true;

   int minute = MinutesOfDay(when);                                       // Current time in minutes since midnight.
   return (minute >= daytradeStartMinute && minute < lastEntryMinute);
  }

//+------------------------------------------------------------------+
//| Forced close window                                              |
//+------------------------------------------------------------------+
bool MustForceCloseEndOfDay(const datetime when)
  {
//--- Day trade protection avoids carrying positions overnight.
   return (useDaytradeWindow &&
           forceCloseAtEndOfDay &&
           MinutesOfDay(when) >= forcedCloseMinute);
  }

//+------------------------------------------------------------------+
//| Log helpers                                                      |
//+------------------------------------------------------------------+
string DirectionName(const bool isBuy)
  {
   return (isBuy ? "BUY" : "SELL");
  }

//+------------------------------------------------------------------+
//| Event logger                                                     |
//+------------------------------------------------------------------+
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                                             |
//+------------------------------------------------------------------+
bool ParametersAreValid()
  {
//--- Basic validation avoids invalid tests and initialization errors.
   if(regimeEMAPeriod <= 0)
      return false;

   if(retracementEMAPeriod <= 0)
      return false;

   if(lots <= 0.0)
      return false;

   if(daytradeStartMinute < 0 ||
      lastEntryMinute <= daytradeStartMinute ||
      forcedCloseMinute < lastEntryMinute ||
      forcedCloseMinute > 1440)
      return false;

   return true;
  }

//+------------------------------------------------------------------+
//| Magic validation                                                 |
//+------------------------------------------------------------------+
bool IsEAMagic(const ulong magic)
  {
   return (magic == buyMagic || magic == sellMagic);
  }

//+------------------------------------------------------------------+
//| Position selection                                               |
//+------------------------------------------------------------------+
bool SelectPositionByMagic(const ulong magic,
                           const ENUM_POSITION_TYPE type,
                           ulong &ticket)
  {
   ticket = 0;

//--- Search open positions from newest to oldest.
   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()
  {
//--- Detects whether this EA already has a position on the current symbol.
   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                                            |
//+------------------------------------------------------------------+
bool HasAnyPositionOnSymbol()
  {
//--- Used when allowHedge is false to keep tests clean and simple.
   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 check                                                     |
//+------------------------------------------------------------------+
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 entry                                                     |
//+------------------------------------------------------------------+
bool SendMarketOrder(const ENUM_ORDER_TYPE orderType,
                     const double volume,
                     const ulong magic)
  {
   double normalizedVolume = NormalizeVolume(volume);                     // Broker-valid order 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 = NormalizePrice(orderType == ORDER_TYPE_BUY ? g_ask : g_bid);

   request.sl = 0.0;                                                      // No initial stop in this diagnostic version.
   request.tp = 0.0;                                                      // No take profit in this diagnostic version.

   if(!OrderSend(request, result))
      return false;

   return (result.retcode == TRADE_RETCODE_DONE ||
           result.retcode == TRADE_RETCODE_DONE_PARTIAL ||
           result.retcode == TRADE_RETCODE_PLACED);
  }

//+------------------------------------------------------------------+
//| Position close                                                   |
//+------------------------------------------------------------------+
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;

//--- A market close is sent as the opposite order type.
   if(positionType == POSITION_TYPE_BUY)
     {
      request.type = ORDER_TYPE_SELL;
      request.price = NormalizePrice(g_bid);
     }
   else
     {
      request.type = ORDER_TYPE_BUY;
      request.price = NormalizePrice(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                                                        |
//+------------------------------------------------------------------+
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                                                       |
//+------------------------------------------------------------------+
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;
  }

//+------------------------------------------------------------------+
//| End-of-day liquidation                                           |
//+------------------------------------------------------------------+
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                                                       |
//+------------------------------------------------------------------+
bool BuyRegime()
  {
//--- Buy regime uses the last closed candle above the regime EMA.
   if(ArraySize(g_regimeRates) <= 1 || ArraySize(g_regimeEMA) <= 1)
      return false;

   return (g_regimeRates[1].close > g_regimeEMA[1]);
  }

//+------------------------------------------------------------------+
//| Sell regime                                                      |
//+------------------------------------------------------------------+
bool SellRegime()
  {
//--- Sell regime uses the last closed candle below the regime EMA.
   if(ArraySize(g_regimeRates) <= 1 || ArraySize(g_regimeEMA) <= 1)
      return false;

   return (g_regimeRates[1].close < g_regimeEMA[1]);
  }

//+------------------------------------------------------------------+
//| Clear buy pullback                                               |
//+------------------------------------------------------------------+
void ClearBuyRetracement()
  {
   g_buyRetracementActive = false;
  }

//+------------------------------------------------------------------+
//| Clear sell pullback                                              |
//+------------------------------------------------------------------+
void ClearSellRetracement()
  {
   g_sellRetracementActive = false;
  }

//+------------------------------------------------------------------+
//| Clear all pullbacks                                              |
//+------------------------------------------------------------------+
void ClearRetracements()
  {
   ClearBuyRetracement();
   ClearSellRetracement();
  }

//+------------------------------------------------------------------+
//| Regime update                                                    |
//+------------------------------------------------------------------+
void UpdateRegime()
  {
   bool buy = BuyRegime();
   bool sell = SellRegime();

//--- When a new regime appears, old pullback states are discarded.
   if(buy)
     {
      if(!g_buyRegimeActive)
        {
          LogEvent("BUY_REGIME_ACTIVE",
                   StringFormat("tf=%d candle=%s close=%s ema=%s",
                                (int)g_regimeTimeframe,
                                TimeToString(g_regimeRates[1].time, TIME_DATE | TIME_MINUTES),
                                PriceToString(g_regimeRates[1].close),
                                PriceToString(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=%s ema=%s",
                                (int)g_regimeTimeframe,
                                TimeToString(g_regimeRates[1].time, TIME_DATE | TIME_MINUTES),
                                PriceToString(g_regimeRates[1].close),
                                PriceToString(g_regimeEMA[1])));

         ClearRetracements();
        }

      g_buyRegimeActive = false;
      g_sellRegimeActive = true;
      return;
     }

   g_buyRegimeActive = false;
   g_sellRegimeActive = false;
   ClearRetracements();
  }

//+------------------------------------------------------------------+
//| Regime exit                                                      |
//+------------------------------------------------------------------+
void ManageExitByRegime()
  {
//--- This simple model exits when the opposite regime appears.
   if(HasBuyPosition() && SellRegime())
     {
      CloseBuyPosition("regime_reversed_to_sell");
      ClearRetracements();
     }

   if(HasSellPosition() && BuyRegime())
     {
      CloseSellPosition("regime_reversed_to_buy");
      ClearRetracements();
     }
  }

//+------------------------------------------------------------------+
//| Buy pullback                                                     |
//+------------------------------------------------------------------+
bool BuyRetracementCandle()
  {
//--- In a buy regime, pullback is a close at or below the retracement EMA.
   if(ArraySize(g_retracementRates) <= 1 || ArraySize(g_retracementEMA) <= 1)
      return false;

   return (g_retracementRates[1].close <= g_retracementEMA[1]);
  }

//+------------------------------------------------------------------+
//| Sell pullback                                                    |
//+------------------------------------------------------------------+
bool SellRetracementCandle()
  {
//--- In a sell regime, pullback is a close at or above the retracement EMA.
   if(ArraySize(g_retracementRates) <= 1 || ArraySize(g_retracementEMA) <= 1)
      return false;

   return (g_retracementRates[1].close >= g_retracementEMA[1]);
  }

//+------------------------------------------------------------------+
//| Buy trigger                                                      |
//+------------------------------------------------------------------+
bool CrossedUpRetracementEMA()
  {
//--- Buy trigger: candle 2 below/at EMA and candle 1 back above EMA.
   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);
  }

//+------------------------------------------------------------------+
//| Sell trigger                                                     |
//+------------------------------------------------------------------+
bool CrossedDownRetracementEMA()
  {
//--- Sell trigger: candle 2 above/at EMA and candle 1 back below EMA.
   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);
  }

//+------------------------------------------------------------------+
//| Entry permission                                                 |
//+------------------------------------------------------------------+
bool CanOpenNewPosition()
  {
   if(HasEAExposure())
      return false;

   if(!allowHedge && HasAnyPositionOnSymbol())
      return false;

   if(!EntryTimeAllowed(g_tick.time))
      return false;

   return true;
  }

//+------------------------------------------------------------------+
//| Trade opening                                                    |
//+------------------------------------------------------------------+
bool OpenTrade(const bool isBuy)
  {
   datetime currentBar = g_retracementRates[0].time;                      // Current retracement candle.

   if(g_lastEntryAttemptBarTime == currentBar)
      return false;

   g_lastEntryAttemptBarTime = currentBar;

   if(!CanOpenNewPosition())
      return false;

   double volume = NormalizeVolume(lots);
   double entryPrice = NormalizePrice(isBuy ? g_ask : g_bid);
   ENUM_ORDER_TYPE orderType = (isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);

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

   if(!sent)
      return false;

   if(isBuy)
     {
       LogEvent("BUY_ENTRY_CORE",
                StringFormat("retracement_tf=%d signal_candle=%s entry=%s retracement_ema=%s sl=0 tp=0",
                             (int)g_retracementTimeframe,
                             TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                             PriceToString(entryPrice),
                             PriceToString(g_retracementEMA[1])));

      ClearBuyRetracement();
     }
   else
     {
       LogEvent("SELL_ENTRY_CORE",
                StringFormat("retracement_tf=%d signal_candle=%s entry=%s retracement_ema=%s sl=0 tp=0",
                             (int)g_retracementTimeframe,
                             TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                             PriceToString(entryPrice),
                             PriceToString(g_retracementEMA[1])));

      ClearSellRetracement();
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Pullback engine                                                  |
//+------------------------------------------------------------------+
void UpdateRetracementAndTrigger()
  {
   if(!CanOpenNewPosition())
      return;

//--- Learning sequence: regime -> pullback armed -> EMA cross trigger -> entry.
   if(g_buyRegimeActive)
     {
      ClearSellRetracement();

      if(!g_buyRetracementActive && BuyRetracementCandle())
        {
         g_buyRetracementActive = true;
          LogEvent("BUY_RETRACEMENT_ARMED",
                   StringFormat("tf=%d candle=%s close=%s ema=%s",
                                (int)g_retracementTimeframe,
                                TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                                PriceToString(g_retracementRates[1].close),
                                PriceToString(g_retracementEMA[1])));
        }

      if(g_buyRetracementActive && CrossedUpRetracementEMA())
        {
          LogEvent("BUY_TRIGGER_RETRACEMENT_EMA_CROSS",
                   StringFormat("tf=%d candle=%s close=%s ema=%s",
                                (int)g_retracementTimeframe,
                                TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                                PriceToString(g_retracementRates[1].close),
                                PriceToString(g_retracementEMA[1])));

         OpenTrade(true);
        }

      return;
     }

   if(g_sellRegimeActive)
     {
      ClearBuyRetracement();

      if(!g_sellRetracementActive && SellRetracementCandle())
        {
         g_sellRetracementActive = true;
          LogEvent("SELL_RETRACEMENT_ARMED",
                   StringFormat("tf=%d candle=%s close=%s ema=%s",
                                (int)g_retracementTimeframe,
                                TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                                PriceToString(g_retracementRates[1].close),
                                PriceToString(g_retracementEMA[1])));
        }

      if(g_sellRetracementActive && CrossedDownRetracementEMA())
        {
          LogEvent("SELL_TRIGGER_RETRACEMENT_EMA_CROSS",
                   StringFormat("tf=%d candle=%s close=%s ema=%s",
                                (int)g_retracementTimeframe,
                                TimeToString(g_retracementRates[1].time, TIME_DATE | TIME_MINUTES),
                                PriceToString(g_retracementRates[1].close),
                                PriceToString(g_retracementEMA[1])));

         OpenTrade(false);
        }

      return;
     }

   ClearRetracements();
  }

//+------------------------------------------------------------------+
//| Market data                                                      |
//+------------------------------------------------------------------+
bool UpdateMarketData()
  {
   int regimeBars = MathMax(120, regimeEMAPeriod + 20);
   int retracementBars = MathMax(120, retracementEMAPeriod + 20);

//--- Copy enough closed-bar data for the EMA calculations.
   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;

   return (ArraySize(g_regimeRates) > 2 &&
           ArraySize(g_retracementRates) > 2 &&
           ArraySize(g_regimeEMA) > 2 &&
           ArraySize(g_retracementEMA) > 2);
  }

//+------------------------------------------------------------------+
//| New candle check                                                 |
//+------------------------------------------------------------------+
bool NewRetracementBar()
  {
   if(ArraySize(g_retracementRates) <= 0)
      return false;

   datetime currentBarTime = g_retracementRates[0].time;                  // Current open candle time.

   if(currentBarTime == g_lastRetracementBarTime)
      return false;

   g_lastRetracementBarTime = currentBarTime;
   return true;
  }

//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(!ParametersAreValid())
     {
      Print("Invalid parameters in Pullback_OneEMA_CORE.");
      return INIT_PARAMETERS_INCORRECT;
     }

   g_regimeTimeframe = TimeframeFromIndex(regimeTimeframeIndex);
   g_retracementTimeframe = TimeframeFromIndex(retracementTimeframeIndex);
   g_symbolDigits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);

   g_regimeEMAHandle = iMA(_Symbol,
                           g_regimeTimeframe,
                           regimeEMAPeriod,
                           0,
                           MODE_EMA,
                           PRICE_CLOSE);

   g_retracementEMAHandle = iMA(_Symbol,
                                g_retracementTimeframe,
                                retracementEMAPeriod,
                                0,
                                MODE_EMA,
                                PRICE_CLOSE);

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

//--- Series mode: index 0 is the current candle, index 1 is the last closed candle.
   ArraySetAsSeries(g_regimeEMA, true);
   ArraySetAsSeries(g_retracementEMA, true);
   ArraySetAsSeries(g_regimeRates, true);
   ArraySetAsSeries(g_retracementRates, true);

   Print("EA initialized: Pullback_OneEMA_CORE | Regime TF=",
          (int)g_regimeTimeframe,
          " | Retracement TF=",
          (int)g_retracementTimeframe,
          " | Symbol digits=",
          g_symbolDigits);

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

//+------------------------------------------------------------------+
//| Main loop                                                        |
//+------------------------------------------------------------------+
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 has priority over new signals.
   if(MustForceCloseEndOfDay(g_tick.time))
     {
      ForceCloseAllPositions();
      ClearRetracements();
      return;
     }

//--- Regime and exits can run every tick because they use closed bars.
   UpdateRegime();
   ManageExitByRegime();

//--- Entries are evaluated only when a new retracement candle starts.
   if(!NewRetracementBar())
      return;

   UpdateRegime();
   ManageExitByRegime();

   if(!EntryTimeAllowed(g_tick.time))
      return;

   UpdateRetracementAndTrigger();
  }

//+------------------------------------------------------------------+
//| Trade transaction                                                |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &transaction,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
  {
//--- When an EA position closes, old pullback context is discarded.
   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");
     }
  }
//+------------------------------------------------------------------+
