//+------------------------------------------------------------------+
//|                                     NinjaEntry_SMC_EA_v5.2.mq5  |
//|   Strategy: Pure SMC/ICT — Liquidity Grab + Market Structure     |
//|   H1 → Swing Structure Bias | M5 → Liq Grab + Rejection Entry   |
//|   SL: Di balik wick grab + ATR buffer | TP: RR 1:2              |
//+------------------------------------------------------------------+
//| Changelog v5.2 (fix drawdown dari backtest v5.1):               |
//| - InpMaxSLPips : 150 → 50                                       |
//|   Trade dengan SL > 50 pip otomatis diskip                      |
//|   Max loss per trade: ~$5 = 5% akun (dari sebelumnya bisa 11%) |
//|   Target: DD turun < 15%                                        |
//+------------------------------------------------------------------+
#define EA_VERSION "5.2"

#property copyright "Custom EA"
#property version   EA_VERSION

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

//--- Input: SMC Structure (H1 Bias)
input int    InpSwingLookbackH1  = 40;    // Lookback candle H1 untuk swing structure
input int    InpSwingDepthH1     = 3;     // Sensitivitas swing H1 (lebih besar = lebih strict)
input bool   InpUseHHHLFilter    = true;  // Wajibkan struktur HH/HL atau LH/LL di H1

//--- Input: SMC Liquidity Pool (M5)
input int    InpLiqLookback      = 20;    // Lookback candle M5 untuk cari equal highs/lows
input int    InpLiqSwingDepth    = 2;     // Sensitivitas swing M5 untuk equal highs/lows
input double InpEqualZonePips    = 12.0;  // Toleransi jarak "equal" highs/lows (pip)
input int    InpMinSwingsForLiq  = 2;     // Minimal swing yang harus "equal" untuk valid liquidity

//--- Input: Liquidity Grab
input double InpGrabWickMinPct   = 0.38;  // Minimum wick grab vs range candle (38%)
input double InpGrabBodyMaxPct   = 0.40;  // Maksimum body grab candle (40% — harus spike)
input double InpMinGrabPips      = 3.0;   // Minimum spike menembus liquidity zone (pip)

//--- Input: ATR Volatility (ganti ADX)
input int    InpATRPeriod        = 14;    // Period ATR
input double InpATRMinPips       = 5.0;   // Minimum ATR (pip) — skip kalau market mati
input int    InpVolATRAvgPeriod  = 20;    // Period rata-rata ATR untuk ratio
input double InpVolATRMinRatio   = 0.60;  // Minimum ratio ATR saat ini vs rata-rata

//--- Input: Swing SL
input int    InpSwingLookback    = 8;     // Lookback swing untuk SL (fallback)
input int    InpSwingDepth       = 2;     // Sensitivitas swing SL
input double InpRangeSL_Buffer   = 0.20; // Buffer SL di luar wick grab (× ATR)
input double InpSL_ATR_Min       = 0.60; // Minimum SL (× ATR)
input double InpMaxSLPips        = 50.0;  // Maksimum SL (pip) — trade SL > 50pip diskip

//--- Input: TP / RR
input double InpRR_Ratio         = 2.0;   // Risk:Reward ratio

//--- Input: Spread Filter
input bool   InpUseSpreadFilter    = true;
input bool   InpUseAdaptiveSpread  = true;
input int    InpSpreadAvgPeriod    = 20;
input double InpSpreadMultiplier   = 2.0;
input double InpSpreadHardCap      = 50.0; // Exness Standard XAU max

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

//--- Input: Wick Filter
input bool   InpUseWickFilter    = true;
input double InpMaxOppositeWick  = 0.5;  // Max wick berlawanan vs range (diperketat)

//--- Input: Candle Body Filter (konfirmasi candle)
input bool   InpUseCandleBodyFilter = true;
input double InpMinBodyRatio        = 0.40; // Min body candle konfirmasi

//--- Input: Session Filter (WIB)
input bool   InpUseSessionFilter  = true;
input int    InpSessionStartWIB   = 10;
input int    InpSessionEndWIB     = 23;
input bool   InpUseNightBlackout  = true;
input int    InpBlackoutStartWIB  = 22;
input int    InpBlackoutEndWIB    = 24;

//--- Input: News Filter
input bool   InpUseNewsFilter     = false;
input int    InpNewsMinutesBefore = 30;
input int    InpNewsMinutesAfter  = 30;

//--- Input: ATR Trailing Stop
input bool   InpUseATRTrail           = true;
input double InpATRTrailActivationPct = 40.0;
input double InpATRTrailLockPct       = 20.0;
input int    InpATRTrailPeriod        = 14;
input double InpATRTrailMultiplier    = 1.5;
input int    InpCooldownATRTrail_M5   = 10;

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

//--- Input: Cooldown
input bool   InpUsePostTradeCooldown = true;
input int    InpCooldownAfterTP      = 10;
input int    InpCooldownAfterSL      = 20;

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

//--- Input: Daily Loss Limit
input bool   InpUseDailyLossLimit = true;
input double InpMaxDailyLossPct   = 3.0;

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

//--- Input: Consecutive Loss Limit
input bool   InpUseConsecLossLimit  = true;
input int    InpMaxConsecLoss       = 3;

//--- Input: Weekly Loss Limit
input bool   InpUseWeeklyLossLimit  = true;
input double InpMaxWeeklyLossPct    = 10.0;

//--- 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
input bool   InpVerboseLog        = false;

//--- Handles ATR
int hATR_M5;
int hATR_VolCtx_M5;
int hATR_Trail;

//--- State
datetime lastBarTime          = 0;
datetime lastSignalEntryTime  = 0;
double   dailyStartBalance    = 0;
datetime currentDayStart      = 0;
bool     dailyLossHit         = false;
datetime cooldownEndTime      = 0;
datetime g_shockCooldownUntil = 0;
string   lastTradeResult      = "";
datetime lastNotifiedDealTime = 0;
datetime g_lastClosingNotifDay  = 0;
bool     g_closingNotifPending  = false;
datetime g_lastOpeningNotifDay  = 0;

//--- ATR Trail State
bool     g_atrTrailActive    = false;
bool     g_atrTrailNotifSent = false;
bool     g_atrTrailWasActive = false;
double   g_atrTrailSL        = 0;
ulong    g_atrTrailTicket    = 0;

//--- Consecutive Loss State
int      g_consecLoss           = 0;
datetime g_consecLossPauseUntil = 0;

//--- Weekly Loss State
double   g_weeklyStartBalance = 0;
datetime g_weeklyStart        = 0;

//--- SMC State: simpan info grab untuk SL
double   g_grabCandleExtreme  = 0; // high (sell) atau low (buy) candle grab

//+------------------------------------------------------------------+
int OnInit()
  {
   hATR_M5       = iATR(_Symbol, _Period, InpATRPeriod);
   hATR_VolCtx_M5= iATR(_Symbol, _Period, InpVolATRAvgPeriod);
   hATR_Trail    = iATR(_Symbol, _Period, InpATRTrailPeriod);

   if(hATR_M5==INVALID_HANDLE || hATR_VolCtx_M5==INVALID_HANDLE || hATR_Trail==INVALID_HANDLE)
     { Print("Gagal buat handle ATR."); return(INIT_FAILED); }

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

   // Restore ATR Trail state jika ada posisi open
   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;
      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;
     }

   // Restore lastNotifiedDealTime
   if(HistorySelect(TimeCurrent()-86400, TimeCurrent()))
     {
      for(int i=HistoryDealsTotal()-1; i>=0; i--)
        {
         ulong tk=HistoryDealGetTicket(i);
         if(tk<=0) continue;
         if(HistoryDealGetString(tk,DEAL_SYMBOL)!=_Symbol) continue;
         if(HistoryDealGetInteger(tk,DEAL_MAGIC)!=InpMagicNumber) continue;
         if(HistoryDealGetInteger(tk,DEAL_ENTRY)!=DEAL_ENTRY_OUT) continue;
         lastNotifiedDealTime=(datetime)HistoryDealGetInteger(tk,DEAL_TIME);
         break;
        }
     }

   Print("NinjaEntry v", EA_VERSION, " ready. Logic: Pure SMC — Liq Grab + Structure");

   if(InpNotifyOnAttach && InpUsePush)
     {
      string msg=StringFormat(
         "NinjaEntry v%s ON\n%s | %s\nSMC LiqGrab+Structure\nGagal? Pelajari. Jatuh? Bangkit.",
         EA_VERSION, _Symbol, EnumToString(_Period));
      if(!SendNotification(msg)) Print("[NOTIF] Push attach gagal: ", GetLastError());
     }

   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   int handles[]={hATR_M5, hATR_VolCtx_M5, hATR_Trail};
   for(int i=0; i<ArraySize(handles); i++)
      if(handles[i]!=INVALID_HANDLE) IndicatorRelease(handles[i]);
  }

//+------------------------------------------------------------------+
//| HELPER FUNCTIONS                                                  |
//+------------------------------------------------------------------+
double GetPip()
  {
   string sym=_Symbol; StringToUpper(sym);
   if(StringFind(sym,"XAU")>=0||StringFind(sym,"GOLD")>=0)
     {
      if(_Digits==2) return _Point*10;
      if(_Digits==3) return _Point*100;
      if(_Digits==1) return _Point;
      return _Point*10;
     }
   return (_Digits==3||_Digits==5) ? _Point*10 : _Point;
  }

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

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

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

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

bool IsNewsBlackout()
  {
   if(!InpUseNewsFilter) return false;
   datetime now=TimeTradeServer();
   MqlCalendarValue values[];
   int total=CalendarValueHistory(values,now-InpNewsMinutesBefore*60-3600,
                                  now+InpNewsMinutesAfter*60+3600,"US",NULL);
   if(total<=0) return false;
   for(int k=0; k<total; k++)
     {
      MqlCalendarEvent ev;
      if(!CalendarEventById(values[k].event_id,ev)) continue;
      if(ev.importance!=CALENDAR_IMPORTANCE_HIGH) continue;
      datetime et=values[k].time;
      if(now>=et-InpNewsMinutesBefore*60 && now<=et+InpNewsMinutesAfter*60) return true;
     }
   return false;
  }

bool HasOpenPosition()
  {
   for(int p=PositionsTotal()-1; p>=0; p--)
     {
      ulong tk=PositionGetTicket(p);
      if(tk<=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 sum=0; int count=0; int arr[];
   if(CopySpread(_Symbol,_Period,1,InpSpreadAvgPeriod,arr)>0)
     {
      ArraySetAsSeries(arr,true);
      for(int i=0; i<ArraySize(arr); i++){sum+=arr[i]*_Point/pip_; count++;}
     }
   if(count==0) return InpSpreadHardCap;
   double avg=sum/count;
   double curSpread=(SymbolInfoDouble(_Symbol,SYMBOL_ASK)-SymbolInfoDouble(_Symbol,SYMBOL_BID))/pip_;
   return MathMax(MathMin(avg*InpSpreadMultiplier,InpSpreadHardCap),curSpread*1.5);
  }

bool IsVolatilityContextOK(double currentATR)
  {
   double buf[];
   if(CopyBuffer(hATR_VolCtx_M5,0,0,InpVolATRAvgPeriod+5,buf)<=0) return true;
   ArraySetAsSeries(buf,true);
   double sum=0; int count=0;
   for(int i=1; i<=InpVolATRAvgPeriod; i++){sum+=buf[i]; count++;}
   if(count==0||sum/count<=0) return true;
   return (currentATR/(sum/count))>=InpVolATRMinRatio;
  }

bool CheckVolatilityShock(double high, double low)
  {
   if(!InpUseVolatilityShock) return false;
   double buf[];
   if(CopyBuffer(hATR_VolCtx_M5,0,0,InpVolATRAvgPeriod+5,buf)<=0) return false;
   ArraySetAsSeries(buf,true);
   double sum=0; int count=0;
   for(int i=1; i<=InpVolATRAvgPeriod; i++){sum+=buf[i]; count++;}
   if(count==0) return false;
   double atrAvg=sum/count; if(atrAvg<=0) return false;
   if((high-low)>=atrAvg*InpShockATRMult)
     { g_shockCooldownUntil=TimeCurrent()+InpShockCooldownMinutes*60; return true; }
   return false;
  }

bool IsVolatilityShockActive()
  { return InpUseVolatilityShock&&(TimeCurrent()<g_shockCooldownUntil); }

bool IsWickOK(double open, double high, double low, double close, bool isBuy)
  {
   if(!InpUseWickFilter) return true;
   double range=high-low; if(range<=0) return true;
   if(isBuy) return ((high-MathMax(open,close))/range)<=InpMaxOppositeWick;
   return ((MathMin(open,close)-low)/range)<=InpMaxOppositeWick;
  }

bool IsCandleBodyOK(double open, double high, double low, double close)
  {
   if(!InpUseCandleBodyFilter) return true;
   double range=high-low;
   if(range<=0) return true;
   double body=MathAbs(close-open);
   return (body/range)>=InpMinBodyRatio;
  }

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

void ResetWeeklyTracking()
  {
   g_weeklyStartBalance=AccountInfoDouble(ACCOUNT_BALANCE);
   MqlDateTime dt; TimeToStruct(TimeCurrent(),dt);
   int dayOfWeek=dt.day_of_week;
   int daysToMonday=(dayOfWeek==0)?6:dayOfWeek-1;
   dt.hour=0; dt.min=0; dt.sec=0;
   datetime todayStart=StructToTime(dt);
   g_weeklyStart=todayStart-daysToMonday*86400;
  }

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("NinjaEntry v%s\nSTOP: Loss harian %.1f%% tercapai. Istirahat.",
                                  EA_VERSION, InpMaxDailyLossPct);
         Print(msg);
         if(InpUseAlert) Alert(msg);
         if(InpUsePush) SendNotification(msg);
        }
      return true;
     }
   return false;
  }

bool CheckWeeklyLossLimit()
  {
   if(!InpUseWeeklyLossLimit) return false;
   MqlDateTime dtNow; TimeToStruct(TimeCurrent(),dtNow);
   int dayOfWeek=dtNow.day_of_week;
   int daysToMonday=(dayOfWeek==0)?6:dayOfWeek-1;
   dtNow.hour=0; dtNow.min=0; dtNow.sec=0;
   datetime thisMonday=StructToTime(dtNow)-daysToMonday*86400;
   if(thisMonday!=g_weeklyStart) ResetWeeklyTracking();
   double weeklyLossPct=(g_weeklyStartBalance-AccountInfoDouble(ACCOUNT_EQUITY))/g_weeklyStartBalance*100.0;
   if(weeklyLossPct>=InpMaxWeeklyLossPct)
     {
      if(InpVerboseLog)
         Print(StringFormat("[WEEKLY] Rugi %.1f%% >= %.1f%%",weeklyLossPct,InpMaxWeeklyLossPct));
      return true;
     }
   return false;
  }

void UpdateConsecLoss()
  {
   if(!InpUseConsecLossLimit) return;
   if(!HistorySelect(TimeCurrent()-86400,TimeCurrent())) return;
   int total=HistoryDealsTotal();
   g_consecLoss=0;
   for(int i=total-1; i>=0; i--)
     {
      ulong tk=HistoryDealGetTicket(i);
      if(tk<=0) continue;
      if(HistoryDealGetString(tk,DEAL_SYMBOL)!=_Symbol) continue;
      if(HistoryDealGetInteger(tk,DEAL_MAGIC)!=InpMagicNumber) continue;
      if(HistoryDealGetInteger(tk,DEAL_ENTRY)!=DEAL_ENTRY_OUT) continue;
      double profit=HistoryDealGetDouble(tk,DEAL_PROFIT);
      ENUM_DEAL_REASON reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(tk,DEAL_REASON);
      if(reason==DEAL_REASON_SL||profit<0) g_consecLoss++;
      else break;
     }
   if(g_consecLoss>=InpMaxConsecLoss)
     {
      MqlDateTime dt; TimeToStruct(TimeCurrent(),dt);
      dt.hour=0; dt.min=0; dt.sec=0;
      g_consecLossPauseUntil=StructToTime(dt)+86400;
      string msg=StringFormat("NinjaEntry v%s\nPAUSE: %d SL beruntun. Lanjut besok.",
                               EA_VERSION, g_consecLoss);
      Print(msg);
      if(InpUseAlert) Alert(msg);
      if(InpUsePush) SendNotification(msg);
     }
  }

bool IsConsecLossPaused()
  {
   if(!InpUseConsecLossLimit) return false;
   return (TimeCurrent()<g_consecLossPauseUntil);
  }

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

bool IsInCooldown()
  { return InpUsePostTradeCooldown&&cooldownEndTime>0&&TimeCurrent()<cooldownEndTime; }

//+------------------------------------------------------------------+
//| SMC: H1 SWING STRUCTURE BIAS                                     |
//| Return: 1=bullish (HH+HL), -1=bearish (LH+LL), 0=choppy        |
//+------------------------------------------------------------------+
int GetH1StructureBias()
  {
   if(!InpUseHHHLFilter) return 1; // kalau filter off, skip cek
   int toCopy=InpSwingLookbackH1+10;
   double highH1[], lowH1[];
   if(CopyHigh(_Symbol,PERIOD_H1,0,toCopy,highH1)<=0) return 0;
   if(CopyLow(_Symbol,PERIOD_H1,0,toCopy,lowH1)<=0) return 0;
   ArraySetAsSeries(highH1,true);
   ArraySetAsSeries(lowH1,true);

   double swingHighs[5], swingLows[5];
   int shCount=0, slCount=0;

   for(int b=1; b<toCopy-InpSwingDepthH1 && (shCount<3||slCount<3); b++)
     {
      if(shCount<3)
        {
         bool isSH=true;
         for(int d=1; d<=InpSwingDepthH1; d++)
           {
            if(b-d<0||b+d>=toCopy){isSH=false;break;}
            if(highH1[b]<=highH1[b-d]||highH1[b]<=highH1[b+d]){isSH=false;break;}
           }
         if(isSH) swingHighs[shCount++]=highH1[b];
        }
      if(slCount<3)
        {
         bool isSL=true;
         for(int d=1; d<=InpSwingDepthH1; d++)
           {
            if(b-d<0||b+d>=toCopy){isSL=false;break;}
            if(lowH1[b]>=lowH1[b-d]||lowH1[b]>=lowH1[b+d]){isSL=false;break;}
           }
         if(isSL) swingLows[slCount++]=lowH1[b];
        }
     }

   if(shCount<2||slCount<2)
     { if(InpVerboseLog) Print("[H1] Tidak cukup swing untuk deteksi struktur"); return 0; }

   bool hhhl=(swingHighs[0]>swingHighs[1])&&(swingLows[0]>swingLows[1]);
   bool lhll=(swingHighs[0]<swingHighs[1])&&(swingLows[0]<swingLows[1]);

   if(hhhl)
     { if(InpVerboseLog) Print("[H1] Bias BULLISH — HH+HL"); return 1; }
   if(lhll)
     { if(InpVerboseLog) Print("[H1] Bias BEARISH — LH+LL"); return -1; }

   if(InpVerboseLog) Print("[H1] Struktur CHOPPY — skip");
   return 0;
  }

//+------------------------------------------------------------------+
//| SMC: DETEKSI EQUAL HIGHS / EQUAL LOWS (Liquidity Pool)          |
//| Cari zona dimana 2+ swing high/low berkumpul dalam toleransi pip |
//| Return: level liquidity pool, 0 jika tidak ada                  |
//+------------------------------------------------------------------+
double FindLiquidityPool(bool isBuySide, double &outPoolLevel)
  {
   // isBuySide=true  → cari equal HIGHS (buy-side liquidity, target untuk grab SELL)
   // isBuySide=false → cari equal LOWS  (sell-side liquidity, target untuk grab BUY)
   outPoolLevel=0;
   int toCopy=InpLiqLookback+10;
   double highM5[], lowM5[];
   if(CopyHigh(_Symbol,_Period,0,toCopy,highM5)<=0) return 0;
   if(CopyLow(_Symbol,_Period,0,toCopy,lowM5)<=0) return 0;
   ArraySetAsSeries(highM5,true);
   ArraySetAsSeries(lowM5,true);

   double pip=GetPip();
   double tolerance=InpEqualZonePips*pip;

   // Kumpulkan swing highs atau swing lows
   double swings[20];
   int swingCount=0;

   for(int b=1; b<toCopy-InpLiqSwingDepth && swingCount<20; b++)
     {
      if(isBuySide)
        {
         bool isSH=true;
         for(int d=1; d<=InpLiqSwingDepth; d++)
           {
            if(b-d<0||b+d>=toCopy){isSH=false;break;}
            if(highM5[b]<=highM5[b-d]||highM5[b]<=highM5[b+d]){isSH=false;break;}
           }
         if(isSH) swings[swingCount++]=highM5[b];
        }
      else
        {
         bool isSL=true;
         for(int d=1; d<=InpLiqSwingDepth; d++)
           {
            if(b-d<0||b+d>=toCopy){isSL=false;break;}
            if(lowM5[b]>=lowM5[b-d]||lowM5[b]>=lowM5[b+d]){isSL=false;break;}
           }
         if(isSL) swings[swingCount++]=lowM5[b];
        }
     }

   if(swingCount<InpMinSwingsForLiq) return 0;

   // Cari cluster swing dalam toleransi
   for(int i=0; i<swingCount-1; i++)
     {
      int clusterCount=1;
      double clusterSum=swings[i];
      for(int j=i+1; j<swingCount; j++)
        {
         if(MathAbs(swings[i]-swings[j])<=tolerance)
           { clusterCount++; clusterSum+=swings[j]; }
        }
      if(clusterCount>=InpMinSwingsForLiq)
        {
         outPoolLevel=clusterSum/clusterCount; // rata-rata level pool
         return outPoolLevel;
        }
     }
   return 0;
  }

//+------------------------------------------------------------------+
//| SMC: DETEKSI LIQUIDITY GRAB CANDLE                              |
//| Cek apakah candle [shift] spike tembus liquidity lalu reject     |
//| Return: true jika grab valid, outGrabExtreme = tip wick grab    |
//+------------------------------------------------------------------+
bool IsLiquidityGrab(bool isBuy, double poolLevel, int shift,
                     const double &open[], const double &high[],
                     const double &low[], const double &close[],
                     double atrVal, double &outGrabExtreme)
  {
   double pip=GetPip();
   double minGrabDist=InpMinGrabPips*pip;
   double range=high[shift]-low[shift];
   if(range<=0) return false;

   if(isBuy)
     {
      // Grab sell-side liquidity: spike di BAWAH poolLevel (equal lows)
      // Candle harus: low tembus poolLevel ke bawah, close kembali di atas poolLevel
      if(low[shift] >= poolLevel) return false;               // tidak tembus
      if(close[shift] <= poolLevel) return false;             // tidak close balik ke atas
      double spikeDepth=poolLevel-low[shift];
      if(spikeDepth<minGrabDist) return false;                // spike terlalu kecil

      // Wick bawah harus dominan (grab candle = spike besar)
      double bottomWick=MathMin(open[shift],close[shift])-low[shift];
      double body=MathAbs(close[shift]-open[shift]);
      if(bottomWick/range<InpGrabWickMinPct) return false;    // wick bawah harus besar
      if(body/range>InpGrabBodyMaxPct) return false;          // body tidak boleh terlalu besar

      outGrabExtreme=low[shift]; // SL nanti di bawah ini
      return true;
     }
   else
     {
      // Grab buy-side liquidity: spike di ATAS poolLevel (equal highs)
      // Candle harus: high tembus poolLevel ke atas, close kembali di bawah poolLevel
      if(high[shift] <= poolLevel) return false;
      if(close[shift] >= poolLevel) return false;
      double spikeHeight=high[shift]-poolLevel;
      if(spikeHeight<minGrabDist) return false;

      // Wick atas harus dominan
      double topWick=high[shift]-MathMax(open[shift],close[shift]);
      double body=MathAbs(close[shift]-open[shift]);
      if(topWick/range<InpGrabWickMinPct) return false;
      if(body/range>InpGrabBodyMaxPct) return false;

      outGrabExtreme=high[shift]; // SL nanti di atas ini
      return true;
     }
  }

//+------------------------------------------------------------------+
//| MAIN SIGNAL DETECTION — Pure SMC                                |
//| Return: 1=buy, -1=sell, 0=no signal                            |
//+------------------------------------------------------------------+
int DetectSMCSignal(int h1Bias, double &outSLDist, double &outATR, string &outLabel)
  {
   outSLDist=0; outATR=0; outLabel="";

   int toCopy=InpLiqLookback+15;
   double open_m5[], high_m5[], low_m5[], close_m5[], atrBuf[];

   if(CopyOpen (_Symbol,_Period,0,toCopy,open_m5) <=0) return 0;
   if(CopyHigh (_Symbol,_Period,0,toCopy,high_m5) <=0) return 0;
   if(CopyLow  (_Symbol,_Period,0,toCopy,low_m5)  <=0) return 0;
   if(CopyClose(_Symbol,_Period,0,toCopy,close_m5) <=0) return 0;
   if(CopyBuffer(hATR_M5,0,0,toCopy,atrBuf)         <=0) return 0;

   ArraySetAsSeries(open_m5,true); ArraySetAsSeries(high_m5,true);
   ArraySetAsSeries(low_m5,true);  ArraySetAsSeries(close_m5,true);
   ArraySetAsSeries(atrBuf,true);

   double pip=GetPip();
   double atrVal=atrBuf[1];
   outATR=atrVal;

   // Filter ATR minimum
   if(atrVal<InpATRMinPips*pip)
     { if(InpVerboseLog) Print("[SIGNAL] ATR terlalu kecil"); return 0; }

   // Filter volatility context (non-lagging)
   if(!IsVolatilityContextOK(atrVal))
     { if(InpVerboseLog) Print("[SIGNAL] Vol context tidak cukup"); return 0; }

   // Tentukan setup berdasarkan bias H1
   // Bias bullish → cari grab sell-side liquidity (equal lows) → BUY
   // Bias bearish → cari grab buy-side liquidity (equal highs) → SELL
   bool lookingForBuy =(h1Bias==1);
   bool lookingForSell=(h1Bias==-1);

   double poolLevel=0;

   if(lookingForBuy)
     {
      // Cari equal lows (sell-side liquidity pool)
      if(FindLiquidityPool(false, poolLevel)<=0)
        { if(InpVerboseLog) Print("[SIGNAL] Tidak ada sell-side liquidity pool"); return 0; }

      // Cek candle [2] sebagai grab candle (sudah close, bukan candle berjalan)
      double grabExtreme=0;
      if(!IsLiquidityGrab(true,poolLevel,2,open_m5,high_m5,low_m5,close_m5,atrVal,grabExtreme))
        { if(InpVerboseLog) Print("[SIGNAL] BUY: Grab candle tidak valid"); return 0; }

      // Candle [1] sebagai konfirmasi (bullish close, wick filter, body filter)
      if(close_m5[1]<=open_m5[1])
        { if(InpVerboseLog) Print("[SIGNAL] BUY: Konfirmasi candle tidak bullish"); return 0; }
      if(!IsWickOK(open_m5[1],high_m5[1],low_m5[1],close_m5[1],true))
        { if(InpVerboseLog) Print("[SIGNAL] BUY: Wick filter gagal"); return 0; }
      if(!IsCandleBodyOK(open_m5[1],high_m5[1],low_m5[1],close_m5[1]))
        { if(InpVerboseLog) Print("[SIGNAL] BUY: Body filter gagal"); return 0; }

      // Hitung SL: di bawah wick grab + buffer
      double entryPrice=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      double slLevel=grabExtreme-(atrVal*InpRangeSL_Buffer);
      double slDist=MathAbs(entryPrice-slLevel);
      double minSLDist=atrVal*InpSL_ATR_Min;
      if(slDist<minSLDist) slDist=minSLDist;
      if(slDist/pip>InpMaxSLPips)
        { if(InpVerboseLog) Print(StringFormat("[SIGNAL] SL terlalu besar: %.1fpip",slDist/pip)); return 0; }

      outSLDist=slDist;
      g_grabCandleExtreme=grabExtreme;
      outLabel=StringFormat("BUY Grab @%.2f Pool=%.2f",grabExtreme,poolLevel);
      if(InpVerboseLog) Print(StringFormat("[SIGNAL] %s valid! SL-dist=%.1fpip",outLabel,slDist/pip));
      return 1;
     }

   if(lookingForSell)
     {
      // Cari equal highs (buy-side liquidity pool)
      if(FindLiquidityPool(true, poolLevel)<=0)
        { if(InpVerboseLog) Print("[SIGNAL] Tidak ada buy-side liquidity pool"); return 0; }

      // Cek candle [2] sebagai grab candle
      double grabExtreme=0;
      if(!IsLiquidityGrab(false,poolLevel,2,open_m5,high_m5,low_m5,close_m5,atrVal,grabExtreme))
        { if(InpVerboseLog) Print("[SIGNAL] SELL: Grab candle tidak valid"); return 0; }

      // Candle [1] sebagai konfirmasi (bearish close, wick filter, body filter)
      if(close_m5[1]>=open_m5[1])
        { if(InpVerboseLog) Print("[SIGNAL] SELL: Konfirmasi candle tidak bearish"); return 0; }
      if(!IsWickOK(open_m5[1],high_m5[1],low_m5[1],close_m5[1],false))
        { if(InpVerboseLog) Print("[SIGNAL] SELL: Wick filter gagal"); return 0; }
      if(!IsCandleBodyOK(open_m5[1],high_m5[1],low_m5[1],close_m5[1]))
        { if(InpVerboseLog) Print("[SIGNAL] SELL: Body filter gagal"); return 0; }

      // Hitung SL: di atas wick grab + buffer
      double entryPrice=SymbolInfoDouble(_Symbol,SYMBOL_BID);
      double slLevel=grabExtreme+(atrVal*InpRangeSL_Buffer);
      double slDist=MathAbs(entryPrice-slLevel);
      double minSLDist=atrVal*InpSL_ATR_Min;
      if(slDist<minSLDist) slDist=minSLDist;
      if(slDist/pip>InpMaxSLPips)
        { if(InpVerboseLog) Print(StringFormat("[SIGNAL] SL terlalu besar: %.1fpip",slDist/pip)); return 0; }

      outSLDist=slDist;
      g_grabCandleExtreme=grabExtreme;
      outLabel=StringFormat("SELL Grab @%.2f Pool=%.2f",grabExtreme,poolLevel);
      if(InpVerboseLog) Print(StringFormat("[SIGNAL] %s valid! SL-dist=%.1fpip",outLabel,slDist/pip));
      return -1;
     }

   return 0;
  }

//+------------------------------------------------------------------+
//| EXECUTE TRADE                                                     |
//+------------------------------------------------------------------+
void ExecuteTrade(bool isBuy, double slDist, double atrVal, string label, int h1Bias)
  {
   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;
   string comment=isBuy ? "NinjaEntry SMC Buy" : "NinjaEntry SMC Sell";

   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; g_atrTrailNotifSent=false;
      lastSignalEntryTime=TimeCurrent();
      FireNotification(isBuy,execPrice,sl,tp,slDist/pip,slDist*InpRR_Ratio/pip,h1Bias);
      if(InpVerboseLog)
         Print(StringFormat("[TRADE] %s Entry=%.2f SL=%.2f TP=%.2f",
                             isBuy?"BUY":"SELL",execPrice,sl,tp));
     }
   else
      Print(StringFormat("[TRADE] Order gagal: %s",trade.ResultRetcodeDescription()));
  }

//+------------------------------------------------------------------+
//| NOTIFIKASI — semua ≤255 karakter                                |
//+------------------------------------------------------------------+
void FireNotification(bool isBuy, double entry, double sl, double tp,
                      double slPips, double tpPips, int h1Bias)
  {
   string dir   =isBuy ? "BUY" : "SELL";
   string bias  =(h1Bias==1) ? "H1:Bull" : "H1:Bear";
   string msg=StringFormat(
      "NinjaEntry v%s %s %s\n%s | RR 1:%.1f\nE:%.2f SL:%.2f TP:%.2f\n"
      "SL:%.1fp TP:%.1fp\nSMC LiqGrab+Structure\nRezeki tak kemana.",
      EA_VERSION, dir, _Symbol,
      bias, InpRR_Ratio,
      entry, sl, tp,
      slPips, tpPips);
   if(InpUseAlert) Alert(msg);
   if(InpUseSound) PlaySound(InpSoundFile);
   if(InpUsePush && !SendNotification(msg))
      Print("[NOTIF] Push entry gagal: ", GetLastError());
  }

void CheckTPSLHit()
  {
   static datetime lastCheck=0;
   if(TimeCurrent()==lastCheck) return; lastCheck=TimeCurrent();
   if(!HistorySelect(TimeCurrent()-86400,TimeCurrent())) return;
   for(int i=HistoryDealsTotal()-1; i>=0; i--)
     {
      ulong tk=HistoryDealGetTicket(i);
      if(tk<=0) continue;
      if(HistoryDealGetString(tk,DEAL_SYMBOL)!=_Symbol) continue;
      if(HistoryDealGetInteger(tk,DEAL_MAGIC)!=InpMagicNumber) continue;
      if(HistoryDealGetInteger(tk,DEAL_ENTRY)!=DEAL_ENTRY_OUT) continue;
      datetime dealTime=(datetime)HistoryDealGetInteger(tk,DEAL_TIME);
      if(dealTime<=lastNotifiedDealTime) break;
      lastNotifiedDealTime=dealTime;

      ENUM_DEAL_REASON reason=(ENUM_DEAL_REASON)HistoryDealGetInteger(tk,DEAL_REASON);
      double pnl=HistoryDealGetDouble(tk,DEAL_PROFIT)
                +HistoryDealGetDouble(tk,DEAL_SWAP)
                +HistoryDealGetDouble(tk,DEAL_COMMISSION);
      double closePrice=HistoryDealGetDouble(tk,DEAL_PRICE);
      string pnlStr=StringFormat("%s%.2f%s",pnl>=0?"+":"",pnl,AccountInfoString(ACCOUNT_CURRENCY));
      bool isTP=(reason==DEAL_REASON_TP);
      bool isSL=(reason==DEAL_REASON_SL);
      bool isExpert=(reason==DEAL_REASON_EXPERT);
      bool isTrail=isSL&&(g_atrTrailActive||g_atrTrailWasActive);

      string msg;
      if(isTrail)
         msg=StringFormat("NinjaEntry v%s TRAIL EXIT\n%s Close:%.2f\nPnL:%s\nAlhamdulillah!",
                           EA_VERSION,_Symbol,closePrice,pnlStr);
      else if(isTP)
         msg=StringFormat("NinjaEntry v%s TP HIT\n%s Close:%.2f\nPnL:%s\nAlhamdulillah!",
                           EA_VERSION,_Symbol,closePrice,pnlStr);
      else if(isExpert)
         msg=StringFormat("NinjaEntry v%s EARLY CLOSE\n%s Close:%.2f\nPnL:%s",
                           EA_VERSION,_Symbol,closePrice,pnlStr);
      else if(isSL)
         msg=StringFormat("NinjaEntry v%s SL HIT\n%s Close:%.2f\nPnL:%s\nEvaluasi & move on.",
                           EA_VERSION,_Symbol,closePrice,pnlStr);
      else
         msg=StringFormat("NinjaEntry v%s CLOSED\n%s Close:%.2f\nPnL:%s",
                           EA_VERSION,_Symbol,closePrice,pnlStr);

      if(InpUseAlert) Alert(msg);
      if(InpUseSound) PlaySound(InpSoundFile);
      if(InpUsePush && !SendNotification(msg))
         Print("[NOTIF] Push close gagal: ", GetLastError());
      if(isTrail) { g_atrTrailActive=false; g_atrTrailWasActive=false; }
      break;
     }
  }

//+------------------------------------------------------------------+
//| EARLY CLOSE                                                       |
//+------------------------------------------------------------------+
void CheckEarlyClose()
  {
   if(!InpUseEarlyClose) return;
   for(int p=PositionsTotal()-1; p>=0; p--)
     {
      ulong tk=PositionGetTicket(p);
      if(tk<=0) continue;
      if(PositionGetString(POSITION_SYMBOL)!=_Symbol) continue;
      if(PositionGetInteger(POSITION_MAGIC)!=InpMagicNumber) continue;
      ENUM_POSITION_TYPE pt=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      double ep=PositionGetDouble(POSITION_PRICE_OPEN);
      double ctp=PositionGetDouble(POSITION_TP);
      if(ctp<=0) continue;
      double curPrice=(pt==POSITION_TYPE_BUY)
                      ? SymbolInfoDouble(_Symbol,SYMBOL_BID)
                      : SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      double totalDist=MathAbs(ctp-ep);
      if(totalDist<=0) continue;
      double curDist=(pt==POSITION_TYPE_BUY) ? curPrice-ep : ep-curPrice;
      if(curDist/totalDist>=(InpEarlyClosePct/100.0))
         trade.PositionClose(tk);
      break;
     }
  }

//+------------------------------------------------------------------+
//| ATR TRAILING STOP                                                 |
//+------------------------------------------------------------------+
void CheckATRTrailing()
  {
   if(!InpUseATRTrail) return;
   ulong ticket=0;
   for(int p=PositionsTotal()-1; p>=0; p--)
     {
      ulong tk=PositionGetTicket(p);
      if(tk<=0) continue;
      if(PositionGetString(POSITION_SYMBOL)!=_Symbol) continue;
      if(PositionGetInteger(POSITION_MAGIC)!=InpMagicNumber) continue;
      ticket=tk; break;
     }
   if(ticket==0) return;

   ENUM_POSITION_TYPE pt=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
   double ep=PositionGetDouble(POSITION_PRICE_OPEN);
   double csl=PositionGetDouble(POSITION_SL);
   double ctp=PositionGetDouble(POSITION_TP);
   double curPrice=(pt==POSITION_TYPE_BUY)
                   ? SymbolInfoDouble(_Symbol,SYMBOL_BID)
                   : SymbolInfoDouble(_Symbol,SYMBOL_ASK);

   double atrBuf[];
   if(CopyBuffer(hATR_Trail,0,0,3,atrBuf)<=0) return;
   ArraySetAsSeries(atrBuf,true);
   double atrVal=atrBuf[1];

   double totalDist=MathAbs(ctp-ep);
   if(totalDist<=0) return;
   double profitDist=(pt==POSITION_TYPE_BUY)?curPrice-ep:ep-curPrice;
   double minDist=SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point;

   if(!g_atrTrailActive)
     {
      if(profitDist/totalDist>=(InpATRTrailActivationPct/100.0))
        {
         g_atrTrailActive=true;
         g_atrTrailWasActive=true;
         g_atrTrailTicket=ticket;
         g_atrTrailSL=(pt==POSITION_TYPE_BUY)
                       ? curPrice-atrVal*InpATRTrailMultiplier
                       : curPrice+atrVal*InpATRTrailMultiplier;
        }
      return;
     }

   double newSL=(pt==POSITION_TYPE_BUY)
                ? curPrice-atrVal*InpATRTrailMultiplier
                : curPrice+atrVal*InpATRTrailMultiplier;

   // Lock profit minimum
   double lockDist=totalDist*(InpATRTrailLockPct/100.0);
   if(pt==POSITION_TYPE_BUY)
     { if(newSL<ep+lockDist) newSL=ep+lockDist; }
   else
     { if(newSL>ep-lockDist) newSL=ep-lockDist; }

   // Aktifkan trailing: pindah SL jika lebih menguntungkan
   if(g_atrTrailSL==0)
     {
      g_atrTrailSL=newSL;
      double slNorm=NormalizeDouble(g_atrTrailSL,_Digits);
      double dist=(pt==POSITION_TYPE_BUY)?curPrice-slNorm:slNorm-curPrice;
      if(MathAbs(slNorm-csl)>_Point&&dist>=minDist)
         trade.PositionModify(ticket,slNorm,ctp);
      if(!g_atrTrailNotifSent)
        {
         string msg=StringFormat("NinjaEntry v%s TRAIL ON\n%s SL→%.2f\nProfit terkunci.",
                                  EA_VERSION,_Symbol,g_atrTrailSL);
         if(InpUseAlert) Alert(msg);
         if(InpUsePush) SendNotification(msg);
         g_atrTrailNotifSent=true;
        }
      return;
     }
   bool improved=(pt==POSITION_TYPE_BUY&&newSL>g_atrTrailSL)||
                 (pt==POSITION_TYPE_SELL&&newSL<g_atrTrailSL);
   if(improved)
     {
      g_atrTrailSL=newSL;
      double slNorm=NormalizeDouble(g_atrTrailSL,_Digits);
      double dist=(pt==POSITION_TYPE_BUY)?curPrice-slNorm:slNorm-curPrice;
      if(MathAbs(slNorm-csl)>_Point*2&&dist>=minDist)
         trade.PositionModify(ticket,slNorm,ctp);
     }
  }

//+------------------------------------------------------------------+
//| NOTIF DAILY CLOSING & OPENING                                    |
//+------------------------------------------------------------------+
void CheckDailyClosingNotif()
  {
   if(!InpNotifyDailyClosing||!InpUsePush) return;
   double wibHour=GetWIBHour();
   if(wibHour<InpBlackoutStartWIB||wibHour>=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;}
   double pnl=AccountInfoDouble(ACCOUNT_BALANCE)-dailyStartBalance;
   string ccy=AccountInfoString(ACCOUNT_CURRENCY);
   string result=MathAbs(pnl)<0.01?"Tidak ada trade hari ini":
                 pnl>0?StringFormat("+%.2f%s Alhamdulillah",pnl,ccy):
                       StringFormat("%.2f%s Evaluasi besok",pnl,ccy);
   string msg=StringFormat("NinjaEntry v%s\nSesi tutup. %s\nIstirahatlah.",EA_VERSION,result);
   if(InpUseAlert) Alert(msg);
   if(!SendNotification(msg)) Print("[NOTIF] Closing notif gagal.");
  }

void ProcessPendingClosingNotif()
  {
   if(!g_closingNotifPending||HasOpenPosition()) return;
   double pnl=AccountInfoDouble(ACCOUNT_BALANCE)-dailyStartBalance;
   string ccy=AccountInfoString(ACCOUNT_CURRENCY);
   string result=pnl>=0?StringFormat("+%.2f%s",pnl,ccy):StringFormat("%.2f%s",pnl,ccy);
   string msg=StringFormat("NinjaEntry v%s\nSesi tutup. PnL:%s\nIstirahatlah.",EA_VERSION,result);
   if(InpUseAlert) Alert(msg);
   if(!SendNotification(msg)) Print("[NOTIF] Closing pending gagal.");
   g_closingNotifPending=false;
  }

void CheckDailyOpeningNotif()
  {
   if(!InpNotifyDailyOpening||!InpUsePush) return;
   double wibHour=GetWIBHour();
   if(wibHour<InpSessionStartWIB||wibHour>=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=StringFormat("NinjaEntry v%s ON\n%s Sesi dimulai.\nFokus. Sabar. Disiplin.",
                            EA_VERSION,_Symbol);
   if(InpUseAlert) Alert(msg);
   if(!SendNotification(msg)) Print("[NOTIF] Opening notif gagal.");
  }

//+------------------------------------------------------------------+
//| OnTick                                                            |
//+------------------------------------------------------------------+
void OnTick()
  {
   CheckTPSLHit();
   CheckEarlyClose();
   CheckATRTrailing();

   datetime barTime=iTime(_Symbol,_Period,0);
   if(barTime==lastBarTime) return;
   lastBarTime=barTime;

   CheckDailyClosingNotif();
   ProcessPendingClosingNotif();
   CheckDailyOpeningNotif();

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

   CheckAndUpdateCooldown();
   UpdateConsecLoss();
   if(IsInCooldown()) return;
   if(IsConsecLossPaused()) return;
   if(!IsSignalSpacingOK()) return;
   if(!IsWithinTradingSession()) return;
   if(IsBlackoutHour()) return;
   if(IsNewsBlackout()) return;

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

   // Spread Check
   double pip=GetPip();
   double spreadNow=(double)SymbolInfoInteger(_Symbol,SYMBOL_SPREAD)*_Point;
   if(InpUseSpreadFilter && spreadNow>GetAdaptiveSpreadThreshold()*pip) return;

   // Step 1: H1 Pure Structure Bias
   int h1Bias=GetH1StructureBias();
   if(h1Bias==0)
     { if(InpVerboseLog) Print("[H1] Choppy — skip"); return; }

   // Step 2: Detect SMC Signal
   double slDist=0, atrVal=0;
   string label="";
   int sig=DetectSMCSignal(h1Bias, slDist, atrVal, label);
   if(sig==0) return;

   // Step 3: Execute
   ExecuteTrade(sig==1, slDist, atrVal, label, h1Bias);
  }
//+------------------------------------------------------------------+
