//+------------------------------------------------------------------+
//|                                                  Peek_Plus_v3.mq5 |
//|   Self-Normalizing, Hysteresis, Exhaustion, Gates                 |
//+------------------------------------------------------------------+
#property copyright "Peek+ v3 (Self-Normalizing Architecture)"
#property link      "http://www.forex-station.com/"
#property version   "3.00"
#property description "v3: Rank-normalized, 1-param horizon, Hysteresis Trend, Exhaustion signals, Gates."
#property indicator_separate_window
#property indicator_buffers 16
#property indicator_plots   7

//---- plots
#property indicator_label1  "Up Rank"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrYellow
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

#property indicator_label2  "Down Rank"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrRed
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1

#property indicator_label3  "Trend State"
#property indicator_type3   DRAW_COLOR_HISTOGRAM
#property indicator_color3  clrLimeGreen, clrOrange, clrGray
#property indicator_style3  STYLE_SOLID
#property indicator_width3  5

#property indicator_label4  "Impulse UP"
#property indicator_type4   DRAW_ARROW
#property indicator_color4  clrYellow
#property indicator_width4  2

#property indicator_label5  "Impulse DN"
#property indicator_type5   DRAW_ARROW
#property indicator_color5  clrRed
#property indicator_width5  2

#property indicator_label6  "Exh BUY"
#property indicator_type6   DRAW_ARROW
#property indicator_color6  clrAqua
#property indicator_width6  3

#property indicator_label7  "Exh SELL"
#property indicator_type7   DRAW_ARROW
#property indicator_color7  clrMagenta
#property indicator_width7  3

//---- enums
enum ENUM_STRICT_MODE
{
   STRICT_OFF      = 0,   // Off
   STRICT_OPPOSITE = 1,   // Opposite rank is low
   STRICT_POSITION = 2    // Price position inside window range
};

enum ENUM_GATE_MODE
{
   GATE_NONE     = 0,   // None
   GATE_ER       = 1,   // Kaufman Efficiency Ratio
   GATE_PRESSURE = 2,   // Close Pressure
   GATE_VOLUME   = 3    // Volume Rank
};

//---- inputs
input group "=== Horizon & Core ==="
input int    bb                 = 100;            // Base Horizon (bb)
input bool   modeHL             = true;           // Use High/Low (false = Close)

input group "=== A. Impulse (Trend) ==="
input double impulseQuantile    = 85.0;           // Impulse Threshold (% rank)
input ENUM_STRICT_MODE strictMode = STRICT_OPPOSITE; // Strict mode
input double oppQuantile        = 20.0;           // Strict Opposite: max rank (%)
input double posLevel           = 0.8;            // Strict Position: min range position
input int    cooldownBars       = 8;              // Cooldown between signals

input group "=== C. Exhaustion (Counter-Trend) ==="
input double exhaustionPeakQuantile = 80.0;       // Min peak rank to consider (%)
input double exhaustionDropRatio  = 0.7;          // Score must drop below this * peak
input int    exhaustionK          = 3;            // Bars without new extreme

input group "=== Gates (Filters) ==="
input ENUM_GATE_MODE gateMode   = GATE_NONE;      // Gate Filter Mode
input double gateQuantile       = 70.0;           // Gate Threshold (% rank)

input group "=== Alerts ==="
input bool   alertPopup         = true;           // Popup alert
input bool   alertPush          = false;          // Push notification
input bool   alertSound         = false;          // Sound alert

//---- buffers
double upRankBuf[];
double dnRankBuf[];
double trendBuf[];
double trendColor[];
double sigImpUp[];
double sigImpDn[];
double sigExhBuy[];
double sigExhSell[];

double maDn[];
double maUp[];
double brkDn[];
double brkUp[];
double atrS[];
double erBuf[];
double pressBuf[];
double volBuf[];

//---- globals
int g_atrShort, g_atrLong, g_rankLookback;
int g_minWin, g_maxWin;
datetime g_lastAlert = 0;

//+------------------------------------------------------------------+
int OnInit()
{
   // Derived periods tied to bb
   g_atrShort = MathMax(2, bb / 8);
   g_atrLong  = MathMax(g_atrShort + 2, bb);
   g_rankLookback = MathMax(30, bb * 3);
   
   g_minWin = MathMax(10, bb / 4);
   g_maxWin = MathMax(g_minWin, bb * 2);

   SetIndexBuffer(0,  upRankBuf,   INDICATOR_DATA);
   SetIndexBuffer(1,  dnRankBuf,   INDICATOR_DATA);
   SetIndexBuffer(2,  trendBuf,    INDICATOR_DATA);
   SetIndexBuffer(3,  trendColor,  INDICATOR_COLOR_INDEX);
   SetIndexBuffer(4,  sigImpUp,    INDICATOR_DATA);
   SetIndexBuffer(5,  sigImpDn,    INDICATOR_DATA);
   SetIndexBuffer(6,  sigExhBuy,   INDICATOR_DATA);
   SetIndexBuffer(7,  sigExhSell,  INDICATOR_DATA);
   
   SetIndexBuffer(8,  maDn,        INDICATOR_CALCULATIONS);
   SetIndexBuffer(9,  maUp,        INDICATOR_CALCULATIONS);
   SetIndexBuffer(10, brkDn,       INDICATOR_CALCULATIONS);
   SetIndexBuffer(11, brkUp,       INDICATOR_CALCULATIONS);
   SetIndexBuffer(12, atrS,        INDICATOR_CALCULATIONS);
   SetIndexBuffer(13, erBuf,       INDICATOR_CALCULATIONS);
   SetIndexBuffer(14, pressBuf,    INDICATOR_CALCULATIONS);
   SetIndexBuffer(15, volBuf,      INDICATOR_CALCULATIONS);

   for(int p = 0; p < 7; p++)
      PlotIndexSetDouble(p, PLOT_EMPTY_VALUE, EMPTY_VALUE);

   PlotIndexSetInteger(3, PLOT_ARROW, 233);
   PlotIndexSetInteger(4, PLOT_ARROW, 234);
   PlotIndexSetInteger(5, PLOT_ARROW, 217);
   PlotIndexSetInteger(6, PLOT_ARROW, 218);

   ArraySetAsSeries(upRankBuf,   true);
   ArraySetAsSeries(dnRankBuf,   true);
   ArraySetAsSeries(trendBuf,    true);
   ArraySetAsSeries(trendColor,  true);
   ArraySetAsSeries(sigImpUp,    true);
   ArraySetAsSeries(sigImpDn,    true);
   ArraySetAsSeries(sigExhBuy,   true);
   ArraySetAsSeries(sigExhSell,  true);
   ArraySetAsSeries(maDn,        true);
   ArraySetAsSeries(maUp,        true);
   ArraySetAsSeries(brkDn,       true);
   ArraySetAsSeries(brkUp,       true);
   ArraySetAsSeries(atrS,        true);
   ArraySetAsSeries(erBuf,       true);
   ArraySetAsSeries(pressBuf,    true);
   ArraySetAsSeries(volBuf,      true);

   IndicatorSetString(INDICATOR_SHORTNAME, "Peek+ v3 (Rank-Norm)");
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
double GetRank(double val, const double &arr[], int idx, int lookback, int total, bool ignoreZero = false)
{
    int count = 0;
    int less = 0;
    int end = MathMin(idx + lookback, total - 1);
    for(int i = idx + 1; i <= end; i++)
    {
        if(arr[i] != EMPTY_VALUE)
        {
            if(ignoreZero && arr[i] == 0.0) continue;
            count++;
            if(arr[i] < val) less++;
            else if(arr[i] == val) less += 0.5; 
        }
    }
    if(count == 0) return 0.5;
    return (double)less / count;
}

//+------------------------------------------------------------------+
bool CooldownOK(const double &sig[], const int i, const int total, int cooldown)
{
   for(int k = 1; k <= cooldown; k++)
   {
      int prevIdx = i + k; 
      if(prevIdx >= total) break;
      if(sig[prevIdx] != EMPTY_VALUE) return false;
   }
   return true;
}

//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated,
                const datetime &time[], const double &open[],
                const double &high[], const double &low[],
                const double &close[], const long &tick_volume[],
                const long &volume[], const int &spread[])
{
   if(rates_total < g_maxWin + g_rankLookback + 10) return(0);

   ArraySetAsSeries(time, true);
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(close, true);
   ArraySetAsSeries(tick_volume, true);

   int limit;
   if(prev_calculated == 0)
   {
      ArrayInitialize(upRankBuf, EMPTY_VALUE);
      ArrayInitialize(dnRankBuf, EMPTY_VALUE);
      ArrayInitialize(trendBuf, 0.0);
      ArrayInitialize(trendColor, 2.0);
      ArrayInitialize(sigImpUp, EMPTY_VALUE);
      ArrayInitialize(sigImpDn, EMPTY_VALUE);
      ArrayInitialize(sigExhBuy, EMPTY_VALUE);
      ArrayInitialize(sigExhSell, EMPTY_VALUE);
      ArrayInitialize(maDn, 0.0);
      ArrayInitialize(maUp, 0.0);
      ArrayInitialize(brkDn, 0.0);
      ArrayInitialize(brkUp, 0.0);
      ArrayInitialize(atrS, 0.0);
      ArrayInitialize(erBuf, 0.0);
      ArrayInitialize(pressBuf, 0.0);
      ArrayInitialize(volBuf, 0.0);
      
      limit = rates_total - g_maxWin - g_rankLookback - 5;
   }
   else
   {
      limit = rates_total - prev_calculated + 1;
   }

   // 1. ATR
   for(int i = limit; i >= 0; i--)
   {
      double tr = MathMax(high[i] - low[i], MathMax(MathAbs(high[i] - close[i + 1]), MathAbs(low[i] - close[i + 1])));
      double ps = atrS[i + 1];
      atrS[i] = (ps > 0.0) ? (ps * (g_atrShort - 1) + tr) / g_atrShort : tr;
   }

   // State variables (persist through loop from oldest to newest)
   double peakDn = 0, peakUp = 0;
   int barsSinceDn = 0, barsSinceUp = 0;
   double prevTrend = 0;

   // 2. Main Loop
   for(int i = limit; i >= 0; i--)
   {
      // A. Window via Volatility Rank
      double volRank = GetRank(atrS[i], atrS, i, g_rankLookback, rates_total);
      int win = (int)MathRound(g_minWin + (g_maxWin - g_minWin) * (1.0 - volRank));
      win = MathMax(g_minWin, MathMin(g_maxWin, win));
      
      if(i + win >= rates_total) continue;

      // B. Extremes
      int hh = ArrayMaximum(high, i + 1, win);
      int ll = ArrayMinimum(low, i + 1, win);
      if(hh < 0 || ll < 0) continue;
      
      double hiV = modeHL ? high[hh] : close[hh];
      double loV = modeHL ? low[ll]  : close[ll];
      
      bool dnCand = (hh == i + 1);
      bool upCand = (ll == i + 1);

      // C. Raw Scores & Breakouts
      double prevDn = maDn[i + 1];
      double prevUp = maUp[i + 1];
      double dn = 0, up = 0;
      
      if(dnCand)
      {
         int h2 = ArrayMaximum(high, i + 2, win - 1);
         double brk = (h2 >= 0) ? hiV - high[h2] : 0;
         double brkRank = GetRank(brk, brkDn, i, g_rankLookback, rates_total, true);
         dn = prevDn + (0.5 + brkRank);
         brkDn[i] = brk;
         peakDn = MathMax(peakDn, dn);
         barsSinceDn = 0;
      }
      else
      {
         dn = MathMax(0.0, 1.0 - (double)(hh - (i + 1)) / win);
         barsSinceDn++;
      }
      maDn[i] = dn;

      if(upCand)
      {
         int l2 = ArrayMinimum(low, i + 2, win - 1);
         double brk = (l2 >= 0) ? low[l2] - loV : 0;
         double brkRank = GetRank(brk, brkUp, i, g_rankLookback, rates_total, true);
         up = prevUp + (0.5 + brkRank);
         brkUp[i] = brk;
         peakUp = MathMax(peakUp, up);
         barsSinceUp = 0;
      }
      else
      {
         up = MathMax(0.0, 1.0 - (double)(ll - (i + 1)) / win);
         barsSinceUp++;
      }
      maUp[i] = up;

      // D. Score Ranks
      double dnRank = GetRank(dn, maDn, i, g_rankLookback, rates_total);
      double upRank = GetRank(up, maUp, i, g_rankLookback, rates_total);
      upRankBuf[i] = upRank;
      dnRankBuf[i] = dnRank;

      // E. Gates Calculation
      double er = 0, press = 0;
      if(win > 1)
      {
         double net = MathAbs(close[i] - close[i + win]);
         double sumAbs = 0;
         double sumP = 0;
         int cntP = 0;
         for(int k = 0; k < win; k++) 
         {
             sumAbs += MathAbs(close[i + k] - close[i + k + 1]);
             double rng = high[i+k] - low[i+k];
             if(rng > 0) { sumP += (close[i+k] - low[i+k]) / rng; cntP++; }
         }
         er = (sumAbs > 0) ? net / sumAbs : 0;
         press = (cntP > 0) ? sumP / cntP : 0.5;
      }
      erBuf[i] = er;
      pressBuf[i] = press;
      volBuf[i] = (double)tick_volume[i];

      double erRank = GetRank(er, erBuf, i, g_rankLookback, rates_total);
      double pressRank = GetRank(press, pressBuf, i, g_rankLookback, rates_total);
      double vRank = GetRank((double)tick_volume[i], volBuf, i, g_rankLookback, rates_total);

      bool gateOK = true;
      double gateThr = gateQuantile / 100.0;
      if(gateMode == GATE_ER) gateOK = (erRank >= gateThr);
      else if(gateMode == GATE_PRESSURE) gateOK = (MathAbs(pressRank - 0.5) >= (gateThr - 0.5));
      else if(gateMode == GATE_VOLUME) gateOK = (vRank >= gateThr);

      // F. Signals A (Impulse)
      double impThr = impulseQuantile / 100.0;
      double oppThr = oppQuantile / 100.0;
      bool dnSigA = false, upSigA = false;
      
      double range = hiV - loV;
      double pos   = (range > 0.0) ? (close[i] - loV) / range : -1.0;

      if(dnCand && dnRank >= impThr && gateOK && CooldownOK(sigImpDn, i, rates_total, cooldownBars))
      {
         bool strictOK = true;
         if(strictMode == STRICT_OPPOSITE) strictOK = (upRank <= oppThr);
         if(strictMode == STRICT_POSITION) strictOK = (pos <= (1.0 - posLevel));
         if(strictOK) dnSigA = true;
      }
      
      if(upCand && upRank >= impThr && gateOK && CooldownOK(sigImpUp, i, rates_total, cooldownBars))
      {
         bool strictOK = true;
         if(strictMode == STRICT_OPPOSITE) strictOK = (dnRank <= oppThr);
         if(strictMode == STRICT_POSITION) strictOK = (pos >= posLevel);
         if(strictOK) upSigA = true;
      }
      
      sigImpDn[i] = dnSigA ? 1.0 : EMPTY_VALUE;
      sigImpUp[i] = upSigA ? 1.0 : EMPTY_VALUE;

      // G. Trend State (Hysteresis)
      double state = prevTrend;
      if(upSigA) state = 1.0;
      else if(dnSigA) state = -1.0;
      trendBuf[i] = state;
      if(state > 0.5) trendColor[i] = 0; 
      else if(state < -0.5) trendColor[i] = 1; 
      else trendColor[i] = 2; 
      prevTrend = state;

      // H. Signals C (Exhaustion)
      bool exhUp = false; // Up streak exhausted -> SELL
      bool exhDn = false; // Down streak exhausted -> BUY
      
      double exhPeakThr = exhaustionPeakQuantile / 100.0;
      
      if(barsSinceDn > exhaustionK && peakDn > 0)
      {
         double pRank = GetRank(peakDn, maDn, i, g_rankLookback, rates_total);
         if(pRank >= exhPeakThr && dn < exhaustionDropRatio * peakDn && upCand)
         {
            double rng = high[i] - low[i];
            double p = (rng > 0) ? (high[i] - close[i]) / rng : 0;
            if(p > 0.5) exhUp = true;
         }
      }
      
      if(barsSinceUp > exhaustionK && peakUp > 0)
      {
         double pRank = GetRank(peakUp, maUp, i, g_rankLookback, rates_total);
         if(pRank >= exhPeakThr && up < exhaustionDropRatio * peakUp && dnCand)
         {
            double rng = high[i] - low[i];
            double p = (rng > 0) ? (close[i] - low[i]) / rng : 0;
            if(p > 0.5) exhDn = true;
         }
      }
      
      sigExhSell[i] = exhUp ? 1.0 : EMPTY_VALUE;
      sigExhBuy[i] = exhDn ? 1.0 : EMPTY_VALUE;
   }

   // 3. Alerts
   if(prev_calculated > 0 && (alertPopup || alertPush || alertSound))
   {
      if(sigImpUp[0] != EMPTY_VALUE || sigImpDn[0] != EMPTY_VALUE || 
         sigExhBuy[0] != EMPTY_VALUE || sigExhSell[0] != EMPTY_VALUE)
      {
         if(time[0] != g_lastAlert)
         {
            g_lastAlert = time[0];
            string msg = "Peek+ v3 Signal on " + _Symbol;
            if(sigImpUp[0] != EMPTY_VALUE) msg += " | IMPULSE UP";
            if(sigImpDn[0] != EMPTY_VALUE) msg += " | IMPULSE DN";
            if(sigExhBuy[0] != EMPTY_VALUE) msg += " | EXHAUSTION BUY";
            if(sigExhSell[0] != EMPTY_VALUE) msg += " | EXHAUSTION SELL";
            
            if(alertPopup) Alert(msg);
            if(alertPush)  SendNotification(msg);
            if(alertSound) PlaySound("alert.wav");
         }
      }
   }

   return(rates_total);
}