The Ehlers Decycler is an elegant trend filter created by John Ehlers, a pioneer in applying digital signal processing techniques to financial markets. Unlike traditional moving averages that smooth all frequency components equally, the Decycler specifically removes short-term cyclical noise while preserving the underlying trend direction with minimal lag.
This indicator packages the two canonical formulations Ehlers describes — the 1-pole “Simple Decycler” and the 2-pole Decycler — into a single configurable script, with optional percentage bands that turn the trend into a soft channel for support/resistance reading.
There is a recurring source of confusion when reading material on the Decycler. In Ehlers’ original notation:
A previously published version on prorealcode by Nicolas — Ehlers Simple Decycler — implements the 2-pole formulation under the “Simple” name, with bands. That indicator is correct in its math; the naming is the only discrepancy. The version published in this article exposes both orders as a switch (poles = 1 or 2) so the user can compare them directly on the same chart and pick the right tool for the job.
Ehlers’ approach treats price as a signal composed of two parts:
Both formulations build a high-pass filter (which isolates cycles) and use the relationship low-pass = original − high-pass to extract the trend. The difference is the order of the filter, which controls how aggressively the cycles are removed.
The recursion is single-term and the result is the trend directly:
alpha = (cos(2π/N) + sin(2π/N) − 1) / cos(2π/N)
Trend = (alpha/2) × (src + src[1]) + (1 − alpha) × Trend[1]
This is a Butterworth low-pass of order 1. Roll-off is 6 dB/octave: cycles shorter than N are attenuated, cycles longer than N pass through nearly intact. Lag is minimal but a small amount of cyclical noise survives, particularly close to the cutoff.
A second-order Butterworth high-pass is built explicitly and subtracted from price. The cutoff is corrected by √2 to keep the −3 dB point aligned with the requested period:
alpha = (cos(2π/(N·√2)) + sin(2π/(N·√2)) − 1) / cos(2π/(N·√2))
HP = (1 − α/2)² × (src − 2·src[1] + src[2])
+ 2·(1 − α) × HP[1]
− (1 − α)² × HP[2]
Trend = src − HP
This is a sharper filter (12 dB/octave). It produces a smoother line with more lag but cleaner cycle rejection — useful when the goal is a stable trend reference rather than fast reaction.
When showBands = 1, two additional lines are plotted at:
These bands turn the indicator into a Decycler channel. They behave as soft dynamic support and resistance: in calm regimes price oscillates inside the bands around the trend; in impulsive regimes price escapes one side, signalling regime change. Default is bandPct = 0.5 (a ±0.5% envelope). The bands are off by default to keep the trend-only use case clean.
The indicator exposes the following inputs:
A higher length produces a smoother line but with more lag; a lower value makes the filter more responsive but allows more cyclical noise through. Ehlers recommends values between 40 and 100 for most markets.
//-----------------------------------------------//
// PRC_Ehlers Decycler
// 1-pole and 2-pole variants with optional bands
// version = 2
// 30.04.2026
// Iván González @ www.prorealcode.com
// Sharing ProRealTime knowledge
//-----------------------------------------------//
//-----Inputs------------------------------------//
src = customclose
length = 60
pole1 = 1 // 1 = Simple Decycler (1-pole), 0 = Two-pole Decycler
showBands = 0 // 0 = trend only, 1 = enable percentage bands
bandPct = 0.5 // % band shift (active when showBands = 1)
cloud = 1 // 1 = fill area between price and trend
//-----------------------------------------------//
once PI = 3.14159265359
once MA = src
//-----1-pole Simple Decycler--------------------//
if pole1 then
if barindex > length then
transf = 360 / length
cosine = COS(transf)
if cosine <> 0 then
alpha = (cosine + SIN(transf) - 1) / cosine
else
alpha = alpha[1]
endif
MA = alpha / 2 * (src + src[1]) + (1 - alpha) * MA[1]
endif
endif
//-----2-pole Decycler---------------------------//
if not pole1 then
if barindex > length then
transf = 360 / (length * SQRT(2))
cosine = COS(transf)
if cosine <> 0 then
alpha = (cosine + SIN(transf) - 1) / cosine
else
alpha = alpha[1]
endif
hp = SQUARE(1 - alpha/2) * (src - 2*src[1] + src[2]) + 2*(1-alpha)*hp[1] - SQUARE(1-alpha)*hp[2]
MA = src - hp
endif
endif
//-----Optional bands----------------------------//
if showBands then
upperBand = (1 + bandPct/100) * MA
lowerBand = (1 - bandPct/100) * MA
else
upperBand = undefined
lowerBand = undefined
endif
//-----------------------------------------------//
//-----Color configuration-----------------------//
r = 220
g = 220
b = 220
a = 0
if MA > MA[1] then
r = 0
g = 230
b = 118
a = 255
elsif MA < MA[1] then
r = 255
g = 82
b = 82
a = 255
endif
//-----------------------------------------------//
//----------------- Cloud -----------------------//
if cloud then
colorbetween(src, MA, r, g, b, a*0.20)
endif
//-----------------------------------------------//
return MA coloured(r, g, b, a) style(line, 2) as "Decycler", upperBand coloured(19, 132, 132) style(line) as "Upper Band", lowerBand coloured(19, 132, 132) style(line) as "Lower Band"