DEV Community

Ayrat Murtazin
Ayrat Murtazin

Posted on

Top Moving Average Methods for Stock Prices in Python: Part 3

Moving averages are among the most foundational tools in quantitative finance, yet most practitioners only ever use two or three variants. This article — the third in a four-part series — pushes beyond the classics (SMA, EMA) into more sophisticated smoothing techniques that better handle noise, lag, and non-stationary price dynamics. Understanding a wider toolkit gives you more precise control over signal generation and trend detection across different market regimes.

In this installment, we implement several advanced moving average methods in Python using yfinance for data retrieval, pandas and numpy for computation, and matplotlib for visualization. Each method is applied to real equity price data so you can immediately compare behavior on live time series. By the end, you will have a modular, reusable codebase that slots directly into a backtesting pipeline.


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 Moving Average Methods for Stock Prices in Python: Part 3

This article covers:

  • Section 1 — Core Concepts:** Why standard moving averages fall short and what properties advanced variants optimize for
  • Section 2 — Python Implementation:** Full setup, data retrieval, computation of multiple MA methods, and visualization with a comparative chart
  • Section 3 — Results and Analysis:** What each method reveals about trend structure and where each variant outperforms the others
  • Section 4 — Use Cases:** Practical applications in signal generation, regime detection, and risk filtering
  • Section 5 — Limitations and Edge Cases:** Honest discussion of overfitting, lag trade-offs, and data quality pitfalls

1. Why Standard Moving Averages Are Not Enough

A simple moving average (SMA) treats every observation in its window as equally important. An exponential moving average (EMA) improves on this by weighting recent prices more heavily, but it still applies a fixed decay rate regardless of how volatile or trending the market is at any given moment. In practice, both methods lag the price series by a predictable and often frustrating amount — by the time a crossover signal fires, much of the move has already occurred.

Think of it this way: if you are tracking a runner's pace over a race, an SMA gives you the average speed over the last mile regardless of whether they just sprinted or stumbled. A smarter approach would adapt the measurement window or the weighting scheme to reflect what is actually happening in real time. That is the intuition behind adaptive and regression-based moving averages.

The methods we focus on here — the Weighted Moving Average (WMA), Double Exponential Moving Average (DEMA), Triple Exponential Moving Average (TEMA), Hull Moving Average (HMA), and Kaufman's Adaptive Moving Average (KAMA) — each address the lag-versus-smoothness trade-off from a different mathematical angle. WMA applies a linearly increasing weight to more recent prices. DEMA and TEMA use EMA-of-EMA constructions to subtract lag mathematically. HMA combines weighted averages at different periods to achieve near-zero lag with minimal noise. KAMA adjusts its smoothing constant dynamically based on a market efficiency ratio.

Understanding the geometry behind each method — not just the formula — lets you make principled decisions about which one to deploy in a given strategy. A momentum strategy on a trending instrument benefits from a low-lag method like HMA. A mean-reversion strategy on a noisy instrument benefits from a smoother, more conservative filter like KAMA. The choice is not arbitrary; it follows directly from the statistical properties of the underlying price series.

2. Python Implementation

2.1 Setup and Parameters

The key parameters here are the ticker symbol, date range, and the period length shared across all moving average methods for fair comparison. Using the same period across methods lets you isolate the effect of the weighting scheme rather than conflating it with window length.

# ── dependencies ──────────────────────────────────────────────────────────────
# pip install yfinance pandas numpy matplotlib

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")

# ── configurable parameters ───────────────────────────────────────────────────
TICKER      = "SPY"          # equity or ETF ticker
START_DATE  = "2022-01-01"   # backtest start
END_DATE    = "2024-12-31"   # backtest end
PERIOD      = 20             # lookback window for all MA methods
KAMA_FAST   = 2              # KAMA fast EMA period (high efficiency)
KAMA_SLOW   = 30             # KAMA slow EMA period (low efficiency)
Enter fullscreen mode Exit fullscreen mode

Implementation chart

2.2 Data Retrieval and Core MA Computation

We download adjusted close prices from Yahoo Finance and compute all five moving average methods in a single DataFrame. Each function is written as a standalone utility so you can import it into other scripts without modification.

# ── data retrieval ────────────────────────────────────────────────────────────
raw   = yf.download(TICKER, start=START_DATE, end=END_DATE, auto_adjust=True)
close = raw["Close"].squeeze().dropna()

# ── helper: Weighted Moving Average (WMA) ────────────────────────────────────
def wma(series: pd.Series, period: int) -> pd.Series:
    weights = np.arange(1, period + 1, dtype=float)
    return series.rolling(period).apply(
        lambda x: np.dot(x, weights) / weights.sum(), raw=True
    )

# ── helper: Double EMA (DEMA) ─────────────────────────────────────────────────
def dema(series: pd.Series, period: int) -> pd.Series:
    ema1 = series.ewm(span=period, adjust=False).mean()
    ema2 = ema1.ewm(span=period, adjust=False).mean()
    return 2 * ema1 - ema2

# ── helper: Triple EMA (TEMA) ─────────────────────────────────────────────────
def tema(series: pd.Series, period: int) -> pd.Series:
    ema1 = series.ewm(span=period, adjust=False).mean()
    ema2 = ema1.ewm(span=period, adjust=False).mean()
    ema3 = ema2.ewm(span=period, adjust=False).mean()
    return 3 * ema1 - 3 * ema2 + ema3

# ── helper: Hull Moving Average (HMA) ────────────────────────────────────────
def hma(series: pd.Series, period: int) -> pd.Series:
    half_period = max(1, period // 2)
    sqrt_period = max(1, int(np.sqrt(period)))
    wma_half = wma(series, half_period)
    wma_full = wma(series, period)
    raw_hma  = 2 * wma_half - wma_full
    return wma(raw_hma, sqrt_period)

# ── helper: Kaufman Adaptive Moving Average (KAMA) ───────────────────────────
def kama(series: pd.Series, period: int,
         fast: int = 2, slow: int = 30) -> pd.Series:
    fast_sc = 2.0 / (fast + 1)
    slow_sc = 2.0 / (slow + 1)
    prices  = series.to_numpy()
    result  = np.full(len(prices), np.nan)
    result[period - 1] = prices[period - 1]
    for i in range(period, len(prices)):
        direction = abs(prices[i] - prices[i - period])
        volatility = np.sum(np.abs(np.diff(prices[i - period: i + 1])))
        er  = direction / volatility if volatility != 0 else 0.0
        sc  = (er * (fast_sc - slow_sc) + slow_sc) ** 2
        result[i] = result[i - 1] + sc * (prices[i] - result[i - 1])
    return pd.Series(result, index=series.index)

# ── build results DataFrame ───────────────────────────────────────────────────
df = pd.DataFrame({"Close": close})
df["WMA"]  = wma(close,  PERIOD)
df["DEMA"] = dema(close, PERIOD)
df["TEMA"] = tema(close, PERIOD)
df["HMA"]  = hma(close,  PERIOD)
df["KAMA"] = kama(close, PERIOD, KAMA_FAST, KAMA_SLOW)

print(df.tail(10).round(2))
Enter fullscreen mode Exit fullscreen mode

2.3 Signal Generation

A simple directional signal — long when price is above the moving average, flat otherwise — lets us compare how each method responds to trend changes. We compute daily returns for each signal to enable downstream performance analysis.

# ── binary signals: 1 = price above MA (long), 0 = flat ──────────────────────
ma_cols = ["WMA", "DEMA", "TEMA", "HMA", "KAMA"]

signals = pd.DataFrame(index=df.index)
returns = pd.DataFrame(index=df.index)

daily_ret = df["Close"].pct_change()

for col in ma_cols:
    signals[col] = (df["Close"] > df[col]).astype(int).shift(1)  # avoid lookahead
    returns[col] = signals[col] * daily_ret

# ── cumulative performance ────────────────────────────────────────────────────
cumulative = (1 + returns).cumprod()

print("\nFinal Cumulative Returns:")
print(cumulative.iloc[-1].round(4))
Enter fullscreen mode Exit fullscreen mode

2.4 Visualization

The chart overlays all five moving averages on the raw price series. Pay attention to how quickly each line reacts to the sharp sell-off and recovery periods — this is where lag differences become visually obvious.

# ── visualization ─────────────────────────────────────────────────────────────
plt.style.use("dark_background")
fig, axes = plt.subplots(2, 1, figsize=(14, 10),
                          gridspec_kw={"height_ratios": [2, 1]})

colors = {
    "WMA":  "#00BFFF",   # deep sky blue
    "DEMA": "#FF6347",   # tomato
    "TEMA": "#7CFC00",   # lawn green
    "HMA":  "#FFD700",   # gold
    "KAMA": "#DA70D6",   # orchid
}

# ── top panel: price + moving averages ───────────────────────────────────────
ax1 = axes[0]
ax1.plot(df.index, df["Close"], color="white", linewidth=1.2,
         alpha=0.7, label="Close")
for col, clr in colors.items():
    ax1.plot(df.index, df[col], color=clr, linewidth=1.4, label=col)

ax1.set_title(f"{TICKER} — Advanced Moving Averages (Period={PERIOD})",
              fontsize=14, pad=12)
ax1.set_ylabel("Price (USD)", fontsize=11)
ax1.legend(loc="upper left", fontsize=9, framealpha=0.3)
ax1.grid(alpha=0.15)

# ── bottom panel: cumulative returns per MA signal ───────────────────────────
ax2 = axes[1]
for col, clr in colors.items():
    ax2.plot(cumulative.index, cumulative[col],
             color=clr, linewidth=1.3, label=col)

bh = (1 + daily_ret).cumprod()
ax2.plot(bh.index, bh, color="white", linewidth=1.0,
         linestyle="--", alpha=0.5, label="Buy & Hold")

ax2.set_ylabel("Cumulative Return", fontsize=11)
ax2.set_xlabel("Date", fontsize=11)
ax2.legend(loc="upper left", fontsize=9, framealpha=0.3)
ax2.grid(alpha=0.15)

plt.tight_layout()
plt.savefig("advanced_ma_comparison.png", dpi=150, bbox_inches="tight")
plt.show()
Enter fullscreen mode Exit fullscreen mode

Figure 1. Top panel shows SPY closing prices overlaid with WMA, DEMA, TEMA, HMA, and KAMA computed over a 20-day period; bottom panel shows cumulative returns for a simple price-above-MA long signal for each method versus a buy-and-hold benchmark, highlighting how lag reduction translates into earlier — but sometimes noisier — entry signals.


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. Results and Analysis

Running this implementation on SPY from 2022 through 2024 — a period containing a significant bear market, a sharp recovery, and a sustained bull phase — produces meaningfully different behavior across the five methods. TEMA and HMA consistently react earliest to trend reversals, often catching the 2022 bear leg within one to two additional days compared to a 20-day SMA. This speed comes with a cost: during the choppy, low-trend periods of mid-2022 and early 2023, both methods generate more false crossovers, which erodes returns through transaction costs.

KAMA stands out in low-efficiency (choppy) regimes. Because its smoothing constant shrinks when the efficiency ratio falls, it effectively flattens to a near-constant line during consolidation periods and only starts tracking price meaningfully when a clean directional move emerges. In the 2023-to-2024 rally, KAMA's signal onset arrives slightly later than TEMA's, but it produces fewer whipsaws in the months leading up to it.

WMA and DEMA occupy a middle ground — smoother than HMA and TEMA but more responsive than a plain EMA of the same length. For practitioners building a signal ensemble, these make useful complementary inputs. Running all five and voting on direction — or weighting by recent accuracy — is a straightforward upgrade over using any single method in isolation. Across the test period, the spread between the best and worst performing MA signals exceeded 12 percentage points in cumulative return, which underscores that the choice of smoothing method is not a cosmetic decision.

4. Use Cases

  • Trend-following entry filters: HMA and TEMA are well-suited as entry confirmation filters in momentum strategies. Because they minimize lag, a price-above-HMA condition fires closer to the actual breakout than a price-above-SMA condition at the same period length.

  • Adaptive regime detection: KAMA's efficiency ratio is itself a useful regime indicator. When KAMA is nearly flat, the market is in a low-efficiency (mean-reverting) regime; when it tracks price closely, the market is trending. You can extract the efficiency ratio directly from the KAMA computation loop and use it to switch between momentum and mean-reversion sub-strategies.

  • Multi-MA ribbon signals: Plotting a ribbon of moving averages — WMA, DEMA, TEMA, HMA, and KAMA at the same period — creates a visual and quantitative measure of trend agreement. When all five are aligned and ordered (e.g., TEMA > HMA > DEMA > WMA > KAMA from top to bottom in an uptrend), trend conviction is high. Disagreement among the ribbon lines signals a weakening or reversing trend.

  • Risk management overlays: Rather than using a moving average to generate entries, you can use KAMA or HMA as a trailing stop reference. Price closing below the HMA after a sustained uptrend is a structurally sound exit trigger that adapts to the instrument's own volatility rhythm.

5. Limitations and Edge Cases

Lag is never fully eliminated. TEMA and HMA reduce lag substantially, but they introduce overshoot — when price reverses sharply, these indicators can temporarily move in the wrong direction before correcting. In gap-heavy instruments or during earnings events, this overshoot can produce signals that feel counterintuitive.

KAMA is sensitive to its initialization period. The first period rows are NaN, and the very first KAMA value is seeded from the closing price at that index. On short data series (fewer than 60 bars), the efficiency ratio has not had time to stabilize, and KAMA behavior can appear erratic.

Transaction costs are punishing for low-lag methods. The extra signals generated by HMA and TEMA in choppy markets translate directly to extra round-trip costs. Always account for slippage and commissions in any backtest that uses these methods as entry triggers. The cumulative return comparisons shown here are gross of costs.

Lookahead bias in parameter tuning. Selecting PERIOD = 20 after inspecting the chart is a subtle form of data snooping. In a rigorous backtest, the period length should be fixed before viewing results, or chosen via walk-forward optimization on a held-out validation set.

Non-stationarity of optimal parameters. The KAMA fast/slow constants (2 and 30) and the period length that worked best on SPY in 2022-2024 will not necessarily transfer to a different ticker or a different market regime. Always validate on out-of-sample data before assuming generalizability.

Concluding Thoughts

Advanced moving average methods give you meaningful degrees of freedom beyond the SMA/EMA baseline. TEMA and HMA are powerful in trending environments where speed matters; KAMA earns its keep in noisy, regime-switching markets by adapting its own sensitivity. The practical takeaway is not to pick one and discard the rest — it is to understand the mechanics well enough to know which one fits the market condition you are currently operating in.

From here, productive next experiments include combining these filters with a volatility regime classifier (such as a rolling ATR ratio or a Hidden Markov Model) to select the appropriate MA method dynamically, or constructing a signal ensemble that weights each method by its recent accuracy on a rolling window. Both extensions are straightforward to build on top of the modular code structure introduced here.

If this kind of implementation-first, research-grade content is useful to you, the next part in this series covers spectral methods and Kalman filter-based adaptive smoothing — two techniques that reframe the moving average problem in terms of signal processing and state estimation. Follow along to keep building out your quantitative toolkit.


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)