//+------------------------------------------------------------------+
//|                                           Range4HFakeout_EA.mq5   |
//| 4-Hour Range Fakeout - Première bougie H4 NY comme S/R - RR 1:2   |
//+------------------------------------------------------------------+
#property copyright "Range 4H Fakeout Strategy"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Mode SL                                                           |
//+------------------------------------------------------------------+
enum ENUM_SL_MODE
{
   SL_SPIKE_ONLY   = 0,  // Spike strict uniquement
   SL_SPIKE_FALLBACK = 1 // Spike ou fallback si trop grand
};

//+------------------------------------------------------------------+
//| Paramètres                                                        |
//+------------------------------------------------------------------+
input group "=== Fuseau New York ==="
input int      InpNYOffsetHours   = -5;      // Décalage serveur→NY (UTC-5 hiver, -4 été)

input group "=== Range H4 ==="
input double   InpSLMarginPoints  = 2.0;     // Marge SL sur niveaux clés (points)

input group "=== Stop Loss (cassure trop grande) ==="
input ENUM_SL_MODE InpSLMode      = SL_SPIKE_FALLBACK;
input double   InpMaxStopPoints   = 200.0;   // Distance max SL (points) avant fallback

input group "=== Risque ==="
input double   InpRiskPercent    = 1.0;     // Risque par trade (%)
input double   InpLotSizeMax     = 10.0;    // Volume max (lots)

input group "=== Discipline ==="
input bool     InpOnePositionMax = true;    // 1 position max simultanée
input ulong    InpMagic          = 456789;  // Magic number

//+------------------------------------------------------------------+
//| États du cycle fakeout                                            |
//+------------------------------------------------------------------+
enum ENUM_SETUP_STATE
{
   STATE_NONE = 0,
   STATE_AWAIT_REINTEGRATION_LONG,
   STATE_AWAIT_REINTEGRATION_SHORT
};

//+------------------------------------------------------------------+
//| Variables globales                                                |
//+------------------------------------------------------------------+
string   g_symbol;
double   g_point;
int      g_digits;
datetime g_lastM5Time;
datetime g_rangeDate;
double   g_rangeHigh;
double   g_rangeLow;
ENUM_SETUP_STATE g_state;
double   g_spikeExtreme;
datetime g_breakoutTime;
CTrade   g_trade;

//+------------------------------------------------------------------+
//| Date actuelle en temps New York (y,m,d)                            |
//+------------------------------------------------------------------+
void GetNYDate(int &year, int &month, int &day)
{
   datetime serverNow = TimeCurrent();
   datetime nyNow = serverNow - InpNYOffsetHours * 3600;
   MqlDateTime dt;
   TimeToStruct(nyNow, dt);
   year  = dt.year;
   month = dt.mon;
   day   = dt.day;
}

//+------------------------------------------------------------------+
//| Heure 00:00 NY pour une date donnée, en temps serveur             |
//+------------------------------------------------------------------+
datetime GetServerTimeForNYMidnight(int y, int m, int d)
{
   MqlDateTime dt;
   dt.year = y; dt.mon = m; dt.day = d;
   dt.hour = -InpNYOffsetHours;
   dt.min  = 0;
   dt.sec  = 0;
   return StructToTime(dt);
}

//+------------------------------------------------------------------+
//| Première bougie H4 du jour NY : High et Low                        |
//| (Attend que la bougie soit close avant d'utiliser le range)        |
//+------------------------------------------------------------------+
bool BuildRangeH4()
{
   int y, m, d;
   GetNYDate(y, m, d);
   datetime nyMidnight = GetServerTimeForNYMidnight(y, m, d);

   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   int c = CopyRates(g_symbol, PERIOD_H4, nyMidnight, 2, rates);
   if (c < 1) return false;

   if (TimeCurrent() < rates[0].time + PeriodSeconds(PERIOD_H4))
      return false;

   g_rangeHigh = rates[0].high;
   g_rangeLow  = rates[0].low;
   g_rangeDate = StringToTime(StringFormat("%04d.%02d.%02d", y, m, d));
   PrintFormat("[R4H] Range NY %04d-%02d-%02d | High=%.5f Low=%.5f", y, m, d, g_rangeHigh, g_rangeLow);
   return true;
}

//+------------------------------------------------------------------+
//| Nouvelle bougie M5 ?                                              |
//+------------------------------------------------------------------+
bool IsNewM5Bar()
{
   datetime t[];
   ArraySetAsSeries(t, true);
   if (CopyTime(g_symbol, PERIOD_M5, 0, 2, t) < 2) return false;
   if (t[0] == g_lastM5Time) return false;
   g_lastM5Time = t[0];
   return true;
}

//+------------------------------------------------------------------+
//| Normaliser prix / volume                                          |
//+------------------------------------------------------------------+
double NormalizePrice(double p) { return NormalizeDouble(p, g_digits); }
double NormalizeVolume(double lots)
{
   double minV = SymbolInfoDouble(g_symbol, SYMBOL_VOLUME_MIN);
   double maxV = SymbolInfoDouble(g_symbol, SYMBOL_VOLUME_MAX);
   double step = SymbolInfoDouble(g_symbol, SYMBOL_VOLUME_STEP);
   lots = MathMax(minV, MathMin(maxV, lots));
   lots = MathFloor(lots / step) * step;
   return NormalizeDouble(lots, 2);
}

double CalcLotFromRisk(double entry, double sl)
{
   double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * (InpRiskPercent / 100.0);
   double riskPrice = MathAbs(entry - sl);
   if (riskPrice <= 0) return 0;
   double tickVal = SymbolInfoDouble(g_symbol, SYMBOL_TRADE_TICK_VALUE);
   double tickSz  = SymbolInfoDouble(g_symbol, SYMBOL_TRADE_TICK_SIZE);
   if (tickSz <= 0 || tickVal <= 0) return 0;
   double lots = riskMoney / ((riskPrice / tickSz) * tickVal);
   return NormalizeVolume(MathMin(lots, InpLotSizeMax));
}

//+------------------------------------------------------------------+
//| Position ouverte ?                                                |
//+------------------------------------------------------------------+
bool HasOpenPosition()
{
   for (int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong t = PositionGetTicket(i);
      if (t > 0 && PositionGetString(POSITION_SYMBOL) == g_symbol && PositionGetInteger(POSITION_MAGIC) == InpMagic)
         return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Prix dans le range ? (inclus)                                     |
//+------------------------------------------------------------------+
bool PriceInRange(double price)
{
   return (price >= g_rangeLow && price <= g_rangeHigh);
}

//+------------------------------------------------------------------+
//| Ouvrir Long                                                       |
//+------------------------------------------------------------------+
bool OpenLong(double entry, double sl, double tp, bool usedFallback)
{
   double ask = SymbolInfoDouble(g_symbol, SYMBOL_ASK);
   double vol = CalcLotFromRisk(ask, sl);
   if (vol <= 0) return false;
   g_trade.SetExpertMagicNumber(InpMagic);
   if (g_trade.Buy(vol, g_symbol, ask, sl, tp, "R4H Long"))
   {
      PrintFormat("[R4H] LONG @ %.5f SL=%.5f TP=%.5f Vol=%.2f | SL=%s",
                  ask, sl, tp, vol, usedFallback ? "fallback" : "spike");
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Ouvrir Short                                                      |
//+------------------------------------------------------------------+
bool OpenShort(double entry, double sl, double tp, bool usedFallback)
{
   double bid = SymbolInfoDouble(g_symbol, SYMBOL_BID);
   double vol = CalcLotFromRisk(bid, sl);
   if (vol <= 0) return false;
   g_trade.SetExpertMagicNumber(InpMagic);
   if (g_trade.Sell(vol, g_symbol, bid, sl, tp, "R4H Short"))
   {
      PrintFormat("[R4H] SHORT @ %.5f SL=%.5f TP=%.5f Vol=%.2f | SL=%s",
                  bid, sl, tp, vol, usedFallback ? "fallback" : "spike");
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Traiter les signaux à la clôture M5                               |
//+------------------------------------------------------------------+
void ProcessSignals()
{
   MqlRates m5[];
   ArraySetAsSeries(m5, true);
   if (CopyRates(g_symbol, PERIOD_M5, 0, 5, m5) < 3) return;

   double close1 = m5[1].close;
   double high1  = m5[1].high;
   double low1   = m5[1].low;
   double slMargin = InpSLMarginPoints * g_point;
   double maxStopDist = InpMaxStopPoints * g_point;

   if (g_state == STATE_NONE)
   {
      if (close1 > g_rangeHigh)
      {
         g_state = STATE_AWAIT_REINTEGRATION_SHORT;
         g_spikeExtreme = high1;
         g_breakoutTime = m5[1].time;
         PrintFormat("[R4H] Cassure HAUT (close=%.5f > %.5f) | attente réintégration", close1, g_rangeHigh);
         return;
      }
      if (close1 < g_rangeLow)
      {
         g_state = STATE_AWAIT_REINTEGRATION_LONG;
         g_spikeExtreme = low1;
         g_breakoutTime = m5[1].time;
         PrintFormat("[R4H] Cassure BAS (close=%.5f < %.5f) | attente réintégration", close1, g_rangeLow);
         return;
      }
      return;
   }

   if (g_state == STATE_AWAIT_REINTEGRATION_LONG)
   {
      if (low1 < g_spikeExtreme) g_spikeExtreme = low1;

      if (PriceInRange(close1))
      {
         double entry = close1;
         double slSpike = NormalizePrice(g_spikeExtreme - slMargin);
         double risk = entry - slSpike;
         bool useFallback = false;
         double sl = slSpike;

         if (InpSLMode == SL_SPIKE_FALLBACK && risk > maxStopDist)
         {
            sl = NormalizePrice(g_rangeLow - slMargin);
            useFallback = true;
            risk = entry - sl;
            PrintFormat("[R4H] SL fallback (spike trop grand) | Long SL=%.5f (RangeLow-marge)", sl);
         }

         double tp = NormalizePrice(entry + 2.0 * risk);
         if (OpenLong(entry, sl, tp, useFallback))
            g_state = STATE_NONE;
         else
            g_state = STATE_NONE;
         return;
      }
   }

   if (g_state == STATE_AWAIT_REINTEGRATION_SHORT)
   {
      if (high1 > g_spikeExtreme) g_spikeExtreme = high1;

      if (PriceInRange(close1))
      {
         double entry = close1;
         double slSpike = NormalizePrice(g_spikeExtreme + slMargin);
         double risk = slSpike - entry;
         bool useFallback = false;
         double sl = slSpike;

         if (InpSLMode == SL_SPIKE_FALLBACK && risk > maxStopDist)
         {
            sl = NormalizePrice(g_rangeHigh + slMargin);
            useFallback = true;
            risk = sl - entry;
            PrintFormat("[R4H] SL fallback (spike trop grand) | Short SL=%.5f (RangeHigh+marge)", sl);
         }

         double tp = NormalizePrice(entry - 2.0 * risk);
         if (OpenShort(entry, sl, tp, useFallback))
            g_state = STATE_NONE;
         else
            g_state = STATE_NONE;
         return;
      }
   }
}

//+------------------------------------------------------------------+
//| OnInit / OnDeinit / OnTick                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   g_symbol = _Symbol;
   g_point  = _Point;
   g_digits = (int)SymbolInfoInteger(g_symbol, SYMBOL_DIGITS);
   g_lastM5Time = 0;
   g_rangeDate = 0;
   g_rangeHigh = 0;
   g_rangeLow = 0;
   g_state = STATE_NONE;
   g_spikeExtreme = 0;
   g_breakoutTime = 0;

   g_trade.SetExpertMagicNumber(InpMagic);
   g_trade.SetDeviationInPoints(15);

   PrintFormat("[R4H] EA init | %s | NY offset %d | RR 1:2 | SL mode %s",
               g_symbol, InpNYOffsetHours, InpSLMode == SL_SPIKE_FALLBACK ? "spike+fallback" : "spike");
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   Print("[R4H] EA arrêté. Raison: ", reason);
}

void OnTick()
{
   if (!IsNewM5Bar()) return;

   int y, m, d;
   GetNYDate(y, m, d);
   datetime todayNY = StringToTime(StringFormat("%04d.%02d.%02d", y, m, d));

   if (g_rangeDate != todayNY)
   {
      g_state = STATE_NONE;
      if (!BuildRangeH4())
         return;
   }

   if (g_rangeHigh <= g_rangeLow) return;
   if (InpOnePositionMax && HasOpenPosition()) return;

   ProcessSignals();
}
