Most traders default to simple or exponential moving averages and stop there. But the landscape of moving average design is far richer — engineers, statisticians, and professional traders have developed dozens of specialized smoothing techniques, each making a different tradeoff between lag, noise rejection, and responsiveness. Understanding these alternatives gives you a more complete toolkit for signal construction, trend filtering, and regime detection.
This article — the final part of a four-part series — covers eight niche but powerful moving averages: the Jurik Moving Average (JMA), End Point Moving Average (EPMA), Chande Moving Average (CMA), Harmonic Moving Average, McGinley Dynamic, Anchored Moving Average, Filtered Moving Average, and the Ichimoku Kijun-sen. Each is implemented from scratch in Python using pandas, numpy, and yfinance, plotted against real price data so you can see exactly how they behave in practice.
Most algo trading content gives you theory.
This gives you the code.3 Python strategies. Fully backtested. Colab notebook included.
Plus a free ebook with 5 more strategies the moment you subscribe.5,000 quant traders already run these:
Subscribe | AlgoEdge Insights
This article covers:
- Section 1 — Core Concepts:** What distinguishes adaptive and phase-based moving averages from classical smoothers; the design goals behind each of the eight methods covered
- Section 2 — Python Implementation:** Full setup, parameter configuration, indicator functions, and visualization code runnable against live equity data
- Section 3 — Results and Behavioral Analysis:** How each average responds to trending versus choppy price action; latency and smoothness comparisons
- Section 4 — Use Cases:** Practical contexts where niche moving averages outperform standard EMA/SMA approaches
- Section 5 — Limitations and Edge Cases:** Honest assessment of complexity costs, parameter sensitivity, and failure modes
1. Beyond EMA: Adaptive, Geometric, and Anchored Smoothing
Classical moving averages — SMA, EMA, WMA — all share a common design assumption: the smoothing coefficient is fixed and independent of price behavior. This works adequately in trending markets but degrades in two common ways. In high-volatility, choppy conditions, fixed-coefficient smoothers lag too much to be useful. In low-volatility trending conditions, they can overfit to noise rather than tracking the underlying drift. The eight methods in this article each address this tradeoff differently.
The Jurik Moving Average (JMA) is a commercial adaptive filter designed by Mark Jurik. It uses a two-stage smoothing process with a volatility-dependent coefficient that tightens during smooth trends and widens during erratic price action. The result is a curve that appears almost phase-correct compared to price — very little lag — while remaining visually smooth. The McGinley Dynamic takes a similar philosophy but implements it as a single recursive formula with a speed factor that automatically adjusts based on how far price has moved from the average.
The End Point Moving Average (EPMA) takes a completely different approach: instead of weighting historical prices, it fits a linear regression line to the last n periods and uses the endpoint of that regression as the smoothed value. This makes EPMA highly responsive to directional moves while remaining mathematically grounded. The Chande Moving Average (CMA) blends a fast and slow EMA adaptively using the Chande Momentum Oscillator, effectively switching between responsive and conservative smoothing based on recent momentum strength.
The remaining four methods cover geometric averaging (Harmonic MA), fixed-reference smoothing (Anchored MA), threshold-based pass-through (Filtered MA), and the structural midpoint used in Ichimoku charting (Kijun-sen). Together, these eight methods represent a broad cross-section of smoothing design philosophies that complement rather than replace standard averages.
2. Python Implementation
2.1 Setup and Parameters
The implementation uses yfinance to pull live OHLCV data, numpy for numerical kernels, and matplotlib for visualization. The key parameters you will configure are: TICKER (the equity symbol), START/END (the date range), and PERIOD (the lookback window applied uniformly across all eight indicators so comparisons remain fair).
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from scipy.stats import linregress
import warnings
warnings.filterwarnings("ignore")
# ── Configuration ──────────────────────────────────────────────
TICKER = "AAPL"
START = "2020-01-01"
END = "2023-12-31"
PERIOD = 20 # lookback window for all indicators
PHASE = 0 # JMA phase parameter (-100 to +100)
JMA_POW = 2 # JMA power factor (1–10, higher = smoother)
MCGINLEY_K = 0.6 # McGinley speed constant (0.5–1.0 typical)
FILTER_PCT = 0.001 # Filtered MA threshold (0.1% of price)
# ── Data fetch ─────────────────────────────────────────────────
raw = yf.download(TICKER, start=START, end=END, auto_adjust=True, progress=False)
df = raw[["Close"]].dropna().copy()
df.index = pd.to_datetime(df.index)
print(f"Loaded {len(df)} sessions for {TICKER} ({START} → {END})")
2.2 Indicator Functions
Each indicator is encapsulated in a standalone function that accepts a pd.Series and returns a pd.Series of equal length, with NaN in the warm-up period. This design makes them composable and easy to drop into any pipeline.
# ── 1. Jurik Moving Average (JMA) ──────────────────────────────
def jurik_ma(series: pd.Series, period: int = 14,
phase: float = 0, power: int = 2) -> pd.Series:
phase_ratio = phase / 100 + 1.5
beta = 0.45 * (period - 1) / (0.45 * (period - 1) + 2)
alpha = beta ** power
jma = np.full(len(series), np.nan)
e0 = e1 = e2 = 0.0
for i, price in enumerate(series):
e0_prev, e1_prev, e2_prev = e0, e1, e2
e0 = (1 - alpha) * price + alpha * e0_prev
e1 = (price - e0) * (1 - beta) + beta * e1_prev
e2 = (e0 + phase_ratio * e1 - (jma[i-1] if i > 0 else price)) * \
(1 - alpha) ** 2 + (alpha ** 2) * e2_prev
jma[i] = e2 + (jma[i-1] if i > 0 else price)
return pd.Series(jma, index=series.index)
# ── 2. End Point Moving Average (EPMA) ────────────────────────
def epma(series: pd.Series, period: int = 20) -> pd.Series:
result = np.full(len(series), np.nan)
vals = series.values
for i in range(period - 1, len(vals)):
y = vals[i - period + 1 : i + 1]
x = np.arange(period)
slope, intercept, *_ = linregress(x, y)
result[i] = intercept + slope * (period - 1)
return pd.Series(result, index=series.index)
# ── 3. Chande Moving Average (CMA) ────────────────────────────
def chande_ma(series: pd.Series, period: int = 20) -> pd.Series:
diff = series.diff()
up = diff.clip(lower=0).rolling(period).sum()
down = diff.clip(upper=0).abs().rolling(period).sum()
cmo = ((up - down) / (up + down).replace(0, np.nan)).fillna(0)
sc = ((cmo.abs() + 1) / 2) ** 2 # adaptive smoothing coefficient
result = series.copy() * np.nan
for i in range(period, len(series)):
prev = result.iloc[i - 1] if not np.isnan(result.iloc[i - 1]) \
else series.iloc[i - 1]
result.iloc[i] = prev + sc.iloc[i] * (series.iloc[i] - prev)
return result
# ── 4. Harmonic Moving Average ────────────────────────────────
def harmonic_ma(series: pd.Series, period: int = 20) -> pd.Series:
return series.rolling(period).apply(
lambda x: period / np.sum(1.0 / x) if np.all(x > 0) else np.nan,
raw=True
)
# ── 5. McGinley Dynamic ───────────────────────────────────────
def mcginley_dynamic(series: pd.Series, period: int = 20,
k: float = 0.6) -> pd.Series:
md = np.full(len(series), np.nan)
md[0] = series.iloc[0]
for i in range(1, len(series)):
prev = md[i - 1]
price = series.iloc[i]
denom = k * period * (price / prev) ** 4
md[i] = prev + (price - prev) / max(denom, 1e-10)
return pd.Series(md, index=series.index)
# ── 6. Anchored Moving Average ────────────────────────────────
def anchored_ma(series: pd.Series, anchor_idx: int = 0) -> pd.Series:
"""Cumulative mean from a fixed anchor point in the series."""
subset = series.iloc[anchor_idx:].expanding().mean()
return subset.reindex(series.index)
# ── 7. Filtered Moving Average ────────────────────────────────
def filtered_ma(series: pd.Series, period: int = 20,
threshold_pct: float = 0.001) -> pd.Series:
base = series.ewm(span=period, adjust=False).mean()
result = base.copy()
for i in range(1, len(result)):
delta = abs(series.iloc[i] - result.iloc[i - 1])
if delta < threshold_pct * result.iloc[i - 1]:
result.iloc[i] = result.iloc[i - 1] # suppress micro-noise
return result
# ── 8. Ichimoku Kijun-sen (Base Line) ────────────────────────
def kijun_sen(series: pd.Series, period: int = 26) -> pd.Series:
return (series.rolling(period).max() +
series.rolling(period).min()) / 2
2.3 Applying All Indicators to Price Data
close = df["Close"].squeeze()
df["JMA"] = jurik_ma(close, period=PERIOD, phase=PHASE, power=JMA_POW)
df["EPMA"] = epma(close, period=PERIOD)
df["CMA"] = chande_ma(close, period=PERIOD)
df["Harmonic"] = harmonic_ma(close, period=PERIOD)
df["McGinley"] = mcginley_dynamic(close, period=PERIOD, k=MCGINLEY_K)
df["Anchored"] = anchored_ma(close, anchor_idx=0)
df["Filtered"] = filtered_ma(close, period=PERIOD, threshold_pct=FILTER_PCT)
df["Kijun"] = kijun_sen(close, period=26)
print(df.tail())
2.4 Visualization
The chart plots all eight moving averages against the raw close price. Pay attention to how each line behaves around sharp reversals — JMA and McGinley hug price tightly, EPMA leads slightly during trends, and the Kijun-sen produces a characteristic stepped appearance due to its min-max definition.
COLORS = {
"JMA": "#00BFFF",
"EPMA": "#FF6347",
"CMA": "#ADFF2F",
"Harmonic": "#FFD700",
"McGinley": "#FF69B4",
"Anchored": "#DDA0DD",
"Filtered": "#20B2AA",
"Kijun": "#FFA07A",
}
plt.style.use("dark_background")
fig, ax = plt.subplots(figsize=(16, 7))
ax.plot(df.index, close, color="white", lw=1.0,
alpha=0.55, label="Close Price", zorder=2)
for label, color in COLORS.items():
ax.plot(df.index, df[label], color=color,
lw=1.5, label=label, zorder=3)
ax.xaxis.set_major_formatter(mdates.DateFormatter("%b '%y"))
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))
plt.xticks(rotation=30, ha="right")
ax.set_title(f"{TICKER} — Eight Niche Moving Averages | Period = {PERIOD}",
fontsize=14, pad=12)
ax.set_ylabel("Price (USD)")
ax.grid(alpha=0.15, linestyle="--")
ax.legend(loc="upper left", fontsize=8, ncol=2,
framealpha=0.3, edgecolor="gray")
plt.tight_layout()
plt.savefig("moving_averages_niche.png", dpi=150, bbox_inches="tight")
plt.show()
Figure 1. All eight niche moving averages plotted against AAPL daily closes from 2020–2023; note the near-zero lag of JMA and McGinley during the 2020 recovery rally versus the deliberate smoothness of the Harmonic and Filtered averages during low-volatility consolidation.
Enjoying this strategy so far? This is only a taste of what's possible.
Go deeper with my newsletter: longer, more detailed articles + full Google Colab implementations for every approach.
Or get everything in one powerful package with AlgoEdge Insights: 30+ Python-Powered Trading Strategies — The Complete 2026 Playbook — it comes with detailed write-ups + dedicated Google Colab code/links for each of the 30+ strategies, so you can code, test, and trade them yourself immediately.
Exclusive for readers: 20% off the book with code
MEDIUM20.Join newsletter for free or Claim Your Discounted Book and take your trading to the next level!
3. Behavioral Analysis Across Market Regimes
Running this code on AAPL from 2020 through 2023 spans four distinct regimes: the COVID crash and recovery (Q1–Q2 2020), the extended bull run into late 2021, the bear market of 2022, and the rebound in 2023. This range stress-tests each indicator across very different volatility and trend conditions.
The JMA and McGinley Dynamic both track the 2020 recovery with minimal lag — they never trail price by more than a few sessions during the sharp 40% rebound. The EPMA actually appears slightly ahead of price during sustained trends because the regression endpoint projects the current directional slope, but it produces noisy reversals because linear regression is sensitive to the most recent data point.
The Harmonic MA and Filtered MA are the smoothest of the group. The Harmonic mean is pulled less aggressively by outlier spikes than the arithmetic mean, making it useful for instruments with occasional fat-tailed gaps. The Filtered MA's threshold mechanism suppresses sub-0.1% price movements entirely, producing a staircase-like curve in quiet periods that reads cleanly as "no trend confirmed." The Kijun-sen, computed as a rolling midpoint rather than a mean, produces its characteristic flat segments whenever the 26-period high or low has not changed — exactly the kind of structural support/resistance signal Ichimoku practitioners use for trade entries.
The Chande MA and Anchored MA are the most context-dependent. CMA behaves like an EMA in trending markets and like an SMA in choppy ones — a useful property but one that requires monitoring the underlying CMO to interpret correctly. The Anchored MA, being a cumulative mean from a fixed start date, is most useful for VWAP-style analysis where you want to express price relative to a specific event or session open.
4. Use Cases
JMA as a signal filter: Because JMA produces very low lag with smooth output, it works well as a replacement for EMA in crossover systems. A JMA(9)/JMA(21) crossover produces fewer whipsaws than its EMA equivalent on hourly equity data.
EPMA for momentum confirmation: The regression-endpoint nature of EPMA means it acts as a directional bias indicator. If EPMA slope is positive and price is above EPMA, momentum is likely still intact. Use it alongside volume to confirm breakouts.
Kijun-sen for structural levels: In Ichimoku-based systems, a flat Kijun-sen indicates a consolidation zone that frequently acts as support or resistance on the first touch. Pair with Tenkan-sen crossovers for entry timing.
Filtered MA for low-noise trend confirmation: In mean-reversion strategies, a Filtered MA with a 0.2–0.5% threshold effectively suppresses intraday noise, giving a clean read on the daily trend direction without requiring higher timeframe data.
5. Limitations and Edge Cases
Parameter sensitivity is high for JMA and CMA. Small changes to the JMA power coefficient or the Chande momentum period can dramatically alter output character. These indicators require systematic optimization rather than intuitive tuning, which adds backtest overfitting risk.
EPMA is unstable near reversals. Because it anchors on linear regression, a single extreme candle at the tail of the window can sharply redirect the endpoint. In practice this means EPMA generates false signals around earnings gaps and macro events unless you apply an additional outlier filter.
The Anchored MA loses meaning over long windows. A cumulative mean from January 2020 becomes a progressively less informative reference as time passes and the anchor date grows distant. It is most useful intraday or over event-specific windows of 20–60 sessions.
Harmonic MA breaks down with zero or near-zero prices. The harmonic mean requires all values to be strictly positive. For instruments that can print zero (certain derivatives, spread products) or for adjusted price series with large negative adjustments, you will get division errors or nonsensical output.
None of these methods are predictive by design. Every indicator here is a lagged transformation of price. Treating low-lag indicators like JMA or McGinley as genuinely leading signals is a common and costly misinterpretation — they reduce lag, they do not eliminate it.
Concluding Thoughts
This final installment completes a survey of 36 moving average methods spanning classical statistics, adaptive filtering, geometric averaging, and structural price analysis. The eight methods covered here — JMA, EPMA, CMA, Harmonic, McGinley, Anchored, Filtered, and Kijun-sen — each encode a specific hypothesis about how price should be smoothed, and understanding those hypotheses is more valuable than memorizing the formulas.
A productive next step is to run these indicators side by side on a range of instruments with different volatility profiles: a large-cap equity, a small-cap, a bond ETF, and a commodity futures series. You will quickly find that no single method dominates across all contexts, which is precisely why having the full toolkit matters. Use the visualization code from Section 2.4 as a starting template and vary TICKER and PERIOD systematically.
If you want deeper dives into backtesting these averages inside a full strategy framework — position sizing, walk-forward validation, transaction cost modeling — the complete implementation notebooks are available through the AlgoEdge Insights newsletter, where each method is tested end-to-end with realistic assumptions rather than idealized curve-fitting.
Most algo trading content gives you theory.
This gives you the code.3 Python strategies. Fully backtested. Colab notebook included.
Plus a free ebook with 5 more strategies the moment you subscribe.5,000 quant traders already run these:
Subscribe | AlgoEdge Insights


Top comments (0)