The Super Smoother filter, developed by John Ehlers, is a powerful tool in a technical analyst’s arsenal, designed to provide significant smoothing of price data (or other series) with substantially less lag than traditional moving averages like the Simple Moving Average (SMA). This article delves into what the Super Smoother is, its mathematical underpinnings, how to implement it in Python, and its applications in trading.
Traders and analysts constantly seek indicators that can clearly define market trends and turning points without the common issue of excessive lag. While Simple Moving Averages (SMAs) provide smoothing, they do so at the cost of significant delay, meaning signals often come too late. Exponential Moving Averages (EMAs) reduce some lag but can be overly sensitive to short-term fluctuations or “whipsaws.”
John Ehlers, a prominent engineer and technical analyst, introduced a range of advanced digital signal processing techniques to trading, including the Super Smoother filter. It’s designed to be a 2-pole Infinite Impulse Response (IIR) filter that offers a good compromise between smoothness and responsiveness.
The Super Smoother is a type of digital filter. Specifically, it’s often implemented as a 2-pole Butterworth-like filter, which provides a flat response in the passband (preserving the trend) and a sharp rolloff in the stopband (attenuating noise).
The core idea is to calculate the current filtered value based on a weighted combination of recent input values and previous filtered values.
The general formula for the 2-pole Super Smoother filter as often presented is:
Filt[i] = c1 * (Input[i] + Input[i-1]) / 2 + c2 * Filt[i-1] + c3 * Filt[i-2]
Where:
Filt[i]
is the current filtered value.Input[i]
is the current input value (e.g., price).Input[i-1]
is the previous input value.Filt[i-1]
is the previous filtered value.Filt[i-2]
is the filtered value from two periods
ago.c1
, c2
, and c3
are
coefficients derived from a length
parameter.The coefficients are determined by a length
parameter,
which is analogous to the period in a moving average. It dictates the
“cutoff frequency” of the filter – essentially, how much smoothing is
applied.
The coefficients are calculated as follows:
a1 = exp(-sqrt(2) * pi / length)
b1 = 2 * a1 * cos(sqrt(2) * pi / length)
c2 = b1
c3 = -a1 * a1
c1 = 1 - c2 - c3
Here:
exp
is the exponential function.cos
is the cosine function.pi
is the mathematical constant sqrt(2)
is the square root of 2, approximately 1.414.
This factor is crucial for the “Super Smoother” characteristic, aiming
for a sharper frequency rolloff.length
is the user-defined parameter that controls the
degree of smoothing. A larger length
leads to more
smoothing (and more lag, though still less than an SMA of similar visual
smoothness), while a smaller length
makes the filter more
responsive but less smooth.The term (Input[i] + Input[i-1]) / 2
represents a simple
pre-smoothing of the input data by averaging the current and previous
values.
Here’s a Python function to calculate the Super Smoother filter for a
pandas.Series
:
import pandas as pd
import numpy as np
def super_smoother(s: pd.Series, length: int) -> pd.Series:
"""
Calculates the Super Smoother filter for a given pandas Series.
Args:
s (pd.Series): The input series (e.g., closing prices).
length (int): The lookback period or cutoff length for the filter.
Must be greater than 0.
Returns:
pd.Series: The Super Smoother filtered series.
"""
if not isinstance(s, pd.Series):
raise TypeError("Input 's' must be a pandas Series.")
if length <= 0:
raise ValueError("length must be positive.")
= pd.Series(np.nan, index=s.index, dtype=float)
filt
# Coefficients
= np.exp(-np.sqrt(2) * np.pi / length)
a1 = 2 * a1 * np.cos(np.sqrt(2) * np.pi / length)
b1
= b1
coef2 = -a1 * a1
coef3 = 1 - coef2 - coef3
coef1
# Iterate using .iloc for positional access to handle all index types
for i in range(len(s)):
if pd.isna(s.iloc[i]): # Skip if current input is NaN
# Optionally, carry forward the last valid filtered value if available
if i > 0 and not pd.isna(filt.iloc[i-1]):
= filt.iloc[i-1]
filt.iloc[i] # else, it remains NaN
continue
if i == 0: # First point: Initialize with the first data point
= s.iloc[i]
filt.iloc[i] elif i == 1: # Second point: Initialize with the second data point
# Full formula cannot be applied yet as filt.iloc[i-2] (filt[-1]) doesn't exist.
# Priming with the input value is a common approach.
= s.iloc[i]
filt.iloc[i] else: # From third point onwards, use the full recursive formula
# Required previous values for the calculation:
= s.iloc[i]
s_current = s.iloc[i-1]
s_prev = filt.iloc[i-1]
filt_prev1 = filt.iloc[i-2]
filt_prev2
# Check if all required previous values are valid numbers
if pd.isna(s_prev) or pd.isna(filt_prev1) or pd.isna(filt_prev2):
# Fallback if some necessary previous value is missing/NaN.
# This might occur if `s` has intermittent NaNs not at the beginning,
# or if the series is too short after initial NaNs.
# A simple fallback is to use the current input value if valid,
# or carry forward the previous filtered value.
if not pd.isna(s_current):
= s_current
filt.iloc[i] elif not pd.isna(filt_prev1):
= filt_prev1
filt.iloc[i] # else, it remains NaN
continue
# Average of current and previous input value
= (s_current + s_prev) / 2.0
input_avg
= coef1 * input_avg + \
filt.iloc[i] * filt_prev1 + \
coef2 * filt_prev2
coef3 return filt
Explanation of the code:
s
is a
pandas Series and length
is positive.pandas.Series
called filt
is created with NaNs to store the filtered
values.a1
,
b1
, coef1
, coef2
,
coef3
are calculated based on the length
.s
.s.iloc[i]
is NaN, the corresponding
filt.iloc[i]
might be carried forward from the previous
filtered value or left as NaN.filt
(filt.iloc[0]
) is
set to the first point of s
(s.iloc[0]
).filt
(filt.iloc[1]
) is
set to the second point of s
(s.iloc[1]
). This
is a common simplification because the full formula requires two
previous filtered values, which aren’t available at the very beginning.
The filter’s output becomes more accurate after these initial priming
steps.i >= 2
,
the full filter formula is applied using the current and previous input
values, and the two preceding filtered values.s.iloc[i-1]
,
filt.iloc[i-1]
, or filt.iloc[i-2]
are NaN. If
so, it applies a fallback logic.The Super Smoother can be applied in several ways:
Smoothing Price Data:
Its primary use is to create a smoothed version of the price series. This smoothed line can make it easier to identify the underlying trend by filtering out market noise.
As a Trend Indicator:
Use in More Complex Strategies:
The Super Smoother is excellent for pre-processing data that will be fed into other indicators or models. For example:
price.diff(n)
) and then apply the Super Smoother to this
momentum series. This can provide clearer momentum signals.length
parameter
allows traders to adjust the filter’s responsiveness and smoothness to
suit different timeframes and market conditions.length
parameter is crucial. An inappropriate length can
lead to either too much noise (if too short) or too much lag (if too
long). This often requires experimentation.Let’s apply the Super Smoother to Bitcoin (BTC-USD) closing prices. I
will use yfinance
to download the data, applying your
preference for auto_adjust=False
and droplevel
for multi-level columns.
import yfinance as yf
import matplotlib.pyplot as plt
# 1) Download BTC-USD data
# Using your preferred settings for yfinance download
try:
= yf.download(
df "BTC-USD",
="2023-01-01",
start="2025-05-22", # Current date or as needed
end=False # As per your saved preference
auto_adjust
)if df.columns.nlevels > 1: # As per your saved preference
= df.columns.droplevel(level=1)
df.columns
# Ensure 'Close' column exists and has data
if 'Close' not in df.columns or df['Close'].isnull().all():
print("Close price data is not available or all NaNs.")
exit()
# Drop rows where essential data like 'Close' is NaN, especially at the beginning
=['Close'], inplace=True)
df.dropna(subsetif df.empty:
print("DataFrame is empty after dropping NaNs from Close price.")
exit()
except Exception as e:
print(f"Error downloading data: {e}")
exit()
# 2) Compute Super Smoother
# Let's try two different lengths to see the effect
'SuperSmoother_10'] = super_smoother(df['Close'], length=10)
df['SuperSmoother_30'] = super_smoother(df['Close'], length=30)
df[
# 3) Plot the results
=(14, 7))
plt.figure(figsize'Close'], label='BTC-USD Close Price', color='lightgray', alpha=0.8)
plt.plot(df.index, df['SuperSmoother_10'], label='Super Smoother (length=10)', color='blue')
plt.plot(df.index, df['SuperSmoother_30'], label='Super Smoother (length=30)', color='red')
plt.plot(df.index, df[
'BTC-USD Close Price and Super Smoother Filters')
plt.title('Date')
plt.xlabel('Price (USD)')
plt.ylabel(
plt.legend()True, alpha=0.3)
plt.grid( plt.show()
Interpreting the Chart:
SuperSmoother_10
(blue line) will follow the price
more closely, reacting faster to changes but showing more
fluctuations.SuperSmoother_30
(red line) will be much smoother,
providing a clearer view of the longer-term trend but reacting more
slowly.The Super Smoother filter is a valuable advancement over traditional moving averages for traders seeking a balance between smooth trend representation and responsive signal generation. Its ability to significantly reduce lag while effectively filtering noise makes it a versatile tool for direct trend analysis, pre-processing data for other indicators, or as a component in sophisticated trading strategies. However, like all tools, its effectiveness is enhanced when used thoughtfully, with an understanding of its parameters and in conjunction with other analytical techniques.