//+------------------------------------------------------------------+
//|                                             DivergenceEngine.mqh |
//|                                                Utility / Library |
//|                                    Copyright 2026, soloharbinger |
//|                      https://www.mql5.com/en/users/soloharbinger |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, soloharbinger"
#property link      "https://www.mql5.com/en/users/soloharbinger"

//+------------------------------------------------------------------+
//| Phase 1/4 — FOUNDATION                                           |
//+------------------------------------------------------------------+

//--- Sentinel values for "no data"
#define DIV_NO_BARS        INT_MAX/4   // e.g., 536870911
#define DIV_NO_LOW_PRICE   DBL_MAX
#define DIV_NO_HIGH_PRICE  -DBL_MAX
#define DIV_NO_LOW_OSC     DBL_MAX
#define DIV_NO_HIGH_OSC    -DBL_MAX

//--- Oscillator source
enum ENUM_DIV_SOURCE
  {
   DIV_SRC_MPO = 0,    // MPO4 Oscillator
   DIV_SRC_RSI = 1     // Standard RSI
  };

//--- Global Structure
struct DivergenceEngine
  {
   //--- Settings
   ENUM_DIV_SOURCE   Source;
   int               MPO_len;
   int               MPO_smooth;
   int               RsiPeriod;
   int               DivPivotLen;
   double            SmoothAlpha;

   //--- Handles
   int               rsiHandle;

   //--- Hidden Calculation Buffers (Standard dynamic arrays)
   double            buf_OscRaw[];
   double            buf_OscSmooth[];
   double            buf_RSI[];

   //--- Hidden State Memory Buffers
   double            LastPivLowPriceBuffer[];
   double            LastPivLowOscBuffer[];
   double            LastPivHighPriceBuffer[];
   double            LastPivHighOscBuffer[];

   int               LastTypeBuffer[];       // 1=Bull, -1=Bear, 0=None
   int               BarsSinceBuffer[];      // Tracks bars since last divergence
  };

//+------------------------------------------------------------------+
//| Initialization helper function                                   |
//+------------------------------------------------------------------+
bool InitializeDivergenceEngine(DivergenceEngine &divEngine,
                                ENUM_DIV_SOURCE source,
                                int mpoLen,
                                int mpoSmooth,
                                int rsiPeriod,
                                int divPivotLen,
                                string symbol = "",
                                ENUM_TIMEFRAMES tf = PERIOD_CURRENT)
  {
   if(mpoLen <= 0 || mpoSmooth <= 0 || rsiPeriod <= 0 || divPivotLen <= 0)
     {
      Print("Invalid DivergenceEngine parameters: all must be > 0");
      return false;
     }
   if(mpoSmooth < mpoLen)
      Print("Caution: MPO smooth less than MPO length may cause short smoothing");

   if(symbol == "")
      symbol = _Symbol;

   divEngine.Source = source;
   divEngine.MPO_len = mpoLen;
   divEngine.MPO_smooth = mpoSmooth;
   divEngine.RsiPeriod = rsiPeriod;
   divEngine.DivPivotLen = divPivotLen;
   divEngine.SmoothAlpha = 2.0 / (mpoSmooth + 1.0);
   divEngine.rsiHandle = INVALID_HANDLE;

   if(divEngine.Source == DIV_SRC_RSI)
     {
      divEngine.rsiHandle = iRSI(symbol, tf, divEngine.RsiPeriod, PRICE_CLOSE);
      if(divEngine.rsiHandle == INVALID_HANDLE)
        {
         Print("Failed to create RSI handle in DivergenceEngine");
         return false;
        }
     }
   return true;
  }

//+------------------------------------------------------------------+
//| Array release helper function                                    |
//+------------------------------------------------------------------+
void ReleaseDivergenceEngine(DivergenceEngine &divEngine)
  {
   if(divEngine.rsiHandle != INVALID_HANDLE)
     {
      if(!IndicatorRelease(divEngine.rsiHandle))
         PrintFormat("IndicatorRelease failed: handle=%d, error=%d",
                     divEngine.rsiHandle, GetLastError());
      divEngine.rsiHandle = INVALID_HANDLE;
     }

   ArrayFree(divEngine.buf_OscRaw);
   ArrayFree(divEngine.buf_OscSmooth);
   ArrayFree(divEngine.buf_RSI);
   ArrayFree(divEngine.LastPivLowPriceBuffer);
   ArrayFree(divEngine.LastPivLowOscBuffer);
   ArrayFree(divEngine.LastPivHighPriceBuffer);
   ArrayFree(divEngine.LastPivHighOscBuffer);
   ArrayFree(divEngine.LastTypeBuffer);
   ArrayFree(divEngine.BarsSinceBuffer);
  }

//+------------------------------------------------------------------+
//| Buffer sizing & mapping function                                 |
//+------------------------------------------------------------------+
bool ResizeDivergenceBuffers(DivergenceEngine &divEngine, int size)
  {
   if(size <= 0)
     {
      PrintFormat("ResizeDivergenceBuffers: invalid size=%d", size);
      return false;
     }

   if(ArrayResize(divEngine.buf_OscRaw, size) != size ||
      ArrayResize(divEngine.buf_OscSmooth, size) != size ||
      ArrayResize(divEngine.buf_RSI, size) != size ||
      ArrayResize(divEngine.LastPivLowPriceBuffer, size) != size ||
      ArrayResize(divEngine.LastPivLowOscBuffer, size) != size ||
      ArrayResize(divEngine.LastPivHighPriceBuffer, size) != size ||
      ArrayResize(divEngine.LastPivHighOscBuffer, size) != size ||
      ArrayResize(divEngine.LastTypeBuffer, size) != size ||
      ArrayResize(divEngine.BarsSinceBuffer, size) != size)
     {
      PrintFormat("ResizeDivergenceBuffers: ArrayResize failed, size=%d, error=%d",
                  size, GetLastError());
      return false;
     }

   if(!ArraySetAsSeries(divEngine.buf_OscRaw, true) ||
      !ArraySetAsSeries(divEngine.buf_OscSmooth, true) ||
      !ArraySetAsSeries(divEngine.buf_RSI, true) ||
      !ArraySetAsSeries(divEngine.LastPivLowPriceBuffer, true) ||
      !ArraySetAsSeries(divEngine.LastPivLowOscBuffer, true) ||
      !ArraySetAsSeries(divEngine.LastPivHighPriceBuffer, true) ||
      !ArraySetAsSeries(divEngine.LastPivHighOscBuffer, true) ||
      !ArraySetAsSeries(divEngine.LastTypeBuffer, true) ||
      !ArraySetAsSeries(divEngine.BarsSinceBuffer, true))
     {
      PrintFormat("ResizeDivergenceBuffers: ArraySetAsSeries failed, error=%d",
                  GetLastError());
      return false;
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Boundary Initialization function (Seed the bar at limit+1)       |
//+------------------------------------------------------------------+
void InitDivergenceBoundaries(DivergenceEngine &divEngine, int limit)
  {
   int size = ArraySize(divEngine.buf_OscSmooth);
   if(limit + 1 >= size)
      return;

//--- The loop's first bar (i = limit) reads i+1, so limit+1 is the only bar to seed
   divEngine.buf_OscRaw[limit+1] = 0;
   divEngine.buf_OscSmooth[limit+1] = 0;
   divEngine.LastTypeBuffer[limit+1] = 0;
   divEngine.BarsSinceBuffer[limit+1] = DIV_NO_BARS;
//--- Any real price passes the price sentinels and no oscillator value passes the
//--- oscillator ones, so the first pivot becomes a reference, never a divergence
   divEngine.LastPivLowPriceBuffer[limit+1] = DIV_NO_LOW_PRICE;
   divEngine.LastPivLowOscBuffer[limit+1] = DIV_NO_LOW_OSC;
   divEngine.LastPivHighPriceBuffer[limit+1] = DIV_NO_HIGH_PRICE;
   divEngine.LastPivHighOscBuffer[limit+1] = DIV_NO_HIGH_OSC;
  }

//+------------------------------------------------------------------+
//| Phase 2/4 — OSCILLATOR                                           |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Update RSI before calculation loop (If using RSI as source)      |
//+------------------------------------------------------------------+
bool UpdateDivergenceRSI(DivergenceEngine &divEngine, int limit)
  {
   if(divEngine.Source == DIV_SRC_RSI && divEngine.rsiHandle != INVALID_HANDLE)
     {
      // Ensure we have enough bars calculated
      int barsCalc = BarsCalculated(divEngine.rsiHandle);
      if(barsCalc < limit + 2)
        {
         Print("RSI is not ready: BarsCalculated = ", barsCalc, ", needed = ", limit+2);
         return false;
        }

      double tempRSI[];
      if(!ArraySetAsSeries(tempRSI, true))
        {
         PrintFormat("ArraySetAsSeries(tempRSI) failed, error=%d", GetLastError());
         return false;
        }
      int copied = CopyBuffer(divEngine.rsiHandle, 0, 0, limit + 2, tempRSI);
      if(copied < limit + 2)
        {
         Print("Failed to copy enough RSI values. Copied ", copied, ", needed ", limit+2);
         return false;
        }

      // Clear the whole target range first
      ArrayFill(divEngine.buf_RSI, 0, limit + 2, 0.0);

      // Copy only up to limit+1
      int max_idx = (int)MathMin(limit + 1, copied - 1);
      for(int k = 0; k <= max_idx; k++)
         divEngine.buf_RSI[k] = tempRSI[k];
      return true;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| Compute the smoothed oscillator value for the selected source    |
//+------------------------------------------------------------------+
void CalculateOscillator(DivergenceEngine &divEngine, int i, const double &open[], const double &close[], double DivPoint)
  {
   int size = (int)MathMin(
                 MathMin(ArraySize(open), ArraySize(close)),
                 MathMin(ArraySize(divEngine.buf_OscRaw), ArraySize(divEngine.buf_OscSmooth))
              );

//--- Guard
   if(i < 0 || i >= size)
      return;

   if(divEngine.Source == DIV_SRC_MPO)
     {
      //--- Check if we have enough bars ahead for MPO
      if(i + divEngine.MPO_len > size)
        {
         // Not enough data: carry forward previous value if possible, else set to 0
         if(i + 1 < size)
           {
            divEngine.buf_OscRaw[i] = divEngine.buf_OscRaw[i+1];
            divEngine.buf_OscSmooth[i] = divEngine.buf_OscSmooth[i+1];
           }
         else
           {
            divEngine.buf_OscRaw[i] = 0;
            divEngine.buf_OscSmooth[i] = 0;
           }
         return;
        }

      double rollingSum = 0;
      double sumBodies = 0;
      for(int k = 0; k < divEngine.MPO_len; k++)
         sumBodies += MathAbs(close[i+k] - open[i+k]);
      double avgBody = (sumBodies > 0) ? sumBodies / divEngine.MPO_len : DivPoint;

      for(int k = 0; k < divEngine.MPO_len; k++)
        {
         int idx = i + k;
         double body = MathAbs(close[idx] - open[idx]);
         double dir = (close[idx] > open[idx]) ? 1.0 : (close[idx] < open[idx]) ? -1.0 : 0.0;
         double weight = (avgBody > 0) ? body / avgBody : 1.0;
         rollingSum += (dir * weight);
        }
      divEngine.buf_OscRaw[i] = (rollingSum / (divEngine.MPO_len * 2.0)) * 100.0;
     }
   else // RSI source
     {
      //--- Ensure buf_RSI has data at i
      if(i < ArraySize(divEngine.buf_RSI))
         divEngine.buf_OscRaw[i] = divEngine.buf_RSI[i];
      else
         divEngine.buf_OscRaw[i] = 0;
     }

//--- Apply Smoothing
   if(i + 1 < size)
      divEngine.buf_OscSmooth[i] = (divEngine.buf_OscRaw[i] * divEngine.SmoothAlpha) + (divEngine.buf_OscSmooth[i+1] * (1.0 - divEngine.SmoothAlpha));
   else
      divEngine.buf_OscSmooth[i] = divEngine.buf_OscRaw[i]; // fallback
  }

//+------------------------------------------------------------------+
//| Phase 3/4 — DIVERGENCE                                           |
//+------------------------------------------------------------------+
void DetectDivergence(DivergenceEngine &divEngine, int i, const double &high[], const double &low[], int limit)
  {
   int size = (int)MathMin(
                 MathMin(ArraySize(high), ArraySize(low)),
                 MathMin(
                    MathMin(ArraySize(divEngine.LastTypeBuffer), ArraySize(divEngine.BarsSinceBuffer)),
                    MathMin(
                       MathMin(ArraySize(divEngine.LastPivLowPriceBuffer), ArraySize(divEngine.LastPivLowOscBuffer)),
                       MathMin(ArraySize(divEngine.LastPivHighPriceBuffer), ArraySize(divEngine.LastPivHighOscBuffer))
                    )
                 )
              );
//--- Guard
   if(i < 0 || i >= size)
      return;
   if(i + 1 < size)
     {
      //--- Inherit state
      divEngine.LastTypeBuffer[i]         = divEngine.LastTypeBuffer[i+1];
      divEngine.BarsSinceBuffer[i]        = divEngine.BarsSinceBuffer[i+1] + 1;
      divEngine.LastPivLowPriceBuffer[i]  = divEngine.LastPivLowPriceBuffer[i+1];
      divEngine.LastPivLowOscBuffer[i]    = divEngine.LastPivLowOscBuffer[i+1];
      divEngine.LastPivHighPriceBuffer[i] = divEngine.LastPivHighPriceBuffer[i+1];
      divEngine.LastPivHighOscBuffer[i]   = divEngine.LastPivHighOscBuffer[i+1];
     }
   else
     {
      //--- Fallback default if at the absolute edge of the array
      divEngine.LastTypeBuffer[i]         = 0;
      divEngine.BarsSinceBuffer[i]        = DIV_NO_BARS;
      divEngine.LastPivLowPriceBuffer[i]  = DIV_NO_LOW_PRICE;
      divEngine.LastPivLowOscBuffer[i]    = DIV_NO_LOW_OSC;
      divEngine.LastPivHighPriceBuffer[i] = DIV_NO_HIGH_PRICE;
      divEngine.LastPivHighOscBuffer[i]   = DIV_NO_HIGH_OSC;
     }

   int pIdx = i + divEngine.DivPivotLen;
//--- Check that we have enough bars on both sides of the pivot and that pIdx is within array
   if(pIdx - divEngine.DivPivotLen >= 0 && pIdx + divEngine.DivPivotLen < size && pIdx < size)
     {
      bool isPivLow = true;
      bool isPivHigh = true;
      for(int k = 1; k <= divEngine.DivPivotLen; k++)
        {
         // Strict pivot. For non-strict (allow equal), use <= and >= accordingly.
         if(!(divEngine.buf_OscSmooth[pIdx] < divEngine.buf_OscSmooth[pIdx+k] && divEngine.buf_OscSmooth[pIdx] < divEngine.buf_OscSmooth[pIdx-k]))
            isPivLow = false;
         if(!(divEngine.buf_OscSmooth[pIdx] > divEngine.buf_OscSmooth[pIdx+k] && divEngine.buf_OscSmooth[pIdx] > divEngine.buf_OscSmooth[pIdx-k]))
            isPivHigh = false;
        }

      //--- Process Bullish Pivot
      if(isPivLow)
        {
         if(low[pIdx] < divEngine.LastPivLowPriceBuffer[pIdx] && divEngine.buf_OscSmooth[pIdx] > divEngine.LastPivLowOscBuffer[pIdx])
           {
            divEngine.LastTypeBuffer[i] = 1;
            divEngine.BarsSinceBuffer[i] = 0;
           }
         divEngine.LastPivLowPriceBuffer[i] = low[pIdx];
         divEngine.LastPivLowOscBuffer[i] = divEngine.buf_OscSmooth[pIdx];
        }

      //--- Process Bearish Pivot
      if(isPivHigh)
        {
         if(high[pIdx] > divEngine.LastPivHighPriceBuffer[pIdx] && divEngine.buf_OscSmooth[pIdx] < divEngine.LastPivHighOscBuffer[pIdx])
           {
            divEngine.LastTypeBuffer[i] = -1;
            divEngine.BarsSinceBuffer[i] = 0;
           }
         divEngine.LastPivHighPriceBuffer[i] = high[pIdx];
         divEngine.LastPivHighOscBuffer[i] = divEngine.buf_OscSmooth[pIdx];
        }
     }
  }

//+------------------------------------------------------------------+
//| Phase 4/4 — ACCESS                                               |
//+------------------------------------------------------------------+
int GetLastDivergenceType(const DivergenceEngine &divEngine, int i)
  {
   if(i < 0 || i >= ArraySize(divEngine.LastTypeBuffer))
      return 0;
   return divEngine.LastTypeBuffer[i];
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int GetBarsSinceDivergence(const DivergenceEngine &divEngine, int i)
  {
   if(i < 0 || i >= ArraySize(divEngine.BarsSinceBuffer))
      return DIV_NO_BARS;
   return divEngine.BarsSinceBuffer[i];
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
double GetOscillatorValue(const DivergenceEngine &divEngine, int i)
  {
   if(i < 0 || i >= ArraySize(divEngine.buf_OscSmooth))
      return 0.0;
   return divEngine.buf_OscSmooth[i];
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void ResetDivergenceState(DivergenceEngine &divEngine, int i)
  {
   if(i < 0 || i >= ArraySize(divEngine.LastTypeBuffer))
      return;
   divEngine.LastTypeBuffer[i] = 0;
   divEngine.BarsSinceBuffer[i] = DIV_NO_BARS;
   divEngine.LastPivLowPriceBuffer[i] = DIV_NO_LOW_PRICE;
   divEngine.LastPivLowOscBuffer[i] = DIV_NO_LOW_OSC;
   divEngine.LastPivHighPriceBuffer[i] = DIV_NO_HIGH_PRICE;
   divEngine.LastPivHighOscBuffer[i] = DIV_NO_HIGH_OSC;
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int GetRequiredLookback(const DivergenceEngine &divEngine)
  {
// Minimum bars needed
   int required = divEngine.MPO_smooth + 2 * divEngine.DivPivotLen + 5;
   if(divEngine.Source == DIV_SRC_RSI)
      required = MathMax(required, divEngine.RsiPeriod + 2 * divEngine.DivPivotLen + 5);
   else // MPO
      required = MathMax(required, divEngine.MPO_len + 2 * divEngine.DivPivotLen + 5);
   return required;
  }

//+------------------------------------------------------------------+

