preview
Digital Signal Processing for Traders: Building Ehlers' Filter Library in MQL5

Digital Signal Processing for Traders: Building Ehlers' Filter Library in MQL5

MetaTrader 5Trading systems |
136 0
Adeolu Kayode Gbadebo
Adeolu Kayode Gbadebo

Contents

  1. Introduction
  2. Why price needs filtering, not just averaging
  3. Designing the library: one source of truth
  4. The Super Smoother: a low-lag low-pass filter
  5. The Roofing Filter: keeping only the tradeable band
  6. The Even Better Sinewave: a cycle oscillator that knows when it is wrong
  7. Conclusion


Introduction

Before he ever wrote a line of trading code, John F. Ehlers spent years as an engineer designing filters for radar and communications systems. When he turned to the markets, he brought that toolbox with him, and he made a claim most technical analysts never stop to examine: a price chart is a signal, in exactly the sense an electrical engineer means it. It carries information mixed with noise, it contains components at different frequencies, and it can be processed with the same mathematics that cleans up a radio transmission. Most indicators traders use every day—especially the simple moving average—are crude filters. An engineer would not accept them because they lag badly and let through the noise they are meant to remove.

This is the first article in a two-part series. It takes Ehlers' DSP approach seriously and implements it in MQL5. By the end of this part you will have a reusable filter library and two working indicators on your chart. By the end of the series you will have an Expert Advisor that identifies the current market regime and trades it accordingly. This is what Ehlers' framework is designed to support.

This ground is not entirely untouched on MQL5, and it is worth being precise about what already exists so you know what this series adds. The article Advanced Adaptive Indicators Theory and Implementation in MQL5 appeared back in 2011 and is the only one on the site that engages Ehlers directly. It is a good piece, but it implements three indicators from his first book, Cybernetic Analysis for Stocks and Futures: the Adaptive Cyber Cycle, the Adaptive Center of Gravity, and the Adaptive RVI. It predates most of the toolkit Ehlers is best known for today. Two older articles, Technical Indicators and Digital Filters and Creating Non-Lagging Digital Filters, treat indicators as digital filters in a general way, but they teach the theory rather than porting Ehlers' specific, named filters. What is missing is a faithful, modern, reusable implementation of Ehlers' published filters (Cycle Analytics for Traders, 2013; MESA papers): Super Smoother, Roofing Filter, dominant-cycle measurement, MESA Adaptive Moving Average, and Even Better Sinewave. That gap is what this series fills.

We will keep the mathematics honest but practical. You will see the formulas and the exact coefficients Ehlers specified, with enough explanation to understand why each one is there, but we will not derive transfer functions from first principles. The goal is practical code a developer can read, trust, and reuse—not a signal-processing course.

A note on scope and honesty. Everything in this article is built and compiled, and the indicators run on a live chart. Where a figure shows the indicator on a chart, that is a screenshot you should reproduce on your own terminal, because the point of these tools is what they do with your data on your symbol. Nothing here is a profit claim; this part builds and visualizes filters, and the trading results belong to Part 2.


Why price needs filtering, not just averaging

Start with the problem every smoothing indicator is trying to solve. Raw price is jagged. Inside any move there is a great deal of small, fast wiggle that tells you almost nothing about direction, and underneath it there is slower structure that is what you actually want to trade. The job of a smoother is to suppress the fast wiggle while preserving the slow structure. In signal-processing language, the fast wiggle is high-frequency content and the slow structure is low-frequency content, so a smoother is a low-pass filter: it passes the low frequencies and attenuates the high ones.

The simple moving average is a low-pass filter, but a poor one, and it is worth being concrete about why. It fails in two ways at once. First, it lags: because it is the mean of the last N bars, its output sits half the window behind the price, so a 20-bar SMA reports the market as it was roughly ten bars ago. Second, and less appreciated, it does a bad job of actually removing noise. Its frequency response has large ripples, so certain high-frequency components are not attenuated cleanly and can even be inverted, which shows up as the SMA wiggling when the trader expected it to be smooth. You pay for the lag and you do not even get a clean signal in return.

Ehlers' response was to borrow the filters engineers use when they care about both smoothness and responsiveness. A well-designed two-pole filter can roll off the high frequencies far more cleanly than a moving average of similar lag, which means less noise gets through for the same delay. That single idea, a properly designed low-pass filter in place of a moving average, is the foundation everything else in this series is built on.

The second idea is just as important and is the reason a low-pass filter alone is not enough. Price does not only contain noise we want gone and trend we want kept. It also contains a slowly drifting baseline, the long trend, which for many purposes is itself a kind of contamination. If you are trying to measure the market's cycle, the rhythmic swing around fair value, then a strong trend riding underneath that cycle distorts the measurement, an effect Ehlers calls spectral dilation. The cure is to also remove the very lowest frequencies with a high-pass filter, leaving only a band of cycle periods in the middle. A low-pass filter and a high-pass filter working together to isolate a band is exactly what the Roofing Filter does, and we will build it in section 5.

Raw price split into noise, cycle, and trend components

Fig. 1. A price series is a sum of components at different time scales: fast noise we want removed, a tradeable cycle in the middle, and a slow trend baseline. Filters let us keep the band we care about.

Hold on to this mental model: a filter is a tool for keeping some frequencies and discarding others. A low-pass filter keeps the slow stuff. A high-pass filter keeps the fast stuff. Cascade a high-pass into a low-pass and you keep a band in between. Every class in the library we are about to write is one of these three things.


Designing the library: one source of truth

Before any filter, a design decision. These DSP routines are going to be used in two different kinds of program: visual indicators that draw a line in a subwindow, and, in Part 2, an Expert Advisor that makes trading decisions from the same numbers. The wrong way to do this is to write the math once for the indicator and again for the EA. They would drift apart, and a bug fixed in one would survive in the other. The right way is to write each filter exactly once, as a class, in a single include file that both the indicators and the EA pull in. The EA then computes the filter values directly in its own process rather than reading them back through iCustom, which is both faster and removes a whole category of handle-management bugs.

So the whole library lives in one header, EhlersDSP.mqh, under an Ehlers subfolder of Include. Here is its file header, which also states the sourcing discipline we held to: the formulas and coefficients are ported verbatim from Ehlers' published listings, with the one unavoidable adjustment that EasyLanguage works in degrees while MQL5's trigonometric functions work in radians.

//+------------------------------------------------------------------+
//|                                                    EhlersDSP.mqh |
//|   Digital Signal Processing toolkit after John F. Ehlers         |
//|                                                                  |
//|   One source of truth for the DSP math used by both the visual   |
//|   indicators and the regime-switching EA. Each class is a        |
//|   stateful recursive (IIR) filter: feed it one new value per bar |
//|   with Update(); read the latest output with Value().            |
//|                                                                  |
//|   Formulas are ported verbatim from Ehlers' published listings   |
//|   (TASC Traders' Tips, "Cycle Analytics for Traders" 2013, and   |
//|   the MESA "MAMA" paper 2001). EasyLanguage trig is in DEGREES;  |
//|   here we convert to radians. Coefficients are preserved exactly |
//+------------------------------------------------------------------+
#property copyright "Adeolu Kayode"
#property strict

#define EHLERS_PI 3.14159265358979323846

The phrase stateful recursive (IIR) filter in that header is the key to the whole design, so it is worth unpacking. IIR stands for infinite impulse response, which is the engineer's name for a filter whose output depends on its own previous outputs, not only on the input. Every filter in this library is recursive in that sense: to compute today's value it needs the one or two values it produced on the previous bars. That makes each filter stateful, it has to remember its recent history, and it dictates the shape of the classes. Each one is an object you create once, then feed one new price at a time with an Update() method, and it carries its own memory forward internally.

A ring buffer that reads like the book. Ehlers writes his formulas in EasyLanguage, where x[n] means the value of x from n bars ago. To keep our ported code line-for-line recognizable against his originals, we give every signal a tiny fixed-size history with the same indexing convention, where index 0 is the current value and index 1 is one bar ago. That is the entire job of the SignalHistory helper class.

//+------------------------------------------------------------------+
//  Small ring buffer for the last N samples of a signal.            |
//  history[0] is the most recent value, history[1] one bar ago, …   |
//  This mirrors EasyLanguage's `x[n]` (n bars ago) notation so the  |
//  ported formulas read identically to Ehlers' originals.           |
//+------------------------------------------------------------------+
class SignalHistory
  {
private:
   double            m_buf[];   // ring storage, m_buf[0] = most recent
   int               m_size;    // fixed capacity (oldest sample dropped past this)
   int               m_count;   // how many real samples seen so far
public:
   void              Init(const int size)
     {
      m_size = (size < 1 ? 1 : size);
      ArrayResize(m_buf, m_size);
      ArrayInitialize(m_buf, 0.0);
      m_count = 0;
     }
   //--- push a new most-recent value, shifting older ones back
   void              Push(const double v)
     {
      for(int i = m_size - 1; i > 0; i--)
         m_buf[i] = m_buf[i - 1];
      m_buf[0] = v;
      if(m_count < m_size)
         m_count++;
     }
   //--- value n bars ago (0 = current). Reads as Ehlers' x[n].
   double            operator[](const int n) const
     {
      if(n < 0 || n >= m_size)
         return(0.0);
      return(m_buf[n]);
     }
   int               Count() const { return(m_count); }
   bool              Ready(const int need) const { return(m_count >= need); }
  };

Two details earn their place. The overloaded index operator is what lets the later code write m_hp[1] for "the high-pass output one bar ago," reading just like the EasyLanguage source. And Ready() exists because a recursive filter is meaningless until it has enough history to fill its formula; before that, every class falls back to a sensible warm-up value rather than reading zeros as if they were real samples. Watch for both of these as we go.


The Super Smoother: a low-lag low-pass filter

The Super Smoother is the foundation of the whole library. It is Ehlers' replacement for the moving average: a two-pole low-pass filter, modeled on the analog filters used in electronics, that removes high-frequency noise far more cleanly than an SMA or EMA of comparable lag. Almost every other tool we build uses it as a component, so we implement it first and reuse it everywhere.

The filter has three coefficients, computed once from a single parameter, the critical period (the cutoff). Cycles much shorter than this period are heavily attenuated; cycles much longer pass through. Ehlers' formula for the coefficients, with his default critical period of 10, is:

a1 = exp(-1.414 * pi / Period)

b1 = 2 * a1 * cos(1.414 * 180 / Period), with the angle in degrees

c2 = b1, c3 = -a1 * a1, c1 = 1 - c2 - c3

The factor 1.414 is the square root of 2, which is what sets the filter's two poles for a smooth, well-damped response (it is the Butterworth condition). The constraint c1 = 1 - c2 - c3 is not arbitrary either: it guarantees the coefficients sum to one, so a constant input passes through at full strength rather than being scaled up or down. With the coefficients in hand, each new output is a blend of the current and previous input plus feedback from the two previous outputs:

Filt = c1 * (Price + Price[1]) / 2 + c2 * Filt[1] + c3 * Filt[2]

The c2 * Filt[1] + c3 * Filt[2] terms are the recursion, the filter feeding on its own past, and they are why this is an IIR filter and why the class must hold state. The practical consequence is the contrast in Fig. 2: for a comparable amount of lag, the Super Smoother delivers a visibly cleaner line than a simple moving average, because its frequency response rolls the noise off smoothly instead of letting ripples through.

Super Smoother versus simple moving average on the same noisy data

Fig. 2. The same noisy series smoothed two ways. The simple moving average lags and still ripples; the Super Smoother tracks closer with a cleaner, noise-free line.

Here is the implementation. Note where the degrees-to-radians conversion happens: the comment on the b1 line documents that 1.414 * 180 / Period degrees is the same angle as 1.414 * pi / Period radians, which is what we pass to MathCos.

//+------------------------------------------------------------------+
//  SuperSmoother — Ehlers' 2-pole Butterworth low-pass filter.      |
//  His low-lag replacement for SMA/EMA. Critical period default 10. |
//  Ref: TASC Jan-2014 Traders' Tips; Cycle Analytics ch.3.          |
//+------------------------------------------------------------------+
class SuperSmoother
  {
private:
   double            m_c1, m_c2, m_c3; // recursion coefficients (set in Init)
   SignalHistory     m_in;      // raw input history (need 2 back)
   SignalHistory     m_out;     // filtered output history (need 2 back)
   int               m_count;   // samples processed so far (for warm-up)
public:
   void              Init(const double period = 10.0)
     {
      double a1 = MathExp(-1.414 * EHLERS_PI / period);
      double b1 = 2.0 * a1 * MathCos(1.414 * EHLERS_PI / period); // 1.414*180/period deg = 1.414*PI/period rad
      m_c2 = b1;
      m_c3 = -a1 * a1;
      m_c1 = 1.0 - m_c2 - m_c3;
      m_in.Init(2);
      m_out.Init(2);
      m_count = 0;
     }
   //--- feed one new raw sample, return the new filtered value
   double            Update(const double x)
     {
      double filt;
      if(m_count < 2)            // warm-up: pass input through
         filt = x;
      else
         filt = m_c1 * (x + m_in[0]) / 2.0 + m_c2 * m_out[0] + m_c3 * m_out[1];
      m_in.Push(x);
      m_out.Push(filt);
      m_count++;
      return(filt);
     }
   double            Value() const { return(m_out[0]); }
  };

The warm-up branch deserves a word, because it is a place Ehlers' raw listings are silent and a careless port goes wrong. For the first two bars the recursion has no real Filt[1] or Filt[2] to draw on; reading them as zero would inject a large false transient that the filter, being recursive, would then take many bars to forget. Passing the input straight through for those first two bars instead gives the recursion something sane to start from. It is a small thing that materially improves the first dozen bars of every chart.


The Roofing Filter: keeping only the tradeable band

The Super Smoother removes the noise above the band we care about. The Roofing Filter adds the other half: it also removes the slow trend below the band, so that what is left is a clean, almost-zero-mean oscillation in the tradeable middle. Ehlers named it the Roofing Filter because the high-pass stage puts a "roof" over the analysis, capping the longest cycle that is allowed through. The default band runs from about 10 bars (set by the Super Smoother) up to 48 bars (set by the high-pass).

The new piece is the two-pole high-pass filter that runs first. Its single coefficient comes from the high-pass period, 48 by default, and the constant 0.707 (one over the square root of two again) which sets the damping:

alpha1 = (cos(0.707 * 360 / 48) + sin(0.707 * 360 / 48) - 1) / cos(0.707 * 360 / 48), angle in degrees

The high-pass output is then a recursion on the second difference of price, which is what kills the trend, plus feedback from its own two previous values:

HP = (1 - alpha1/2)^2 * (Price - 2*Price[1] + Price[2]) + 2*(1 - alpha1)*HP[1] - (1 - alpha1)^2 * HP[2]

That high-pass output, now free of trend but still noisy, is fed straight into a Super Smoother to remove the noise. Because we already wrote SuperSmoother as a class, the Roofing Filter simply contains one and delegates to it. This is the payoff of the library design: the cascade in code is as short as the cascade in the math. Fig. 3 shows the signal path.

Roofing Filter signal path: price into high-pass into Super Smoother

Fig. 3. The Roofing Filter as a cascade. Price enters a 2-pole high-pass that strips the slow trend, and the result passes through the Super Smoother that strips the fast noise, leaving the tradeable band.

//+------------------------------------------------------------------+
//  RoofingFilter — 2-pole high-pass (removes periods > 48 bars)     |
//  cascaded into a SuperSmoother (removes periods < ~10 bars).      |
//  Passes only the tradeable band; output has ~zero mean.           |
//  Ref: Cycle Analytics ch.7; EasyLanguageMastery code listing.     |
//+------------------------------------------------------------------+
class RoofingFilter
  {
private:
   double            m_alpha1;  // high-pass coefficient (set in Init)
   SignalHistory     m_price;   // raw price history (need 2 back)
   SignalHistory     m_hp;      // high-pass output history (need 2 back)
   SuperSmoother     m_ss;      // low-pass stage applied to the HP output
   int               m_count;   // samples processed so far (for warm-up)
public:
   void              Init(const double hpPeriod = 48.0, const double ssPeriod = 10.0)
     {
      //--- 2-pole high-pass coefficient (.707 = critical damping)
      double arg = 0.707 * 2.0 * EHLERS_PI / hpPeriod;   // .707*360/hp deg
      m_alpha1 = (MathCos(arg) + MathSin(arg) - 1.0) / MathCos(arg);
      m_price.Init(3);
      m_hp.Init(2);
      m_ss.Init(ssPeriod);
      m_count = 0;
     }
   //--- feed raw price; returns the roofing-filtered value
   double            Update(const double price)
     {
      double hp;
      if(m_count < 2)
         hp = 0.0;               // warm-up
      else
        {
         double one = 1.0 - m_alpha1;
         hp = (1.0 - m_alpha1 / 2.0) * (1.0 - m_alpha1 / 2.0) *
              (price - 2.0 * m_price[0] + m_price[1])
              + 2.0 * one * m_hp[0] - one * one * m_hp[1];
        }
      m_price.Push(price);
      m_hp.Push(hp);
      m_count++;
      return(m_ss.Update(hp));   // smooth the HP output
     }
   double            Value() const { return(m_ss.Value()); }
  };

One subtlety to flag, because it is exactly the kind of thing a reader copying the code should understand rather than just trust. In the formula, Price[1] and Price[2] mean price one and two bars ago. In the code, at the moment we compute hp, we have not yet pushed the current price, so the buffer still holds the previous bars: m_price[0] is one bar ago and m_price[1] is two bars ago. The current price is the live price argument. The mapping is correct, but it is offset by the timing of the push, and reading it carelessly will look like an off-by-one bug when it is not.

Turning the filter into an indicator. The library is the hard part; an indicator built on it is thin. The Roofing Filter indicator declares one buffer, instantiates one RoofingFilter, and fills the buffer bar by bar. The only design point worth discussing is in OnCalculate.

//+------------------------------------------------------------------+
//|                                       EhlersRoofingFilter.mq5    |
//|   Roofing Filter (2-pole HP + Super Smoother) after J. F. Ehlers |
//|   Passes only the tradeable band of cycle periods (~10..48 bars).|
//+------------------------------------------------------------------+
#property copyright "Adeolu Kayode"
#property strict
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1

#property indicator_label1  "Roofing"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_width1  2
#property indicator_level1  0.0

#include <Ehlers/EhlersDSP.mqh>

input double InpHPPeriod = 48.0;   // High-pass period (longest passed cycle)
input double InpSSPeriod = 10.0;   // Super Smoother period (shortest passed cycle)

double         RoofBuffer[];       // plotted indicator buffer (roofing-filtered series)
RoofingFilter  g_roof;             // stateful roofing filter (HP + Super Smoother)

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0, RoofBuffer, INDICATOR_DATA);
   IndicatorSetString(INDICATOR_SHORTNAME,
                      StringFormat("Roofing(%.0f,%.0f)", InpHPPeriod, InpSSPeriod));
   IndicatorSetInteger(INDICATOR_DIGITS, 5);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Stateful IIR filters must be replayed from the start, so we      |
//| rebuild the whole series whenever a fresh calc is requested.     |
//+------------------------------------------------------------------+
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[])
  {
   if(rates_total < 8)
      return(0);

//--- Recursive IIR state must stay consistent across the whole series.
//--- Re-initialise and replay every call: simplest correct approach, and
//--- cheap for the few thousand bars a chart holds.
   g_roof.Init(InpHPPeriod, InpSSPeriod);
   for(int i = 0; i < rates_total; i++)
     {
      double price = (high[i] + low[i]) / 2.0;
      RoofBuffer[i] = g_roof.Update(price);
     }
   return(rates_total);
  }

Most indicators optimize OnCalculate by only processing bars since prev_calculated, recomputing just the newest values. We deliberately do not do that here, and the reason is the recursive nature of the filter. The class carries internal state, the previous outputs the recursion feeds on, and that state only makes sense if the filter has been fed every bar in order from the beginning. There is no correct way to resume a stateful IIR filter from the middle without having saved its exact internal state at that point. The honest, robust choice is to re-initialize the filter and replay the whole series on every call. For the few thousand bars a chart holds this is imperceptibly fast, and it is impossible to get subtly wrong. We feed the filter the median price, (high + low) / 2, which Ehlers uses throughout as a slightly cleaner input than the close.

On the chart, the Roofing Filter draws as a line oscillating around zero in its own subwindow. Because the trend has been removed by the high-pass stage and the noise by the Super Smoother stage, what remains is the market's swing in the 10-to-48-bar band, centered on zero, which is far easier to read for cycle structure than raw price.

Roofing Filter in a subwindow below the price chart

Fig. 4. The Roofing Filter (lower subwindow) applied to price. Trend and noise are removed, leaving a near-zero-mean oscillation in the tradeable band of cycle periods.


The Even Better Sinewave: a cycle oscillator that knows when it is wrong

The last tool in this part is the most interesting, and it sets up the entire strategy of Part 2. The Even Better Sinewave is a cycle oscillator, but its real value is in how it behaves when the market is not cycling. Most swing oscillators fail catastrophically in a trend: they push to an extreme, scream "overbought," and then sit there pinned while the trend runs on and the trader who faded it bleeds. Ehlers' design turns that failure into a signal. When the market trends, the Even Better Sinewave deliberately rails near +1 or -1 and stays there, and that railing is precisely how you detect that you have left cycle mode and entered trend mode.

It achieves this through a normalization trick. The price is first passed through a high-pass filter and a Super Smoother, the same roofing idea as before, though here Ehlers uses a simpler single-pole high-pass tuned by a longer duration so that more of the trend is retained on purpose. Then, over the last three bars, two quantities are computed: the average wave amplitude and the average power (the average of the squared values). The output is the amplitude divided by the square root of the power:

Wave = (Filt + Filt[1] + Filt[2]) / 3

Pwr = (Filt^2 + Filt[1]^2 + Filt[2]^2) / 3

Output = Wave / sqrt(Pwr)

The reason this works is worth seeing. When the market is genuinely cycling, the filtered wave swings symmetrically through zero, the three-bar average amplitude stays well below the root-power, and the ratio traces out a smooth oscillation roughly between -1 and +1. When the market trends, the filtered values stop changing sign and all carry the same magnitude, so the amplitude and the root-power become nearly equal and the ratio saturates at +1 or -1. The single-pole high-pass coefficient that starts it all off comes from the duration parameter, 40 by default:

alpha1 = (1 - sin(360 / Duration)) / cos(360 / Duration), angle in degrees

Here is the class. It follows the now-familiar shape: compute coefficients once in Init(), then in Update() run the single-pole high-pass, feed it through the Super Smoother math inline, and apply the wave-power normalization once three filtered samples are available.

//+------------------------------------------------------------------+
//  EvenBetterSinewave — normalized cycle oscillator that also flags |
//  trend mode. Single-pole HP (retains longer trend) -> Super       |
//  Smoother -> 3-bar wave power normalization (Wave/sqrt(Pwr)).     |
//  Output ~ -1..+1 in cycle mode; rails near +/-1 in trend mode.    |
//  Ref: Cycle Analytics ch.12, code listing 12-1. Duration def 40.  |
//+------------------------------------------------------------------+
class EvenBetterSinewave
  {
private:
   double            m_alpha1;    // single-pole high-pass coefficient (set in Init)
   double            m_c1, m_c2, m_c3; // super smoother coefficients (set in Init)
   SignalHistory     m_price;     // raw price history (need 1 back; HP uses close-close[1])
   SignalHistory     m_hp;        // high-pass output history (need 1 back)
   SignalHistory     m_filt;      // smoothed output history (need 2 back: smoother + wave)
   double            m_wave;      // normalized sinewave value (the public output)
   int               m_count;     // samples processed so far (for warm-up)
public:
   void              Init(const double duration = 40.0, const double ssPeriod = 10.0)
     {
      //--- single-pole high-pass (book listing 12-1)
      double angle = 2.0 * EHLERS_PI / duration;      // 360/Duration deg
      m_alpha1 = (1.0 - MathSin(angle)) / MathCos(angle);
      //--- super smoother coefficients
      double a1 = MathExp(-1.414 * EHLERS_PI / ssPeriod);
      double b1 = 2.0 * a1 * MathCos(1.414 * EHLERS_PI / ssPeriod);
      m_c2 = b1;
      m_c3 = -a1 * a1;
      m_c1 = 1.0 - m_c2 - m_c3;
      m_price.Init(2);
      m_hp.Init(2);
      m_filt.Init(3);
      m_wave = 0.0;
      m_count = 0;
     }
   //--- feed raw price; returns the normalized sinewave value
   double            Update(const double price)
     {
      double hp;
      if(m_count < 1)
         hp = 0.0;
      else
         hp = 0.5 * (1.0 + m_alpha1) * (price - m_price[0]) + m_alpha1 * m_hp[0];
      m_price.Push(price);
      m_hp.Push(hp);

      double filt;
      if(m_count < 2)
         filt = hp;
      else
         filt = m_c1 * (hp + m_hp[1]) / 2.0 + m_c2 * m_filt[0] + m_c3 * m_filt[1];
      m_filt.Push(filt);

      if(m_filt.Ready(3))
        {
         double wave = (m_filt[0] + m_filt[1] + m_filt[2]) / 3.0;
         double pwr  = (m_filt[0] * m_filt[0] + m_filt[1] * m_filt[1] + m_filt[2] * m_filt[2]) / 3.0;
         m_wave = (pwr > 0.0 ? wave / MathSqrt(pwr) : 0.0);
        }
      m_count++;
      return(m_wave);
     }
   double            Value() const { return(m_wave); }
   bool              Ready() const { return(m_count >= 3); }
  };

The guard pwr > 0.0 before the division is small but necessary: in a perfectly flat stretch the filtered values are all zero, the power is zero, and dividing would produce a not-a-number that, in a recursive filter, would poison every value after it. Returning zero in that degenerate case keeps the output well-behaved. This is the same defensive instinct as the warm-up branches; Ehlers' published listings assume an environment that tolerates these edge cases silently, and a careful MQL5 port should not.

The indicator wrapper mirrors the Roofing Filter's almost exactly, with the level lines at +0.85, 0, and -0.85 drawn in so the railing is visible at a glance. The single point of difference worth showing is the use of Ready() when filling the buffer.

//+------------------------------------------------------------------+
//| 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[])
  {
   if(rates_total < 8)
      return(0);

   g_ebsw.Init(InpDuration, InpSSPeriod);
   for(int i = 0; i < rates_total; i++)
     {
      double price = (high[i] + low[i]) / 2.0;
      double v = g_ebsw.Update(price);
      EbswBuffer[i] = (g_ebsw.Ready() ? v : EMPTY_VALUE);
     }
   return(rates_total);
  }

The difference from the Roofing indicator is the g_ebsw.Ready() ? v : EMPTY_VALUE on the buffer assignment. Until the filter has its three samples, there is no meaningful wave value, and writing EMPTY_VALUE tells the terminal to draw nothing there rather than plotting a misleading flat segment during warm-up. The Roofing indicator does not need this because its warm-up branch already returns a continuous zero that reads correctly as "no swing yet."

On the chart the behavior is the whole point. In ranging conditions the line traces a clean oscillation between the +0.85 and -0.85 levels, and crossings of those levels mark the cycle turns. When a trend takes over, the line jumps to a rail near +1 or -1 and holds there for the duration of the move. That visual, oscillation giving way to a flat rail, is the regime signal the Expert Advisor in Part 2 will act on.

Even Better Sinewave oscillating in a range then railing during a trend

Fig. 5. The Even Better Sinewave. It oscillates between the level lines while the market cycles, then rails near +/-1 once a trend dominates. The transition is the cycle-versus-trend mode signal.


Conclusion

We set out to take Ehlers' signal-processing view of price seriously and implement it as real, reusable MQL5. We started from the idea that a chart is a signal carrying noise, a tradeable cycle, and a slow trend, and that filters are the right tool for separating them. We built a small library whose design lets the same math serve both indicators and, later, an Expert Advisor without duplication. Inside it we implemented the Super Smoother, a low-lag low-pass filter that outperforms the moving average it replaces; the Roofing Filter, which cascades a high-pass and the Super Smoother to isolate the tradeable band; and the Even Better Sinewave, a cycle oscillator whose railing behavior turns the usual failure of swing indicators into a detector for trend mode. Along the way we were careful about the things published listings leave unsaid: warm-up transients, division-by-zero guards, and the correct handling of stateful recursive filters in OnCalculate.

You now have two indicators you can use immediately and a library that the rest of the series builds on. In Part 2 we measure the dominant cycle with the Hilbert transform, build the MESA Adaptive Moving Average, and put everything together into a regime-switching Expert Advisor that we test in the Strategy Tester. The filters built here are the foundation that makes that strategy possible.


File
Type
Description
EhlersDSP.mqh
Library
The full DSP class library: SignalHistory, SuperSmoother, RoofingFilter, CyclePeriod, MAMA, and EvenBetterSinewave.
EhlersRoofingFilter.mq5
Indicator
Roofing Filter (2-pole high-pass cascaded into a Super Smoother) plotted in a subwindow.
EhlersEvenBetterSinewave.mq5
Indicator
Even Better Sinewave oscillator with level lines, showing cycle swings and trend-mode railing.

Attached files |
MQL5.zip (7.65 KB)
Beyond GARCH (Part VII): Monte Carlo Volatility Forecasting in MQL5 Beyond GARCH (Part VII): Monte Carlo Volatility Forecasting in MQL5
We implement the CMonteCarlo module that turns the fitted MMAR parameters into a volatility forecast via Monte Carlo. It runs N independent simulations over a chosen horizon and reports mean, median, standard deviation, and a percentile-based 95% confidence interval, with access to per-run values if needed. Adaptive cascade depth selects the minimal k such that b^k covers the horizon, keeping the run fast and consistent.
Heatmap Visualization of Intraday Return Patterns in MQL5 Using CCanvas Heatmap Visualization of Intraday Return Patterns in MQL5 Using CCanvas
MetaTrader 5 provides no native tool for visualizing intraday return patterns across time dimensions simultaneously. This article implements a custom indicator that aggregates historical bar returns into a 5×24 matrix indexed by weekday and hour of day, then renders the result as a color-interpolated heatmap inside an indicator subwindow using CCanvas. Green cells represent positive average returns, red cells negative, with color intensity encoding return magnitude.
Implementation of the Quantum Reservoir Computing (QRC) circuit Implementation of the Quantum Reservoir Computing (QRC) circuit
A revolutionary approach to machine learning in trading through quantum computing. The article demonstrates a practical implementation of an adaptive QRC system with continuous retraining for predicting market movements in real time.
Market Microstructure in MQL5 (Part 7): Regime Classification Market Microstructure in MQL5 (Part 7): Regime Classification
We integrate eleven one-minute microstructure measurements from Parts 2–6 into a composite regime label with confidence and direction. A rule-based RegimeClassifier() assigns one of six regimes—Normal, Stressed, Noisy, Informed, Trending, Mean-Reverting—using empirically derived thresholds from 514 NQ M1 sessions (May 2024–May 2026). The deliverable includes MARKET_REGIME, RegimeAnalysis, and PopulateRegimeAnalysis(), enabling position sizing, stop placement, and signal filtering from a single call.