preview
Building a Divergence System (Part IV): Creating a Reusable Divergence Engine for MQL5

Building a Divergence System (Part IV): Creating a Reusable Divergence Engine for MQL5

MetaTrader 5 — Examples |
149 0
Solomon Anietie Sunday
Solomon Anietie Sunday

Table of Contents

  1. Introduction
  2. Why Refactor?
  3. Designing the Header
  4. Integration Checklist
  5. Choosing and Adapting Parabolic SAR
  6. Building the Demo EA
  7. Results
  8. Conclusion


Introduction

Across the first three parts of this series, we built a complete adaptive trading system from scratch. The first part of the series introduced the MPO4 oscillator, a momentum measure derived from weighted candle bodies. Part II used that oscillator, with RSI as an alternative, to detect divergence and apply it to the SuperTrend indicator, shrinking its ATR multiplier whenever an opposing divergence signaled that the current trend was losing strength. Part III carried the same adaptive core into a fully self-contained Expert Advisor, adding position sizing, ATR-based stops, trailing logic, and session filters around it.

The Reusability Problem

One detail became harder to ignore as the series progressed. The divergence engine—oscillator calculation, pivot detection, divergence classification, and state memory—has existed only inside the SuperTrend implementation. It was written once, for one indicator. Reusing it elsewhere would mean copying the same block of code into a new project and hoping nothing breaks in translation. That is not a design decision; it has become a maintenance liability.

The Solution

This article removes that limitation. We will extract the divergence engine into its own file, DivergenceEngine.mqh, a self-contained header that many indicators or Expert Advisors can include and initialize independently of any indicator. Just as importantly, we will focus on how to integrate the header into a project you design yourself, rather than only into the demonstration built in this article.

The demonstration is deliberately lightweight: a small Expert Advisor built around Parabolic SAR that adapts its Acceleration Factor whenever an opposing divergence appears. The goal is to show the divergence engine at work with as little surrounding code as possible, keeping the integration pattern at the center of the article rather than the trading logic built on top of it.

Scope of This Article

In the sections that will follow, we will:

  • Examine why the current architecture resists reuse and lay out the new design.
  • Design DivergenceEngine.mqh in four phases: Foundation, Oscillator, Divergence, and Access.
  • Walk through a general checklist for integrating the header into any indicator or EA of your own.
  • Use Parabolic SAR as a demonstration case and address a constraint of MQL5's built-in handle that shapes how the adaptive logic must be implemented.
  • Build the lightweight Adaptive Parabolic SAR EA around the header.
  • Test the standard and adaptive behavior against each other and review the results.


Why Refactor?

Across the first three parts, Adaptive SuperTrend accumulated everything needed to detect divergence: oscillator calculation (MPO4 or RSI), pivot detection, divergence classification, and state management. That state includes the last pivot price, the last oscillator reading, the last divergence type, and the bars-since-divergence count. Each component was added where the SuperTrend calculation needed it, using the same series-ordered arrays as the indicator.

That is a reasonable approach for a single, self-contained system, but it comes with a cost: none of these components exist independently. They are embedded in the SuperTrend loop, sized around its lookback, and initialized with SuperTrend-specific boundary values. If another indicator or an unrelated EA needs the same divergence behavior, there is no clean interface to provide it. The practical alternative is to copy the relevant code into the new project and adjust it until it compiles. That kind of "reuse" becomes a maintenance problem when one copy gets a bug fix and the other doesn't. The illustration below visually simplifies this:

Divergence header illustration_Old

Oscillator, pivot detection, divergence detection, and state memory live inside a single implementation and can only be used there.

The New Architecture: DivergenceEngine.mqh

The solution is to separate the divergence logic into its own header, with no dependency on SuperTrend, ATR multipliers, or trend bands. DivergenceEngine.mqh accepts price data and produces a divergence state. It retains the same core functionality as before: oscillator calculation, pivot detection, divergence detection, and state management. The difference is that these components are inside a single struct rather than scattered across global arrays tied to one project. Any indicator or EA that needs divergence-aware behavior can include the header, create its own DivergenceEngine instance, and query it independently. What happens with the result is entirely up to the caller: shrink a trend band, adjust an acceleration factor, gate an entry, or support some future adaptive mechanism. The engine has one responsibility: to determine whether a divergence exists and how recent it is. The illustration below visually simplifies this:

Divergence header illustration_New

DivergenceEngine.mqh stands independently, consumed by the Adaptive SuperTrend indicator, the Adaptive SuperTrend EA, the Adaptive Parabolic SAR EA built later in this article, and any future project that needs divergence detection.

Benefits of Refactoring

Moving the divergence engine into its own module brings several practical benefits:

  • No code duplication. The divergence logic is implemented once and shared by every indicator or EA that includes the header.
  • Simpler maintenance. Bug fixes and improvements to DivergenceEngine.mqh propagate to every project that uses it.
  • Independent testing. The engine can be validated separately from any trading strategy built around it.
  • Greater reusability. Any indicator or EA that can provide the required price data can use the engine, regardless of how it responds to divergence.
  • Cleaner projects. Indicators and EAs remain focused on their own responsibilities instead of re-implementing oscillator and pivot logic.


Designing the Header

With the reasoning for the refactor established, we can now build DivergenceEngine.mqh. The header is organized into four phases—Foundation, Oscillator, Divergence, and Access—each handling one part of the engine's responsibility. Together, they form the complete module.

We start in MetaEditor with a new .mqh file containing only its basic properties:

//+------------------------------------------------------------------+
//|                                             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: Foundation

The Foundation phase defines the data the rest of the header operates on, along with the functions responsible for initialization, release, and buffer management. Everything begins with a source selector, the DivergenceEngine struct, and sentinel values for neutral defaults:

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

Everything the engine needs—settings, an optional RSI handle, calculation buffers, and state buffers—now lives inside a single struct. A consumer can declare one DivergenceEngine instance instead of managing a collection of loose global arrays. Multiple instances can also run side by side without interfering with each other, such as one using MPO4 and another using RSI.

Initialization is handled by a dedicated helper rather than requiring the caller to configure the struct manually:

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

The RSI handle is created only when DIV_SRC_RSI is selected. MPO4 requires no indicator handle because it is calculated directly from price data. Deinitialization mirrors this:

//+------------------------------------------------------------------+
//| 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);
  }

ReleaseDivergenceEngine() belongs in the consumer's OnDeinit(), just like the release of any other indicator handle.

Finally, the Foundation phase manages the buffer lifecycle. It sizes the arrays and seeds the bar that the oldest processed bar inherits from, so recursive calculations start from a valid state.

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

InitDivergenceBoundaries() runs on every pass, before the per-bar loop. That loop starts at i = limit, and CalculateOscillator() and DetectDivergence() both read i + 1, so the first bar processed reads index limit + 1. The function writes that one index and nothing else, filling it with the sentinel values: no divergence, a bar count of DIV_NO_BARS, and placeholder pivot references. Any real price passes the price half of the divergence test against those placeholders, and no real oscillator value passes the oscillator half, so a pivot compared against them is only stored as the reference for the next one. This follows the original SuperTrend boundary-seeding pattern. The difference is that it now lives in the engine, not duplicated in every consumer.

Phase 2: The Oscillator Module

The Oscillator phase converts raw price data into the smoothed oscillator used by the divergence logic. When RSI is selected, UpdateDivergenceRSI() copies the RSI indicator buffer into the engine's internal array before the main calculation loop:

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

CalculateOscillator() is then called for each processed bar. Its job is to calculate the selected raw oscillator and apply the same exponential smoothing used by the original implementation:

//+------------------------------------------------------------------+
//| 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
  }

The MPO4 branch retains the weighted-body calculation introduced in the first part of the series: each candle's directional body is weighted against the average body size of the window, summed over MPO_len bars, and scaled into the oscillator's range. The main addition here is protection against insufficient look-ahead data. Because the engine processes bars backwards, the oldest bars in a limited history may not have a complete MPO_len window. Rather than produce a distorted value, the calculation carries forward the previous value when possible.

The RSI branch does not calculate anything itself. It reads from buf_RSI, which was populated by UpdateDivergenceRSI(). Both sources then pass through the same exponential smoothing step controlled by SmoothAlpha. This gives Phase 3 a single oscillator buffer to work with, regardless of whether the source is MPO4 or RSI.

Phase 3: The Divergence Module

On each bar, DetectDivergence() does two things: it carries forward prior state/pivots and then checks whether the current window confirms a new pivot.
//+------------------------------------------------------------------+
//| 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];
        }
     }
  }

The pivot itself sits DivPivotLen bars behind the current processing index, at pIdx. A pivot low is confirmed when the oscillator value there is lower than every value within DivPivotLen bars on both sides. A pivot high uses the opposite condition. This is the same symmetric-window pivot detection introduced in Part II, but it now operates entirely on the engine's internal oscillator buffer rather than on arrays belonging to a specific project.

Divergence is classified only when a new pivot is confirmed. A bullish divergence occurs when price makes a lower low while the oscillator makes a higher low. A bearish divergence is the opposite: price makes a higher high while the oscillator makes a lower high. When either condition is detected, LastTypeBuffer[i] records the divergence, and BarsSinceBuffer[i] resets to zero. On subsequent bars, the previous state is simply inherited, and the bar count increases. This is what allows the engine to track how long it has been since the last confirmed divergence.

Phase 4: Access Functions

The final phase is intentionally small. Rather than exposing the internal buffers directly, the engine provides a compact set of access functions:

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

GetLastDivergenceType() returns 1, -1, or 0 for bullish, bearish, or no divergence. GetBarsSinceDivergence() reports the number of bars since the last confirmed divergence, allowing a consumer to reduce an adaptive effect over time rather than apply it indefinitely. Until a divergence is recorded, and again after a reset, the count starts from DIV_NO_BARS and grows from there. An index outside the buffer returns DIV_NO_BARS itself, so an age check such as barsSince <= DivBarsLimit fails in both cases without special handling. GetOscillatorValue() exposes the smoothed oscillator for plotting or additional filtering, while ResetDivergenceState() allows a consumer to clear a divergence after it has been acted upon, for example, when a trend reversal invalidates an older signal. GetRequiredLookback() calculates the minimum history required by the selected oscillator and pivot length. This gives the consumer a simple way to validate or adjust its lookback before the engine buffers are sized.

These five functions form the engine's public interface. Nothing outside the header needs direct access to LastTypeBuffer, buf_OscSmooth, or any other internal array. Every consumer, including the Adaptive Parabolic SAR EA built later in this article, interacts with the engine through Phase 4.


Integration Checklist

The header's responsibility: given price data, produce a divergence state. Every consumer will follow the same integration sequence around its existing OnInit(), main calculation loop, and OnDeinit().

Stage Function(s) Called From Purpose
Setup. InitializeDivergenceEngine(). OnInit(). Store settings and create the RSI handle if DIV_SRC_RSI is selected.
Lookback GetRequiredLookback(). OnInit(). Determine the minimum history required for the selected oscillator and pivot configuration before sizing the working buffers.
Sizing. ResizeDivergenceBuffers(). OnInit(), and again before a pass whenever the number of bars processed changes.
Size the engine's internal buffers to the number of bars the consumer processes.
Per-update seeding. InitDivergenceBoundaries(). Every pass, before the per-bar loop.
Seed index limit + 1, which the first processed bar reads, with neutral defaults.
RSI refresh (if used).
UpdateDivergenceRSI(). Before the main bar loop. Copy the RSI buffer into the engine.
Per-bar loop. CalculateOscillator(), DetectDivergence().
Inside the main loop, i = limit down to 0. Calculate the oscillator and classify divergence for bar i.
Query. GetLastDivergenceType(), GetBarsSinceDivergence(), GetOscillatorValue(), ResetDivergenceState(). Wherever the adaptive response is decided.
Read or clear the divergence state.
Teardown. ReleaseDivergenceEngine(). OnDeinit(). Release the RSI handle and free the buffers.

One last detail: the per-bar loop must run backward, from the oldest bar being processed to the newest. Both CalculateOscillator() and DetectDivergence() read i + 1, so that value must already contain a valid result before the current bar is processed. Like this:

for(int i = limit; i >= 0; i--)
  {
   CalculateOscillator(divEngine, i, open, close, point);
   DetectDivergence(divEngine, i, high, low, limit);

   // Indicator or EA logic
  }

The consumer's own logic, like position sizing, trend bands, acceleration factors, entry filters, and everything else, remains outside the engine and is under its control.

What the Engine Does Not Do

DivergenceEngine.mqh stops at determining that a divergence exists and how many bars old it is. It does not:

  • Calculate the consumer indicator or trading logic.
  • Decide what the adaptive response should be. Things like shrinking a multiplier, changing a parameter, or gating an entry remain the caller's decision.
  • Open trades, manage risks, or draw chart objects.
  • Know what its output will ultimately be used for.

This preserves a separation of responsibility.

Consumption Context for EAs and Indicators

The integration sequence is the same for indicators (OnCalculate, full history) and EAs (OnTick, smaller local window). The context changes how much history is available and how often the loop executes, but the engine calls themselves remain the same.


Choosing and Adapting Parabolic SAR

We have established that DivergenceEngine.mqh follows the same integration sequence regardless of its consumer. Parabolic SAR provides a second example to demonstrate that the engine is genuinely reusable, rather than simply separated from SuperTrend. A few practical reasons make it a good fit for a demonstration:

  • It ships as a built-in MQL5 indicator, so many readers already recognize it.
  • It exposes a single sensitivity input, the acceleration factor, which gives the adaptive logic one clear parameter to act on.
  • Its behavior against an unmodified version is easy to compare visually, which is useful for verifying results.

The point is not that Parabolic SAR requires a special integration path. It is simple enough to keep the focus on how the divergence engine is reused.

Practical Notes on Built-in Handles

There is one implementation constraint worth addressing before continuing. MQL5's built-in iSAR() is accessed through a handle, and its parameters, including the acceleration factor, are fixed when that handle is created. That makes it unsuitable as the calculation source for this EA, because the acceleration factor needs to change when a divergence is detected. Therefore, we will calculate the Parabolic SAR directly inside the EA using its standard formula. This also keeps a single SAR series for both adaptation and plotting, making the comparison straightforward.

Separation of Responsibility

DivergenceEngine.mqh never calculates SAR, and it never will. Nothing in the header knows that Parabolic SAR exists. It returns the divergence state and its age through Phase 4, with the smoothed oscillator also available to the consumer when needed. What a consumer does with that information, and in which direction, is entirely its own decision.

The Adaptive SuperTrend EA used that signal to reduce the ATR multiplier whenever a divergence opposed the prevailing trend, interpreting the divergence as an early reversal warning and tightening accordingly. The Adaptive Parabolic SAR EA uses the same signal but does not force a single interpretation. An opposing divergence may signal a weakening trend and a developing reversal. It may also be just a pullback before continuation. The trader can therefore choose which interpretation to trade through the direction of the adjustment:

Adaptive Parameter = Base Parameter * (1.0 - AdaptiveFactor)    // Reduce
Adaptive Parameter = Base Parameter * (1.0 + AdaptiveFactor)    // Increase

Reducing the acceleration factor slows how quickly SAR advances toward price after each new high or low, keeping it farther away for longer. This corresponds to treating the divergence as a pullback or temporary loss of momentum that the current trend should be given room to absorb. Increasing it does the opposite: SAR advances faster, stays closer to price, and reverses sooner. This corresponds to treating the divergence as an early reversal warning worth acting on. Both are valid responses to the same signal. Which interpretation is more appropriate depends on how divergences typically resolve in that market. The engine itself cannot make that determination; therefore, it remains neutral.

DivergenceEngine.mqh provides the signal and its age, while the consumer decides how to interpret and act on it. The next section shows how that choice is wired into the EA.


Building the Demo EA

With the integration pattern established, we can now assemble the demonstration EA. Because this is a demonstration rather than a production trading system, the surrounding trade logic is intentionally lightweight. The emphasis is on how DivergenceEngine.mqh is initialized, updated, queried, and kept separate from the Parabolic SAR logic.

Inputs and Global Variables

Next, define the EA entry points and required #property directives (OnInit, OnTick, OnDeinit). It is as follows:

//+------------------------------------------------------------------+
//|                                           Demo_Adaptive_PSAR.mq5 |
//|                                    Copyright 2026, soloharbinger |
//|                      https://www.mql5.com/en/users/soloharbinger |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, soloharbinger"
#property link      "https://www.mql5.com/en/users/soloharbinger"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }

The EA starts with two includes and four input groups. These inputs reflect the different responsibilities within the system:

#include <Trade/Trade.mqh>
#include <DivergenceEngine.mqh>          // Reusable divergence detection header

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input group "=== Parabolic SAR Settings ==="
input double   InpSARStep = 0.02;                                   // SAR Step (Acceleration)
input double   InpSARMaximum = 0.2;                                 // SAR Maximum

input group "=== Adaptive Settings ==="
enum ENUM_ADAPTIVE_MODE
  {
   ADAPTIVE_REDUCE,   // Reduce SAR step (slower acceleration)
   ADAPTIVE_INCREASE  // Increase SAR step (faster acceleration)
  };
input bool     EnableAdaptive = false;                              // Enable Adaptive Signals
input ENUM_ADAPTIVE_MODE InpAdaptiveMode = ADAPTIVE_REDUCE;         // Adaptive mode
input double   AdaptiveFactor = 0.3;                                // Adaptive factor [0..1]
input int      DivBarsLimit = 200;                                  // Max bars since divergence to apply adaptation
input bool     ResetOnTrendChange = false;                          // Reset divergence when trend changes

input group "=== Divergence Engine Settings ==="
input ENUM_DIV_SOURCE InpSource = DIV_SRC_MPO;                      // Oscillator Source
input int    MPO_len = 6;                                           // MPO Length
input int    MPO_smooth = 7;                                        // MPO Smoothing
input int    DivRsiPeriod = 14;                                     // RSI Period
input int    DivPivotLen = 2;                                       // Pivot Length

input group "=== Trade Management ==="
input double RiskPercent = 0.5;                                     // Risk per trade [% of balance]
input double TPRatio = 2.0;                                         // Take-profit ratio (risk-reward)
input double FixedLotSize = 0.0;                                    // Fixed lot (0 = use risk-based sizing)
input int    MagicNumber = 23456;                                   // EA Magic Number
input int    Lookback = 300;                                        // Number of bars for calculation

Parabolic SAR Settings defines the baseline behavior of the indicator. Adaptive Settings controls how strongly divergence is allowed to influence the SAR acceleration factor. Divergence Engine Settings maps directly to the initialization requirements of DivergenceEngine.mqh. Trade Management is intentionally kept separate, as it only exists to provide a minimal execution layer for the demonstration.

Global Variables

After inputs, the EA defines its runtime state:

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade trade;
DivergenceEngine engine;                  // Divergence engine instance

double   g_SARCurrent[];                    // SAR values used for current bar (hybrid)
double   openPrices[], highPrices[], lowPrices[], closePrices[];
double   point;
datetime lastBarTime;
int      lastResizeSize = 0;
int      lookback = 0;

The most important variable here is g_SARCurrent[]. There is only one SAR series because the EA does not maintain separate "standard" and "adaptive" indicators. Instead, a single Parabolic SAR calculation is continuously modified by the current adaptive step. This design choice ensures that:

  • The chart reflects the actual value used for trading decisions.
  • There is no duplication of indicator state.
  • The adaptive behavior is expressed as a parameter change, not a second indicator.

In other words, the system does not switch between two SAR implementations; that will be the case with two iSAR() handles. Instead, it recalculates a single SAR series using the currently selected adaptive rules, keeping both visualization and execution aligned with the same underlying series.

OnInit() and OnDeinit()

Initialization follows the same separation of responsibilities:

//+------------------------------------------------------------------+
//| Initialization                                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   trade.SetExpertMagicNumber(MagicNumber);
   double pointValue = 0.0;
   if(!SymbolInfoDouble(_Symbol, SYMBOL_POINT, pointValue) || pointValue <= 0.0)
     {
      PrintFormat("Failed to get SYMBOL_POINT, error=%d", GetLastError());
      return INIT_FAILED;
     }
   point = pointValue;

//--- Validate inputs
   if(InpSARStep <= 0 || InpSARMaximum <= InpSARStep)
     {
      Print("Invalid PSAR parameters. Step > 0 and Maximum > Step.");
      return INIT_PARAMETERS_INCORRECT;
     }
   if(AdaptiveFactor < 0 || AdaptiveFactor > 1)
     {
      Print("AdaptiveFactor must be in [0..1]");
      return INIT_PARAMETERS_INCORRECT;
     }
   if(RiskPercent < 0 || TPRatio <= 0)
     {
      Print("RiskPercent must be > = 0 and TPRatio > 0");
      return INIT_PARAMETERS_INCORRECT;
     }

//--- Initialize divergence engine
   if(!InitializeDivergenceEngine(engine, InpSource, MPO_len, MPO_smooth,
                                  DivRsiPeriod, DivPivotLen, _Symbol, PERIOD_CURRENT))
     {
      Print("Failed to initialize Divergence Engine");
      return INIT_FAILED;
     }

//--- Set lookback based on required minimum
   int required = GetRequiredLookback(engine);
   if(Lookback < required)
     {
      Print("Lookback (", Lookback, ") is less than required (", required, "). Using required.");
      lookback = required;
     }
   else
      lookback = Lookback;

//--- Resize divergence buffers
   if(!ResizeDivergenceBuffers(engine, lookback))
     {
      Print("Failed to resize divergence buffers");
      ReleaseDivergenceEngine(engine);
      return INIT_FAILED;
     }
   lastResizeSize = lookback;

//--- Prepare price arrays
   if(ArrayResize(openPrices, lookback) != lookback ||
      ArrayResize(highPrices, lookback) != lookback ||
      ArrayResize(lowPrices, lookback) != lookback ||
      ArrayResize(closePrices, lookback) != lookback ||
      ArrayResize(g_SARCurrent, lookback) != lookback)
     {
      PrintFormat("ArrayResize for price arrays failed, lookback=%d, error=%d",
                  lookback, GetLastError());
      ReleaseDivergenceEngine(engine);
      return INIT_FAILED;
     }

   if(!ArraySetAsSeries(openPrices, true) ||
      !ArraySetAsSeries(highPrices, true) ||
      !ArraySetAsSeries(lowPrices, true) ||
      !ArraySetAsSeries(closePrices, true) ||
      !ArraySetAsSeries(g_SARCurrent, true))
     {
      PrintFormat("ArraySetAsSeries for price arrays failed, error=%d", GetLastError());
      ReleaseDivergenceEngine(engine);
      return INIT_FAILED;
     }

//--- Delete old chart objects to start fresh
   int deleted = ObjectsDeleteAll(0, "DynSAR_");
   if(deleted < 0)
      PrintFormat("ObjectsDeleteAll DynSAR_ failed, error=%d", GetLastError());

   deleted = ObjectsDeleteAll(0, "DivArrow_");
   if(deleted < 0)
      PrintFormat("ObjectsDeleteAll DivArrow_ failed, error=%d", GetLastError());

   ChartRedraw(0);
   UpdateMarketData();

   Print("Adaptive PSAR EA initialized.");
   return INIT_SUCCEEDED;
  }

First, the input parameters are validated. The divergence engine is then initialized through InitializeDivergenceEngine(), after which the EA sizes its price and SAR arrays and its engine buffers. The EA does not configure the engine manually. It passes the required settings to the initialization function and leaves the engine responsible for its own internal state and optional RSI handle.

OnDeinit() reverses that relationship:

//+------------------------------------------------------------------+
//| Deinitialization                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Delete all visuals and release indicators
   ReleaseDivergenceEngine(engine);
   int deleted = ObjectsDeleteAll(0, "DynSAR_");
   if(deleted < 0)
      PrintFormat("ObjectsDeleteAll DynSAR_ failed, error=%d", GetLastError());

   deleted = ObjectsDeleteAll(0, "DivArrow_");
   if(deleted < 0)
      PrintFormat("ObjectsDeleteAll DivArrow_ failed, error=%d", GetLastError());

   ChartRedraw(0);
  }

ReleaseDivergenceEngine() handles the engine's internal resources, while the EA remains responsible for its own chart objects.

OnTick() and the Unified Processing Loop

The EA evaluates once per bar rather than on every tick.

//+------------------------------------------------------------------+
//| Main Tick                                                        |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- New bar check
   datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
   if(currentBarTime == 0 || currentBarTime == lastBarTime)
      return;
   lastBarTime = currentBarTime;

//--- Update prices and SAR/divergence
   if(!UpdateMarketData())
      return;

//--- Check for trade signals and execute
   CheckAndExecuteTrade();
  }

Once a new bar is detected, UpdateMarketData() becomes the central processing function. It handles price retrieval, divergence engine updates, SAR calculation, divergence queries, and visual updates before the trading logic is called.

The first part is straightforward price preparation:

//+------------------------------------------------------------------+
//| Update market data, compute SAR and divergence, draw signals     |
//+------------------------------------------------------------------+
bool UpdateMarketData()
  {
//--- Copy price data
   if(CopyOpen(_Symbol, PERIOD_CURRENT, 0, lookback, openPrices) < lookback ||
      CopyHigh(_Symbol, PERIOD_CURRENT, 0, lookback, highPrices) < lookback ||
      CopyLow(_Symbol, PERIOD_CURRENT, 0, lookback, lowPrices) < lookback ||
      CopyClose(_Symbol, PERIOD_CURRENT, 0, lookback, closePrices) < lookback)
     {
      Print("Failed to copy price data");
      return false;
     }

The engine is then prepared for the current pass:

//--- Update divergence engine (RSI if used)
   if(lookback != lastResizeSize)
     {
      if(!ResizeDivergenceBuffers(engine, lookback))
         return false;
      lastResizeSize = lookback;
     }
   if(!UpdateDivergenceRSI(engine, lookback))
      return false;

   int limit = lookback - MPO_len - DivPivotLen - 3;
   if(limit < 0)
      limit = 0;
   InitDivergenceBoundaries(engine, limit);

//--- Internal Parabolic SAR calculation (backward loop)
   int    trend = closePrices[limit] > openPrices[limit] ? 1 : -1;
   double sar   = (trend == 1) ? lowPrices[limit] : highPrices[limit];
   double ep    = (trend == 1) ? highPrices[limit] : lowPrices[limit];
   double af    = InpSARStep;

These calls are important because they show exactly where the reusable module fits into an existing EA. The consumer controls when data is requested and how much history is processed; the engine controls its own internal buffers and calculations. limit is also kept far enough from the oldest available bar to provide the oscillator and pivot logic with the history they require.

The SAR calculation and divergence processing now share one backward loop:

   for(int i = limit; i >= 0; i--)
     {
      //--- Calculate oscillator and detect divergence at this bar
      CalculateOscillator(engine, i, openPrices, closePrices, point);
      DetectDivergence(engine, i, highPrices, lowPrices, limit);

      //--- Trend flip detection and reset
      if(trend == 1 && lowPrices[i] < sar)
        {
         trend = -1;
         sar   = ep;
         ep    = lowPrices[i];
         af    = InpSARStep;
         if(ResetOnTrendChange)
            ResetDivergenceState(engine, i);
        }
      else
         if(trend == -1 && highPrices[i] > sar)
           {
            trend = 1;
            sar   = ep;
            ep    = highPrices[i];
            af    = InpSARStep;
            if(ResetOnTrendChange)
               ResetDivergenceState(engine, i);
           }

      //--- Determine adaptive step if divergence opposes current trend
      double currentStep = InpSARStep;
      if(EnableAdaptive)
        {
         int divType   = GetLastDivergenceType(engine, i);
         int barsSince = GetBarsSinceDivergence(engine, i);
         if(barsSince <= DivBarsLimit)
           {
            bool isUptrend = (trend == 1);
            if((isUptrend && divType == -1) || (!isUptrend && divType == 1))
              {
               if(InpAdaptiveMode == ADAPTIVE_REDUCE)
                  currentStep = InpSARStep * (1.0 - AdaptiveFactor);  // Apply adaptive factor
               else // INCREASE
                  currentStep = InpSARStep * (1.0 + AdaptiveFactor);  // Apply adaptive factor
               // Confine to reasonable bounds
               if(currentStep < 0.001)
                  currentStep = 0.001;
               if(currentStep > InpSARMaximum)
                  currentStep = InpSARMaximum;
              }
           }
        }

First, the EA asks the divergence engine to calculate the oscillator and detect divergence for the current bar. Only after that does it query the engine for the divergence state. This ensures that the adaptive SAR logic is responding to the state produced for that same bar rather than to stale data.

The trend-flip block remains SAR logic. When the price crosses the current SAR value, the trend reverses, the extreme point is reset, and the acceleration factor returns to its base step. ResetOnTrendChange adds an optional decision: the consumer may also tell the divergence engine to forget the previous divergence when that divergence has already contributed to a trend reversal.

The adaptive block then reads the engine through its public interface. The EA only asks for the divergence type and its age, determines whether that divergence opposes the current trend, and converts that information into a temporary currentStep. In other words, the divergence engine does not directly modify SAR. It supplies information; the SAR calculation decides how that information changes its own parameters.

The rest of the loop is SAR state management:

      //--- Update EP and acceleration factor
      if(trend == 1)
        {
         if(highPrices[i] > ep)
           {
            ep = highPrices[i];
            af = MathMin(af + currentStep, InpSARMaximum);
           }
        }
      else
        {
         if(lowPrices[i] < ep)
           {
            ep = lowPrices[i];
            af = MathMin(af + currentStep, InpSARMaximum);
           }
        }

      //--- Store SAR value and draw SAR dot
      g_SARCurrent[i] = sar;
      DrawSARDot(i, g_SARCurrent[i]);

      //--- Draw divergence arrow if divergence just occurred
      if(GetBarsSinceDivergence(engine, i) == 0)
        {
         int divType = GetLastDivergenceType(engine, i);
         if(divType != 0)
            DrawDivergenceArrow(i, divType);
        }

      //--- Calculate SAR for next bar (i-1)
      if(trend == 1)
        {
         sar = sar + af * (ep - sar);
         sar = MathMin(sar, MathMin(lowPrices[i], (i < limit ? lowPrices[i+1] : lowPrices[i])));
        }
      else
        {
         sar = sar + af * (ep - sar);
         sar = MathMax(sar, MathMax(highPrices[i], (i < limit ? highPrices[i+1] : highPrices[i])));
        }
     }
   ChartRedraw(0);
   return true;
  }

We updated the extreme point and acceleration factor, stored the calculated SAR value, drew it, marked newly confirmed divergences, and calculated the SAR value for the next bar.

Visualizing a Single Adaptive Dot

Because there is only one SAR calculation, there is also only one SAR visual series:

//+------------------------------------------------------------------+
//| Draw a SAR dot on the chart                                      |
//+------------------------------------------------------------------+
void DrawSARDot(int barIndex, double price)
  {
   if(price <= 0)
      return;
   datetime time = iTime(_Symbol, PERIOD_CURRENT, barIndex);
   if(time == 0)
      return;
   string objName = "DynSAR_" + IntegerToString((int)time);
   if(ObjectFind(0, objName) < 0)
     {
      if(!ObjectCreate(0, objName, OBJ_ARROW, 0, time, price))
        {
         PrintFormat("ObjectCreate failed for %s, error=%d", objName, GetLastError());
         return;
        }

      if(!ObjectSetInteger(0, objName, OBJPROP_ARROWCODE, 159))
         PrintFormat("Object ARROWCODE failed for %s, error=%d", objName, GetLastError());
      if(!ObjectSetInteger(0, objName, OBJPROP_COLOR, clrDarkOrange))
         PrintFormat("Object COLOR failed for %s, error=%d", objName, GetLastError());
      if(!ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1))
         PrintFormat("Object WIDTH failed for %s, error=%d", objName, GetLastError());
      if(!ObjectSetInteger(0, objName, OBJPROP_BACK, true))
         PrintFormat("Object BACK failed for %s, error=%d", objName, GetLastError());
      if(!ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true))
         PrintFormat("Object HIDDEN failed for %s, error=%d", objName, GetLastError());
     }
   else
     {
      if(!ObjectMove(0, objName, 0, time, price))
         PrintFormat("ObjectMove failed for %s, error=%d", objName, GetLastError());
     }
  }

Each dot is identified solely by its bar timestamp. The first pass creates it; later passes move the same object. This means changing the adaptive setting changes the position of the existing SAR line rather than producing a second "standard" or "adaptive" line if we had used indicator handles. The EA is now comparing two behaviors of the same implementation.

Divergence arrows use the same one-object-per-bar pattern:

//+------------------------------------------------------------------+
//| Draw Divergence Arrow                                            |
//+------------------------------------------------------------------+
void DrawDivergenceArrow(int barIndex, int divType)
  {
   datetime time = iTime(_Symbol, PERIOD_CURRENT, barIndex);
   if(time == 0)
      return;
   string objName = "DivArrow_" + IntegerToString((int)time);
   if(ObjectFind(0, objName) >= 0)
      return; // already exists

   double price;
   int arrowCode;
   color clr;
   if(divType == 1) // Bullish divergence
     {
      price = openPrices[barIndex] - 50 * point;
      arrowCode = 233; // up arrow
      clr = clrDodgerBlue;
     }
   else // Bearish divergence
     {
      price = openPrices[barIndex] + 50 * point;
      arrowCode = 234; // down arrow
      clr = clrCrimson;
     }

   if(!ObjectCreate(0, objName, OBJ_ARROW, 0, time, price))
     {
      PrintFormat("ObjectCreate failed for %s, error=%d", objName, GetLastError());
      return;
     }

   if(!ObjectSetInteger(0, objName, OBJPROP_ARROWCODE, arrowCode))
      PrintFormat("Object ARROWCODE failed for %s, error=%d", objName, GetLastError());
   if(!ObjectSetInteger(0, objName, OBJPROP_COLOR, clr))
      PrintFormat("Object COLOR failed for %s, error=%d", objName, GetLastError());
   if(!ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1))
      PrintFormat("Object WIDTH failed for %s, error=%d", objName, GetLastError());
   if(!ObjectSetInteger(0, objName, OBJPROP_BACK, false))
      PrintFormat("Object BACK failed for %s, error=%d", objName, GetLastError());
   if(!ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true))
      PrintFormat("Object HIDDEN failed for %s, error=%d", objName, GetLastError());
  }

Unlike the SAR dots, a divergence marker is created the first time a divergence is confirmed on that bar. This keeps the chart readable while preserving the location of each signal.

Signal Detection and Trade Execution

The trading layer gives the demonstration something measurable to execute rather than introducing another strategy design problem. CheckAndExecuteTrade() treats a close crossing the SAR line as an entry signal:

//+------------------------------------------------------------------+
//| Check for a crossover signal and execute trade                   |
//+------------------------------------------------------------------+
void CheckAndExecuteTrade()
  {
   if(ArraySize(g_SARCurrent) < 3)
      return;

   double sar1 = g_SARCurrent[1];
   double sar2 = g_SARCurrent[2];
   if(sar1 <= 0 || sar2 <= 0)
      return;

//--- Crossover detection (close vs SAR)
   bool buySignal  = (closePrices[2] <= sar2 && closePrices[1] > sar1);
   bool sellSignal = (closePrices[2] >= sar2 && closePrices[1] < sar1);
   if(!buySignal && !sellSignal)
      return;

//--- Check if we already have an open position
   if(PositionsTotal() > 0)
     {
      for(int i = PositionsTotal() - 1; i >= 0; i--)
        {
         ulong ticket = PositionGetTicket(i);
         if(ticket == 0)
           {
            PrintFormat("PositionGetTicket failed at index=%d, error=%d", i, GetLastError());
            return; // state unknown; do not risk duplicate trade
           }

         if(!PositionSelectByTicket(ticket))
           {
            PrintFormat("PositionSelectByTicket failed, ticket=%I64u, error=%d",
                        ticket, GetLastError());
            return;
           }

         if(PositionGetString(POSITION_SYMBOL) == _Symbol &&
            PositionGetInteger(POSITION_MAGIC) == MagicNumber)
            return; // already have our position
        }
     }
//--- Execute trade
   int direction = buySignal ? 1 : -1;
   ExecuteTrade(direction);
  }

Both buy and sell conditions use the same g_SARCurrent[] series that is displayed on the chart. This ensures the trading decision always reflects the active adaptive calculation. Before opening a position, the EA checks for an existing position matching both the current symbol and MagicNumber.

The execution function then uses the SAR value as the stop reference, calculates take-profit from a fixed risk-reward ratio, and supports either fixed-lot or risk-based position sizing:

//+------------------------------------------------------------------+
//| Execute trade with SL based on current SAR and TP based on RRR   |
//+------------------------------------------------------------------+
void ExecuteTrade(int direction)
  {
   double price = 0.0;
   if(direction == 1)
     {
      if(!SymbolInfoDouble(_Symbol, SYMBOL_ASK, price) || price <= 0.0)
        {
         PrintFormat("Failed to get the symbol ASK price, error=%d", GetLastError());
         return;
        }
     }
   else
     {
      if(!SymbolInfoDouble(_Symbol, SYMBOL_BID, price) || price <= 0.0)
        {
         PrintFormat("Failed to get the symbol BID price, error=%d", GetLastError());
         return;
        }
     }
   double sar = g_SARCurrent[1]; // SAR at bar 1 (entry bar)
   if(sar <= 0)
      return;

   double stopDist = MathAbs(price - sar);
   long digitsValue = 0;
   if(!SymbolInfoInteger(_Symbol, SYMBOL_DIGITS, digitsValue))
     {
      PrintFormat("Failed to get symbol digits, error=%d", GetLastError());
      return;
     }
   int digits = (int)digitsValue;

   long stopLevelValue = 0;
   if(!SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL, stopLevelValue))
     {
      PrintFormat("Failed to get SYMBOL_TRADE_STOPS_LEVEL, error=%d", GetLastError());
      return;
     }
   int stopLevel = (int)stopLevelValue;

   if(point <= 0)
      return;
   double sl = NormalizeDouble(sar, digits);
   double minDist = stopLevel * point;
   if(stopDist < minDist)
     {
      stopDist = minDist * 1.5;
      // Recalculate SL based on new distance
      if(direction == 1)
         sl = NormalizeDouble(price - stopDist, digits);
      else
         sl = NormalizeDouble(price + stopDist, digits);
     }

   double tp = (direction == 1) ? price + stopDist * TPRatio : price - stopDist * TPRatio;
   tp = NormalizeDouble(tp, digits);

//--- Lot size: either fixed or risk-based
   double lotSize = FixedLotSize;
   if(lotSize <= 0 && RiskPercent > 0)
     {
      ResetLastError();
      double balance = AccountInfoDouble(ACCOUNT_BALANCE);
      if(GetLastError() != 0)
        {
         PrintFormat("Failed to get ACCOUNT_BALANCE, error=%d", GetLastError());
         return;
        }

      if(balance <= 0.0)
        {
         Print("Account balance is zero or negative");
         return;
        }

      double tickSize = 0.0, tickValue = 0.0;
      double minLot = 0.0, maxLot = 0.0, stepLot = 0.0;
      if(!SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE, tickSize) ||
         !SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE, tickValue) ||
         !SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN, minLot) ||
         !SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX, maxLot) ||
         !SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP, stepLot))
        {
         PrintFormat("Failed to read symbol trade properties, error=%d", GetLastError());
         return;
        }

      double riskAmount = balance * RiskPercent / 100.0;
      if(tickValue <= 0 || tickSize <= 0)
         return;
      // Risk per lot in account currency
      double moneyPerPriceUnit = tickValue / tickSize;
      double monetaryRiskPerLot = stopDist * moneyPerPriceUnit;
      if(monetaryRiskPerLot <= 0)
         return;
      lotSize = riskAmount / monetaryRiskPerLot;
      if(stepLot <= 0)
         return;
      lotSize = MathMax(minLot, MathMin(lotSize, maxLot));
      lotSize = NormalizeDouble(lotSize / stepLot, 0) * stepLot;
     }
   if(lotSize <= 0)
      return;

//--- Open trade
   bool success = false;
   if(direction == 1)
      success = trade.Buy(lotSize, _Symbol, 0, sl, tp, "Adaptive PSAR Buy");
   else
      success = trade.Sell(lotSize, _Symbol, 0, sl, tp, "Adaptive PSAR Sell");

   if(success)
      Print("Trade opened: ", (direction==1?"Buy":"Sell"), " lot=", lotSize, " SL=", sl, " TP=", tp);
   else
      PrintFormat("Trade failed: retcode=%u, description=%s, error=%d",
                  trade.ResultRetcode(),
                  trade.ResultRetcodeDescription(),
                  GetLastError());
  }

This concludes building the demonstration EA.

The demonstration therefore has a structure: the EA owns the market data, SAR calculation, adaptation policy, visualization, and trade execution; DivergenceEngine.mqh owns divergence detection and state. The integration between them is limited to initialization, buffer updates, per-bar calculation, and a small public query interface. That boundary is the main result this example is intended to demonstrate.


Results

The EA is tested in two stages: first, a live split-chart comparison to confirm the mechanism visibly changes behavior on a running chart, then a Strategy Tester comparison to see whether that behavior changes trading outcomes over a stretch of history. Both stages change the adaptive settings and their related inputs and hold everything else constant.

Split-Chart Comparison

The demo EA was attached to two charts of the same symbol and timeframe, side by side, both left on default settings to start.

Split-Chart _default

Chart 1 is on the left, and Chart 2 is on the right.

With the adaptive settings and everything else left at its default on both charts, the two internally calculated SAR lines are identical. This shows that the calculation is repeatable: the same inputs over the same bars produce the same dots, so any difference after Chart 1's inputs change comes from the adaptive settings alone. It does not show that the manual line matches the built-in Parabolic SAR. The EA's SAR loop starts from the oldest bar of its own window rather than from the start of the chart's history, so its dots are not guaranteed to match the built-in indicator's.

Chart 1's inputs were then changed: EnableAdaptive was set to true, with adaptive mode set to a slower acceleration at an adaptive factor of 0.7.

Split-Chart_settings

Chart 2 was left untouched, acting as the constant reference for the rest of this comparison. Here is the result:

Split-Chart_result

The difference to look for after the divergence arrows is that Chart 1's dots trail further from price, which matches the adaptive mode used, than the corresponding dots on Chart 2.

Equity Comparison

The second stage uses the Strategy Tester to see whether the divergence-based adaptation changes trading outcomes and not just appearance. Both runs use the same symbol, timeframe, small testing period (August 1st to August 15th), and deposit, along with identical trade management inputs; only the Adaptive group will differ between them. First, we check the equity of default values.

Equity_default

Next, we enable the adaptive settings, with adaptive mode set to a faster acceleration at an adaptive factor of 0.7.

Equity_settings

With the rest of the input and settings left untouched, here is the new equity:

Equity_adaptive-result

The equity result for this article is intended to illustrate behavior rather than demonstrate a statistical edge.

Changing the adaptive settings produced different results on the Strategy Tester output during the test period. The same engine that adapted SuperTrend's ATR multiplier in Part II of the series is doing the same thing on a different indicator.


Possibilities

The DivergenceEngine.mqh header and integration pattern aren't limited to the Parabolic SAR dots. A Bollinger Band width, a MACD histogram threshold, a moving average period, an ADX filter, or a grid EA's spacing could all be wired to the same divergence state, since they all have adaptable parameters; nothing is limited to a single EA or indicator.

Some indicators and EAs can also serve as tools for other EAs or indicators. This demonstration used Adaptive Parabolic SAR to decide entries, but Parabolic SAR, in most systems, is a trailing stop tool, not an entry trigger. The same adaptive acceleration factor could also be used as a tool to tighten or loosen a trailing stop on an already-open position from any other Expert Advisor.


Conclusion

This article addressed a specific limitation: the divergence logic built across Parts I–III worked but remained tied to SuperTrend. Extracting it into DivergenceEngine.mqh turned that logic into a reusable engine that any indicator or EA can integrate through a consistent set of setup, update, calculation, query, and teardown calls.

The Parabolic SAR demonstrated the separation in practice. The engine required no changes for a different indicator; it continued to provide the divergence state and its age, with the smoothed oscillator available through the same access layer, while the EA decided how to interpret and apply that information. The acceleration factor also showed that an adaptive response can work in either direction, depending on the consumer's design.

The source files used throughout this article are attached below.

Filename Description
DivergenceEngine.mqh A reusable MQL5 header that encapsulates oscillator calculation, pivot detection, divergence classification, and divergence state management for use by indicators and Expert Advisors.
Demo_Adaptive_PSAR.mq5 A lightweight demonstration EA that integrates DivergenceEngine.mqh with a manually calculated Parabolic SAR, adapting its acceleration factor when an opposing divergence is detected. 
Attached files |
Neural Networks in Trading: Robust Trading Signals in Any Market Regime (Attention Modules) Neural Networks in Trading: Robust Trading Signals in Any Market Regime (Attention Modules)
In this article, we continue implementing the ST-Expert framework approaches, focusing on the practical aspects of applying them using MQL5. Earlier, we examined the theoretical foundations and key components of the model; now we move on to working directly with graph attention algorithms and local and global attention distribution. The main goal of this work is to demonstrate how ST-Expert's conceptual ideas are transformed into workable solutions for analyzing and forecasting financial time series.
State Persistence in MQL5 (Part 1): A Crash-Safe State Store That Survives a Restart State Persistence in MQL5 (Part 1): A Crash-Safe State Store That Survives a Restart
The series develops state persistence for MQL5 Expert Advisors. Part 1 delivers a crash-safe key-value store: a CStateStore class that saves through a temporary file and a rename, carries a versioned header with a checksum, and stores integers, doubles, strings, booleans, and double arrays, plus a demo advisor that resumes a counter and a rolling window after a restart. Readers get a compact include file and a pattern that protects the live state file if the process dies mid-save.
Win Rate and Edge Ratio Heatmap by Hour and Symbol in MQL5 Win Rate and Edge Ratio Heatmap by Hour and Symbol in MQL5
This MQL5 dashboard aggregates closed deals by symbol and UTC hour, calculates win rate and average reward‑to‑risk, and displays the results as two color‑coded heatmaps. A companion table in the Experts tab highlights the strongest and weakest symbol/hour cells, with an option to require a minimum trade count to reduce noise.
Symbolic Fourier Approximation in MQL5: Benchmarking SFA Against SAX Symbolic Fourier Approximation in MQL5: Benchmarking SFA Against SAX
We implement Symbolic Fourier Approximation in MQL5 and compare it to SAX under a shared harness on identical price windows. SFA keeps low‑frequency Fourier coefficients and learns per‑position bins (MCB), with a proven, sound lower bound. The measurements show how the same bit budget behaves under different splits of word length and alphabet, and give a practical rule for choosing settings for your symbol.