DEV Community

Ayrat Murtazin
Ayrat Murtazin

Posted on

Top 36 Moving Average Methods for Stock Prices in Python: Part 1

Moving averages are the backbone of technical analysis and quantitative trading systems. Despite their apparent simplicity, the choice of moving average method significantly affects signal timing, noise reduction, lag, and ultimately the profitability of any trend-following strategy. Most practitioners default to the simple moving average without exploring the richer landscape of alternatives — many of which offer materially better performance in specific market regimes.

This article is the first in a four-part series covering 36 moving average methods implemented in Python. In this installment, we implement nine foundational methods — from the classic Simple Moving Average (SMA) through to the Triple Exponential Moving Average (TEMA) — compute them on live price data fetched via yfinance, and visualize them side by side for direct comparison. Each method is explained from first principles before the code is introduced.


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

This article covers:

  • Section 1 — The Lag Problem:** Why standard moving averages fail trend traders and what the mathematical trade-off looks like
  • Section 2 — Python Implementation:** Full setup, data fetching, computation of nine MA methods, and a comparative visualization
  • Section 3 — Results and Comparative Analysis:** What the chart reveals about smoothness vs. responsiveness across all nine methods
  • Section 4 — Use Cases:** Where each method category is most effective in real trading systems
  • Section 5 — Limitations and Edge Cases:** Honest constraints — overfitting risk, look-ahead bias, and regime sensitivity

1. The Lag Problem in Moving Averages

Every moving average is a low-pass filter. It suppresses high-frequency noise in price data — the random tick-to-tick fluctuations — while attempting to preserve the lower-frequency trend signal. The fundamental tension is that the more aggressively you smooth, the more you delay. A 200-period SMA is extremely smooth, but by the time it confirms a trend reversal, a substantial portion of the move has already occurred.

Think of it like steering a large ship: the longer your lookback window, the harder it is to turn quickly when conditions change. Short windows respond faster but generate more false signals — the classic bias-variance trade-off applied to time series filtering. This trade-off is not just a nuisance; it directly translates to slippage, missed entries, and drawdown in live systems.

The 36 methods in this series represent different engineering solutions to the same core problem. Some methods — like the Weighted Moving Average (WMA) or Exponential Moving Average (EMA) — assign higher weights to recent prices so the average responds faster without dramatically shortening the window. Others, like DEMA and TEMA, apply algebraic combinations of EMAs to cancel out lag almost entirely, at the cost of increased sensitivity to whipsaws.

Understanding which method to use is not a matter of finding the single best filter. It is a matter of matching the filter's characteristics to your strategy's holding period, the asset's volatility regime, and your tolerance for false signals. The implementations that follow give you the tools to make that judgment empirically.

2. Python Implementation

2.1 Setup and Parameters

The parameters below control the ticker, date range, and the primary lookback window applied to all nine methods. Changing WINDOW from 20 to 50 shifts all methods toward slower, trend-following behaviour. The TICKER can be any symbol supported by yfinance — equities, ETFs, or crypto pairs.

# ── 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 matplotlib.dates as mdates
import warnings
warnings.filterwarnings("ignore")

# ── Configuration ─────────────────────────────────────────────
TICKER     = "SPY"          # Any yfinance-compatible symbol
START      = "2023-01-01"
END        = "2024-12-31"
WINDOW     = 20             # Lookback period for all MA methods
PRICE_COL  = "Close"        # Column to smooth

# ── Data Fetch ────────────────────────────────────────────────
raw = yf.download(TICKER, start=START, end=END, auto_adjust=True, progress=False)
df  = raw[[PRICE_COL]].copy().dropna()
df.index = pd.to_datetime(df.index)
print(f"Loaded {len(df)} trading days for {TICKER}")
print(df.tail(3))
Enter fullscreen mode Exit fullscreen mode

Implementation chart

2.2 Computing Nine Moving Average Methods

Each method is added as a new column on df. The formulas are implemented from scratch where possible so you can see exactly what each method computes — no black-box wrappers.

n = WINDOW
c = df[PRICE_COL]

# 1. Simple Moving Average (SMA) ───────────────────────────────
# Unweighted mean of the last n closes.
df["SMA"] = c.rolling(n).mean()

# 2. Exponential Moving Average (EMA) ─────────────────────────
# Geometric decay: alpha = 2/(n+1). More weight on recent prices.
df["EMA"] = c.ewm(span=n, adjust=False).mean()

# 3. Weighted Moving Average (WMA) ────────────────────────────
# Linear weights: most recent bar gets weight n, oldest gets 1.
weights = np.arange(1, n + 1, dtype=float)
df["WMA"] = c.rolling(n).apply(
    lambda x: np.dot(x, weights) / weights.sum(), raw=True
)

# 4. Double Exponential Moving Average (DEMA) ─────────────────
# DEMA = 2*EMA(n) - EMA(EMA(n))  → cancels one layer of EMA lag.
ema1       = c.ewm(span=n, adjust=False).mean()
ema2       = ema1.ewm(span=n, adjust=False).mean()
df["DEMA"] = 2 * ema1 - ema2

# 5. Triple Exponential Moving Average (TEMA) ─────────────────
# TEMA = 3*EMA - 3*EMA(EMA) + EMA(EMA(EMA)) → cancels two layers.
ema3       = ema2.ewm(span=n, adjust=False).mean()
df["TEMA"] = 3 * ema1 - 3 * ema2 + ema3

# 6. Hull Moving Average (HMA) ────────────────────────────────
# HMA = WMA(2*WMA(n/2) - WMA(n), sqrt(n))  → low lag + smooth.
half_n     = max(1, n // 2)
sqrt_n     = max(1, int(np.sqrt(n)))

def _wma(series, period):
    w = np.arange(1, period + 1, dtype=float)
    return series.rolling(period).apply(
        lambda x: np.dot(x, w) / w.sum(), raw=True
    )

raw_hull   = 2 * _wma(c, half_n) - _wma(c, n)
df["HMA"]  = _wma(raw_hull, sqrt_n)

# 7. Triangular Moving Average (TMA / TRIMA) ──────────────────
# SMA of an SMA → double-smoothed, bell-shaped weight distribution.
half_w     = (n + 1) // 2
df["TMA"]  = c.rolling(half_w).mean().rolling(half_w).mean()

# 8. Volume-Weighted Moving Average (VWMA) ────────────────────
# Price weighted by volume — tracks where the real money traded.
vol        = raw["Volume"].reindex(df.index)
pv         = c * vol
df["VWMA"] = pv.rolling(n).sum() / vol.rolling(n).sum()

# 9. Least-Squares Moving Average (LSMA / LinReg MA) ──────────
# Endpoint of an OLS regression fitted over the last n bars.
df["LSMA"] = c.rolling(n).apply(
    lambda y: np.polyval(np.polyfit(np.arange(len(y)), y, 1), len(y) - 1),
    raw=True
)

df.dropna(inplace=True)
print(f"\nMA columns computed. Shape after dropna: {df.shape}")
print(df[["SMA","EMA","WMA","DEMA","TEMA","HMA","TMA","VWMA","LSMA"]].tail(3))
Enter fullscreen mode Exit fullscreen mode

2.3 Lag Quantification

Before visualizing, we measure each method's average absolute lag relative to the raw price series using a simple cross-correlation peak offset. Lower values indicate a more responsive filter.

ma_cols = ["SMA","EMA","WMA","DEMA","TEMA","HMA","TMA","VWMA","LSMA"]

lag_results = {}
price_norm = (c.reindex(df.index) - c.mean()) / c.std()

for col in ma_cols:
    ma_norm = (df[col] - df[col].mean()) / df[col].std()
    # Cross-correlation: positive lag = MA trails price
    xcorr = np.correlate(price_norm.values, ma_norm.values, mode="full")
    lags  = np.arange(-(len(price_norm)-1), len(price_norm))
    peak_lag = lags[np.argmax(xcorr)]
    lag_results[col] = abs(peak_lag)

lag_df = pd.Series(lag_results).sort_values()
print("\nEstimated lag (bars) by method:")
print(lag_df.to_string())
Enter fullscreen mode Exit fullscreen mode

2.4 Visualization

The chart overlays all nine moving averages on the SPY close price. The most lag-sensitive methods (TEMA, DEMA, HMA) should track price most closely, while TMA and SMA will appear most displaced during sharp moves.

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

ax1 = axes[0]
ax2 = axes[1]

colors = {
    "SMA":  "#8888ff", "EMA":  "#55ccff", "WMA":  "#ffaa00",
    "DEMA": "#ff4466", "TEMA": "#ff0000", "HMA":  "#00ff88",
    "TMA":  "#cc88ff", "VWMA": "#ffff44", "LSMA": "#ff8800",
}
styles = {
    "SMA": "-",  "EMA": "-",  "WMA": "--",
    "DEMA":"--", "TEMA":":",  "HMA": "-",
    "TMA": "-.", "VWMA":"-.", "LSMA":":",
}

ax1.plot(df.index, df[PRICE_COL], color="white", lw=1.2,
         alpha=0.5, label=f"{TICKER} Close", zorder=1)

for col in ma_cols:
    ax1.plot(df.index, df[col],
             color=colors[col], lw=1.4,
             linestyle=styles[col], label=col, alpha=0.85)

ax1.set_title(f"{TICKER} — Nine Moving Average Methods (Window = {WINDOW})",
              fontsize=13, pad=10)
ax1.set_ylabel("Price (USD)", fontsize=10)
ax1.legend(loc="upper left", fontsize=7.5, ncol=5,
           framealpha=0.3, facecolor="#111111")
ax1.xaxis.set_major_formatter(mdates.DateFormatter("%b '%y"))
ax1.grid(alpha=0.15)

# Bottom panel: lag bar chart
ax2.barh(lag_df.index, lag_df.values,
         color=[colors[c] for c in lag_df.index], alpha=0.85)
ax2.set_xlabel("Estimated Lag (bars)", fontsize=9)
ax2.set_title("Cross-Correlation Lag Estimate by Method", fontsize=10)
ax2.grid(axis="x", alpha=0.2)

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

Figure 1. Top panel: SPY closing price with all nine moving averages overlaid for a 20-bar window; methods with near-zero lag (TEMA, HMA) hug price tightly while TMA and SMA trail visibly during trending periods. Bottom panel: cross-correlation lag estimate in bars — lower values indicate a more responsive filter.


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. Comparative Analysis

Running the above on SPY for 2023–2024 with a 20-bar window produces a clear hierarchy. TEMA and DEMA register the lowest lag estimates (often 0–1 bars) because their construction algebraically cancels EMA delay. The HMA follows closely, typically 1–2 bars behind price. The SMA and TMA consistently show the highest lag — on a trending day, the SMA can trail price by 8–12 bars, meaning the signal arrives well after the optimal entry.

The VWMA is the most interesting outlier. Its lag estimate is similar to the EMA, but during high-volume trending sessions it diverges meaningfully from price-only methods, anchoring closer to the volume-weighted fair value. This makes it particularly useful for intraday and swing strategies on liquid ETFs where institutional order flow clusters around specific price levels.

One practical finding from this comparison: the lowest-lag methods (TEMA, HMA) are not uniformly superior. In choppy, mean-reverting regimes — which characterized parts of 2023 — their tight tracking of price produces excessive whipsaws. The SMA and TMA, despite their lag, generate far fewer false crossovers during consolidation. The right choice is regime-dependent, which is the central lesson of this series.

4. Use Cases

  • Trend-following systems (momentum): TEMA and HMA are well-suited for medium-frequency trend strategies (daily/4H) because their low lag allows earlier entry and exit signals. Pair with an ATR-based stop to manage the increased whipsaw risk.

  • Mean-reversion systems: TMA and SMA are preferred as the reference line in Bollinger Band or Keltner Channel strategies, where excessive responsiveness would cause the bands to contract and expand erratically.

  • Volume-informed signals: VWMA is the natural choice for ETF and large-cap equity strategies where institutional participation is high. A price crossover of the VWMA carries more informational weight than an SMA crossover because it incorporates where volume actually traded.

  • Regression-based entries: LSMA is useful for slope-based regime detection — the sign and magnitude of the slope of the regression line provides a clean, continuous measure of trend strength that integrates naturally into rule-based systems.

5. Limitations and Edge Cases

  • Look-ahead bias: All methods here use only past data, but if you compute them in a vectorized backtest on the full price series, ensure your signal generation correctly shifts by one bar before acting. Failure to do so introduces look-ahead bias and will inflate backtest returns.

  • Overfitting to window size: The performance ranking of these methods is sensitive to the window parameter and the specific date range. A TEMA tuned to 20 bars on 2023 SPY data is not guaranteed to outperform on 2025 data or on a different asset. Always test out-of-sample.

  • Regime sensitivity: Low-lag methods (TEMA, DEMA, HMA) degrade in choppy markets. Without a regime filter — such as ADX or a volatility percentile — they generate excessive false signals during consolidation, eroding edge.

  • Computation on sparse data: The LSMA and HMA rely on rolling windows of rolling windows. On assets with gaps (halted stocks, thinly traded names) or very short histories, these methods can produce NaN-heavy outputs. Always call dropna() and verify the output length before backtesting.

  • VWMA requires reliable volume data: For assets where reported volume is unreliable (some crypto exchanges, OTC equities), the VWMA can be distorted. Validate volume data quality before using it as a weighting factor.

Concluding Thoughts

This first installment establishes the computational foundation for all 36 methods. The nine filters covered here — SMA, EMA, WMA, DEMA, TEMA, HMA, TMA, VWMA, and LSMA — span the core design space of price smoothing: equal weighting, exponential decay, linear weighting, lag cancellation, double smoothing, geometric construction, volume weighting, and regression fitting. Having all nine implemented on a shared DataFrame makes it straightforward to run systematic comparisons on any asset and timeframe.

The key takeaway from Part 1 is that no single method dominates across all market conditions. The lag-responsiveness trade-off is real and regime-dependent. The practical next step is to implement a simple regime classifier — even a basic volatility percentile threshold — and switch between a low-lag method and a high-smoothing method depending on the current regime. That pattern will recur throughout this series.

Parts 2 through 4 extend this foundation into adaptive methods (Kaufman AMA, VIDYA, McGinley Dynamic), statistical filters (Kalman, Hodrick-Prescott, wavelet-based), and composite indicators that combine several of these methods into unified trading signals. Each part ships with a full runnable notebook covering real data, out-of-sample validation, and production-ready signal generation logic.


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)