Volatility is not constant. Anyone who has watched equity markets during an earnings announcement or a macro shock knows that large moves tend to cluster together — calm periods are interrupted by bursts of turbulence, and that turbulence persists for days or weeks before fading. Modeling this behavior accurately is one of the most practically important problems in quantitative finance, underpinning everything from options pricing to risk management and portfolio construction.
In this article we implement a complete volatility forecasting pipeline using ARCH and GARCH models in Python. We pull real price data with yfinance, compute log returns, fit a GARCH(1,1) model using the arch library, extract conditional volatility estimates, generate rolling out-of-sample forecasts, and visualize the results. By the end you will have a working research notebook you can drop onto any ticker.
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 — What ARCH/GARCH Models Are**: Intuitive explanation of volatility clustering, the ARCH mechanism, and the GARCH extension. No unnecessary math — just enough to understand what the model is doing.
- Section 2 — Python Implementation**: Full working code split across setup and parameters (2.1), data acquisition and return computation (2.2), model fitting and diagnostics (2.3), and visualization of conditional volatility with forecasts (2.4).
- Section 3 — Results and Analysis**: What the fitted model reveals about real volatility dynamics, how to read the parameter output, and what realistic forecasting accuracy looks like.
- Section 4 — Use Cases**: Practical applications of GARCH-based volatility estimates in trading and risk workflows.
- Section 5 — Limitations and Edge Cases**: Where GARCH breaks down, distributional assumptions, and what to watch for in production.
1. Understanding Volatility Clustering and ARCH Models
If you plot the daily returns of almost any liquid financial asset, you will immediately notice something: the returns themselves look roughly unpredictable, but their magnitudes cluster. Big moves follow big moves. Quiet days follow quiet days. This phenomenon — known as volatility clustering — was first documented rigorously by Benoit Mandelbrot in the 1960s and became the empirical motivation for Robert Engle's 1982 paper introducing the ARCH model, work that eventually earned him a Nobel Prize in Economics.
The key insight of ARCH (Autoregressive Conditional Heteroskedasticity) is that while we may not be able to predict the direction of tomorrow's return, we can say something useful about its likely variance. In an ARCH(q) model, today's conditional variance is modeled as a weighted sum of the past q squared return innovations. Intuitively: if the last few returns were large in magnitude, the model expects the next one to also be volatile. The variance is not fixed — it is conditional on recent history.
GARCH (Generalized ARCH), introduced by Tim Bollerslev in 1986, extends this by also including lagged variance terms alongside lagged squared returns. The GARCH(1,1) model — one lag of squared returns, one lag of variance — is by far the most widely used specification in practice. It can be written compactly as:
σ²ₜ = ω + α·εₜ₋₁² + β·σ²ₜ₋₁
Here, ω is a long-run variance baseline, α captures how quickly the model reacts to new shocks, and β governs how persistent those shocks are. When α + β is close to 1, volatility is highly persistent — a property consistently observed in equity markets. Think of it like a thermostat with memory: a cold snap raises the baseline temperature estimate, and that elevated estimate decays only slowly back toward normal.
2. Python Implementation
2.1 Setup and Parameters
The implementation requires four libraries: yfinance for price data, pandas and numpy for data manipulation, arch for model fitting, and matplotlib for visualization. Install any missing packages with pip install yfinance arch.
The key configurable parameters are the ticker symbol, the historical window length, and the GARCH order. For most equity applications GARCH(1,1) is the right starting point — higher-order models rarely improve out-of-sample performance enough to justify the added complexity.
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf
from arch import arch_model
# ── Configurable Parameters ──────────────────────────────────────────────────
TICKER = "SPY" # Any yfinance-supported ticker
START_DATE = "2018-01-01"
END_DATE = "2024-12-31"
GARCH_P = 1 # Lag order for conditional variance (GARCH term)
GARCH_Q = 1 # Lag order for squared residuals (ARCH term)
DIST = "Normal" # Error distribution: "Normal", "t", "skewt"
SCALE = 100 # Scale returns to percent — improves optimizer stability
FORECAST_HORIZON = 10 # Days ahead to forecast conditional volatility
2.2 Data Acquisition and Return Computation
We download adjusted closing prices, compute continuously compounded log returns, and drop any NaN values introduced by the first difference. Scaling returns by 100 (expressing them as percentage returns) is a standard preprocessing step when using the arch library — it keeps parameter magnitudes numerically stable and prevents the optimizer from running into convergence issues with very small floating-point values.
# ── Download Price Data ───────────────────────────────────────────────────────
raw = yf.download(TICKER, start=START_DATE, end=END_DATE, progress=False)
prices = raw["Close"].squeeze()
# Log returns scaled to percent
log_returns = np.log(prices / prices.shift(1)).dropna() * SCALE
print(f"Ticker : {TICKER}")
print(f"Obs : {len(log_returns)}")
print(f"Period : {log_returns.index[0].date()} → {log_returns.index[-1].date()}")
print(f"\nReturn summary (%):")
print(log_returns.describe().round(4))
2.3 Model Fitting and Diagnostics
We instantiate a GARCH(1,1) model with a Normal error distribution and fit it using maximum likelihood. After fitting, we print the model summary — pay particular attention to the ω, α, and β parameter estimates and their p-values, and to the sum α + β which tells you how persistent volatility shocks are in this series.
We then extract the in-sample conditional volatility series (annualized by multiplying by √252) and generate a multi-step forecast for the next FORECAST_HORIZON trading days.
# ── Fit GARCH(1,1) Model ──────────────────────────────────────────────────────
am = arch_model(
log_returns,
vol="Garch",
p=GARCH_P,
q=GARCH_Q,
dist=DIST,
rescale=False
)
result = am.fit(disp="off")
print(result.summary())
# ── Extract Conditional Volatility (annualized) ───────────────────────────────
cond_vol_daily = result.conditional_volatility # percent, daily
cond_vol_annual = cond_vol_daily * np.sqrt(252) # annualized percent
# ── Rolling Out-of-Sample Forecast ───────────────────────────────────────────
forecasts = result.forecast(horizon=FORECAST_HORIZON, reindex=False)
# Forecast variance → daily vol → annualized vol
forecast_var = forecasts.variance.iloc[-1] # shape: (horizon,)
forecast_vol = np.sqrt(forecast_var) * np.sqrt(252) # annualized percent
forecast_dates = pd.bdate_range(
start=log_returns.index[-1] + pd.Timedelta(days=1),
periods=FORECAST_HORIZON
)
forecast_series = pd.Series(forecast_vol.values, index=forecast_dates)
print(f"\n{FORECAST_HORIZON}-Day Annualized Volatility Forecast (%):")
print(forecast_series.round(2).to_string())
# ── Key Model Statistics ──────────────────────────────────────────────────────
params = result.params
alpha = params.get("alpha[1]", np.nan)
beta = params.get("beta[1]", np.nan)
print(f"\nα (ARCH term) : {alpha:.4f}")
print(f"β (GARCH term): {beta:.4f}")
print(f"α + β : {alpha + beta:.4f} ← persistence (closer to 1 = more persistent)")
2.4 Visualization
The chart below shows three panels: the raw percentage log returns (highlighting the heteroskedastic clustering pattern), the in-sample annualized conditional volatility estimated by the GARCH model, and the 10-day out-of-sample volatility forecast. Look for how the conditional volatility spikes during the COVID crash of 2020 and the rate-shock period of 2022 — and how quickly or slowly it mean-reverts afterward.
# ── Visualization ─────────────────────────────────────────────────────────────
plt.style.use("dark_background")
fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=False)
fig.suptitle(
f"GARCH(1,1) Volatility Model — {TICKER} | {START_DATE} to {END_DATE}",
fontsize=13, fontweight="bold", y=0.98
)
# Panel 1 — Log Returns
axes[0].plot(log_returns.index, log_returns.values, color="#4FC3F7", linewidth=0.6, alpha=0.85)
axes[0].axhline(0, color="white", linewidth=0.4, linestyle="--", alpha=0.4)
axes[0].set_ylabel("Return (%)", fontsize=10)
axes[0].set_title("Daily Log Returns (scaled to %)", fontsize=10, pad=6)
# Panel 2 — In-Sample Conditional Volatility
axes[1].fill_between(
cond_vol_annual.index, cond_vol_annual.values,
alpha=0.45, color="#FF7043"
)
axes[1].plot(cond_vol_annual.index, cond_vol_annual.values, color="#FF7043", linewidth=0.8)
axes[1].set_ylabel("Ann. Vol (%)", fontsize=10)
axes[1].set_title("GARCH(1,1) Conditional Volatility — Annualized", fontsize=10, pad=6)
# Panel 3 — Out-of-Sample Forecast
axes[2].plot(
forecast_series.index, forecast_series.values,
color="#FFCA28", linewidth=1.8, marker="o", markersize=5, label="Forecast Vol"
)
last_realized = cond_vol_annual.iloc[-1]
axes[2].axhline(
last_realized, color="white", linestyle="--", linewidth=0.8, alpha=0.5,
label=f"Last Realized Vol: {last_realized:.1f}%"
)
axes[2].set_ylabel("Ann. Vol (%)", fontsize=10)
axes[2].set_title(f"{FORECAST_HORIZON}-Day Forward Volatility Forecast", fontsize=10, pad=6)
axes[2].legend(fontsize=9)
for ax in axes:
ax.tick_params(axis="both", labelsize=9)
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.tight_layout()
plt.savefig("garch_volatility_forecast.png", dpi=150, bbox_inches="tight")
plt.show()
Figure 1. Three-panel GARCH(1,1) output for SPY: daily percentage log returns (top), in-sample annualized conditional volatility with clear clustering during 2020 and 2022 stress periods (middle), and a 10-day forward annualized volatility forecast with the last realized level shown as a reference line (bottom).
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 notebook on SPY from 2018 through 2024 produces a well-identified GARCH(1,1) model with typical equity-market parameter values. You should observe an α (ARCH term) in the range of 0.08–0.15 and a β (GARCH term) in the range of 0.82–0.90, yielding a persistence sum α + β of approximately 0.95–0.98. This high persistence is a defining feature of equity volatility: shocks do not dissipate quickly. A volatility spike caused by a macro event will still be partially visible in the conditional variance estimate one to two weeks later.
The conditional volatility series makes the clustering behavior quantitatively explicit. During the COVID crash in late February and March 2020, annualized conditional volatility on SPY surged to 60–80% before gradually mean-reverting to the 12–18% range by mid-2021. The 2022 rate-hiking cycle produced a second, more sustained period of elevated volatility in the 22–30% range. These are not just visual features — they are parameters the model has learned and can project forward.
The 10-day forward forecast produced by result.forecast() will typically show a slight mean-reversion path: if current volatility is elevated, the forecast will trend downward toward the long-run variance implied by ω / (1 − α − β); if it is below average, it will trend upward. The practical value of this is not in its point-forecast accuracy — no model predicts exact volatility — but in providing a calibrated, model-consistent estimate you can feed directly into a position-sizing algorithm, a VaR calculator, or an options pricing engine.
4. Use Cases
Dynamic position sizing: Scale trade size inversely with GARCH-implied volatility. When the model forecasts high volatility, reduce exposure; when it forecasts calm conditions, increase it. This is a principled, data-driven alternative to fixed-fraction sizing.
Options strategy selection: A GARCH forecast provides an estimate of realized volatility over the next N days. Comparing it to the implied volatility surface gives a quantitative signal for strategies like variance swaps, straddles, or covered calls — if GARCH vol is materially below implied vol, selling premium has a statistical edge.
Value-at-Risk and CVaR estimation: GARCH conditional volatility is a direct input to parametric VaR models. By updating the volatility estimate daily with new return data, the risk model remains responsive to current market conditions rather than relying on a static rolling-window standard deviation.
Regime detection: Tracking the level and rate of change of GARCH conditional volatility provides a lightweight, model-free regime signal. Sustained readings above a threshold (e.g., annualized vol > 25%) can be used to switch portfolio construction logic or hedge ratios.
5. Limitations and Edge Cases
Distributional misspecification. The Normal error distribution underestimates the probability of extreme returns. In practice, equity returns have fat tails. Switching dist to "t" (Student's t) or "skewt" (skewed t) typically produces better log-likelihood and more realistic tail risk estimates. This is a one-line change in the setup parameters.
Structural breaks. GARCH models assume a stationary variance process with fixed parameters. They do not handle structural breaks — persistent regime shifts in the level of volatility driven by fundamental changes in market microstructure or macro policy. A model fit on pre-2020 data will be slow to adapt to post-COVID market dynamics without periodic re-estimation.
Single-asset scope. The model as implemented operates on a single return series. For portfolio risk management you need a multivariate extension (DCC-GARCH, BEKK) to capture time-varying correlations across assets. Summing univariate GARCH volatilities ignores diversification effects.
Forecast horizon degradation. GARCH forecasts mean-revert rapidly toward the unconditional variance. Beyond 20–30 trading days, multi-step forecasts carry very little information beyond the long-run average. Use short-horizon forecasts (1–5 days) for tactical decisions and treat longer horizons with appropriate skepticism.
Overfitting higher-order models. It is tempting to try GARCH(2,2) or EGARCH to capture asymmetric leverage effects. These can improve in-sample fit while degrading out-of-sample performance. Always validate on a held-out test set before promoting a more complex specification.
Concluding Thoughts
ARCH and GARCH models remain the workhorses of volatility forecasting in quantitative finance precisely because they capture the most robust empirical feature of financial returns: that large moves beget large moves. The GARCH(1,1) model implemented here — despite being over 35 years old — consistently outperforms naive rolling-window volatility estimates in out-of-sample tests on equity data, and it serves as the foundation layer beneath more sophisticated approaches like regime-switching volatility, stochastic volatility, and realized variance models.
The most productive next experiments from this codebase are: swapping the error distribution to Student's t and comparing log-likelihoods; implementing a rolling re-estimation loop to generate a true out-of-sample conditional volatility series; and testing EGARCH to capture the leverage effect (the asymmetry between negative and positive return shocks on subsequent volatility). Each of these changes is a small modification to the code above but meaningfully expands the model's realism.
If you found this walkthrough useful, the same research-notebook format applies to every strategy in this series — from ARIMA-GARCH hybrid forecasters to Hidden Markov Model regime classifiers. Follow along for the next implementation, and feel free to drop questions or results from your own ticker experiments in the comments.
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)