preview
Building a Divergence System: Creating the MPO4 Custom Indicator

Building a Divergence System: Creating the MPO4 Custom Indicator

MetaTrader 5Trading |
112 0
Solomon Anietie Sunday
Solomon Anietie Sunday

Table of Contents

  1. Introduction
  2. Introducing the MPO4: Quantifying Market Effort
  3. Mathematical Construction of MPO4
  4. Normalization and EMA Smoothing
  5. Creating the Indicator Skeleton
  6. Implementing the Raw MPO4 Calculation
  7. Understanding Divergence Theory and Building the Pivot Detection Engine
  8. State Management and Non-repainting Design
  9. Practical Interpretation of the MPO4 Oscillator
  10. Limitations and Design Trade-offs
  11. Conclusion

INTRODUCTION

Most traders are familiar with oscillators like the Relative Strength Index (RSI) or the Stochastic. They answer the question: Where is the price compared to its recent range? They are effective but can be reactive, often lagging at the exact moment you need them most. Furthermore, they treat all price bars as equal, whether it is a tiny doji or a massive volatile candle. This is where the MPO4 diverges, not just in its signals, but in its foundational philosophy.

Introducing the MPO4: Quantifying Market Effort

Understanding the Original MPO4 Concept

It is important to understand what makes the MPO4 fundamentally different from most traditional oscillators. Many popular oscillators are built around the idea of measuring price movement. The Relative Strength Index (RSI), for example, compares recent gains against recent losses. Stochastic oscillators measure where price is located relative to its recent trading range. While these approaches have proven useful over the years, they share a common characteristic: they primarily focus on the final location of price. The MPO4 approaches momentum from a different perspective.

Instead of asking where the price closed, the MPO4 attempts to measure how much effort was required to move the price. The indicator examines the body of each candlestick and evaluates both its size and direction. This creates a representation of market pressure rather than simply market position.

To understand this concept, consider two bullish candles. Both candles may close higher than they opened, but they do not necessarily carry the same information. A small bullish candle with a narrow body may indicate only modest buying pressure. A large bullish candle with a body several times larger than average may indicate significantly stronger participation from buyers.

Traditional oscillators often treat these two candles similarly because both contribute to upward price movement. MPO4, however, recognizes that the larger candle represents a higher amount of directional effort and therefore assigns it a larger influence on the final oscillator value.

This idea forms the foundation of the entire indicator.

Measuring Directional Pressure

At its core, MPO4 evaluates three characteristics of every candle:

  • the size of the candle body,
  • the direction of the candle, and
  • the significance of that body size relative to recent market activity.

The body size is calculated as the absolute difference between the opening and closing prices. Larger bodies represent stronger directional conviction, while smaller bodies represent weaker conviction.

The candle direction determines whether the contribution is bullish or bearish. Bullish candles contribute positive pressure, bearish candles contribute negative pressure, and neutral candles contribute little or no directional information. However, body size alone is not sufficient. Market volatility constantly changes. A candle that appears large during a quiet session may be completely ordinary during a highly volatile market environment.

To address this, MPO4 compares each candle's body size against the average body size observed during a recent lookback period. This process transforms raw candle size into a relative measure of market effort.

As a result, the indicator does not simply identify large candles. Instead, it identifies candles that are unusually large or unusually small compared to current market conditions.

Market Pressure Logic

Once the directional contribution of each candle has been evaluated, the indicator aggregates these contributions across a rolling lookback window. This aggregation process allows MPO4 to answer if: "Has bullish or bearish pressure been dominating the market recently?"

If bullish candles consistently outweigh bearish candles, the oscillator rises. If bearish candles dominate, the oscillator falls. When buying and selling pressure remains relatively balanced, the oscillator gravitates toward its center. The resulting value represents a measure of directional pressure rather than a simple measure of price movement.

This distinction is subtle but important. Two markets may exhibit similar price changes while producing very different MPO4 readings. A gradual advance consisting of many small candles may generate less pressure than a shorter move driven by a series of exceptionally large bullish candles.

Why This Approach Matters

The primary objective of MPO4 is to make momentum more sensitive to the quality of price movement rather than simply its direction. By incorporating candle body size into the calculation, the indicator attempts to distinguish between weak participation and strong participation. Large, decisive candles exert greater influence on the oscillator, while smaller and less significant candles contribute less.

This allows MPO4 to respond not only to where price is moving but also to how aggressively market participants are driving that movement.

The divergence engine built into the indicator later builds upon this foundation. Because the oscillator is designed to measure directional pressure, divergences can reveal situations where price continues to push to new highs or lows while the underlying buying or selling pressure begins to weaken.

Before implementing the mathematical calculations, it is therefore helpful to think of MPO4 not as another variation of RSI or Stochastic, but as a pressure-based oscillator whose primary goal is to quantify the strength of bullish and bearish participation within the market.

Mathematical Construction of MPO4

Now that we understand the philosophy behind MPO4, we can examine how the indicator transforms candlestick information into a measurable oscillator.

Unlike many oscillators that begin with price changes, MPO4 begins with candle bodies. Every calculation in the indicator originates from the relationship between a candle's body size and the average body size observed over a recent lookback period.

The entire process can be broken into four stages:

  1. Measure candle body size.
  2. Determine candle direction.
  3. Weight each candle relative to the average body size.
  4. Aggregate the weighted contributions into a normalized oscillator value.

    The indicator performs these calculations continuously for every new bar, producing a rolling measure of directional market pressure.

    Stage 1: Measuring Candle Body Size

    The first step is calculating the size of each candle body. A candle body represents the distance between the opening and closing prices.

    For any candle: Body Size = |Close − Open|

    A bullish candle and a bearish candle may have identical body sizes even though their directions differ. At this stage, MPO4 is interested only in the magnitude of movement.

    The illustration below shows an image of a candle size, which is the difference between the close and open of a candle regardless of direction.

    Candle Size Illustration

    In MQL5 code, body size is calculated as:

    double body = MathAbs(close[i] - open[i]);

    Stage 2: Determine Direction

    Once the body size is known, MPO4 determines whether the candle contributes bullish pressure or bearish pressure.

    The logic is straightforward:

    int direction = (close[i] > open[i]) ? 1 : (close[i] < open[i]) ? -1 : 0;

    This creates three possible states:

    Candle Type Direction Value
    Bullish +1 
    Bearish -1
    Doji/Neutral 0

    The direction acts as a sign multiplier that determines whether a candle contributes positive or negative pressure to the oscillator.

    Stage 3: Relative Body Weighting

    This is the feature that makes MPO4 fundamentally different from many traditional oscillators. A large candle should have more influence than a small candle. However, "large" is always relative to current market conditions.

    To determine whether a candle is significant, MPO4 first calculates the average body size over the selected lookback period.

    For a six-bar lookback:

    Bodies:
    4, 5, 6, 5, 4, 6
    
    Average Body:
    (4 + 5 + 6 + 5 + 4 + 6) / 6
    = 5

    The current candle is then compared against this average.

    Weight = Body Size / Average Body Size

    Suppose the current candle has a body size of 10.

    Weight = 10 / 5 = 2

    This means the candle contributes twice the normal pressure.

    If the candle body is only 2.5:

    Weight = 2.5 / 5 = 0.5

    This means the candle contributes only half the normal pressure. The weighting process allows MPO4 to adapt automatically to changing volatility conditions. Relative contribution based on body size compared to the average body size of 5 can be visualized below:

    Body Weight
    2.5 0.5
    5 1
    10 2
    15 3 (and so on).

    The weight is calculated as:

    double weight = body / avgBody;

    This is one of the most important calculations in the entire indicator because it transforms raw candle size into a relative measure of market effort.

    Stage 4: Build the Pressure Score

    Once both direction and weight have been calculated, MPO4 combines them into a single contribution value.

    Contribution = Direction × Weight

    Consider the following examples:

    Candle Direction Weight Contribution
    Large candle +1 2.0 +2.0
    Average candle +1 1.0 +1.0
    Large bearish -1 2.0 -2.0
    Small bearish -1 0.5 -0.5

    The indicator then sums all contributions inside the lookback window.

    rawSum += direction * weight;

    • A sequence dominated by strong bullish candles will produce a positive score.
    • A sequence dominated by strong bearish candles will produce a negative score.
    • Balanced conditions will cause positive and negative contributions to offset each other, keeping the oscillator near its midpoint.

    At this stage, we have successfully transformed individual candlesticks into a single measure of directional market pressure. However, the resulting values are still unbounded and difficult to interpret consistently across different market conditions. The next step is to normalize and smooth these pressure values into the final MPO4 oscillator.

    Normalization and EMA Smoothing

    At the end of the previous section, we successfully transformed a collection of candlesticks into a directional pressure score. The problem is that the raw pressure value is not particularly useful on its own. A lookback period of six candles will naturally produce different raw values than a lookback period of twelve candles. Likewise, highly volatile markets may generate much larger pressure readings than quieter markets.

    To make the oscillator interpretable across different market conditions, MPO4 performs two additional operations:

    1. Normalization
    2. Exponential smoothing

    These final steps convert the raw pressure score into a stable oscillator that can be analyzed visually and used for divergence detection.

    Why Normalization Is Necessary

    Consider a six-bar lookback period. Suppose every candle in the lookback window is strongly bullish and receives a weight of 2.

    The raw pressure score becomes:

    Raw Sum = 2 + 2 + 2 + 2 + 2 + 2
    Raw Sum = 12

    Now imagine a different market environment where every candle receives a weight of 1.

    Raw Sum = 1 + 1 + 1 + 1 + 1 + 1
    Raw Sum = 6

    Both situations indicate sustained bullish pressure, yet the numerical values differ significantly. Without normalization, interpreting oscillator readings becomes difficult because the scale constantly changes.

    To solve this problem, MPO4 converts the raw pressure score into a bounded range.

    Theoretical Maximum Pressure

    The indicator first estimates the largest possible directional score that could occur within the selected lookback window. If every candle contributes the maximum allowable weight: Maximum Possible Sum = Lookback × Maximum Weight

    For example,

    Lookback = 6.

    Maximum Weight = 3.

    Maximum Possible Sum = 18.

    This theoretical maximum becomes the reference point for scaling the oscillator.

    Scaling the Pressure Score

    The normalized value is obtained by dividing the raw pressure score by the theoretical maximum pressure.

    Conceptually:

    Normalized= \frac{RawSum}{MaximumPossible}

    The result is then scaled into the oscillator range. In our implementation:

    double maxPossible = MPOLength * 3.0;
    double normalized  = (rawSum / maxPossible) * 30.0;

    This converts the raw pressure score into a bounded oscillator whose values fluctuate between approximately -100 and +100.

    To ensure numerical stability, the indicator also applies a final safety clamp:

    normalized = MathMax(-30.0, MathMin(30.0, normalized));

    This guarantees that unexpected market conditions cannot push the oscillator beyond its intended boundaries. The relationship is linear. As buying pressure increases, the oscillator rises toward +30. As selling pressure increases, the oscillator falls toward -30.

    At this point, we have successfully converted directional market pressure into a bounded oscillator. However, there is still one problem.

    The oscillator is noisy. Large candles can cause abrupt jumps in the pressure score, producing sharp fluctuations from one bar to the next. This is where smoothing becomes important.

    Applying Exponential Smoothing

    Instead of displaying the raw normalized value directly, we will apply an Exponential Moving Average (EMA).

    Unlike a Simple Moving Average (SMA), an EMA places greater emphasis on the most recent observation while still retaining information from previous values.

    The smoothing coefficient is calculated as:

    EMA Exponential Formula

    In code:

    SmoothAlpha = 2.0 / (SmoothPeriod + 1.0);

    For a smoothing period of seven:

    Alpha = 2 / (7 + 1)

    Alpha = 0.25

    This means the newest observation contributes 25% of the final value, while the remaining 75% comes from previous oscillator history.

    EMA Recursion

    Once the smoothing coefficient has been calculated, each new oscillator value is blended with the previous value.

    The recursive EMA equation becomes:

    EMA Recursive Formula

    The implementation follows this formula directly:

    SmoothedBuffer[i] = RawBuffer[i] * SmoothAlpha + SmoothedBuffer[i - 1] * (1.0 - SmoothAlpha);

    This calculation is repeated for every bar, producing a smoother oscillator that reacts to changes in pressure without becoming excessively erratic.

    The Final MPO4 Oscillator

    After smoothing, the indicator finally produces the value displayed on the chart. This final oscillator becomes the foundation for every feature that follows, including trend assessment, signal generation, and the divergence detection engine that we will implement later in the article.

    Now that the mathematical engine is fully understood, we can begin constructing the indicator itself and translating these calculations into a structured MQL5 implementation.

    Creating the Indicator Skeleton

    To build a high—performance MQL5 custom indicator that measures market effort and directional pressure, we start with a solid code skeleton. In MQL5, an indicator's performance depends on correct initialization: preprocessor directives, properties, inputs, and buffers. A technical indicator demands a clean segregation of components to optimize computational overhead across live chart updates and rapid historical testing cycles.

    Let us inspect the concrete architectural layout of our structural workspace, focusing entirely on setting up compilation parameters, establishing layout directives, defining strategic enumerations, and allocating data streams safely.

    Architectural Breakdown: Preprocessor Directives and Memory Management

    The preprocessor section dictates how the client terminal interacts with our program thread. We will use:

    #property indicator_separate_window

    To display MPO4 as a bounded oscillator, it should be placed in a separate window below the main chart. We will configure indicator buffers and plots to connect calculations to terminal rendering.

    1. Indicator_buffers 9: This directive instructs the subsystem memory allocator to manage exactly nine sequential data blocks. While only three arrays are explicitly drawn on the chart interface, the remaining six calculations act as hidden operational engines. These auxiliary tracks prevent structural repainting by managing raw calculation states, historical price metrics, and oscillator peak tracking.

    2. Indicator_plots 3: This setting ensures that only our designated visual indicators—the primary smoothed pressure line, the bullish divergence arrows, and the bearish divergence arrows—are mapped to the visual memory cache.

    Separating calculation buffers from plot buffers improve readability and reduces overhead on live ticks. Code below:

    //+------------------------------------------------------------------+
    //|                                       MPO4_Oscillator System.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"
    #property indicator_separate_window
    
    // We need 9 buffers total: 3 for plotting, 6 for state tracking/calculations
    #property indicator_buffers 9
    #property indicator_plots   3
    
    //--- Plot 1: The Oscillator Line
    #property indicator_label1  "Oscillator"
    #property indicator_type1   DRAW_LINE
    #property indicator_color1  clrDodgerBlue
    #property indicator_style1  STYLE_SOLID
    #property indicator_width1  2
    
    //--- Plot 2: Bullish Divergence Arrow
    #property indicator_label2  "Bull Div"
    #property indicator_type2   DRAW_ARROW
    #property indicator_color2  clrLimeGreen
    #property indicator_width2  2
    
    //--- Plot 3: Bearish Divergence Arrow
    #property indicator_label3  "Bear Div"
    #property indicator_type3   DRAW_ARROW
    #property indicator_color3  clrRed
    #property indicator_width3  2

    Configuring Global Inputs and Enumeration Options

    To make this engine flexible for varied deployment conditions, we expose modular inputs that segregate selection domains, tuning variables, and algorithmic calculation limits into readable visual blocks within the user interface.

    //--- Enumerations
    enum ENUM_DIV_SOURCE
    {
        DIV_SRC_MPO = 0,    // Custom MPO4 Modal Engine Oscillator
        DIV_SRC_RSI = 1     // Standard Relative Strength Index (RSI)
    };
    
    //--- Input Parameters
    input group "=== Oscillator Selection ==="
    input ENUM_DIV_SOURCE InpSource = DIV_SRC_MPO; // Oscillator Source
    
    input group "=== MPO4 Settings ==="
    input int InpMpoLen = 6;                       // MPO4 Lookback Period
    input int InpSmooth = 7;                       // Oscillator Smoothing (EMA)
    input double InpMpoOB = 30.0;                  // MPO4 Overbought Level
    input double InpMpoOS = -30.0;                 // MPO4 Oversold Level
    
    input group "=== RSI Settings ==="
    input int InpRsiPeriod = 14;                   // RSI Period
    input double InpRsiOB = 70.0;                  // RSI Overbought Level
    input double InpRsiOS = 30.0;                  // RSI Oversold Level
    
    input group "=== Divergence ==="
    input int InpPivotLen = 2;                     // Pivot Lookback Length for Divergence Confirmation

    Explaining Input Parameters

    Dividing parameters into explicit input group segments improves user configurations on live charts. The variables are configured with specific technical purposes; below are the main inputs to take note of:

    • ENUM_DIV_SOURCE: This enumeration enables users to switch the indicator engine between evaluating raw momentum shifts via the custom MPO4 algorithm or standard Relative Strength Index (RSI). It is also used to cross-check discoveries between the RSI and MPO4 frameworks.
    • InpMpoLen: This parameter governs the size of the rolling window used to establish current market volatility. A value of 6 tells the algorithm to evaluate the scale of contemporary market efforts against a rolling average of six candles.
    • InpSmooth: This control adjusts the sensitivity of the final oscillator line. It uses the Exponential Moving Average (EMA) to filter out localized market noise.
    • InpPivotLen: This value acts as the filter for our divergence logic. Setting it to 2 requires a localized high or low to be flanked by two candles with lower highs or higher lows on both sides, ensuring the engine relies on unalterable market turning points.

    Global Arrays, Memory Structure, and Variable Tracking

    With our data properties established, we declare the dynamic arrays that handle our visual output, internal calculation caching, and non-repainting state verification.

    //--- Visual Indicator Buffers
    double OscBuffer[];          // Smoothed Final Line
    double BullDivBuffer[];      // Plotted Arrows
    double BearDivBuffer[];      // Plotted Arrows
    
    // Hidden Calculation Buffers
    double RawBuffer[];          // Pre-smoothing data
    double RsiBuffer[];          // Holds raw iRSI data (if selected)
    
    // State Management Buffers (Prevents repainting on live ticks)
    double LastPivLowPriceBuffer[];
    double LastPivLowOscBuffer[];
    double LastPivHighPriceBuffer[];
    double LastPivHighOscBuffer[];
    
    //--- Handles and variables
    int RsiHandle = INVALID_HANDLE;
    double SmoothAlpha;

    Understanding the operational separation of these dynamic buffer structures:

    1. Plotting Buffers (OscBuffer, BullDivBuffer, BearDivBuffer): These arrays link directly to chart rendering layers. Values modified inside these indices are plotted directly to the terminal interface using standard lines or character symbols.
    2. Calculation Workspace Buffers (RawBuffer, RsiBuffer): This buffer will be marked as calculation-only; these arrays store temporary transformations, such as raw, un-normalized market pressure scores, without wasting rendering resources.
    3. State Management Arrays (LastPivLowPriceBuffer, LastPivLowOscBuffer, LastPivHighPriceBuffer, LastPivHighOscBuffer): These buffers store historical turning-point metrics across trading ticks. They track old peak parameters chronologically to prevent historical calculation values from updating retroactively when live chart ticks update.

    This layout provides a blueprint for an MQL5 indicator. We have established our preprocessor directives, input clusters, and data tracking matrices.

    Preparing the Initialization Function

    With the buffers declared, the final step is configuring them inside the OnInit() function. Each buffer is mapped using their appropriate functions:

    //+------------------------------------------------------------------+
    //| Custom indicator initialization function                         |
    //+------------------------------------------------------------------+
    int OnInit()
      {
    //--- Map buffers
       SetIndexBuffer(0, OscBuffer, INDICATOR_DATA);
       SetIndexBuffer(1, BullDivBuffer, INDICATOR_DATA);
       SetIndexBuffer(2, BearDivBuffer, INDICATOR_DATA);
       SetIndexBuffer(3, RawBuffer, INDICATOR_CALCULATIONS);
       SetIndexBuffer(4, RsiBuffer, INDICATOR_CALCULATIONS);
       
       SetIndexBuffer(5, LastPivLowPriceBuffer, INDICATOR_CALCULATIONS);
       SetIndexBuffer(6, LastPivLowOscBuffer, INDICATOR_CALCULATIONS);
       SetIndexBuffer(7, LastPivHighPriceBuffer, INDICATOR_CALCULATIONS);
       SetIndexBuffer(8, LastPivHighOscBuffer, INDICATOR_CALCULATIONS);
    
    //--- Set Arrow Codes
       PlotIndexSetInteger(1, PLOT_ARROW, 233); // Up arrow
       PlotIndexSetInteger(2, PLOT_ARROW, 234); // Down arrow
    
    //--- Setup Dynamic OB/OS Levels Based on Source
       if(InpSource == DIV_SRC_MPO)
         {
          IndicatorSetDouble(INDICATOR_LEVELVALUE, 0, InpMpoOB);
          IndicatorSetDouble(INDICATOR_LEVELVALUE, 1, 0.0);
          IndicatorSetDouble(INDICATOR_LEVELVALUE, 2, InpMpoOS);
          IndicatorSetString(INDICATOR_SHORTNAME, "MPO4 Div Engine");
         }
       else
         {
          IndicatorSetDouble(INDICATOR_LEVELVALUE, 0, InpRsiOB);
          IndicatorSetDouble(INDICATOR_LEVELVALUE, 1, 50.0);
          IndicatorSetDouble(INDICATOR_LEVELVALUE, 2, InpRsiOS);
          IndicatorSetString(INDICATOR_SHORTNAME, "RSI Div Engine");
          
          // Initialize RSI handle
          RsiHandle = iRSI(_Symbol, _Period, InpRsiPeriod, PRICE_CLOSE);
          if(RsiHandle == INVALID_HANDLE)
            {
             Print("Failed to load RSI Handle");
             return(INIT_FAILED);
            }
         }
    
    //--- Set Level Styles
       IndicatorSetInteger(INDICATOR_LEVELCOLOR, 0, clrDarkGray);
       IndicatorSetInteger(INDICATOR_LEVELSTYLE, 0, STYLE_DOT);
    
    //--- Calculate Alpha for Smoothing
       SmoothAlpha = 2.0 / (InpSmooth + 1.0);
    
       return(INIT_SUCCEEDED);
      }
    

    The distinction between INDICATOR_DATA and INDICATOR_CALCULATIONS is important.

    • INDICATOR_DATA buffers are rendered visually on the chart.
    • INDICATOR_CALCULATIONS buffers remain internal and are used strictly for processing logic.

    Arrow symbols are assigned using Wingdings code to determine how bullish and bearish divergence markers appear visually on the indicator window. Dynamic overbought and oversold (OB/OS) levels are set up and named based on the enum indicator source, the standard RSI handle is initialized, and finally, the smoothing coefficient for EMA processing is precomputed. Precomputing the alpha value during initialization improves runtime efficiency because the calculation does not need to be repeated on every tick.

    At this point, the MPO4 indicator skeleton is fully prepared.

    The oscillator still contains no active calculations yet, but the complete infrastructure for plotting, smoothing, divergence detection, and state management is now in place. The next step is implementing the raw MPO4 pressure calculation itself.

    Implementing the Raw MPO4 Calculation

    With the indicator skeleton fully prepared and all buffers correctly mapped in OnInit(), we can now turn our attention to the core computational engine: the OnCalculate function. This is where the transformation from raw candlestick data into the final oscillator value actually occurs.

    The OnCalculate function in MQL5 is called on every tick and on each new bar. It receives the complete price history arrays, and its most important responsibility is to determine how many bars need to be recalculated. Understanding the prev_calculated parameter is essential for writing efficient indicators. When the function runs for the first time, prev_calculated equals zero, indicating that every bar must be processed. On subsequent calls, prev_calculated contains the number of bars that were already calculated during the previous call, allowing us to skip historical bars and process only the new data.

    We begin by establishing the minimum bar requirement and the calculation limit:

    //+------------------------------------------------------------------+
    //| Custom indicator iteration function                              |
    //+------------------------------------------------------------------+
    int OnCalculate(const int rates_total,
                    const int prev_calculated,
                    const datetime &time[],
                    const double &open[],
                    const double &high[],
                    const double &low[],
                    const double &close[],
                    const long &tick_volume[],
                    const long &volume[],
                    const int &spread[])
      {
    //--- Minimum bars check
       if(rates_total < MathMax(InpMpoLen, InpRsiPeriod) + InpPivotLen + 5)
          return 0;
    
       int limit = prev_calculated == 0 ? 0 : prev_calculated - 1;
    
    //--- Clear State Arrays on First Run
       if(prev_calculated == 0)
         {
          ArrayInitialize(BullDivBuffer, EMPTY_VALUE);
          ArrayInitialize(BearDivBuffer, EMPTY_VALUE);
          ArrayInitialize(LastPivLowPriceBuffer, DBL_MAX);
          ArrayInitialize(LastPivLowOscBuffer, -99999);
          ArrayInitialize(LastPivHighPriceBuffer, 0);
          ArrayInitialize(LastPivHighOscBuffer, 99999);
         }
    

    The minimum bar check ensures enough historical data exists before any calculation begins. The limit variable determines the starting index for the calculation loop. By setting it to prev_calculated - 1, we avoid reprocessing bars that have already been computed, significantly improving performance in live trading.

    Handling the RSI Source Option

    Our indicator provides a choice between the custom MPO4 oscillator and the standard RSI. When the user selects RSI as the source, we must copy the RSI values into our RawBuffer for further processing. This is handled before the main calculation loop:

       // Handle RSI buffering to match the loop direction
       if(InpSource == DIV_SRC_RSI)
         {
           int to_copy = rates_total - limit;
           double tempRsi[];
           if(CopyBuffer(RsiHandle, 0, 0, to_copy, tempRsi) <= 0) return 0;
           for(int i = 0; i < to_copy; i++)
           {
               RawBuffer[limit + i] = tempRsi[i];
           }
         }

    Notice the deliberate alignment of array indices. The CopyBuffer function copies RSI values starting from the most recent bar, and we map them directly into our RawBuffer at the corresponding positions. This design allows the divergence detection engine that follows to work identically regardless of whether the input data comes from MPO4 or RSI.

    The MPO4 Pressure Calculation

    When the user selects the custom MPO4 mode, we enter the core mathematical process described earlier in the article. The loop iterates from the limit index up to rates_total - 1, calculating the directional pressure score for each bar based on the preceding InpMpoLen candles:

       else // MPO4 Calculation
         {
          for(int i = limit; i < rates_total; i++)
            {
             double sumBodies = 0;
             int count = 0;
             
             for(int j = 0; j < InpMpoLen; j++)
               {
                int idx = i - j;
                if(idx < 0) break;
                sumBodies += MathAbs(close[idx] - open[idx]);
                count++;
               }
               
             double avgBody = (count > 0 && sumBodies > 0) ? (sumBodies / count) : SymbolInfoDouble(_Symbol, SYMBOL_POINT);
             double rollingSum = 0;
             
             for(int j = 0; j < InpMpoLen; j++)
               {
                int idx = i - j;
                if(idx < 0) break;
                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 = body / avgBody;
                rollingSum += (dir * weight);
               }
               
             RawBuffer[i] = (rollingSum / (InpMpoLen * 2.0)) * 100.0;
            }
         }

    Let us examine what happens inside this loop step by step.

    First pass: calculating the average body size. For each bar index i, the indicator looks back InpMpoLen bars (including the current bar) and sums the absolute body sizes of each candle. The average body size is then computed by dividing this sum by the number of valid bars found. A safety check ensures avgBody is never zero; if no valid bodies are found, the indicator falls back to the symbol's point value as a minimal reference.

    Second pass: computing weighted directional contributions. With the average body size established, the loop iterates again over the same lookback window. For each candle, three pieces of information are extracted:

    • body—the absolute size of the candle,
    • dir—directional sign: +1 for bullish, -1 for bearish, 0 for doji,
    • weight—the ratio of the candle body size to the average body candle.

    The contribution of each candle is dir * weight. Strong bullish candles produce large positive contributions; strong bearish candles produce large negative contributions. Weak candles contribute proportionally less.

    Final normalization. The sum of all contributions is divided by InpMpoLen * 2 and multiplied by 100. The denominator reflects the maximum possible directional sum under normal conditions, scaling the result into a range roughly between -100 and +100. However, this raw value is not yet suitable for direct display. It contains significant noise and may occasionally exceed practical boundaries.

    EMA Smoothing and State Propagation

    After the raw MPO4 value is stored in RawBuffer, the indicator applies exponential smoothing and prepares the state management buffers for divergence detection. This is done in a unified loop that follows immediately after the raw calculation:

    //--- Smoothing and Divergence Detection
       for(int i = limit; i < rates_total; i++)
         {
          // Apply EMA Smoothing
          if(i == 0) 
             OscBuffer[i] = RawBuffer[i];
          else 
             OscBuffer[i] = (RawBuffer[i] * SmoothAlpha) + (OscBuffer[i-1] * (1.0 - SmoothAlpha));
    
          // Inherit state from previous bar
          if(i > 0)
            {
             LastPivLowPriceBuffer[i] = LastPivLowPriceBuffer[i-1];
             LastPivLowOscBuffer[i]   = LastPivLowOscBuffer[i-1];
             LastPivHighPriceBuffer[i]= LastPivHighPriceBuffer[i-1];
             LastPivHighOscBuffer[i]  = LastPivHighOscBuffer[i-1];
             BullDivBuffer[i]         = EMPTY_VALUE;
             BearDivBuffer[i]         = EMPTY_VALUE;
            }

    The EMA smoothing uses the precomputed SmoothAlpha value from OnInit(). The first bar receives the raw value directly; every subsequent bar blends the current raw reading with the previous smoothed value. This produces the final OscBuffer that will be plotted as the solid line in the indicator window.

    State propagation is equally important. The four state buffers—LastPivLowPriceBuffer, LastPivLowOscBuffer, LastPivHighPriceBuffer, and LastPivHighOscBuffer—store the most recent pivot values for price and the oscillator. By copying these values forward from the previous bar, we ensure that divergence comparisons can be made at any index without scanning backward through history. This technique is what prevents repainting and keeps the indicator consistent across ticks.

    Finally, the arrow buffers are reset to EMPTY_VALUE at each bar, ensuring that divergence signals appear only on the specific bars where they are detected and do not linger incorrectly.

    This completes the raw MPO4 calculation and smoothing layer. The oscillator line is now ready for display, but the divergence detection engine—which is arguably the most valuable feature of the indicator—requires its own dedicated implementation. In the next section, we will explore the theory behind divergence detection and then build the pivot detection logic that forms its foundation.

    Understanding Divergence Theory and Building the Pivot Detection Engine

    With the smoothed oscillator line successfully plotted, we now turn to an important application for any momentum indicator: divergence detection. Divergence occurs when price makes a new extreme—higher high or lower low—but the oscillator fails to confirm that extreme, instead forming a shallower peak or trough. This non-confirmation often signals weakening momentum and a potential reversal.

    First, we must understand the two primary types of divergence our indicator will detect:

    1. Bullish Divergence: this appears when price makes a lower low, but the oscillator makes a higher low. This suggests that selling pressure is diminishing even though the price continues to fall. The market may be exhausting its downward momentum, and a reversal to the upside becomes increasingly probable.
    2. Bearish Divergence: appears when price makes a higher high, but the oscillator makes a lower high. This indicates that buying pressure is waning while price still pushes upward. The rally may be running out of steam, and a downward reversal could follow.

    The key to reliable divergence detection lies in identifying valid pivot points—meaningful highs and lows in both price and the oscillator. Without a robust pivot detection engine, an indicator will generate false signals on every minor fluctuation.

    The Pivot Detection Logic

    Our indicator uses a simple but effective method to identify local extremes. A bar is considered a pivot low if its oscillator value is lower than the values of the InpPivotLen bars immediately to its left and the InpPivotLen bars immediately to its right. Similarly, a pivot high occurs when the oscillator value is higher than the surrounding bars.

    The code implements this check using the pIdx variable, which represents the bar index being evaluated as a potential pivot. Notice that we evaluate pivots at i - InpPivotLen rather than at the current bar i. This delay ensures that we have enough future bars to confirm that the candidate is indeed a true local extreme:

    // Check for Pivots (Evaluated at i - InpPivotLen to ensure both sides form completely)
          int pIdx = i - InpPivotLen;
          if(pIdx >= InpPivotLen)
            {
             bool isPivLow = true;
             bool isPivHigh = true;
    
             // Verify the pivot high/low
             for(int k = 1; k <= InpPivotLen; k++)
               {
                if(OscBuffer[pIdx] > OscBuffer[pIdx - k] || OscBuffer[pIdx] > OscBuffer[pIdx + k]) isPivLow = false;
                if(OscBuffer[pIdx] < OscBuffer[pIdx - k] || OscBuffer[pIdx] < OscBuffer[pIdx + k]) isPivHigh = false;
               }
    

    We will walk through this logic carefully. Suppose InpPivotLen = 2. For a given pIdx, the loop checks k = 1 and k = 2, comparing OscBuffer[pIdx] against the oscillator values two bars before and two bars after. If OscBuffer[pIdx] is less than all of those neighboring values, then isPivLow remains true, and we have identified a pivot low. Conversely, if it is greater than all neighbors, isPivHigh remains true, identifying a pivot high.

    The condition pIdx >= InpPivotLen ensures we do not attempt to access negative indices. Also, by evaluating pivots at pIdx = i - InpPivotLen, we guarantee that when we are at bar i, the pivot candidate at pIdx has at least InpPivotLen bars after it, making the confirmation valid.

    Tracking Previous Pivot Values

    Divergence requires comparing the current pivot against the previous pivot of the same type. Therefore, the indicator must store the price and oscillator values of the most recent pivot low and pivot high encountered. The four state buffers handle this responsibility:

    • LastPivLowPriceBuffer—stores the price low at the most recent pivot low,
    • LastPivLowOscBuffer—stores the oscillator value at the most recent pivot low,
    • LastPivHighPriceBuffer—stores the price high at the most recent pivot high,
    • LastPivHighOscBuffer—stores the oscillator value at the most recent pivot high.

    These buffers are propagated forward from bar to bar (as we saw earlier in the state inheritance block) so that at any index, the indicator knows the last valid pivot values without recalculating history.

    Detecting Bullish Divergence

    When a new pivot low is detected, the indicator compares it against the previous pivot low stored in the state buffers. The conditions for a bullish divergence are:

    1. The price low at the new pivot is lower than the previous pivot low (low[pIdx] < prevLowPrice).
    2. The oscillator value at the new pivot is higher than the oscillator value at the previous pivot low (OscBuffer[pIdx] > prevLowOsc).

    If both conditions hold, the price has made a lower low, but the oscillator has formed a higher low. The indicator then places an upward arrow just below the oscillator line:

    // Process Bullish Divergence
             if(isPivLow)
               {
                double prevLowPrice = LastPivLowPriceBuffer[pIdx - 1];
                double prevLowOsc   = LastPivLowOscBuffer[pIdx - 1];
    
                if(low[pIdx] < prevLowPrice && OscBuffer[pIdx] > prevLowOsc)
                  {
                   BullDivBuffer[pIdx] = OscBuffer[pIdx] - 10.0; // Draw arrow just below the line
                  }
                  
                // Update state forward to current bar
                for(int j = pIdx; j <= i; j++)
                  {
                   LastPivLowPriceBuffer[j] = low[pIdx];
                   LastPivLowOscBuffer[j]   = OscBuffer[pIdx];
                  }
               }
    

    Once a new pivot low is identified (whether it produced a divergence signal), the state buffers from pIdx up to the current bar i are updated with the new pivot values. This ensures that subsequent bars carry forward the correct reference point.

    The arrow is placed at OscBuffer[pIdx] - 10.0, positioning it slightly below the oscillator line for visual clarity. The exact offset can be adjusted based on the typical range of the oscillator (here, the MPO4 ranges from -30 to +30, so 10 units provide adequate separation).

    Detecting Bearish Divergence

    Bearish divergence detection mirrors the bullish logic but applies to pivot highs. The conditions are:

    1. The price high at the new pivot is higher than the previous pivot high (high[pIdx] > prevHighPrice).
    2. The oscillator value at the new pivot is lower than the oscillator value at the previous pivot high (OscBuffer[pIdx] < prevHighOsc).

    When these conditions are satisfied, a bearish divergence signal is generated, and a downward arrow is placed above the oscillator line:

    // Process Bearish Divergence
             if(isPivHigh)
               {
                double prevHighPrice = LastPivHighPriceBuffer[pIdx - 1];
                double prevHighOsc   = LastPivHighOscBuffer[pIdx - 1];
    
                if(high[pIdx] > prevHighPrice && OscBuffer[pIdx] < prevHighOsc)
                  {
                   BearDivBuffer[pIdx] = OscBuffer[pIdx] + 10.0; // Draw arrow just above the line
                  }
                  
                // Update state forward to current bar
                for(int j = pIdx; j <= i; j++)
                  {
                   LastPivHighPriceBuffer[j] = high[pIdx];
                   LastPivHighOscBuffer[j]   = OscBuffer[pIdx];
                  }
               }

    The arrow is placed at OscBuffer[pIdx] + 10.0, positioning it above the oscillator line. The use of high[pIdx] and low[pIdx] for price comparisons—rather than close or open—is intentional. Pivot highs are defined by the highest price reached, and pivot lows by the lowest price reached, which is—for most traders—a standard practice in divergence analysis.

    The Importance of Lookback Delay

    We made an intentional decision to evaluate pivots at pIdx = i - InpPivotLen. This introduces a fixed delay: a divergence arrow will only appear after enough bars have formed to confirm the pivot. Without it, the indicator would repaint, marking a signal that could disappear as new bars arrive and invalidate the pivot.

    In live trading, a repainting indicator is dangerous because backtest results become misleading—signals appear perfect in hindsight but degrade in real time. By committing to the confirmation delay, our MPO4 indicator provides signals that are stable and actionable. The trader can enter a trade at the confirmed pivot bar or on the subsequent candle, knowing the signal will not vanish.

    This concludes the divergence detection engine. The indicator now fully computes the smoothed oscillator, identifies valid pivot points, and places non-repainting arrows for bullish and bearish divergences. In the next section, we will examine how the state management buffers work across multiple bars to maintain consistency and then discuss practical interpretation of the signals.

    State Management and Non-Repainting Design

    With the divergence detection engine now fully implemented, we must address two aspects that improve this indicator's efficiency: state management across multiple bars and the non-repainting behavior. After explaining these technical safeguards, we will move into practical interpretation—how to read the oscillator, identify valid divergences, and avoid common pitfalls.

    Our indicator uses four persistent buffers that propagate values forward in time:

    LastPivLowPriceBuffer[]
    
    LastPivLowOscBuffer[]
    
    LastPivHighPriceBuffer[]
    
    LastPivHighOscBuffer[]

    These arrays store, for every bar index, the most recent pivot low (price and oscillator value) and the most recent pivot high. When OnCalculate runs for a new bar i, the first action (after smoothing) is to copy the state from i - 1 into i. This inheritance ensures that no matter which bar the algorithm examines, it always knows the last valid pivot without scanning backward.

    Why not simply use global variables?

    In a real-time environment, OnCalculate may be called multiple times for the same historical bar during a tick update. If we stored only a single pair of global variables, the indicator would "forget" previous pivot values once a new pivot appeared, breaking divergence comparisons. By keeping a per‑bar history, the indicator remains correct even when recalculating partial ranges.

    Non-Repainting Divergence Signals

    Repainting occurs when an indicator changes a signal on a historical bar after new data arrives. For divergence arrows, this would mean an arrow appears, then disappears or moves to a different bar as the market evolves. Repainting makes backtesting worthless and live trading dangerous.

    The indicator prevents repainting through two design choices:

    1. Pivot confirmation delay: we evaluate pivots at pIdx = i - InpPivotLen never at the current bar i. This means a divergence arrow is only drawn after at least InpPivotLen bars have passed beyond the potential pivot. By the time the arrow appears, the surrounding bars are fixed and cannot invalidate the pivot. The trade-off is a slight lag where signals appear a few bars after the true pivot—but this is the price of reliability.
    2. State buffers are never updated backward: When a new pivot is detected at pIdx, the update loop runs forward from pIdx to i:
    for(int j = pIdx; j <= i; j++)
    {
       LastPivLowPriceBuffer[j] = low[pIdx];
       LastPivLowOscBuffer[j]   = OscBuffer[pIdx];
    }

    This writes the new pivot value into all bars from the pivot index up to the current bar. Crucially, it does not modify bars before pIdx. Historical bars before the pivot remain untouched, preserving the integrity of earlier divergences.

    Practical Interpretation of the MPO4 Oscillator

    The MPO4 oscillator is bounded between approximately -30 and +30, with the center line at 0. This structure makes it visually similar to RSI or Stochastics, but the underlying meaning is different.

    Overbought and Oversold Zones

    By default, the indicator draws horizontal lines at +30 (overbought) and -30 (oversold). When the oscillator rises above +30, it means the weighted directional pressure from recent candles has been exceptionally bullish—large bullish bodies have dominated the lookback window. This suggests the market may be extended and due for a pullback, but alone it is not a sell signal. In strong bullish trends, the oscillator can remain high for extended periods. Conversely, readings below -30 indicate extreme selling pressure. Waiting for the oscillator to cross back above -30 can help time entries in oversold conditions.

    Divergence as the Primary Signal

    While the absolute oscillator level provides context, the divergence arrows are the indicator's most valuable output. A bullish divergence arrow that appears while the oscillator is already oversold carries more weight than one occurring near the center line. Similarly, a bearish divergence from overbought territory is more reliable.

    Identifying Valid Divergences:

    • Trend context: in a clear downtrend, a bullish divergence may only produce a short-term bounce. In a sideways market, divergences often lead to genuine reversals.
    • Multiple timeframes: a divergence on the 1‑hour chart is stronger if accompanied by a divergence on the 4‑hour chart. The indicator can be applied to any timeframe.
    • Additional filters: for discretionary traders, confirming with volume surges or support/resistance levels increases confidence.

    Common False Signals and How to Avoid Them

    False Signal Why It Happens Mitigation
    Divergence during low volatility. Small pivots are easily identified but lack significance. Increase InpPivotLen to 3 or more.
    Divergence that fails immediately. The market continues in the original direction. Wait for extra confirmation from other technical analyses before acting.
    Multiple divergences in a row. The oscillator oscillates around a pivot zone. Only take the first divergence; subsequent ones often fail.

    Sample Scenario

    Consider EURUSD on a 1‑hour chart. Price makes a low at 1.0500, and the MPO4 oscillator reads -28 (near oversold). Over the next several bars, price drops to a new low at 1.0480, but the oscillator bottoms at -25—a higher low. A bullish divergence arrow appears two bars after the second low.

    Interpretation: Selling pressure has weakened despite the marginal new price low. A contrarian trader might enter a long position with a stop below 1.0480, targeting a return to the previous range. The oscillator's movement from -28 to -25, though still negative, signals the shift.

    If, instead, the oscillator had made a lower low (e.g., -32) at the second price low, there would be no divergence—pressure is still increasing, and selling could continue.

    Sample Image

    Custom MPO4 Indicator Div Image

    The above image shows how divergence detection works on a real chart using MPO4 as the source. It finds a price/oscillator pivot, stores it, then compares the next pivot to detect divergence.

    Standard RSI Indicator Div Image

    When the source is switched to RSI, the window appears smoother because the underlying calculation differs.

    Although both indicators use the same pivot-detection and divergence framework, the underlying oscillator behavior changes how divergences form and how early they appear. Both indicators can be used to complement each other; each can be used as a confirmation on different timeframes. Their performance is tied to their configuration and cannot be concluded based on the above images.

    The divergence engine itself is oscillator-dependent. This design makes it possible to compare how different oscillators interpret the same market structure under identical divergence rules.

    Limitations and Design Trade-offs

    No indicator is perfect, and the MPO4 is no exception. Understanding its limitations is essential for responsible use. Below are the most important trade-offs embedded in the design.

    • The delay between pivot and signal: as explained, the indicator waits InpPivotLen bars after a potential pivot before drawing a divergence arrow. This eliminates repainting but introduces lag. On a 5‑minute chart with InpPivotLen = 2, the delay is about 10 minutes. On a daily chart, it is two days. Traders who prefer early entry may find this frustrating; those who value reliability will accept the trade-off.
    • Parameter sensitivity: the lookback length (InpMpoLen) dramatically changes the oscillator's behavior. A value of 6 makes the MPO4 very responsive to recent candles, suitable for short‑term trading. A value of 14 or 21 produces a smoother, slower oscillator that may miss quick divergences but filter out noise. There is no universally optimal setting; each market and timeframe requires experimentation and optimization.
    • False divergence in ranging markets: in sideways, choppy markets, price and the oscillator can form many small pivots that meet the mathematical conditions for divergence but never lead to a real reversal. The indicator will print arrows on each one, overwhelming the chart with noise. Using a higher InpPivotLen (e.g., 3 or 4) helps but does not eliminate the problem. The trader can apply additional technical filters.

    Conclusion

    The MPO4 oscillator is a fundamentally different approach that measures directional market pressure by weighting each candle's body size relative to recent volatility. Starting from the core idea—that large, decisive candles should influence the oscillator more than small, hesitant ones—we derived the mathematical formulation, implemented it in MQL5 for technical analysis, and then added a divergence detection engine for signal output.

    This oscillator lives in the same family as RSI, but its pressure‑based calculation offers a unique window into market sentiment. The divergence arrows provided by this indicator are actionable, non‑repainting signals that can be backtested with confidence. Whether you are a discretionary trader looking for early reversal cues or a systematic developer seeking a momentum measure, the MPO4 is a valuable tool.

    Advice on using MPO4 effectively in your trading

    1. Apply the indicator in any timeframe or symbol. The default parameters are (MPO4 Len = 6, Smooth = 7, and Pivot Len = 2).
    2. Watch for divergence arrows near the overbought (+30) or oversold (-30) levels. A bullish arrow near -30 is stronger than one near zero.
    3. Ignore isolated arrows in flat, low-volatility markets. Use a trend filter to confirm potential reversals.
    4. Combine with other techniques such as support/resistance, candlestick patterns, or volume analysis. Divergence is more of a warning, not a guarantee.
    5. Experiment with parameters—longer InpMpoLen (10-14) for smoother trends, shorter (4-5) for scalping. Increase InpPivotLen to 3 or 4 to reduce noise in choppy markets.
    6. Automate your trades based on this idea and optimize the parameters.

    The complete indicator source code is included with this article. Copy it into a new MQL5 indicator file, compile it, and attach it to any chart to start exploring how market pressure diverges from price. Test thoroughly on a demo account before deploying to a live trading.

    If you have questions or want to share your own modifications, join the discussion in the MQL5 community comments section below.



    Attached files |
    Features of Custom Indicators Creation Features of Custom Indicators Creation
    Creation of Custom Indicators in the MetaTrader trading system has a number of features.
    Implementing the Decorator Pattern in MQL5: Adding Logging, Timing, and Filtering to Any Indicator Non-Invasively Implementing the Decorator Pattern in MQL5: Adding Logging, Timing, and Filtering to Any Indicator Non-Invasively
    Cross-cutting concerns like logging, timing, and threshold filtering should not live inside indicator classes. We show how to apply the decorator pattern in MQL5 with a shared IIndicator interface, an owning CBaseDecorator, and concrete CLoggingDecorator, CTimingDecorator, and CThresholdFilterDecorator layers. You can stack behaviors per EA, keep computation code closed to modification, and get deterministic cleanup by deleting only the outermost decorator.
    Features of Experts Advisors Features of Experts Advisors
    Creation of expert advisors in the MetaTrader trading system has a number of features.
    Neural Networks in Trading: LSTM Optimization for Multivariate Time Series Forecasting (Final Part) Neural Networks in Trading: LSTM Optimization for Multivariate Time Series Forecasting (Final Part)
    We continue to implement the DA-CG-LSTM framework, which offers innovative methods for time series analysis and forecasting. The use of CG-LSTM and dual attention allows for more accurate detection of both long-term and short-term dependencies in data, which is particularly useful for working with financial markets.