Digital Signal Processing for Traders: Building Ehlers' Filter Library in MQL5
Contents
- Introduction
- Why price needs filtering, not just averaging
- Designing the library: one source of truth
- The Super Smoother: a low-lag low-pass filter
- The Roofing Filter: keeping only the tradeable band
- The Even Better Sinewave: a cycle oscillator that knows when it is wrong
- 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.

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.

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.

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.

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.

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. |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Beyond GARCH (Part VII): Monte Carlo Volatility Forecasting in MQL5
Heatmap Visualization of Intraday Return Patterns in MQL5 Using CCanvas
Implementation of the Quantum Reservoir Computing (QRC) circuit
Market Microstructure in MQL5 (Part 7): Regime Classification
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use