//+------------------------------------------------------------------+
//|                                             CUSUM_Breakpoint.mq5 |
//|           Structural Breakpoint Detector via CUSUM Control Chart |
//+------------------------------------------------------------------+

#property description "Dual-sided CUSUM structural breakpoint detector on log-returns."
#property description "Plots S+ and S- accumulators in a sub-window with ±h boundaries."
#property description "Draws vertical lines on the main chart at every declared breakpoint."

//--- Indicator windows and buffer allocations
#property indicator_separate_window
#property indicator_buffers 6
#property indicator_plots   4

//--- Plot 1: Positive CUSUM Accumulator (S+)
#property indicator_label1  "S+"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  2

//--- Plot 2: Negative CUSUM Accumulator (S-)
#property indicator_label2  "S-"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrOrangeRed
#property indicator_style2  STYLE_SOLID
#property indicator_width2  2

//--- Plot 3: Upper Detection Boundary (+h)
#property indicator_label3  "+h Threshold"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrLimeGreen
#property indicator_style3  STYLE_DOT
#property indicator_width3  1

//--- Plot 4: Lower Detection Boundary (-h)
#property indicator_label4  "-h Threshold"
#property indicator_type4   DRAW_LINE
#property indicator_color4  clrMagenta
#property indicator_style4  STYLE_DOT
#property indicator_width4  1

//--- Input Parameters
input int    inp_CalibrationWindow    = 100;          // Rolling window size for mean and variance calibration
input double inp_AllowanceK           = 0.50;         // Slack/Drift parameter (k) in standard deviations
input double inp_ThresholdH           = 4.00;         // Critical decision threshold (h) for break detection
input int    inp_VLineWidth           = 2;            // Breakpoint vertical line thickness
input color  inp_VLineColor           = clrYellow;    // Breakpoint vertical line color
input int    inp_MinBarsBetweenBreaks = 10;           // Refractory period (minimum gap between structural breaks)

//--- Global Indicator Buffers
double g_BufSplus[];
double g_BufSminus[];
double g_BufUpperH[];
double g_BufLowerH[];
double g_BufLogReturn[];
double g_BufZScore[];

//--- Global State Tracking Variables
int    g_LastBreakBar    = -1;                     // Index of the most recently flagged structural break
int    g_TotalBarsOnInit = 0;                      // Cached historical bar count at initialization
string g_IndicatorName   = "CUSUM_BP";             // Prefix string used for chart object management

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Validate input boundaries to avoid runtime zero-division or logical failures
   if(inp_CalibrationWindow < 10)
     {
      Print("CUSUM ERROR: inp_CalibrationWindow must be >= 10. Received: ",
            inp_CalibrationWindow);
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(inp_AllowanceK <= 0.0)
     {
      Print("CUSUM ERROR: inp_AllowanceK must be strictly positive. Received: ",
            inp_AllowanceK);
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(inp_ThresholdH <= 0.0)
     {
      Print("CUSUM ERROR: inp_ThresholdH must be strictly positive. Received: ",
            inp_ThresholdH);
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(inp_MinBarsBetweenBreaks < 1)
     {
      Print("CUSUM ERROR: inp_MinBarsBetweenBreaks must be >= 1. Received: ",
            inp_MinBarsBetweenBreaks);
      return(INIT_PARAMETERS_INCORRECT);
     }

//--- Map indicator arrays to explicit structural data and calculation channels
   SetIndexBuffer(0, g_BufSplus,      INDICATOR_DATA);
   SetIndexBuffer(1, g_BufSminus,     INDICATOR_DATA);
   SetIndexBuffer(2, g_BufUpperH,     INDICATOR_DATA);
   SetIndexBuffer(3, g_BufLowerH,     INDICATOR_DATA);
   SetIndexBuffer(4, g_BufLogReturn,  INDICATOR_CALCULATIONS);
   SetIndexBuffer(5, g_BufZScore,     INDICATOR_CALCULATIONS);

//--- Define values to be ignored by the visual rendering engine
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(3, PLOT_EMPTY_VALUE, EMPTY_VALUE);

//--- Configure the sub-window shorthand dynamic label string
   IndicatorSetString(INDICATOR_SHORTNAME,
                      StringFormat("CUSUM(W=%d, k=%.2f, h=%.2f)",
                                   inp_CalibrationWindow,
                                   inp_AllowanceK,
                                   inp_ThresholdH));

//--- Configure static horizontal sub-window reference levels
   IndicatorSetDouble(INDICATOR_LEVELVALUE,   0, inp_ThresholdH);
   IndicatorSetDouble(INDICATOR_LEVELVALUE,   1, -inp_ThresholdH);
   IndicatorSetInteger(INDICATOR_LEVELCOLOR,  0, clrLimeGreen);
   IndicatorSetInteger(INDICATOR_LEVELCOLOR,  1, clrMagenta);
   IndicatorSetInteger(INDICATOR_LEVELSTYLE,  0, STYLE_DOT);
   IndicatorSetInteger(INDICATOR_LEVELSTYLE,  1, STYLE_DOT);

//--- Offset plot rendering to ensure the calibration window has completely warmed up
   PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, inp_CalibrationWindow + 1);
   PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, inp_CalibrationWindow + 1);
   PlotIndexSetInteger(2, PLOT_DRAW_BEGIN, inp_CalibrationWindow + 1);
   PlotIndexSetInteger(3, PLOT_DRAW_BEGIN, inp_CalibrationWindow + 1);

//--- Flush historical operational variables
   g_LastBreakBar    = -1;
   g_TotalBarsOnInit = iBars(_Symbol, _Period);

   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Isolate and wipe indicator-generated vertical lines from the current chart space
   string vline_prefix  = g_IndicatorName + "_VL_";
   int    total_objects = ObjectsTotal(0, 0, OBJ_VLINE);

   for(int i = total_objects - 1; i >= 0; i--)
     {
      string obj_name = ObjectName(0, i, 0, OBJ_VLINE);
      if(StringFind(obj_name, vline_prefix) == 0)
         ObjectDelete(0, obj_name);
     }

//--- Clear state identifiers
   g_LastBreakBar    = -1;
   g_TotalBarsOnInit = 0;

   Print("CUSUM_Breakpoint: All chart objects removed. Deinitialization complete. Reason: ",
         reason);
  }

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
  {
//--- Enforce minimum data requirements before executing calculations
   if(rates_total < inp_CalibrationWindow + 2)
      return(0);

//--- Determine historical data processing entry bar
   int start_bar;
   if(prev_calculated == 0)
     {
      //--- Wipe previous indicator objects if full recalculation is triggered
      string vline_prefix  = g_IndicatorName + "_VL_";
      int    total_objects = ObjectsTotal(0, 0, OBJ_VLINE);
      for(int i = total_objects - 1; i >= 0; i--)
        {
         string obj_name = ObjectName(0, i, 0, OBJ_VLINE);
         if(StringFind(obj_name, vline_prefix) == 0)
            ObjectDelete(0, obj_name);
        }
      g_LastBreakBar = -1;
      start_bar      = 1; // Begin at the first valid relative close shift
     }
   else
     {
      //--- Re-evaluate the last calculated bar to account for tick fluctuations on live candles
      start_bar = prev_calculated - 1;
      if(start_bar < 1)
         start_bar = 1;
     }

//--- Step 1: Calculate continuous logarithmic close-to-close returns
   for(int bar = start_bar; bar < rates_total; bar++)
     {
      if(close[bar] <= 0.0 || close[bar - 1] <= 0.0)
        {
         g_BufLogReturn[bar] = 0.0;
         continue;
        }
      g_BufLogReturn[bar] = MathLog(close[bar] / close[bar - 1]);
     }
   g_BufLogReturn[0] = 0.0; // Terminal anchor point

//--- Step 2: Calculate rolling historical Z-Scores via statistical vectors
   int zscore_start = (prev_calculated == 0) ? inp_CalibrationWindow : start_bar;
   if(zscore_start < inp_CalibrationWindow)
      zscore_start = inp_CalibrationWindow;

   for(int bar = zscore_start; bar < rates_total; bar++)
     {
      vector v_window;
      v_window.Init(inp_CalibrationWindow);

      //--- Extract historical returns for the current execution frame
      for(int w = 0; w < inp_CalibrationWindow; w++)
        {
         int window_idx = bar - 1 - w;
         if(window_idx < 0)
            v_window[w] = 0.0;
         else
            v_window[w] = g_BufLogReturn[window_idx];
        }

      double rolling_mean    = v_window.Mean();
      double rolling_std_pop = v_window.Std(); // Outputs population standard deviation
      double n_dbl           = (double)inp_CalibrationWindow;
      double rolling_std;

      //--- Guard against division-by-zero errors in non-volatile or flat series
      if(rolling_std_pop < 1e-10)
        {
         g_BufZScore[bar] = 0.0;
         continue;
        }
      else
        {
         //--- Convert population standard deviation to unbiased sample standard deviation (Bessel's correction)
         rolling_std = rolling_std_pop * MathSqrt(n_dbl / (n_dbl - 1.0));
        }

      //--- Map standardized returns to the processing calculation buffer
      g_BufZScore[bar] = (g_BufLogReturn[bar] - rolling_mean) / rolling_std;
     }

//--- Step 3: Zero-out or warm up initial indexes beneath the calibration threshold
   for(int bar = 0; bar < inp_CalibrationWindow && bar < rates_total; bar++)
     {
      g_BufZScore[bar]  = EMPTY_VALUE;
      g_BufSplus[bar]   = EMPTY_VALUE;
      g_BufSminus[bar]  = EMPTY_VALUE;
      g_BufUpperH[bar]  = EMPTY_VALUE;
      g_BufLowerH[bar]  = EMPTY_VALUE;
     }

//--- Step 4: Core cumulative sum (CUSUM) state tracking preparation
   double S_plus, S_minus;
   int    cusum_start;

   if(prev_calculated == 0)
     {
      cusum_start = inp_CalibrationWindow;
      S_plus      = 0.0;
      S_minus     = 0.0;
     }
   else
     {
      //--- Restore accumulator states from the bar prior to the current parsing step
      cusum_start   = start_bar;
      int prior_bar = cusum_start - 1;

      if(prior_bar < inp_CalibrationWindow ||
         g_BufSplus[prior_bar]  == EMPTY_VALUE ||
         g_BufSminus[prior_bar] == EMPTY_VALUE)
        {
         S_plus  = 0.0;
         S_minus = 0.0;
        }
      else
        {
         S_plus  = g_BufSplus[prior_bar];
         S_minus = g_BufSminus[prior_bar];
        }
     }

//--- Step 5: Execute dual-sided CUSUM recurrence and structural breach validation
   for(int bar = MathMax(cusum_start, inp_CalibrationWindow); bar < rates_total; bar++)
     {
      if(g_BufZScore[bar] == EMPTY_VALUE)
        {
         g_BufSplus[bar]  = EMPTY_VALUE;
         g_BufSminus[bar] = EMPTY_VALUE;
         g_BufUpperH[bar] = EMPTY_VALUE;
         g_BufLowerH[bar] = EMPTY_VALUE;
         continue;
        }

      double zt = g_BufZScore[bar];

      //--- Non-linear recurrence equations mapping cumulative variance deviations
      S_plus  = MathMax(0.0, S_plus  + zt - inp_AllowanceK);
      S_minus = MathMin(0.0, S_minus + zt + inp_AllowanceK);

      //--- Commit calculation states to viewable indicators
      g_BufSplus[bar]  = S_plus;
      g_BufSminus[bar] = S_minus;
      g_BufUpperH[bar] = inp_ThresholdH;
      g_BufLowerH[bar] = -inp_ThresholdH;

      //--- Assess if statistical boundary breaches occurred on the current bar
      bool upper_breach = (S_plus  >  inp_ThresholdH);
      bool lower_breach = (S_minus < -inp_ThresholdH);

      if(upper_breach || lower_breach)
        {
         //--- Verify structural time constraints (refractory minimum gap window)
         bool gap_ok = (g_LastBreakBar < 0) ||
                       ((bar - g_LastBreakBar) >= inp_MinBarsBetweenBreaks);
         if(gap_ok)
           {
            g_LastBreakBar = bar;
            string vline_name = g_IndicatorName + "_VL_" + IntegerToString(bar);

            //--- Instantiate visualization markers for the structural breakpoint
            if(ObjectFind(0, vline_name) < 0)
              {
               ObjectCreate(0, vline_name, OBJ_VLINE, 0, time[bar], 0);
               ObjectSetInteger(0, vline_name, OBJPROP_COLOR,      inp_VLineColor);
               ObjectSetInteger(0, vline_name, OBJPROP_STYLE,      STYLE_DASH);
               ObjectSetInteger(0, vline_name, OBJPROP_WIDTH,      inp_VLineWidth);
               ObjectSetInteger(0, vline_name, OBJPROP_SELECTABLE, false);
               ObjectSetInteger(0, vline_name, OBJPROP_HIDDEN,     true);
               ObjectSetString(0,  vline_name, OBJPROP_TOOLTIP,
                               StringFormat("CUSUM Break | Bar %d | %s",
                                            bar,
                                            TimeToString(time[bar],
                                                  TIME_DATE | TIME_MINUTES)));
              }
           }
         //--- Reset accumulators immediately upon breach verification to start tracking new regime shifts
         S_plus  = 0.0;
         S_minus = 0.0;
        }
     }

   return(rates_total);
  }
//+------------------------------------------------------------------+