//+------------------------------------------------------------------+
//|                                        NinjaEntry_ORB_EA_v1.01  |
//|   Expert Advisor - Opening Range Breakout Strategy              |
//|   Signal: London Open (14:00 WIB) + NY Open (20:00 WIB)        |
//|   Entry: M5 + M15 | SL: Sisi lain Opening Range + ATR buffer   |
//|   Filter: Session, Spread, Cooldown, ATR Trail, Early Close,    |
//|           Asia Range, Swing, Wick, MaxSL, Vol Context, Blackout |
//+------------------------------------------------------------------+
//| Changelog v1.01 (dari v1.00):                                   |
//| - FIX: GetPip() digit gold terbalik (2 digit vs 3 digit)        |
//| - FIX: Struct ORB dipisah jadi 4 (London/NY x M5/M15)          |
//|        Sebelumnya rangeHigh/rangeLow bisa tertimpa antar TF     |
//+------------------------------------------------------------------+
//| Changelog v1.00:                                                 |
//| - Initial release berbasis NinjaEntry v4.07                     |
//| - GANTI signal generator: Session Momentum+Pullback -> ORB      |
//| - Logic: Rekam High/Low 15 menit pertama London/NY open         |
//|          Entry saat candle CLOSE breakout dari range            |
//|          SL di sisi lain range + ATR buffer                     |
//| - Semua filter lama dipertahankan                               |
//| - Tambah filter baru: Range Size Min/Max, Breakout Confirm      |
//+------------------------------------------------------------------+
#define EA_VERSION "1.01"

#property copyright "Custom EA"
#property version   EA_VERSION

#include <Trade\Trade.mqh>
CTrade trade;

//--- Input: Session ORB
input int    InpLondonOpenWIB      = 14;    // Jam London Open (WIB)
input int    InpNYOpenWIB          = 20;    // Jam NY Open (WIB)
input int    InpORBDurationMinutes = 15;    // Durasi building Opening Range (menit)
input int    InpORBMonitorMinutes  = 75;    // Durasi monitor breakout setelah range terkunci (menit)
input double InpORBMinRangeATR     = 0.5;   // Range minimum (x ATR) - terlalu sempit = false breakout
input double InpORBMaxRangeATR     = 3.0;   // Range maksimum (x ATR) - terlalu lebar = SL kejauhan
input bool   InpUseRetestEntry     = false; // Tunggu retest batas range sebelum entry (lebih selektif)

//--- Input: Multi-TF Scan (M5 + M15)
input bool   InpUseMultiTFScan   = true;   // Aktifkan scan sinyal dari M5 DAN M15

//--- Input: ADX filter
input bool   InpUseADX           = true;   // Aktifkan filter kekuatan trend
input int    InpADXPeriod        = 14;     // Periode ADX
input double InpADXMinLevel      = 20.0;   // Minimum ADX

//--- Input: Volatility & Spread filter
input int    InpATRPeriod        = 14;     // Periode ATR
input double InpATRMinPips       = 8.0;    // Minimum ATR dalam pips
input bool   InpUseSpreadFilter  = true;   // Aktifkan filter spread
input double InpMaxSpreadPips    = 35.0;   // Hard cap spread maksimum (pips)

//--- Input: Adaptive Spread Filter
input bool   InpUseAdaptiveSpread  = true;  // Aktifkan Adaptive Spread
input int    InpSpreadAvgPeriod    = 20;    // Candle untuk hitung rata-rata spread
input double InpSpreadMultiplier   = 2.0;   // Kelonggaran dari rata-rata spread
input double InpSpreadHardCap      = 35.0;  // Batas mutlak spread (pip)

//--- Input: Volatility Context Filter
input bool   InpUseVolatilityContext = true;  // Aktifkan Volatility Context Filter
input int    InpVolATRAvgPeriod      = 20;    // Periode rata-rata ATR
input double InpVolATRMinRatio       = 0.60;  // ATR sekarang minimum 60% dari rata-rata

//--- Input: Wick Rejection Filter
input bool   InpUseWickFilter    = true;   // Aktifkan filter wick rejection
input double InpMaxOppositeWick  = 0.6;    // Maks rasio wick lawan arah

//--- Input: Swing Structure Filter
input bool   InpUseSwingFilter   = true;   // Aktifkan filter struktur swing
input int    InpSwingLookback    = 20;     // Jumlah bar untuk cek struktur swing
input int    InpSwingDepth       = 3;      // Sensitivitas deteksi swing high/low

//--- Input: Asia Range Breakout Filter
input bool   InpUseAsiaRangeFilter  = true;   // Aktifkan filter Asia Range
input int    InpAsiaSessionStartWIB = 5;      // Jam mulai sesi Asia (WIB)
input int    InpAsiaSessionEndWIB   = 14;     // Jam selesai sesi Asia (WIB)
input bool   InpAsiaRangeStrictMode = false;  // Strict: hanya entry kalau sudah breakout range Asia

//--- Input: Volatility Shock Detector
input bool   InpUseVolatilityShockDetector = true;
input double InpShockATRMult          = 4.0;
input int    InpShockCooldownMinutes  = 20;

//--- Input: SL/TP
input double InpSL_ATR_Buffer = 0.5;    // Buffer SL di luar sisi range (x ATR)
input double InpRR_Ratio      = 2.0;    // Risk:Reward ratio
input bool   InpOnlyBuy       = false;  // Hanya BUY (disable SELL signal)
input double InpMaxSLPips     = 200.0;  // Batas maksimum SL (pips)

//--- Input: ATR Trailing Stop
input bool   InpUseATRTrail           = true;
input double InpATRTrailActivationPct = 50.0;
input double InpATRTrailLockPct       = 25.0;
input int    InpATRTrailPeriod        = 14;
input double InpATRTrailMultiplier    = 1.5;
input int    InpCooldownATRTrail_M5   = 10;
input int    InpCooldownATRTrail_M15  = 15;

//--- Input: Near-TP Early Close
input bool   InpUseEarlyClose  = true;
input double InpEarlyClosePct  = 85.0;

//--- Input: Session Time Filter (WIB)
input bool   InpUseSessionFilter = true;
input int    InpSessionStartWIB  = 14;
input int    InpSessionEndWIB    = 23;

//--- Input: Night Blackout
input bool   InpUseNightBlackout = true;
input int    InpBlackoutStartWIB = 22;
input int    InpBlackoutEndWIB   = 24;

//--- Input: Post-Trade Cooldown
input bool   InpUsePostTradeCooldown = true;
input int    InpCooldownAfterTP      = 15;
input int    InpCooldownAfterSL      = 30;

//--- Input: Money Management
input double InpFixedLot     = 0.01;
input int    InpMagicNumber  = 778900;

//--- Input: Max Daily Loss
input bool   InpUseDailyLossLimit = true;
input double InpMaxDailyLossPct   = 5.0;

//--- Input: Anti-repeat
input int    InpMinSecondsBetweenSignals = 600;

//--- Input: Notifikasi
input bool   InpUseAlert            = true;
input bool   InpUseSound            = true;
input string InpSoundFile           = "alert.wav";
input bool   InpUsePush             = true;
input bool   InpNotifyOnAttach      = true;
input bool   InpNotifyDailyClosing  = true;
input bool   InpNotifyDailyOpening  = true;

//--- Input: Diagnostic Logging
input bool   InpVerboseLog = false;

//--- Handles
int hATR, hADX;
int hATR_M15, hADX_M15;
int hATR_VolCtx, hATR_VolCtx_M15;
int hATR_Trail;

//--- State
datetime lastBarTime         = 0;
datetime lastSignalEntryTime = 0;
double   dailyStartBalance   = 0;
datetime currentDayStart     = 0;
bool     dailyLossHit        = false;

// Asia Range
double   asiaRangeHigh  = 0;
double   asiaRangeLow   = 0;
bool     asiaRangeReady = false;
datetime asiaRangeDate  = 0;

// Cooldown
datetime cooldownEndTime      = 0;
datetime g_shockCooldownUntil = 0;

// Notif harian
datetime g_lastClosingNotifDay = 0;
bool     g_closingNotifPending = false;
datetime g_lastOpeningNotifDay = 0;

// Trade result
string   lastTradeResult      = "";
datetime lastNotifiedDealTime = 0;

// ATR Trail
bool   g_atrTrailActive    = false;
bool   g_atrTrailNotifSent = false;
bool   g_atrTrailWasActive = false;
double g_atrTrailSL        = 0;
ulong  g_atrTrailTicket    = 0;
string g_entryTF           = "";

//+------------------------------------------------------------------+
//| ORB State - DIPISAH per sesi PER TF (FIX v1.01)                 |
//| 4 struct terpisah agar rangeHigh/rangeLow tidak tertimpa         |
//+------------------------------------------------------------------+
struct ORBSession
  {
   bool     rangeBuilding;
   bool     rangeLocked;
   double   rangeHigh;
   double   rangeLow;
   bool     entryDone;
   datetime sessionDate;
  };

ORBSession g_londonORB_M5;
ORBSession g_londonORB_M15;
ORBSession g_nyORB_M5;
ORBSession g_nyORB_M15;

//+------------------------------------------------------------------+
void ResetORBSession(ORBSession &orb)
  {
   orb.rangeBuilding = false;
   orb.rangeLocked   = false;
   orb.rangeHigh     = 0;
   orb.rangeLow      = 0;
   orb.entryDone     = false;
   orb.sessionDate   = 0;
  }

//+------------------------------------------------------------------+
//| FIX v1.01: GetPip() - urutan digit gold dikoreksi               |
//| 2 digit  (1234.56)  = broker standard   -> * 100                |
//| 3 digit  (1234.567) = broker 3 digit    -> * 10                 |
//+------------------------------------------------------------------+
double GetPip()
  {
   string sym = _Symbol;
   StringToUpper(sym);
   if(StringFind(sym, "XAU") >= 0 || StringFind(sym, "GOLD") >= 0)
     {
      if(_Digits == 2) return _Point * 100;  // FIX: was * 10
      if(_Digits == 3) return _Point * 10;   // FIX: was * 100
      if(_Digits == 1) return _Point;
      return _Point * 100;
     }
   double pip = _Point;
   if(_Digits == 3 || _Digits == 5) pip = _Point * 10;
   return pip;
  }

double GetBrokerGMTOffsetHours()
  {
   return (double)(TimeCurrent() - TimeGMT()) / 3600.0;
  }

double GetWIBHour()
  {
   double brokerOffsetGMT = GetBrokerGMTOffsetHours();
   double brokerToWIB     = 7.0 - brokerOffsetGMT;
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   double wibHour = dt.hour + (dt.min / 60.0) + brokerToWIB;
   while(wibHour < 0)     wibHour += 24.0;
   while(wibHour >= 24.0) wibHour -= 24.0;
   return wibHour;
  }

bool IsWithinTradingSession()
  {
   if(!InpUseSessionFilter) return true;
   double wibHour = GetWIBHour();
   return (wibHour >= (double)InpSessionStartWIB && wibHour < (double)InpSessionEndWIB);
  }

bool IsBlackoutHour()
  {
   if(!InpUseNightBlackout) return false;
   double wibHour = GetWIBHour();
   double startH  = (double)InpBlackoutStartWIB;
   double endH    = (double)InpBlackoutEndWIB;
   if(startH <= endH) return (wibHour >= startH && wibHour < endH);
   return (wibHour >= startH || wibHour < endH);
  }

bool HasOpenPosition()
  {
   for(int p = PositionsTotal() - 1; p >= 0; p--)
     {
      ulong ticket = PositionGetTicket(p);
      if(ticket <= 0) continue;
      if(PositionGetString(POSITION_SYMBOL)  != _Symbol)        continue;
      if(PositionGetInteger(POSITION_MAGIC)  != InpMagicNumber) continue;
      return true;
     }
   return false;
  }

bool IsSignalSpacingOK()
  {
   if(lastSignalEntryTime == 0) return true;
   return ((double)(TimeCurrent() - lastSignalEntryTime) >= InpMinSecondsBetweenSignals);
  }

//+------------------------------------------------------------------+
double GetAdaptiveSpreadThreshold()
  {
   if(!InpUseAdaptiveSpread) return InpSpreadHardCap;
   double pip_ = GetPip();
   if(pip_ <= 0) return InpSpreadHardCap;
   double sumSpread = 0; int count = 0;
   int spreadArr[];
   if(CopySpread(_Symbol, _Period, 1, InpSpreadAvgPeriod, spreadArr) > 0)
     {
      ArraySetAsSeries(spreadArr, true);
      for(int i = 0; i < ArraySize(spreadArr); i++) { sumSpread += spreadArr[i] * _Point / pip_; count++; }
     }
   if(count == 0) return InpSpreadHardCap;
   double avgSpread = sumSpread / count;
   double threshold = MathMin(avgSpread * InpSpreadMultiplier, InpSpreadHardCap);
   double curSpread = (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / pip_;
   return MathMax(threshold, curSpread * 1.5);
  }

//+------------------------------------------------------------------+
bool IsVolatilityContextOK(int hAtrAvg, double currentATR)
  {
   if(!InpUseVolatilityContext) return true;
   double atrAvgBuf[];
   int toCopy = InpVolATRAvgPeriod + 5;
   if(CopyBuffer(hAtrAvg, 0, 0, toCopy, atrAvgBuf) <= 0) return true;
   ArraySetAsSeries(atrAvgBuf, true);
   double sum = 0; int count = 0;
   for(int i = 1; i <= InpVolATRAvgPeriod && i < toCopy; i++) { sum += atrAvgBuf[i]; count++; }
   if(count == 0 || sum / count <= 0) return true;
   double ratio = currentATR / (sum / count);
   if(InpVerboseLog) Print(StringFormat("[VOL-CONTEXT] ratio=%.2f (min %.2f) -> %s",
                                         ratio, InpVolATRMinRatio, ratio >= InpVolATRMinRatio ? "OK" : "BLOKIR"));
   return (ratio >= InpVolATRMinRatio);
  }

//+------------------------------------------------------------------+
bool CheckVolatilityShock(double candleHigh, double candleLow, int hAtrAvg)
  {
   if(!InpUseVolatilityShockDetector) return false;
   double atrAvgBuf[];
   int toCopy = InpVolATRAvgPeriod + 5;
   if(CopyBuffer(hAtrAvg, 0, 0, toCopy, atrAvgBuf) <= 0) return false;
   ArraySetAsSeries(atrAvgBuf, true);
   double sum = 0; int count = 0;
   for(int i = 1; i <= InpVolATRAvgPeriod && i < toCopy; i++) { sum += atrAvgBuf[i]; count++; }
   if(count == 0) return false;
   double atrAvg = sum / count;
   if(atrAvg <= 0) return false;
   if((candleHigh - candleLow) >= atrAvg * InpShockATRMult)
     {
      g_shockCooldownUntil = TimeCurrent() + InpShockCooldownMinutes * 60;
      if(InpVerboseLog) Print("[VOL-SHOCK] Shock terdeteksi -> pause entry ", InpShockCooldownMinutes, " menit");
      return true;
     }
   return false;
  }

bool IsVolatilityShockActive()
  {
   if(!InpUseVolatilityShockDetector) return false;
   return (TimeCurrent() < g_shockCooldownUntil);
  }

//+------------------------------------------------------------------+
bool IsWickRejectionOK(const double &open[], const double &high[], const double &low[],
                       const double &close[], int idx, bool isBuy)
  {
   if(!InpUseWickFilter) return true;
   if(idx < 1) return true;
   int candles[2]; candles[0] = idx; candles[1] = idx - 1;
   for(int c = 0; c < 2; c++)
     {
      int i = candles[c];
      double range = high[i] - low[i];
      if(range <= 0) continue;
      double upperWick = high[i] - MathMax(open[i], close[i]);
      double lowerWick = MathMin(open[i], close[i]) - low[i];
      if(isBuy  && (upperWick / range) > InpMaxOppositeWick) return false;
      if(!isBuy && (lowerWick / range) > InpMaxOppositeWick) return false;
     }
   return true;
  }

//+------------------------------------------------------------------+
bool IsSwingStructureOK(const double &high[], const double &low[], int idx, int total, bool isBuy)
  {
   if(!InpUseSwingFilter) return true;
   if(idx - InpSwingLookback - InpSwingDepth < 0) return true;
   double swingHighs[]; double swingLows[];
   int shCount = 0, slCount = 0;
   ArrayResize(swingHighs, InpSwingLookback); ArrayResize(swingLows, InpSwingLookback);
   for(int b = idx - InpSwingLookback; b <= idx - InpSwingDepth; b++)
     {
      bool isSwingHigh = true, isSwingLow = true;
      for(int d = 1; d <= InpSwingDepth; d++)
        {
         if(b - d < 0 || b + d >= total) { isSwingHigh = false; isSwingLow = false; break; }
         if(high[b] <= high[b-d] || high[b] <= high[b+d]) isSwingHigh = false;
         if(low[b]  >= low[b-d]  || low[b]  >= low[b+d])  isSwingLow  = false;
        }
      if(isSwingHigh && shCount < InpSwingLookback) swingHighs[shCount++] = high[b];
      if(isSwingLow  && slCount < InpSwingLookback) swingLows[slCount++]  = low[b];
     }
   if(isBuy)  { if(slCount  < 2) return true; return swingLows[slCount-1]  > swingLows[slCount-2]; }
   else       { if(shCount < 2) return true; return swingHighs[shCount-1] < swingHighs[shCount-2]; }
  }

//+------------------------------------------------------------------+
void UpdateAsiaRange(double highPrice, double lowPrice)
  {
   if(!InpUseAsiaRangeFilter) return;
   double wibHour = GetWIBHour();
   MqlDateTime dtToday; TimeToStruct(TimeCurrent(), dtToday);
   dtToday.hour = 0; dtToday.min = 0; dtToday.sec = 0;
   datetime todayStart = StructToTime(dtToday);
   if(todayStart != asiaRangeDate)
     { asiaRangeHigh = 0; asiaRangeLow = 0; asiaRangeReady = false; asiaRangeDate = todayStart; }
   bool inAsiaScan = (wibHour >= InpAsiaSessionStartWIB && wibHour < InpAsiaSessionEndWIB);
   bool afterAsia  = (wibHour >= InpAsiaSessionEndWIB);
   if(inAsiaScan)
     {
      if(asiaRangeHigh == 0 || highPrice > asiaRangeHigh) asiaRangeHigh = highPrice;
      if(asiaRangeLow  == 0 || lowPrice  < asiaRangeLow)  asiaRangeLow  = lowPrice;
      asiaRangeReady = false;
     }
   else if(afterAsia && asiaRangeHigh > 0 && asiaRangeLow > 0)
     {
      if(!asiaRangeReady) { asiaRangeReady = true; Print(StringFormat("[ASIA-RANGE] High=%.5f Low=%.5f", asiaRangeHigh, asiaRangeLow)); }
     }
  }

int GetAsiaRangeBias(double currentPrice)
  {
   if(!InpUseAsiaRangeFilter || !asiaRangeReady) return 0;
   if(currentPrice > asiaRangeHigh) return 1;
   if(currentPrice < asiaRangeLow)  return -1;
   return 0;
  }

//+------------------------------------------------------------------+
void ResetDailyTracking()
  {
   dailyStartBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   MqlDateTime dt; TimeToStruct(TimeCurrent(), dt);
   dt.hour = 0; dt.min = 0; dt.sec = 0;
   currentDayStart = StructToTime(dt);
   dailyLossHit    = false;
  }

bool CheckDailyLossLimit()
  {
   if(!InpUseDailyLossLimit) return false;
   MqlDateTime dt; TimeToStruct(TimeCurrent(), dt);
   dt.hour = 0; dt.min = 0; dt.sec = 0;
   if(StructToTime(dt) != currentDayStart) ResetDailyTracking();
   double lossPct = (dailyStartBalance - AccountInfoDouble(ACCOUNT_EQUITY)) / dailyStartBalance * 100.0;
   if(lossPct >= InpMaxDailyLossPct)
     {
      if(!dailyLossHit)
        {
         dailyLossHit = true;
         string msg = StringFormat("EA STOP: Limit rugi harian %.1f%% tercapai.", InpMaxDailyLossPct);
         Print(msg); if(InpUseAlert) Alert(msg); if(InpUsePush) SendNotification(msg);
        }
      return true;
     }
   return false;
  }

//+------------------------------------------------------------------+
void CheckAndUpdateCooldown()
  {
   if(!InpUsePostTradeCooldown) return;
   if(!HistorySelect(TimeCurrent() - 86400, TimeCurrent())) return;
   int totalDeals = HistoryDealsTotal();
   for(int i = totalDeals - 1; i >= 0; i--)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket <= 0) continue;
      if(HistoryDealGetString(ticket, DEAL_SYMBOL)  != _Symbol)        continue;
      if(HistoryDealGetInteger(ticket, DEAL_MAGIC)  != InpMagicNumber) continue;
      if(HistoryDealGetInteger(ticket, DEAL_ENTRY)  != DEAL_ENTRY_OUT) continue;
      datetime dealTime = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
      ENUM_DEAL_REASON reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(ticket, DEAL_REASON);
      int cooldownMinutes;
      if(reason == DEAL_REASON_TP)          { cooldownMinutes = InpCooldownAfterTP;  lastTradeResult = "TP"; }
      else if(reason == DEAL_REASON_SL)
        {
         if(g_atrTrailWasActive)
           { cooldownMinutes = (g_entryTF == "M15") ? InpCooldownATRTrail_M15 : InpCooldownATRTrail_M5; lastTradeResult = "ATR TRAIL"; g_atrTrailWasActive = false; }
         else
           { cooldownMinutes = InpCooldownAfterSL; lastTradeResult = "SL"; }
        }
      else if(reason == DEAL_REASON_EXPERT) { cooldownMinutes = InpCooldownAfterTP;  lastTradeResult = "EARLY CLOSE"; }
      else                                   { cooldownMinutes = InpCooldownAfterSL;  lastTradeResult = "MANUAL CLOSE"; }
      cooldownEndTime = dealTime + (cooldownMinutes * 60);
      break;
     }
  }

bool IsInCooldown()
  {
   if(!InpUsePostTradeCooldown || cooldownEndTime == 0) return false;
   return (TimeCurrent() < cooldownEndTime);
  }

//+------------------------------------------------------------------+
void FireNotification(bool isBuy, double entry, double sl, double tp,
                      double slPips, double tpPips, string sessionLabel,
                      ENUM_TIMEFRAMES sourceTF, double orbHigh, double orbLow)
  {
   string dir    = isBuy ? "BUY" : "SELL";
   string tfName = (sourceTF == PERIOD_M15) ? "M15" : "M5";
   string msg = StringFormat(
      "NinjaEntry ORB by IKY x Claude\n"
      "====================\n"
      "%s %s\n"
      "====================\n"
      "Entry  : %s\n"
      "SL     : %s (%.1f pip)\n"
      "TP     : %s (%.1f pip)\n"
      "====================\n"
      "Strategy : Opening Range Breakout\n"
      "Sesi     : %s Open | TF: %s\n"
      "ORB High : %s\n"
      "ORB Low  : %s\n"
      "Breakout : %s\n"
      "RR       : 1:%.1f\n"
      "====================\n"
      "Note: Allah suka hamba yg terus kembali walau sering jatuh. "
      "Jaga MM, jangan overtrade.",
      dir, _Symbol,
      DoubleToString(entry, _Digits),
      DoubleToString(sl, _Digits), slPips,
      DoubleToString(tp, _Digits), tpPips,
      sessionLabel, tfName,
      DoubleToString(orbHigh, _Digits),
      DoubleToString(orbLow, _Digits),
      isBuy ? "BULLISH (close > ORB High)" : "BEARISH (close < ORB Low)",
      InpRR_Ratio
   );
   if(InpUseAlert) Alert(msg);
   if(InpUseSound) PlaySound(InpSoundFile);
   if(InpUsePush && !SendNotification(msg))
      Print("Push notification gagal. Error: ", GetLastError());
  }

//+------------------------------------------------------------------+
void SendClosingNotifNow()
  {
   double dailyPnL = AccountInfoDouble(ACCOUNT_BALANCE) - dailyStartBalance;
   string ccy = AccountInfoString(ACCOUNT_CURRENCY);
   string tail;
   if(MathAbs(dailyPnL) < 0.01)  tail = "hari ini tidak ada trade, tenang aja";
   else if(dailyPnL > 0)          tail = StringFormat("cuan hari ini +%s %s, alhamdulillah!", DoubleToString(dailyPnL, 2), ccy);
   else                            tail = StringFormat("minus %s %s, gapapa besok coba lagi", DoubleToString(MathAbs(dailyPnL), 2), ccy);
   string msg = StringFormat("NinjaEntry ORB\nSesi hari ini ditutup. %s", tail);
   if(InpUseAlert) Alert(msg);
   if(!SendNotification(msg)) Print("[NOTIF] Closing notif gagal.");
  }

void ProcessPendingClosingNotif()
  {
   if(!g_closingNotifPending) return;
   if(HasOpenPosition()) return;
   SendClosingNotifNow();
   g_closingNotifPending = false;
  }

void CheckDailyClosingNotif()
  {
   if(!InpNotifyDailyClosing || !InpUsePush) return;
   double wibHour = GetWIBHour();
   if(wibHour < (double)InpBlackoutStartWIB || wibHour >= (double)InpBlackoutStartWIB + 1.0) return;
   datetime wibNow = TimeCurrent() + (datetime)MathRound((7.0 - GetBrokerGMTOffsetHours()) * 3600);
   MqlDateTime dtWIB; TimeToStruct(wibNow, dtWIB);
   dtWIB.hour = 0; dtWIB.min = 0; dtWIB.sec = 0;
   datetime todayWIB = StructToTime(dtWIB);
   if(todayWIB == g_lastClosingNotifDay) return;
   g_lastClosingNotifDay = todayWIB;
   if(HasOpenPosition()) { g_closingNotifPending = true; return; }
   SendClosingNotifNow();
  }

void CheckDailyOpeningNotif()
  {
   if(!InpNotifyDailyOpening || !InpUsePush) return;
   double wibHour = GetWIBHour();
   if(wibHour < (double)InpSessionStartWIB || wibHour >= (double)InpSessionStartWIB + 1.0) return;
   datetime wibNow = TimeCurrent() + (datetime)MathRound((7.0 - GetBrokerGMTOffsetHours()) * 3600);
   MqlDateTime dtWIB; TimeToStruct(wibNow, dtWIB);
   dtWIB.hour = 0; dtWIB.min = 0; dtWIB.sec = 0;
   datetime todayWIB = StructToTime(dtWIB);
   if(todayWIB == g_lastOpeningNotifDay) return;
   g_lastOpeningNotifDay = todayWIB;
   string msg = "NinjaEntry ORB\nWaktunya kerja! Sesi trading hari ini dimulai. Semangat boss!";
   if(InpUseAlert) Alert(msg);
   if(!SendNotification(msg)) Print("[NOTIF] Opening notif gagal.");
  }

//+------------------------------------------------------------------+
void CheckTPSLHit()
  {
   static datetime lastTPSLCheck = 0;
   if(TimeCurrent() == lastTPSLCheck) return;
   lastTPSLCheck = TimeCurrent();
   if(!HistorySelect(TimeCurrent() - 86400, TimeCurrent())) return;
   int totalDeals = HistoryDealsTotal();
   for(int i = totalDeals - 1; i >= 0; i--)
     {
      ulong ticket = HistoryDealGetTicket(i);
      if(ticket <= 0) continue;
      if(HistoryDealGetString(ticket, DEAL_SYMBOL)  != _Symbol)        continue;
      if(HistoryDealGetInteger(ticket, DEAL_MAGIC)  != InpMagicNumber) continue;
      if(HistoryDealGetInteger(ticket, DEAL_ENTRY)  != DEAL_ENTRY_OUT) continue;
      datetime dealTime = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
      if(dealTime <= lastNotifiedDealTime) break;
      ENUM_DEAL_REASON reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(ticket, DEAL_REASON);
      double profit     = HistoryDealGetDouble(ticket, DEAL_PROFIT);
      double swap       = HistoryDealGetDouble(ticket, DEAL_SWAP);
      double commission = HistoryDealGetDouble(ticket, DEAL_COMMISSION);
      double totalPnL   = profit + swap + commission;
      double closePrice = HistoryDealGetDouble(ticket, DEAL_PRICE);
      double volume     = HistoryDealGetDouble(ticket, DEAL_VOLUME);
      string ccy        = AccountInfoString(ACCOUNT_CURRENCY);
      string pnlStr     = StringFormat("%s%.2f %s", totalPnL >= 0 ? "+" : "", totalPnL, ccy);
      bool isTP       = (reason == DEAL_REASON_TP);
      bool isSL       = (reason == DEAL_REASON_SL);
      bool isExpert   = (reason == DEAL_REASON_EXPERT);
      bool isATRTrail = isSL && (g_atrTrailActive || g_atrTrailWasActive);
      string msg;
      if(isATRTrail)
         msg = StringFormat("NinjaEntry ORB by IKY x Claude\nTRAILING EXIT\n%s | Vol: %.2f lot\nClose: %s\nP&L: %s\nProfit terkunci ATR Trail. Alhamdulillah!", _Symbol, volume, DoubleToString(closePrice, _Digits), pnlStr);
      else if(isTP)
         msg = StringFormat("NinjaEntry ORB by IKY x Claude\nTP HIT\n%s | Vol: %.2f lot\nClose: %s\nP&L: %s\nAlhamdulillah target tercapai!", _Symbol, volume, DoubleToString(closePrice, _Digits), pnlStr);
      else if(isExpert)
         msg = StringFormat("NinjaEntry ORB by IKY x Claude\nEARLY CLOSE\n%s | Vol: %.2f lot\nClose: %s\nP&L: %s", _Symbol, volume, DoubleToString(closePrice, _Digits), pnlStr);
      else
         msg = StringFormat("NinjaEntry ORB by IKY x Claude\nSL HIT\n%s | Vol: %.2f lot\nClose: %s\nP&L: %s\nEvaluasi entry, jaga MM. Besok coba lagi!", _Symbol, volume, DoubleToString(closePrice, _Digits), pnlStr);
      g_atrTrailWasActive = false; g_atrTrailActive = false;
      g_atrTrailNotifSent = false; g_atrTrailSL = 0; g_atrTrailTicket = 0;
      if(InpUseAlert) Alert(msg);
      if(InpUseSound) PlaySound(InpSoundFile);
      if(InpUsePush && !SendNotification(msg)) Print("[NOTIF] Push TP/SL gagal.");
      lastNotifiedDealTime = dealTime;
      break;
     }
  }

//+------------------------------------------------------------------+
void CheckEarlyClose()
  {
   if(!InpUseEarlyClose) return;
   for(int p = PositionsTotal() - 1; p >= 0; p--)
     {
      ulong ticket = PositionGetTicket(p);
      if(ticket <= 0) continue;
      if(PositionGetString(POSITION_SYMBOL)  != _Symbol)        continue;
      if(PositionGetInteger(POSITION_MAGIC)  != InpMagicNumber) continue;
      double entryPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double tpPrice    = PositionGetDouble(POSITION_TP);
      double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      if(tpPrice <= 0) continue;
      double totalDist, currentDist;
      if(posType == POSITION_TYPE_BUY)
        { totalDist = tpPrice - entryPrice; currentDist = currentBid - entryPrice; }
      else
        { totalDist = entryPrice - tpPrice; currentDist = entryPrice - currentAsk; }
      if(totalDist <= 0) continue;
      double pct = (currentDist / totalDist) * 100.0;
      if(pct >= InpEarlyClosePct)
        {
         if(trade.PositionClose(ticket))
           {
            string msg = StringFormat("NinjaEntry ORB\nEARLY CLOSE %s\nProfit dikunci di %.0f%% dari TP", _Symbol, pct);
            if(InpUseAlert) Alert(msg);
            if(InpUseSound) PlaySound(InpSoundFile);
            if(InpUsePush) SendNotification(msg);
            lastNotifiedDealTime = TimeCurrent();
           }
        }
     }
  }

//+------------------------------------------------------------------+
void CheckATRTrailing()
  {
   if(!InpUseATRTrail) return;
   if(PositionsTotal() == 0)
     {
      if(g_atrTrailActive) g_atrTrailWasActive = true;
      g_atrTrailActive = false; g_atrTrailNotifSent = false; g_atrTrailSL = 0; g_atrTrailTicket = 0;
      return;
     }
   ulong ticket = 0;
   for(int i = 0; i < PositionsTotal(); i++)
     {
      ulong tk = PositionGetTicket(i);
      if(tk == 0) continue;
      if(PositionGetString(POSITION_SYMBOL)  != _Symbol)        continue;
      if(PositionGetInteger(POSITION_MAGIC)  != InpMagicNumber) continue;
      ticket = tk; break;
     }
   if(ticket == 0)
     {
      if(g_atrTrailActive) g_atrTrailWasActive = true;
      g_atrTrailActive = false; g_atrTrailNotifSent = false; g_atrTrailSL = 0;
      return;
     }
   if(g_atrTrailTicket != ticket)
     { g_atrTrailActive = false; g_atrTrailNotifSent = false; g_atrTrailSL = 0; g_atrTrailTicket = ticket; }
   ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
   double entryPrice   = PositionGetDouble(POSITION_PRICE_OPEN);
   double currentSL    = PositionGetDouble(POSITION_SL);
   double currentTP    = PositionGetDouble(POSITION_TP);
   double bid          = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double ask          = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double currentPrice = (posType == POSITION_TYPE_BUY) ? bid : ask;
   long   stopLevel    = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double minDist      = stopLevel * _Point;
   double tpDist       = MathAbs(currentTP - entryPrice);
   double profitDist   = (posType == POSITION_TYPE_BUY) ? currentPrice - entryPrice : entryPrice - currentPrice;
   if(tpDist <= 0 || (profitDist / tpDist * 100.0) < InpATRTrailActivationPct) return;
   double atrBuf[];
   if(CopyBuffer(hATR_Trail, 0, 0, 3, atrBuf) <= 0) return;
   ArraySetAsSeries(atrBuf, true);
   double atrVal     = atrBuf[1];
   double newTrailSL = (posType == POSITION_TYPE_BUY) ? currentPrice - (atrVal * InpATRTrailMultiplier)
                                                       : currentPrice + (atrVal * InpATRTrailMultiplier);
   if(!g_atrTrailActive)
     {
      double lockDist  = tpDist * (InpATRTrailLockPct / 100.0);
      double lockSL    = (posType == POSITION_TYPE_BUY) ? entryPrice + lockDist : entryPrice - lockDist;
      g_atrTrailSL     = (posType == POSITION_TYPE_BUY) ? MathMax(lockSL, newTrailSL) : MathMin(lockSL, newTrailSL);
      g_atrTrailActive = true;
      double slNorm    = NormalizeDouble(g_atrTrailSL, _Digits);
      double distCheck = (posType == POSITION_TYPE_BUY) ? currentPrice - slNorm : slNorm - currentPrice;
      if(MathAbs(slNorm - currentSL) > _Point && distCheck >= minDist) trade.PositionModify(ticket, slNorm, currentTP);
      if(!g_atrTrailNotifSent)
        {
         string msg = StringFormat("NinjaEntry ORB\nTRAILING AKTIF\n%s\nEntry: %s | Harga: %s\nSL Lock: %s",
                                    _Symbol, DoubleToString(entryPrice, _Digits),
                                    DoubleToString(currentPrice, _Digits), DoubleToString(g_atrTrailSL, _Digits));
         if(InpUseAlert) Alert(msg);
         if(InpUsePush) SendNotification(msg);
         g_atrTrailNotifSent = true;
        }
      return;
     }
   bool slImproved = (posType == POSITION_TYPE_BUY  && newTrailSL > g_atrTrailSL) ||
                     (posType == POSITION_TYPE_SELL && newTrailSL < g_atrTrailSL);
   if(slImproved)
     {
      g_atrTrailSL = newTrailSL;
      double slNorm    = NormalizeDouble(g_atrTrailSL, _Digits);
      double distCheck = (posType == POSITION_TYPE_BUY) ? currentPrice - slNorm : slNorm - currentPrice;
      if(MathAbs(slNorm - currentSL) > _Point * 2 && distCheck >= minDist)
         trade.PositionModify(ticket, slNorm, currentTP);
     }
  }

//+------------------------------------------------------------------+
//| UPDATE ORB STATE per sesi per TF - FIX v1.01                    |
//| Sekarang tiap TF punya struct sendiri, tidak saling override     |
//+------------------------------------------------------------------+
void UpdateORBState(ORBSession &orb, int sessionOpenWIB, ENUM_TIMEFRAMES tf)
  {
   double wibHour   = GetWIBHour();
   double buildEndH = sessionOpenWIB + (InpORBDurationMinutes / 60.0);
   double monEndH   = sessionOpenWIB + ((InpORBDurationMinutes + InpORBMonitorMinutes) / 60.0);

   // Reset harian
   MqlDateTime dtToday; TimeToStruct(TimeCurrent(), dtToday);
   dtToday.hour = 0; dtToday.min = 0; dtToday.sec = 0;
   datetime todayStart = StructToTime(dtToday);
   if(orb.sessionDate != todayStart)
     { ResetORBSession(orb); orb.sessionDate = todayStart; }

   bool inBuildPhase   = (wibHour >= sessionOpenWIB && wibHour < buildEndH);
   bool inMonitorPhase = (wibHour >= buildEndH && wibHour < monEndH);

   // Fase 1: Building range - rekam High/Low candle closed
   if(inBuildPhase && !orb.rangeLocked)
     {
      double highArr[], lowArr[];
      if(CopyHigh(_Symbol, tf, 1, 1, highArr) <= 0) return;
      if(CopyLow (_Symbol, tf, 1, 1, lowArr)  <= 0) return;
      if(orb.rangeHigh == 0 || highArr[0] > orb.rangeHigh) orb.rangeHigh = highArr[0];
      if(orb.rangeLow  == 0 || lowArr[0]  < orb.rangeLow)  orb.rangeLow  = lowArr[0];
      orb.rangeBuilding = true;
      if(InpVerboseLog) Print(StringFormat("[ORB-%s] Building: High=%.5f Low=%.5f", EnumToString(tf), orb.rangeHigh, orb.rangeLow));
     }
   // Fase 2: Kunci range saat build selesai
   else if(inMonitorPhase && orb.rangeBuilding && !orb.rangeLocked)
     {
      orb.rangeLocked   = true;
      orb.rangeBuilding = false;
      Print(StringFormat("[ORB-%s] Range LOCKED: High=%.5f Low=%.5f Spread=%.5f",
                          EnumToString(tf), orb.rangeHigh, orb.rangeLow, orb.rangeHigh - orb.rangeLow));
     }
  }

//+------------------------------------------------------------------+
//| CORE SIGNAL: Opening Range Breakout                              |
//| Return: 1=BUY, -1=SELL, 0=no signal                             |
//+------------------------------------------------------------------+
int DetectORBSignal(ORBSession &orb, int sessionOpenWIB, ENUM_TIMEFRAMES tf,
                    int hAtr, int hAdx, int hAtrVolCtx,
                    double &outSLDist, double &outATR, string &outSessName)
  {
   outSLDist = 0; outATR = 0;
   outSessName = (sessionOpenWIB == InpLondonOpenWIB) ? "London" : "NY";

   if(!orb.rangeLocked || orb.entryDone) return 0;

   double pip    = GetPip();
   int    toCopy = 50;

   double atrBuf[], adxBuf[];
   double open_[], high_[], low_[], close_[];
   if(CopyBuffer(hAtr,  0, 0, toCopy, atrBuf)    <= 0) return 0;
   if(CopyBuffer(hAdx,  0, 0, toCopy, adxBuf)    <= 0) return 0;
   if(CopyOpen (_Symbol, tf, 0, toCopy, open_)   <= 0) return 0;
   if(CopyHigh (_Symbol, tf, 0, toCopy, high_)   <= 0) return 0;
   if(CopyLow  (_Symbol, tf, 0, toCopy, low_)    <= 0) return 0;
   if(CopyClose(_Symbol, tf, 0, toCopy, close_)  <= 0) return 0;
   ArraySetAsSeries(atrBuf, true); ArraySetAsSeries(adxBuf, true);
   ArraySetAsSeries(open_,  true); ArraySetAsSeries(high_,  true);
   ArraySetAsSeries(low_,   true); ArraySetAsSeries(close_, true);

   int    shift   = 1;
   outATR         = atrBuf[shift];
   double minATR  = InpATRMinPips * pip;

   if(outATR < minATR)
     { if(InpVerboseLog) Print("[ORB] ATR terlalu kecil, skip"); return 0; }

   if(InpUseADX && adxBuf[shift] < InpADXMinLevel)
     { if(InpVerboseLog) Print(StringFormat("[ORB] ADX %.1f < %.1f, skip", adxBuf[shift], InpADXMinLevel)); return 0; }

   if(!IsVolatilityContextOK(hAtrVolCtx, outATR))
     { if(InpVerboseLog) Print("[ORB] Volatility context tidak cukup, skip"); return 0; }

   // Validasi ukuran range
   double rangeSize    = orb.rangeHigh - orb.rangeLow;
   double minRangeSize = outATR * InpORBMinRangeATR;
   double maxRangeSize = outATR * InpORBMaxRangeATR;
   if(rangeSize < minRangeSize)
     { if(InpVerboseLog) Print(StringFormat("[ORB] Range terlalu kecil: %.5f < %.5f", rangeSize, minRangeSize)); return 0; }
   if(rangeSize > maxRangeSize)
     { if(InpVerboseLog) Print(StringFormat("[ORB] Range terlalu besar: %.5f > %.5f", rangeSize, maxRangeSize)); return 0; }

   // Deteksi breakout: candle CLOSE di luar range
   double closePrice = close_[shift];
   bool   breakBull  = (closePrice > orb.rangeHigh);
   bool   breakBear  = (closePrice < orb.rangeLow);

   if(!breakBull && !breakBear)
     { if(InpVerboseLog) Print(StringFormat("[ORB] Belum breakout. Close=%.5f Range=[%.5f,%.5f]", closePrice, orb.rangeLow, orb.rangeHigh)); return 0; }

   bool isBuy = breakBull;

   if(InpOnlyBuy && !isBuy)
     { if(InpVerboseLog) Print("[ORB] SELL diblokir - InpOnlyBuy aktif"); return 0; }

   // Retest filter (opsional)
   if(InpUseRetestEntry)
     {
      double currentPrice = isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
      bool retestOK = isBuy ? (currentPrice <= orb.rangeHigh * 1.001) : (currentPrice >= orb.rangeLow * 0.999);
      if(!retestOK) { if(InpVerboseLog) Print("[ORB] Menunggu retest sebelum entry"); return 0; }
     }

   // Konversi ke linear index untuk filter wick & swing
   double linOpen[], linHigh[], linLow[], linClose[];
   ArrayResize(linOpen, toCopy); ArrayResize(linHigh, toCopy);
   ArrayResize(linLow,  toCopy); ArrayResize(linClose, toCopy);
   for(int k = 0; k < toCopy; k++)
     {
      linOpen[k]  = open_[toCopy-1-k];
      linHigh[k]  = high_[toCopy-1-k];
      linLow[k]   = low_[toCopy-1-k];
      linClose[k] = close_[toCopy-1-k];
     }
   int linShift = toCopy - 1 - shift;

   if(!IsWickRejectionOK(linOpen, linHigh, linLow, linClose, linShift, isBuy))
     { if(InpVerboseLog) Print("[ORB] Wick rejection filter gagal"); return 0; }

   if(!IsSwingStructureOK(linHigh, linLow, linShift, toCopy, isBuy))
     { if(InpVerboseLog) Print("[ORB] Swing structure filter gagal"); return 0; }

   // Asia Range Bias
   double entryPrice = isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
   int    asiaBias   = GetAsiaRangeBias(entryPrice);
   if(InpUseAsiaRangeFilter && asiaRangeReady)
     {
      bool asiaOK = true;
      if(InpAsiaRangeStrictMode)
           asiaOK = isBuy ? (asiaBias == 1) : (asiaBias == -1);
      else
        {
         if(asiaBias ==  1 && !isBuy) asiaOK = false;
         if(asiaBias == -1 &&  isBuy) asiaOK = false;
        }
      if(!asiaOK) { if(InpVerboseLog) Print("[ORB] Asia Range bias bertentangan"); return 0; }
     }

   // SL: sisi lain range + ATR buffer
   double slLevel = isBuy ? orb.rangeLow  - (outATR * InpSL_ATR_Buffer)
                           : orb.rangeHigh + (outATR * InpSL_ATR_Buffer);
   outSLDist = MathAbs(entryPrice - slLevel);

   if(outSLDist / pip > InpMaxSLPips)
     { if(InpVerboseLog) Print(StringFormat("[ORB] SL %.1fp > MaxSLPips %.1fp, skip", outSLDist/pip, InpMaxSLPips)); return 0; }

   // Floor SL minimum 1x ATR
   if(outSLDist < outATR) outSLDist = outATR;

   if(InpVerboseLog)
      Print(StringFormat("[ORB] %s %s VALID! Entry=%.5f SL-dist=%.5f (%.1fp)",
                          outSessName, isBuy ? "BUY" : "SELL", entryPrice, outSLDist, outSLDist/pip));

   return isBuy ? 1 : -1;
  }

//+------------------------------------------------------------------+
//| Helper: eksekusi order dan notif                                 |
//+------------------------------------------------------------------+
void ExecuteOrder(bool isBuy, double slDist, double atr, ORBSession &orb,
                  string sessName, ENUM_TIMEFRAMES tf, string comment)
  {
   double pip        = GetPip();
   double execPrice  = isBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double sl         = isBuy ? execPrice - slDist : execPrice + slDist;
   double tp         = isBuy ? execPrice + slDist * InpRR_Ratio : execPrice - slDist * InpRR_Ratio;
   bool ok = isBuy ? trade.Buy(InpFixedLot, _Symbol, execPrice, sl, tp, comment)
                   : trade.Sell(InpFixedLot, _Symbol, execPrice, sl, tp, comment);
   if(ok)
     {
      g_atrTrailActive    = false;
      g_atrTrailWasActive = false;
      lastSignalEntryTime = TimeCurrent();
      g_entryTF           = (tf == PERIOD_M15) ? "M15" : "M5";
      orb.entryDone       = true;
      FireNotification(isBuy, execPrice, sl, tp, slDist/pip, slDist*InpRR_Ratio/pip, sessName, tf, orb.rangeHigh, orb.rangeLow);
     }
   else
      Print("Order gagal [", comment, "]: ", trade.ResultRetcodeDescription());
  }

//+------------------------------------------------------------------+
//| OnInit                                                            |
//+------------------------------------------------------------------+
int OnInit()
  {
   hATR       = iATR(_Symbol, _Period,    InpATRPeriod);
   hADX       = iADX(_Symbol, _Period,    InpADXPeriod);
   hATR_VolCtx     = iATR(_Symbol, _Period,    InpVolATRAvgPeriod);
   hATR_VolCtx_M15 = iATR(_Symbol, PERIOD_M15, InpVolATRAvgPeriod);

   if(InpUseMultiTFScan)
     {
      hATR_M15 = iATR(_Symbol, PERIOD_M15, InpATRPeriod);
      hADX_M15 = iADX(_Symbol, PERIOD_M15, InpADXPeriod);
      if(hATR_M15 == INVALID_HANDLE || hADX_M15 == INVALID_HANDLE)
        { Print("Gagal membuat handle M15."); return(INIT_FAILED); }
     }

   hATR_Trail = iATR(_Symbol, _Period, InpATRTrailPeriod);

   if(hATR == INVALID_HANDLE || hADX == INVALID_HANDLE ||
      hATR_VolCtx == INVALID_HANDLE || hATR_VolCtx_M15 == INVALID_HANDLE ||
      hATR_Trail == INVALID_HANDLE)
     { Print("Gagal membuat handle indikator."); return(INIT_FAILED); }

   trade.SetExpertMagicNumber(InpMagicNumber);
   trade.SetDeviationInPoints(20);

   ResetDailyTracking();
   ResetORBSession(g_londonORB_M5);
   ResetORBSession(g_londonORB_M15);
   ResetORBSession(g_nyORB_M5);
   ResetORBSession(g_nyORB_M15);

   // Restore ATR Trail dari posisi terbuka
   for(int i = 0; i < PositionsTotal(); i++)
     {
      ulong tk = PositionGetTicket(i);
      if(tk == 0) continue;
      if(PositionGetString(POSITION_SYMBOL)  != _Symbol)        continue;
      if(PositionGetInteger(POSITION_MAGIC)  != InpMagicNumber) continue;
      string cmt = PositionGetString(POSITION_COMMENT);
      g_entryTF        = (StringFind(cmt, "M15") >= 0) ? "M15" : "M5";
      g_atrTrailTicket = tk;
      ENUM_POSITION_TYPE pt  = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      double ep  = PositionGetDouble(POSITION_PRICE_OPEN);
      double csl = PositionGetDouble(POSITION_SL);
      if((pt == POSITION_TYPE_BUY && csl > ep) || (pt == POSITION_TYPE_SELL && csl < ep))
        { g_atrTrailActive = true; g_atrTrailNotifSent = true; g_atrTrailSL = csl; }
      break;
     }

   Print("NinjaEntry ORB v", EA_VERSION, " initialized. Symbol=", _Symbol,
         " Digits=", _Digits, " Pip=", GetPip(), " Broker GMT=", GetBrokerGMTOffsetHours());

   if(InpNotifyOnAttach && InpUsePush)
     {
      string msg = StringFormat(
         "NinjaEntry ORB by IKY x Claude\n"
         "EA Attached/Restarted\n"
         "Symbol: %s | TF: %s\n"
         "Broker: %d digit | GMT+%.0f\n"
         "Versi: v%s\n"
         "London ORB: %d:00 WIB (%d menit range)\n"
         "NY ORB    : %d:00 WIB (%d menit range)\n"
         "Salah? Perbaiki. Gagal? Coba lagi. Jangan menyerah.",
         _Symbol, EnumToString(_Period),
         _Digits, GetBrokerGMTOffsetHours(),
         EA_VERSION,
         InpLondonOpenWIB, InpORBDurationMinutes,
         InpNYOpenWIB, InpORBDurationMinutes
      );
      if(!SendNotification(msg)) Print("[NOTIF] Push attach gagal.");
     }

   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   IndicatorRelease(hATR);  IndicatorRelease(hADX);
   IndicatorRelease(hATR_Trail);
   if(hATR_M15        != INVALID_HANDLE) IndicatorRelease(hATR_M15);
   if(hADX_M15        != INVALID_HANDLE) IndicatorRelease(hADX_M15);
   if(hATR_VolCtx     != INVALID_HANDLE) IndicatorRelease(hATR_VolCtx);
   if(hATR_VolCtx_M15 != INVALID_HANDLE) IndicatorRelease(hATR_VolCtx_M15);
  }

//+------------------------------------------------------------------+
//| OnTick                                                            |
//+------------------------------------------------------------------+
void OnTick()
  {
   // Tick-level: trailing, early close, TP/SL monitor (tidak butuh candle baru)
   CheckTPSLHit();
   CheckEarlyClose();
   CheckATRTrailing();

   // Candle-level gate
   datetime barTime = iTime(_Symbol, _Period, 0);
   if(barTime == lastBarTime) return;
   lastBarTime = barTime;

   // Notif harian
   CheckDailyClosingNotif();
   ProcessPendingClosingNotif();
   CheckDailyOpeningNotif();

   if(CheckDailyLossLimit()) return;
   if(HasOpenPosition())     return;

   CheckAndUpdateCooldown();
   if(IsInCooldown())            return;
   if(!IsSignalSpacingOK())      return;
   if(!IsWithinTradingSession()) return;
   if(IsBlackoutHour())          return;

   // Volatility Shock
   double shockHigh[], shockLow[];
   if(CopyHigh(_Symbol, _Period, 1, 1, shockHigh) > 0 &&
      CopyLow (_Symbol, _Period, 1, 1, shockLow)  > 0)
      CheckVolatilityShock(shockHigh[0], shockLow[0], hATR_VolCtx);
   if(IsVolatilityShockActive()) return;

   // Update Asia Range
   double highArr[], lowArr[];
   if(CopyHigh(_Symbol, _Period, 1, 1, highArr) > 0 &&
      CopyLow (_Symbol, _Period, 1, 1, lowArr)  > 0)
      UpdateAsiaRange(highArr[0], lowArr[0]);

   // Spread check
   double pip       = GetPip();
   double spreadNow = (double)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * _Point;
   if(InpUseSpreadFilter && spreadNow > GetAdaptiveSpreadThreshold() * pip)
     {
      if(InpVerboseLog) Print(StringFormat("[SPREAD] Spread %.1fp > threshold, skip", spreadNow/pip));
      return;
     }

   // ===== UPDATE ORB STATE - M5 (tiap candle baru M5) =====
   UpdateORBState(g_londonORB_M5, InpLondonOpenWIB, _Period);
   UpdateORBState(g_nyORB_M5,     InpNYOpenWIB,     _Period);

   // ===== SCAN & ENTRY M15 (prioritas utama) =====
   if(InpUseMultiTFScan && hATR_M15 != INVALID_HANDLE)
     {
      static datetime lastBarM15 = 0;
      datetime barM15 = iTime(_Symbol, PERIOD_M15, 0);
      if(barM15 != lastBarM15)
        {
         lastBarM15 = barM15;

         // Update ORB state M15 hanya saat candle M15 baru
         UpdateORBState(g_londonORB_M15, InpLondonOpenWIB, PERIOD_M15);
         UpdateORBState(g_nyORB_M15,     InpNYOpenWIB,     PERIOD_M15);

         // Cek signal London M15
         double atr_L = 0, slDist_L = 0; string sess_L = "";
         int sigL = DetectORBSignal(g_londonORB_M15, InpLondonOpenWIB, PERIOD_M15,
                                     hATR_M15, hADX_M15, hATR_VolCtx_M15,
                                     slDist_L, atr_L, sess_L);
         if(sigL != 0)
           {
            ExecuteOrder(sigL == 1, slDist_L, atr_L, g_londonORB_M15,
                          sess_L, PERIOD_M15,
                          sigL == 1 ? "NinjaORB M15 Buy London" : "NinjaORB M15 Sell London");
            return;
           }

         // Cek signal NY M15
         double atr_N = 0, slDist_N = 0; string sess_N = "";
         int sigN = DetectORBSignal(g_nyORB_M15, InpNYOpenWIB, PERIOD_M15,
                                     hATR_M15, hADX_M15, hATR_VolCtx_M15,
                                     slDist_N, atr_N, sess_N);
         if(sigN != 0)
           {
            ExecuteOrder(sigN == 1, slDist_N, atr_N, g_nyORB_M15,
                          sess_N, PERIOD_M15,
                          sigN == 1 ? "NinjaORB M15 Buy NY" : "NinjaORB M15 Sell NY");
            return;
           }
        }
     }

   // ===== SCAN & ENTRY M5 (prioritas kedua) =====
   // London M5
   double atr_L5 = 0, slDist_L5 = 0; string sess_L5 = "";
   int sigL5 = DetectORBSignal(g_londonORB_M5, InpLondonOpenWIB, _Period,
                                 hATR, hADX, hATR_VolCtx,
                                 slDist_L5, atr_L5, sess_L5);
   if(sigL5 != 0)
     {
      ExecuteOrder(sigL5 == 1, slDist_L5, atr_L5, g_londonORB_M5,
                    sess_L5, _Period,
                    sigL5 == 1 ? "NinjaORB M5 Buy London" : "NinjaORB M5 Sell London");
      return;
     }

   // NY M5
   double atr_N5 = 0, slDist_N5 = 0; string sess_N5 = "";
   int sigN5 = DetectORBSignal(g_nyORB_M5, InpNYOpenWIB, _Period,
                                 hATR, hADX, hATR_VolCtx,
                                 slDist_N5, atr_N5, sess_N5);
   if(sigN5 != 0)
     {
      ExecuteOrder(sigN5 == 1, slDist_N5, atr_N5, g_nyORB_M5,
                    sess_N5, _Period,
                    sigN5 == 1 ? "NinjaORB M5 Buy NY" : "NinjaORB M5 Sell NY");
     }
  }
//+------------------------------------------------------------------+
