//+------------------------------------------------------------------+
//|                              Breakout_FirstHour_Seban_EA.mq5      |
//| Breakout 1ère heure + Filtre tendance Daily + Stops complets     |
//| (Stratégie Olivier Seban - Breakout sur action)                 |
//+------------------------------------------------------------------+
#property copyright "Breakout First Hour - Seban"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

enum ENUM_BIAS
{
   BIAS_NONE  = 0,
   BIAS_LONG  = 1,
   BIAS_SHORT = -1
};

enum ENUM_TP_MODE
{
   TP_RR       = 0,  // RR fixe
   TP_PERCENT  = 1   // Objectif % fixe
};

input group "=== Filtre tendance Daily ===";
input int      InpMAPeriod         = 20;     // MA période (10 ou 20, pas en dessous)
input bool     InpMASlopeFilter    = true;   // Exiger MA ascendante/descendante

input group "=== Référence 1ère heure (M30) ===";
input double   InpBreakoutOffsetPercent = 0.1;  // Déclenchement ± % (ex: 0.1 = +0.1% long / -0.1% short)
input double   InpTriggerBandPercent    = 0.0;   // Bande déclenchement (0 = désactivé)

input group "=== Filtres qualité ===";
input int      InpSpreadMax        = 50;     // Spread max (points)
input int      InpATRPeriod        = 14;     // ATR période (Daily pour volatilité)
input double   InpATRMin           = 0.0;    // ATR min (0 = désactivé)

input group "=== Stops ===";
input double   InpSLMarginPts      = 10.0;   // Marge SL (points) sous RefLow / sur RefHigh
input ENUM_TP_MODE InpTPMode       = TP_RR;  // Mode TP
input double   InpRR               = 2.0;     // Risk/Reward (si mode RR)
input double   InpProfitPercent    = 1.0;     // Objectif % (si mode %)

input group "=== Stop suiveur ===";
input double   InpTrailingStartOffsetPercent = 3.0;  // Départ suiveur (% de la fourchette ref)
input double   InpTrailingOffsetPts        = 20.0;  // Distance suiveur (points) sous plus haut / sur plus bas
input bool     InpUpdateTrailingOnM30Close = true;  // Mise à jour à chaque clôture M30

input group "=== Fin de journée ===";
input bool     InpCloseEndOfDay    = false;  // Fermer en fin de journée
input int      InpCloseHour        = 21;     // Heure de clôture (serveur)
input int      InpCloseMinute      = 0;      // Minute

input group "=== Risque ===";
input double   InpRiskPercent      = 1.0;    // Risque par trade (% capital)
input double   InpLotSizeMax        = 10.0;   // Volume max

input group "=== Autres ===";
input ulong    InpMagic            = 700100; // Magic number

//--- globales
string   g_symbol;
double   g_point;
int      g_digits;
int      g_handleMA_D1;
int      g_handleATR_D1;
CTrade   g_trade;

// Référence du jour
datetime g_refDayStart;      // début jour (D1) pour lequel la ref est valide
double   g_refHigh;
double   g_refLow;
bool     g_refValid;
datetime g_lastM30Time;
ENUM_BIAS g_bias;

// Pending orders (1 par sens par jour)
ulong    g_pendingBuyTicket;
ulong    g_pendingSellTicket;

// Pour le suiveur (position ouverte)
double   g_baseSL;
double   g_refHighPos;
double   g_refLowPos;
double   g_extremeHigh;      // plus haut depuis entrée (long)
double   g_extremeLow;       // plus bas depuis entrée (short)
bool     g_trailingActive;

//+------------------------------------------------------------------+
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));
   return NormalizeDouble(MathFloor(lots / step) * step, 2);
}

double CalcLotFromRisk(double entry, double sl)
{
   double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * (InpRiskPercent / 100.0);
   double riskPrice = MathAbs(entry - sl);
   if (riskPrice <= 0.0) return 0.0;
   double tickVal = SymbolInfoDouble(g_symbol, SYMBOL_TRADE_TICK_VALUE);
   double tickSz  = SymbolInfoDouble(g_symbol, SYMBOL_TRADE_TICK_SIZE);
   if (tickSz <= 0.0 || tickVal <= 0.0) return 0.0;
   double lots = riskMoney / ((riskPrice / tickSz) * tickVal);
   return NormalizeVolume(MathMin(lots, InpLotSizeMax));
}

bool IsNewM30Bar()
{
   datetime t[2];
   ArraySetAsSeries(t, true);
   if (CopyTime(g_symbol, PERIOD_M30, 0, 2, t) < 2) return false;
   if (t[0] == g_lastM30Time) return false;
   g_lastM30Time = t[0];
   return true;
}

bool IsSameDay(datetime a, datetime b)
{
   MqlDateTime da, db;
   TimeToStruct(a, da);
   TimeToStruct(b, db);
   return (da.year == db.year && da.mon == db.mon && da.day == db.day);
}

datetime GetDayStart(datetime t)
{
   MqlDateTime dt;
   TimeToStruct(t, dt);
   dt.hour = 0; dt.min = 0; dt.sec = 0;
   return StructToTime(dt);
}

//+------------------------------------------------------------------+
//| Biais Daily : MM20 ascendante + prix au-dessus = LONG             |
//|               MM20 descendante + prix en dessous = SHORT          |
//+------------------------------------------------------------------+
ENUM_BIAS GetDailyBias()
{
   double ma[], close[];
   ArraySetAsSeries(ma, true);
   ArraySetAsSeries(close, true);
   int need = InpMAPeriod + 5;
   if (CopyBuffer(g_handleMA_D1, 0, 0, need, ma) < need) return BIAS_NONE;
   if (CopyClose(g_symbol, PERIOD_D1, 0, 3, close) < 3) return BIAS_NONE;

   double price = close[1];
   double maNow = ma[1];
   double maPrev = ma[2];

   bool rising  = (maNow > maPrev);
   bool falling = (maNow < maPrev);

   if (InpMASlopeFilter)
   {
      if (price > maNow && rising)  return BIAS_LONG;
      if (price < maNow && falling) return BIAS_SHORT;
   }
   else
   {
      if (price > maNow) return BIAS_LONG;
      if (price < maNow) return BIAS_SHORT;
   }
   return BIAS_NONE;
}

//+------------------------------------------------------------------+
//| Référence = plus haut / plus bas des 2 premières bougies M30     |
//| du jour (1ère heure = 60 min).                                    |
//+------------------------------------------------------------------+
bool BuildReference()
{
   datetime now = TimeCurrent();
   datetime dayStart = GetDayStart(now);

   if (g_refValid && IsSameDay(g_refDayStart, now))
      return true;

   g_refValid = false;
   g_refHigh = 0.0;
   g_refLow = 0.0;

   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   int copied = CopyRates(g_symbol, PERIOD_M30, dayStart, 3, rates);
   if (copied < 2) return false;

   datetime secondBarEnd = rates[0].time + 2 * PeriodSeconds(PERIOD_M30);
   if (now < secondBarEnd)
      return false;

   g_refHigh = MathMax(rates[0].high, rates[1].high);
   g_refLow  = MathMin(rates[0].low, rates[1].low);
   if (g_refHigh <= g_refLow) return false;

   g_refDayStart = dayStart;
   g_refValid = true;
   PrintFormat("[Breakout] Ref 1ère heure | High=%.5f Low=%.5f", g_refHigh, g_refLow);
   return true;
}

//+------------------------------------------------------------------+
bool PassQualityFilters()
{
   if ((int)SymbolInfoInteger(g_symbol, SYMBOL_SPREAD) > InpSpreadMax)
   {
      Print("[Breakout] Filtre refusé: spread > ", InpSpreadMax);
      return false;
   }
   if (InpATRMin > 0.0)
   {
      double atr[];
      ArraySetAsSeries(atr, true);
      if (CopyBuffer(g_handleATR_D1, 0, 1, 1, atr) < 1) return false;
      if (atr[0] < InpATRMin)
      {
         PrintFormat("[Breakout] Filtre refusé: ATR=%.5f < ATR_Min=%.5f", atr[0], InpATRMin);
         return false;
      }
   }
   return true;
}

//+------------------------------------------------------------------+
void CancelPendingOrders()
{
   for (int i = OrdersTotal() - 1; i >= 0; i--)
   {
      ulong ticket = OrderGetTicket(i);
      if (ticket <= 0) continue;
      if (OrderGetString(ORDER_SYMBOL) != g_symbol) continue;
      if (OrderGetInteger(ORDER_MAGIC) != (long)InpMagic) continue;
      g_trade.OrderDelete(ticket);
   }
   g_pendingBuyTicket = 0;
   g_pendingSellTicket = 0;
}

bool HasOpenPosition()
{
   for (int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if (ticket > 0 &&
          PositionGetString(POSITION_SYMBOL) == g_symbol &&
          PositionGetInteger(POSITION_MAGIC) == (long)InpMagic)
         return true;
   }
   return false;
}

bool HasPendingOrder(bool isBuy)
{
   for (int i = OrdersTotal() - 1; i >= 0; i--)
   {
      ulong ticket = OrderGetTicket(i);
      if (ticket <= 0) continue;
      if (OrderGetString(ORDER_SYMBOL) != g_symbol) continue;
      if (OrderGetInteger(ORDER_MAGIC) != (long)InpMagic) continue;
      ENUM_ORDER_TYPE t = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
      if (isBuy && (t == ORDER_TYPE_BUY_STOP || t == ORDER_TYPE_BUY_LIMIT)) return true;
      if (!isBuy && (t == ORDER_TYPE_SELL_STOP || t == ORDER_TYPE_SELL_LIMIT)) return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| Place ordres au seuil : +0.1% au-dessus RefHigh (long),           |
//| -0.1% en dessous RefLow (short).                                  |
//+------------------------------------------------------------------+
void PlacePendingOrders()
{
   if (!g_refValid || !PassQualityFilters()) return;
   if (HasOpenPosition()) return;

   double margin = InpSLMarginPts * g_point;
   double buyTrigger  = g_refHigh * (1.0 + InpBreakoutOffsetPercent / 100.0);
   double sellTrigger = g_refLow  * (1.0 - InpBreakoutOffsetPercent / 100.0);
   buyTrigger  = NormalizePrice(buyTrigger);
   sellTrigger = NormalizePrice(sellTrigger);

   double slLong  = NormalizePrice(g_refLow - margin);
   double slShort = NormalizePrice(g_refHigh + margin);
   double riskLong  = buyTrigger - slLong;
   double riskShort = slShort - sellTrigger;
   double tpLong = 0.0, tpShort = 0.0;
   if (InpTPMode == TP_RR)
   {
      tpLong  = NormalizePrice(buyTrigger  + InpRR * riskLong);
      tpShort = NormalizePrice(sellTrigger - InpRR * riskShort);
   }
   else
   {
      tpLong  = NormalizePrice(buyTrigger  * (1.0 + InpProfitPercent / 100.0));
      tpShort = NormalizePrice(sellTrigger * (1.0 - InpProfitPercent / 100.0));
   }

   double lots = 0.01;
   if (riskLong > 0.0)  lots = CalcLotFromRisk(buyTrigger, slLong);
   if (riskShort > 0.0) lots = CalcLotFromRisk(sellTrigger, slShort);
   lots = NormalizeVolume(lots);
   if (lots <= 0.0) return;

   g_trade.SetExpertMagicNumber(InpMagic);
   g_trade.SetDeviationInPoints(20);

   if (g_bias == BIAS_LONG && !HasPendingOrder(true))
   {
      if (g_trade.BuyStop(lots, buyTrigger, g_symbol, slLong, tpLong, ORDER_TIME_GTC, 0, "Breakout_Long"))
      {
         g_pendingBuyTicket = g_trade.ResultOrder();
         PrintFormat("[Breakout] Ordre LONG @ %.5f (RefHigh+%.2f%%) SL=%.5f TP=%.5f RR=%.1f",
                     buyTrigger, InpBreakoutOffsetPercent, slLong, tpLong, InpRR);
      }
   }
   if (g_bias == BIAS_SHORT && !HasPendingOrder(false))
   {
      if (g_trade.SellStop(lots, sellTrigger, g_symbol, slShort, tpShort, ORDER_TIME_GTC, 0, "Breakout_Short"))
      {
         g_pendingSellTicket = g_trade.ResultOrder();
         PrintFormat("[Breakout] Ordre SHORT @ %.5f (RefLow-%.2f%%) SL=%.5f TP=%.5f RR=%.1f",
                     sellTrigger, InpBreakoutOffsetPercent, slShort, tpShort, InpRR);
      }
   }
}

//+------------------------------------------------------------------+
//| Calcule ref High/Low pour une date (pour suiveur après redémarrage) |
//+------------------------------------------------------------------+
bool GetRefForDate(datetime dayStart, double &outHigh, double &outLow)
{
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   if (CopyRates(g_symbol, PERIOD_M30, dayStart, 3, rates) < 2) return false;
   outHigh = MathMax(rates[0].high, rates[1].high);
   outLow  = MathMin(rates[0].low, rates[1].low);
   return (outHigh > outLow);
}

//+------------------------------------------------------------------+
//| À chaque clôture M30 : mettre à jour le stop suiveur si actif.    |
//+------------------------------------------------------------------+
void UpdateTrailingStop()
{
   if (!InpUpdateTrailingOnM30Close) return;

   ulong ticket = 0;
   for (int i = 0; i < PositionsTotal(); i++)
   {
      ulong t = PositionGetTicket(i);
      if (t > 0 && PositionGetString(POSITION_SYMBOL) == g_symbol && PositionGetInteger(POSITION_MAGIC) == (long)InpMagic)
      {
         ticket = t; break;
      }
   }
   if (ticket == 0) return;
   if (!PositionSelectByTicket(ticket)) return;

   if (g_refHighPos == 0.0 || g_refLowPos == 0.0)
   {
      datetime openTime = (datetime)PositionGetInteger(POSITION_TIME);
      datetime posDayStart = GetDayStart(openTime);
      if (GetRefForDate(posDayStart, g_refHighPos, g_refLowPos))
         g_baseSL = PositionGetDouble(POSITION_SL);
   }

   long posType = PositionGetInteger(POSITION_TYPE);
   double posOpen = PositionGetDouble(POSITION_PRICE_OPEN);
   double posSL   = PositionGetDouble(POSITION_SL);
   double posTP   = PositionGetDouble(POSITION_TP);

   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   datetime openTime = (datetime)PositionGetInteger(POSITION_TIME);
   int barsCount = (int)((TimeCurrent() - openTime) / PeriodSeconds(PERIOD_M30)) + 2;
   barsCount = MathMin(barsCount, 200);
   int cnt = CopyRates(g_symbol, PERIOD_M30, openTime, barsCount, rates);
   if (cnt <= 0) return;

   double refRange = g_refHighPos - g_refLowPos;
   if (refRange <= 0.0) return;

   double trailStartDist = refRange * (InpTrailingStartOffsetPercent / 100.0);
   double trailOffset = InpTrailingOffsetPts * g_point;

   if (posType == POSITION_TYPE_BUY)
   {
      double extreme = rates[0].high;
      for (int i = 1; i < cnt; i++)
         if (rates[i].high > extreme) extreme = rates[i].high;
      if (extreme > g_extremeHigh) g_extremeHigh = extreme;

      if (!g_trailingActive)
      {
         if (g_extremeHigh - g_baseSL >= trailStartDist)
            g_trailingActive = true;
      }
      if (g_trailingActive)
      {
         double newSL = NormalizePrice(g_extremeHigh - trailOffset);
         if (newSL > posSL && newSL < SymbolInfoDouble(g_symbol, SYMBOL_BID) && newSL >= g_baseSL)
         {
            if (g_trade.PositionModify(ticket, newSL, posTP))
               PrintFormat("[Breakout] Trailing LONG SL=%.5f (extreme=%.5f)", newSL, g_extremeHigh);
         }
      }
   }
   else
   {
      double extreme = rates[0].low;
      for (int i = 1; i < cnt; i++)
         if (rates[i].low < extreme) extreme = rates[i].low;
      if (extreme < g_extremeLow || g_extremeLow == 0.0) g_extremeLow = extreme;

      if (!g_trailingActive)
      {
         if (g_baseSL - g_extremeLow >= trailStartDist)
            g_trailingActive = true;
      }
      if (g_trailingActive)
      {
         double newSL = NormalizePrice(g_extremeLow + trailOffset);
         if ((posSL <= 0.0 || newSL < posSL) && newSL <= g_baseSL)
         {
            if (g_trade.PositionModify(ticket, newSL, posTP))
               PrintFormat("[Breakout] Trailing SHORT SL=%.5f (extreme=%.5f)", newSL, g_extremeLow);
         }
      }
   }
}

//+------------------------------------------------------------------+
//| Détecte nouvelle position (remplit ref pour suiveur).             |
//+------------------------------------------------------------------+
void OnPositionOpened()
{
   g_refHighPos = g_refHigh;
   g_refLowPos  = g_refLow;
   g_extremeHigh = 0.0;
   g_extremeLow  = 0.0;
   g_trailingActive = false;

   for (int i = 0; i < PositionsTotal(); i++)
   {
      ulong t = PositionGetTicket(i);
      if (t == 0) continue;
      if (PositionGetString(POSITION_SYMBOL) != g_symbol) continue;
      if (PositionGetInteger(POSITION_MAGIC) != (long)InpMagic) continue;

      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double sl = PositionGetDouble(POSITION_SL);
      long posType = PositionGetInteger(POSITION_TYPE);

      g_baseSL = sl;
      if (posType == POSITION_TYPE_BUY)
         g_extremeHigh = openPrice;
      else
         g_extremeLow = openPrice;
      break;
   }
   CancelPendingOrders();
}

//+------------------------------------------------------------------+
void CheckCloseEndOfDay()
{
   if (!InpCloseEndOfDay) return;
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   if (dt.hour < InpCloseHour) return;
   if (dt.hour == InpCloseHour && dt.min < InpCloseMinute) return;

   for (int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if (ticket > 0 &&
          PositionGetString(POSITION_SYMBOL) == g_symbol &&
          PositionGetInteger(POSITION_MAGIC) == (long)InpMagic)
      {
         g_trade.PositionClose(ticket);
         PrintFormat("[Breakout] Fermeture fin de journée (heure %d:%02d)", InpCloseHour, InpCloseMinute);
      }
   }
}

//+------------------------------------------------------------------+
int OnInit()
{
   g_symbol = _Symbol;
   g_point  = _Point;
   g_digits = (int)SymbolInfoInteger(g_symbol, SYMBOL_DIGITS);
   g_refValid = false;
   g_lastM30Time = 0;
   g_pendingBuyTicket = 0;
   g_pendingSellTicket = 0;
   g_trailingActive = false;

   if (InpMAPeriod < 10)
   {
      Print("[Breakout] MA_Period doit être >= 10");
      return INIT_PARAMETERS_INCORRECT;
   }

   g_handleMA_D1  = iMA(g_symbol, PERIOD_D1, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
   g_handleATR_D1 = iATR(g_symbol, PERIOD_D1, InpATRPeriod);
   if (g_handleMA_D1 == INVALID_HANDLE || g_handleATR_D1 == INVALID_HANDLE)
   {
      Print("[Breakout] Erreur indicateurs");
      return INIT_FAILED;
   }

   g_trade.SetExpertMagicNumber(InpMagic);
   g_trade.SetDeviationInPoints(20);

   PrintFormat("[Breakout] Init | MA=%d RR=%.1f TrailingStart=%.1f%%", InpMAPeriod, InpRR, InpTrailingStartOffsetPercent);
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   if (g_handleMA_D1 != INVALID_HANDLE)  IndicatorRelease(g_handleMA_D1);
   if (g_handleATR_D1 != INVALID_HANDLE) IndicatorRelease(g_handleATR_D1);
   CancelPendingOrders();
   Print("[Breakout] EA arrêté. Raison: ", reason);
}

void OnTick()
{
   g_bias = GetDailyBias();

   datetime now = TimeCurrent();
   datetime dayStart = GetDayStart(now);
   if (!IsSameDay(g_refDayStart, now) && g_refDayStart != 0)
   {
      g_refValid = false;
      CancelPendingOrders();
   }

   if (!BuildReference())
   {
      if (HasOpenPosition())
      {
         if (IsNewM30Bar())
            UpdateTrailingStop();
         CheckCloseEndOfDay();
      }
      return;
   }

   if (HasOpenPosition())
   {
      if (IsNewM30Bar())
         UpdateTrailingStop();
      CheckCloseEndOfDay();
      return;
   }

   if (IsNewM30Bar())
   {
      PlacePendingOrders();
   }

   for (int i = 0; i < PositionsTotal(); i++)
   {
      ulong t = PositionGetTicket(i);
      if (t > 0 && PositionGetString(POSITION_SYMBOL) == g_symbol && PositionGetInteger(POSITION_MAGIC) == (long)InpMagic)
      {
         if (g_refHighPos == 0.0 && g_refLowPos == 0.0)
            OnPositionOpened();
         break;
      }
   }
}
