Building a Divergence System (Part IV): Creating a Reusable Divergence Engine for MQL5
Table of Contents
- Introduction
- Why Refactor?
- Designing the Header
- Phase 1: Foundation
- Phase 2: The Oscillator Module
- Phase 3: The Divergence Module
- Phase 4: Access Functions
- Integration Checklist
- Choosing and Adapting Parabolic SAR
- Building the Demo EA
- Results
- 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:

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:

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.

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.

Chart 2 was left untouched, acting as the constant reference for the rest of this comparison. Here is the 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.

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

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

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. |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Neural Networks in Trading: Robust Trading Signals in Any Market Regime (Attention Modules)
State Persistence in MQL5 (Part 1): A Crash-Safe State Store That Survives a Restart
Win Rate and Edge Ratio Heatmap by Hour and Symbol in MQL5
Symbolic Fourier Approximation in MQL5: Benchmarking SFA Against SAX
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use