//+------------------------------------------------------------------+
//|                                          PSI18_PO3_STDV.mq5      |
//| PO3 STDV v1.0: Power of Three windows with CISD and standard     |
//| deviation projections.                                           |
//| 1. Each PO3 window (auto: M1-M3 -> H1, M5-M15 -> H4,             |
//|    M30-H1 -> D1, H2-H12 -> W1, D1+ -> MN1) is the PO3 candle.    |
//| 2. Manipulation: a leg of down (up) candles that makes the       |
//|    window's low (high).                                          |
//| 3. CISD: a candle closes above (below) the open of the first     |
//|    candle of that leg. Confirmed on bar close only.              |
//| 4. Fib 0 = CISD level, 1 = leg extreme. Projections at -1, -1.5  |
//|    (Silver Bullet zone), -2/-2.5 (reaction zone), -3.5/-4        |
//|    (completion band).                                            |
//| 5. A body close beyond -4 re-anchors 0: Open -> High/Low of the  |
//|    leg -> window swing. Beyond -4 on the last anchor = Terminus. |
//| One bullish and one bearish structure per window; a newer one    |
//| replaces the older. A close back beyond 1 invalidates it.        |
//+------------------------------------------------------------------+
#property copyright "ProSuite"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2
#property indicator_label1  "Bullish CISD"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrDodgerBlue
#property indicator_width1  2
#property indicator_label2  "Bearish CISD"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrCrimson
#property indicator_width2  2

input group "PO3 window"
input ENUM_TIMEFRAMES InpWindowTF   = PERIOD_CURRENT; // PO3 window (current = automatic)
input bool   InpShowWindows   = true;   // Draw window separators and open
input int    InpLookbackBars  = 3000;   // Bars to scan
input group "CISD"
input bool   InpRequireExtreme = true;  // Leg must make the window high/low
input double InpMinLegAtr      = 0.3;   // Min leg size (x ATR14, 0 = off)
input group "Projections"
input string InpLevels    = "-1,-1.5,-2,-2.5,-3,-3.5,-4"; // Projection levels
input bool   InpZoneSB    = true;   // Shade -1 to -1.5 (Silver Bullet zone)
input bool   InpZoneReact = true;   // Shade -2 to -2.5 (reaction zone)
input bool   InpZoneDone  = true;   // Shade -3.5 to -4 (completion band)
input bool   InpEscalate  = true;   // Re-anchor 0 on a body close beyond -4
input int    InpExtendBars = 0;     // Extend lines past window end (bars)
input group "Style"
input color  InpBullColor  = clrDodgerBlue; // Bullish colour
input color  InpBearColor  = clrCrimson;    // Bearish colour
input bool   InpShowLabels = true;          // Level labels
input group "Alerts"
input bool   InpAlertCisd     = true;  // CISD confirmed
input bool   InpAlertBand     = true;  // Completion band first touch
input bool   InpAlertTerminus = true;  // Terminus reached
input bool   InpPush          = false; // Also send push notification

#define PFX "PSI18_"

struct Proj
  {
   bool     bull;
   bool     live;
   bool     bandHit;
   bool     terminus;
   bool     failed;
   int      id;
   int      stage;      // 0 = Open, 1 = High/Low, 2 = Swing
   double   anchors[3];
   double   one;        // leg extreme
   datetime tExt;
   datetime tCisd;
   datetime tEnd;
  };

double   g_up[], g_dn[], g_atr[];
double   g_lv[];
int      g_hAtr = INVALID_HANDLE;
ENUM_TIMEFRAMES g_wtf;
Proj     g_p[];
int      g_nextId, g_next;
int      g_bullSlot, g_bearSlot;
// window
datetime g_ws;
int      g_wsIdx;
double   g_wHigh, g_wLow;
// bullish CISD (down leg)
bool     g_prevDn, g_pendBull;
double   g_bullLevel, g_dnRunHigh, g_legLow;
datetime g_tLow;
// bearish CISD (up leg)
bool     g_prevUp, g_pendBear;
double   g_bearLevel, g_upRunLow, g_legHigh;
datetime g_tHigh;

ENUM_TIMEFRAMES AutoWindow()
  {
   int m = PeriodSeconds(_Period) / 60;
   if(m <= 3)   return PERIOD_H1;
   if(m <= 15)  return PERIOD_H4;
   if(m <= 60)  return PERIOD_D1;
   if(m <= 720) return PERIOD_W1;
   return PERIOD_MN1;
  }

datetime WinStart(const datetime t)
  {
   if(g_wtf == PERIOD_MN1)
     {
      MqlDateTime d;
      TimeToStruct(t, d);
      d.day = 1; d.hour = 0; d.min = 0; d.sec = 0;
      return StructToTime(d);
     }
   if(g_wtf == PERIOD_W1)   // MT5 weeks start on Sunday; 1970-01-04 was a Sunday
      return t - (datetime)(((long)t - 3 * 86400) % 604800);
   int s = PeriodSeconds(g_wtf);
   return t - (datetime)((long)t % s);
  }

datetime WinEnd(const datetime ws)
  {
   if(g_wtf == PERIOD_MN1)
     {
      MqlDateTime d;
      TimeToStruct(ws, d);
      d.mon++;
      if(d.mon > 12) { d.mon = 1; d.year++; }
      return StructToTime(d);
     }
   return ws + PeriodSeconds(g_wtf);
  }

double Lvl(const Proj &p, const double k)
  {
   double z = p.anchors[p.stage];
   return z + k * (p.one - z);
  }

color Blend(const color c, const double a)
  {
   color bg = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   int r = (int)((c & 0xFF) * a + (bg & 0xFF) * (1 - a));
   int g = (int)(((c >> 8) & 0xFF) * a + ((bg >> 8) & 0xFF) * (1 - a));
   int b = (int)(((c >> 16) & 0xFF) * a + ((bg >> 16) & 0xFF) * (1 - a));
   return (color)(r | (g << 8) | (b << 16));
  }

void Notify(const string msg)
  {
   string s = StringFormat("PO3 STDV %s %s: %s", _Symbol, EnumToString((ENUM_TIMEFRAMES)_Period), msg);
   Alert(s);
   if(InpPush)
      SendNotification(s);
  }

void HLine(const string n, const datetime t1, const datetime t2, const double p, const color c,
           const ENUM_LINE_STYLE st, const int w)
  {
   ObjectCreate(0, n, OBJ_TREND, 0, t1, p, t2, p);
   ObjectSetInteger(0, n, OBJPROP_COLOR, c);
   ObjectSetInteger(0, n, OBJPROP_STYLE, st);
   ObjectSetInteger(0, n, OBJPROP_WIDTH, w);
   ObjectSetInteger(0, n, OBJPROP_RAY_RIGHT, false);
   ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
  }

void Text(const string n, const datetime t, const double p, const string s, const color c)
  {
   if(!InpShowLabels)
      return;
   ObjectCreate(0, n, OBJ_TEXT, 0, t, p);
   ObjectSetString(0, n, OBJPROP_TEXT, s);
   ObjectSetInteger(0, n, OBJPROP_COLOR, c);
   ObjectSetInteger(0, n, OBJPROP_FONTSIZE, 7);
   ObjectSetInteger(0, n, OBJPROP_ANCHOR, ANCHOR_RIGHT_LOWER);
   ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
  }

void Zone(const string n, const Proj &p, const double k1, const double k2, const color c)
  {
   ObjectCreate(0, n, OBJ_RECTANGLE, 0, p.tCisd, Lvl(p, k1), p.tEnd, Lvl(p, k2));
   ObjectSetInteger(0, n, OBJPROP_COLOR, c);
   ObjectSetInteger(0, n, OBJPROP_FILL, true);
   ObjectSetInteger(0, n, OBJPROP_BACK, true);
   ObjectSetInteger(0, n, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n, OBJPROP_HIDDEN, true);
  }

void Draw(const Proj &p)
  {
   string pre = PFX + "P" + IntegerToString(p.id) + "_";
   ObjectsDeleteAll(0, pre);
   color c = p.bull ? InpBullColor : InpBearColor;
   if(p.failed)
      c = Blend(c, 0.45);
   string stage[3] = {"Open", "High/Low", "Swing"};
   if(!p.bull)
      stage[1] = "Low/High";
   HLine(pre + "L0", p.tExt, p.tEnd, Lvl(p, 0), c, STYLE_SOLID, 1);
   HLine(pre + "L1", p.tExt, p.tEnd, p.one, c, STYLE_SOLID, 1);
   Text(pre + "T0", p.tEnd, Lvl(p, 0), "0 CISD (" + stage[p.stage] + ")", c);
   Text(pre + "T1", p.tEnd, p.one, "1", c);
   for(int i = 0; i < ArraySize(g_lv); i++)
     {
      double y = Lvl(p, g_lv[i]);
      bool key = (g_lv[i] == -2.0 || g_lv[i] == -4.0);
      HLine(pre + "L" + IntegerToString(i + 2), p.tCisd, p.tEnd, y, c, key ? STYLE_SOLID : STYLE_DOT, 1);
      Text(pre + "T" + IntegerToString(i + 2), p.tEnd, y, DoubleToString(g_lv[i], 1), c);
     }
   if(InpZoneSB)    Zone(pre + "Z1", p, -1.0, -1.5, Blend(c, 0.10));
   if(InpZoneReact) Zone(pre + "Z2", p, -2.0, -2.5, Blend(c, 0.18));
   if(InpZoneDone)  Zone(pre + "Z3", p, -3.5, -4.0, Blend(c, 0.26));
   if(p.terminus)
      Text(pre + "TT", p.tEnd, Lvl(p, -4.0), "TERMINUS", c);
   if(p.failed)
      Text(pre + "TF", p.tEnd, p.one, "invalidated", c);
   ChartRedraw();
  }

void DrawWindow(const datetime ws, const double open)
  {
   if(!InpShowWindows)
      return;
   string n = PFX + "W" + IntegerToString((long)ws);
   ObjectCreate(0, n + "V", OBJ_VLINE, 0, ws, 0);
   ObjectSetInteger(0, n + "V", OBJPROP_COLOR, clrGray);
   ObjectSetInteger(0, n + "V", OBJPROP_STYLE, STYLE_DOT);
   ObjectSetInteger(0, n + "V", OBJPROP_BACK, true);
   ObjectSetInteger(0, n + "V", OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, n + "V", OBJPROP_HIDDEN, true);
   HLine(n + "O", ws, WinEnd(ws), open, clrGray, STYLE_DASH, 1);
  }

void ResetState()
  {
   ObjectsDeleteAll(0, PFX);
   ArrayResize(g_p, 0);
   g_nextId = 0;
   g_bullSlot = -1; g_bearSlot = -1;
   g_ws = 0; g_wsIdx = 0; g_wHigh = 0; g_wLow = 0;
   g_prevDn = false; g_pendBull = false; g_bullLevel = 0; g_dnRunHigh = 0; g_legLow = 0; g_tLow = 0;
   g_prevUp = false; g_pendBear = false; g_bearLevel = 0; g_upRunLow = 0; g_legHigh = 0; g_tHigh = 0;
  }

// New structure in the current window; replaces the earlier one of the same direction.
void AddProj(const bool bull, const double zero, const double runExt, const double swing,
             const double one, const datetime tExt, const datetime tCisd)
  {
   int slot = bull ? g_bullSlot : g_bearSlot;
   if(slot >= 0)
     {
      ObjectsDeleteAll(0, PFX + "P" + IntegerToString(g_p[slot].id) + "_");
      g_p[slot].live = false;
     }
   int n = ArraySize(g_p);
   ArrayResize(g_p, n + 1, 64);
   g_p[n].bull = bull;
   g_p[n].live = true;
   g_p[n].bandHit = false;
   g_p[n].terminus = false;
   g_p[n].failed = false;
   g_p[n].id = g_nextId++;
   g_p[n].stage = 0;
   g_p[n].anchors[0] = zero;
   g_p[n].anchors[1] = bull ? MathMax(zero, runExt) : MathMin(zero, runExt);
   g_p[n].anchors[2] = bull ? MathMax(g_p[n].anchors[1], swing) : MathMin(g_p[n].anchors[1], swing);
   g_p[n].one = one;
   g_p[n].tExt = tExt;
   g_p[n].tCisd = tCisd;
   g_p[n].tEnd = WinEnd(g_ws) + InpExtendBars * PeriodSeconds(_Period);
   if(bull) g_bullSlot = n; else g_bearSlot = n;
   Draw(g_p[n]);
  }

void UpdateProj(Proj &p, const datetime t, const double hi, const double lo, const double cl, const bool alerts)
  {
   if(!p.live)
      return;
   if(t >= p.tEnd)
     {
      p.live = false;   // projection expired with its window
      return;
     }
   double s = p.bull ? 1.0 : -1.0;
   if(!p.bandHit && s * ((p.bull ? hi : lo) - Lvl(p, -3.5)) >= 0)
     {
      p.bandHit = true;
      if(alerts && InpAlertBand)
         Notify((p.bull ? "bullish" : "bearish") + string(" completion band (-3.5) reached"));
     }
   if(s * (cl - Lvl(p, -4.0)) > 0)
     {
      // escalate to the next anchor that actually widens the range
      int next = p.stage;
      while(InpEscalate && next < 2 && s * (p.anchors[next + 1] - p.anchors[p.stage]) <= 0)
         next++;
      if(InpEscalate && next < 2)
        {
         p.stage = next + 1;
         p.bandHit = false;
        }
      else
        {
         p.terminus = true;
         p.live = false;
         if(alerts && InpAlertTerminus)
            Notify((p.bull ? "bullish" : "bearish") + string(" terminus: close beyond -4"));
        }
      Draw(p);
      return;
     }
   if(s * (cl - p.one) < 0)
     {
      p.failed = true;
      p.live = false;
      Draw(p);
     }
  }

void ProcessBar(const int i, const datetime &time[], const double &open[], const double &high[],
                const double &low[], const double &close[], const bool alerts)
  {
   g_up[i] = EMPTY_VALUE;
   g_dn[i] = EMPTY_VALUE;

   // existing projections first: a new CISD on this bar starts tracking from the next bar
   for(int k = 0; k < ArraySize(g_p); k++)
      UpdateProj(g_p[k], time[i], high[i], low[i], close[i], alerts);

   datetime ws = WinStart(time[i]);
   if(ws != g_ws)
     {
      g_ws = ws; g_wsIdx = i;
      g_wHigh = high[i]; g_wLow = low[i];
      g_bullSlot = -1; g_bearSlot = -1;
      DrawWindow(ws, open[i]);
     }
   else
     {
      g_wHigh = MathMax(g_wHigh, high[i]);
      g_wLow = MathMin(g_wLow, low[i]);
     }

   double minLeg = InpMinLegAtr > 0 && g_atr[i] != EMPTY_VALUE ? InpMinLegAtr * g_atr[i] : 0;
   bool dn = close[i] < open[i], up = close[i] > open[i];

   // ---- bullish CISD: close above the open of the first candle of the down leg
   if(g_pendBull && low[i] < g_legLow) { g_legLow = low[i]; g_tLow = time[i]; }
   if(g_pendBull && close[i] > g_bullLevel)
     {
      g_pendBull = false;
      bool ok = g_bullLevel - g_legLow >= minLeg;
      if(InpRequireExtreme)
         ok = ok && g_tLow >= g_ws && g_legLow <= g_wLow;
      if(ok)
        {
         double swing = high[g_wsIdx];
         for(int j = g_wsIdx; j < i && time[j] <= g_tLow; j++)
            swing = MathMax(swing, high[j]);
         AddProj(true, g_bullLevel, g_dnRunHigh, swing, g_legLow, g_tLow, time[i]);
         g_up[i] = low[i];
         if(alerts && InpAlertCisd)
            Notify(StringFormat("bullish CISD above %s", DoubleToString(g_bullLevel, _Digits)));
        }
     }
   if(dn)
     {
      if(!g_prevDn)
        {
         g_bullLevel = open[i]; g_dnRunHigh = high[i];
         g_legLow = low[i]; g_tLow = time[i];
         g_pendBull = true;
        }
      else
         g_dnRunHigh = MathMax(g_dnRunHigh, high[i]);
     }

   // ---- bearish CISD: close below the open of the first candle of the up leg
   if(g_pendBear && high[i] > g_legHigh) { g_legHigh = high[i]; g_tHigh = time[i]; }
   if(g_pendBear && close[i] < g_bearLevel)
     {
      g_pendBear = false;
      bool ok = g_legHigh - g_bearLevel >= minLeg;
      if(InpRequireExtreme)
         ok = ok && g_tHigh >= g_ws && g_legHigh >= g_wHigh;
      if(ok)
        {
         double swing = low[g_wsIdx];
         for(int j = g_wsIdx; j < i && time[j] <= g_tHigh; j++)
            swing = MathMin(swing, low[j]);
         AddProj(false, g_bearLevel, g_upRunLow, swing, g_legHigh, g_tHigh, time[i]);
         g_dn[i] = high[i];
         if(alerts && InpAlertCisd)
            Notify(StringFormat("bearish CISD below %s", DoubleToString(g_bearLevel, _Digits)));
        }
     }
   if(up)
     {
      if(!g_prevUp)
        {
         g_bearLevel = open[i]; g_upRunLow = low[i];
         g_legHigh = high[i]; g_tHigh = time[i];
         g_pendBear = true;
        }
      else
         g_upRunLow = MathMin(g_upRunLow, low[i]);
     }

   // dojis do not break a run
   if(dn || up)
     {
      g_prevDn = dn;
      g_prevUp = up;
     }
  }

int OnInit()
  {
   g_wtf = InpWindowTF == PERIOD_CURRENT ? AutoWindow() : InpWindowTF;
   if(PeriodSeconds(g_wtf) <= PeriodSeconds(_Period) && g_wtf != PERIOD_MN1)
     {
      Print("PO3 window must be higher than the chart timeframe");
      return INIT_PARAMETERS_INCORRECT;
     }
   string parts[];
   int n = StringSplit(InpLevels, ',', parts);
   ArrayResize(g_lv, 0);
   for(int i = 0; i < n; i++)
     {
      StringTrimLeft(parts[i]);
      StringTrimRight(parts[i]);
      if(parts[i] == "")
         continue;
      int m = ArraySize(g_lv);
      ArrayResize(g_lv, m + 1);
      g_lv[m] = StringToDouble(parts[i]);
     }
   SetIndexBuffer(0, g_up, INDICATOR_DATA);
   SetIndexBuffer(1, g_dn, INDICATOR_DATA);
   PlotIndexSetInteger(0, PLOT_ARROW, 233);
   PlotIndexSetInteger(1, PLOT_ARROW, 234);
   PlotIndexSetInteger(0, PLOT_ARROW_SHIFT, 12);
   PlotIndexSetInteger(1, PLOT_ARROW_SHIFT, -12);
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   g_hAtr = iATR(_Symbol, _Period, 14);
   if(g_hAtr == INVALID_HANDLE)
      return INIT_FAILED;
   IndicatorSetString(INDICATOR_SHORTNAME, "PO3 STDV v1.0 (" + StringSubstr(EnumToString(g_wtf), 7) + ")");
   ResetState();
   return INIT_SUCCEEDED;
  }

void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, PFX);
   if(g_hAtr != INVALID_HANDLE)
      IndicatorRelease(g_hAtr);
  }

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 < 50 || BarsCalculated(g_hAtr) < rates_total)
      return 0;
   if(prev_calculated == 0 || prev_calculated > rates_total)
     {
      ResetState();
      ArrayInitialize(g_up, EMPTY_VALUE);
      ArrayInitialize(g_dn, EMPTY_VALUE);
      g_next = MathMax(15, rates_total - InpLookbackBars);
     }
   g_up[rates_total - 1] = EMPTY_VALUE;
   g_dn[rates_total - 1] = EMPTY_VALUE;
   if(g_next > rates_total - 2)
      return rates_total;
   if(CopyBuffer(g_hAtr, 0, 0, rates_total, g_atr) != rates_total)
      return 0;
   // decisions on closed bars only; alerts only for bars that close while the chart is live
   for(int i = g_next; i <= rates_total - 2; i++)
      ProcessBar(i, time, open, high, low, close, prev_calculated > 0 && i == rates_total - 2);
   g_next = rates_total - 1;
   return rates_total;
  }
//+------------------------------------------------------------------+
