Volatility is not constant. Anyone who has watched a calm market suddenly erupt into a week of wild swings — only to settle back into quiet drift — has witnessed volatility clustering firsthand. This phenomenon, where large price moves tend to be followed by more large moves (and calm periods by more calm), is one of the most reliable stylized facts in financial data. ARCH and GARCH models were designed specifically to capture this behavior, and they remain a cornerstone of quantitative risk management and derivatives pricing today.
In this article, we build a complete GARCH(1,1) volatility forecasting pipeline in Python. We download real equity return data using yfinance, fit an ARCH family model using the arch library, extract conditional volatility estimates, generate multi-step forecasts, and visualize the results in a way that is immediately interpretable. Every section is self-contained and runnable in your local environment or Google Colab.
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 — The Intuition Behind ARCH Models:** What volatility clustering is, why OLS fails to model it, and how ARCH/GARCH encode time-varying variance mathematically
- Section 2 — Python Implementation:** Full pipeline from data download to GARCH model fitting, conditional volatility extraction, rolling forecasting, and visualization with matplotlib
- Section 3 — Reading the Results:** What the fitted model parameters tell you and how to interpret forecast quality in practice
- Section 4 — Use Cases:** Where GARCH volatility forecasts provide genuine edge in real-world quant workflows
- Section 5 — Limitations and Edge Cases:** Honest assessment of where the model breaks down and what to watch for in production
1. The Intuition Behind ARCH Models
Imagine a river. Most days it flows calmly and predictably. But after a heavy storm, the current becomes turbulent — and critically, that turbulence does not vanish overnight. One storm day makes the next day more likely to be turbulent too. Financial markets behave in almost exactly the same way. A large shock to returns — an earnings miss, a central bank surprise, a geopolitical event — tends to elevate the variance of returns for days or weeks afterward. This is volatility clustering.
The problem with classical regression models is that they assume the variance of the error term is constant (homoskedasticity). In financial time series, this assumption is almost always violated. Robert Engle's 1982 paper introduced the Autoregressive Conditional Heteroskedasticity (ARCH) model to fix precisely this. Instead of treating variance as a fixed parameter, ARCH models the variance at time t as a function of past squared residuals. The intuition is simple: if yesterday's return shock was large, today's variance is likely to be elevated.
The more practical and widely used extension is the GARCH(1,1) model, introduced by Bollerslev in 1986. It adds a lagged variance term alongside the lagged squared residual, making the model far more parsimonious. The conditional variance equation looks like this:
σ²_t = ω + α · ε²_(t-1) + β · σ²_(t-1)
Here, ω is a long-run variance baseline, α measures how much yesterday's shock feeds into today's variance, and β measures how much yesterday's variance persists into today. When α + β is close to 1, the model exhibits high volatility persistence — shocks decay slowly, which matches what we typically observe in equity markets.
The power of GARCH is not just in-sample fit. It gives us a genuine forward-looking estimate of uncertainty — a volatility forecast — which can be used to size positions, price options, set stop-losses, or construct risk-adjusted signals. That is what we build next.
2. Python Implementation
2.1 Setup and Parameters
The implementation requires four libraries: yfinance for price data, arch for GARCH model fitting, pandas and numpy for data manipulation, and matplotlib for visualization. Install any missing packages with pip install yfinance arch.
Key configurable parameters are kept at the top for easy experimentation. Change TICKER to any equity, ETF, or index symbol. START_DATE and END_DATE define the historical window. FORECAST_HORIZON controls how many trading days ahead the model projects volatility. P and Q define the GARCH lag order — (1,1) is the standard starting point for most applications.
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import warnings
from arch import arch_model
warnings.filterwarnings("ignore")
# --- Configurable Parameters ---
TICKER = "SPY"
START_DATE = "2018-01-01"
END_DATE = "2024-12-31"
FORECAST_HORIZON = 10 # trading days ahead
P = 1 # GARCH lag order (variance)
Q = 1 # ARCH lag order (squared residuals)
ANNUALIZE = 252 # trading days per year for scaling
ROLLING_WINDOW = 60 # window for rolling re-estimation (days)
2.2 Data Download and Return Calculation
We pull adjusted closing prices from Yahoo Finance and compute log returns. Log returns are preferred over simple returns in volatility modeling because they are approximately normally distributed, additive across time, and better behaved in the tails. We scale returns by 100 before fitting — this keeps model coefficients in a numerically stable range for the optimizer.
# --- Download Price Data ---
raw = yf.download(TICKER, start=START_DATE, end=END_DATE, progress=False)
prices = raw["Close"].dropna()
# Log returns scaled by 100 for numerical stability
log_returns = np.log(prices / prices.shift(1)).dropna() * 100
print(f"Ticker : {TICKER}")
print(f"Samples : {len(log_returns)}")
print(f"Period : {log_returns.index[0].date()} → {log_returns.index[-1].date()}")
print(f"\nReturn Statistics:")
print(log_returns.describe().round(4))
2.3 GARCH Model Fitting and Volatility Extraction
We fit the GARCH(1,1) model on the full sample first to inspect parameters, then implement a rolling re-estimation loop to generate pseudo-out-of-sample conditional volatility and a terminal multi-step forecast. The rolling approach re-fits the model every ROLLING_WINDOW steps, which is more realistic than a single in-sample fit.
# --- Full-Sample GARCH Fit (for parameter inspection) ---
am = arch_model(log_returns, vol="Garch", p=P, q=Q, dist="normal", rescale=False)
res = am.fit(disp="off")
print(res.summary())
omega = res.params["omega"]
alpha = res.params["alpha[1]"]
beta = res.params["beta[1]"]
persistence = alpha + beta
print(f"\nomega : {omega:.6f}")
print(f"alpha (ARCH): {alpha:.4f}")
print(f"beta (GARCH): {beta:.4f}")
print(f"Persistence : {persistence:.4f} (closer to 1 = longer-lasting shocks)")
# --- Rolling Conditional Volatility (annualized %) ---
cond_vol = pd.Series(index=log_returns.index, dtype=float)
step = ROLLING_WINDOW
n = len(log_returns)
for end_idx in range(step, n):
window_returns = log_returns.iloc[:end_idx]
try:
m = arch_model(window_returns, vol="Garch", p=P, q=Q,
dist="normal", rescale=False)
r = m.fit(disp="off", show_warning=False)
# One-step-ahead conditional std dev (annualized)
cv = r.conditional_volatility.iloc[-1] * np.sqrt(ANNUALIZE)
cond_vol.iloc[end_idx] = cv
except Exception:
cond_vol.iloc[end_idx] = np.nan
cond_vol.dropna(inplace=True)
# --- Multi-Step Forecast from Full-Sample Model ---
forecasts = res.forecast(horizon=FORECAST_HORIZON, reindex=False)
fc_var = forecasts.variance.iloc[-1].values # variance series
fc_vol = np.sqrt(fc_var) * np.sqrt(ANNUALIZE) # annualized vol %
forecast_dates = pd.bdate_range(
start=log_returns.index[-1] + pd.Timedelta(days=1),
periods=FORECAST_HORIZON
)
fc_series = pd.Series(fc_vol, index=forecast_dates)
print(f"\n{FORECAST_HORIZON}-Day Annualized Volatility Forecast:")
print(fc_series.round(2).to_string())
2.4 Visualization
The chart overlays three data series: the raw log return (gray bars), the rolling conditional volatility estimate (blue line), and the forward forecast (red dashed line). This makes it immediately clear where the model identifies elevated risk periods — and where it expects volatility to be in the near future.
# --- Visualization ---
plt.style.use("dark_background")
fig, axes = plt.subplots(2, 1, figsize=(14, 9), sharex=False)
fig.suptitle(f"GARCH(1,1) Volatility Model — {TICKER}", fontsize=15, y=0.98)
# Panel 1: Log Returns
ax1 = axes[0]
ax1.bar(log_returns.index, log_returns.values,
color=np.where(log_returns.values >= 0, "#4CAF50", "#F44336"),
width=1, alpha=0.7)
ax1.axhline(0, color="white", linewidth=0.5, linestyle="--")
ax1.set_ylabel("Log Return (%)", fontsize=11)
ax1.set_title("Daily Log Returns", fontsize=12)
ax1.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
ax1.xaxis.set_major_locator(mdates.YearLocator())
# Panel 2: Conditional Volatility + Forecast
ax2 = axes[1]
ax2.plot(cond_vol.index, cond_vol.values,
color="#2196F3", linewidth=1.2, label="Rolling Conditional Vol (Annualized %)")
ax2.plot(fc_series.index, fc_series.values,
color="#FF5722", linewidth=2, linestyle="--",
marker="o", markersize=4, label=f"{FORECAST_HORIZON}-Day Forecast")
ax2.axhline(cond_vol.mean(), color="#FFC107", linewidth=0.8,
linestyle=":", label=f"Mean Vol ({cond_vol.mean():.1f}%)")
ax2.set_ylabel("Annualized Volatility (%)", fontsize=11)
ax2.set_title("GARCH Conditional Volatility and Forward Forecast", fontsize=12)
ax2.legend(fontsize=9)
ax2.xaxis.set_major_formatter(mdates.DateFormatter("%Y"))
ax2.xaxis.set_major_locator(mdates.YearLocator())
plt.tight_layout()
plt.savefig("garch_volatility_forecast.png", dpi=150, bbox_inches="tight")
plt.show()
Figure 1. Top panel shows daily SPY log returns with green/red coloring by sign; bottom panel shows the rolling GARCH(1,1) conditional volatility (blue) with clear spikes during the 2020 COVID crash and 2022 Fed rate cycle, alongside the 10-day forward volatility forecast (red dashed) and the historical mean volatility reference line (yellow dotted).
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. Reading the Results
For SPY over the 2018–2024 window, a typical GARCH(1,1) fit yields an alpha near 0.08–0.12 and a beta near 0.85–0.88, giving a persistence value (alpha + beta) of roughly 0.93–0.97. This confirms what practitioners know: equity volatility is highly persistent. A volatility shock caused by an event like the March 2020 COVID crash takes weeks or months to fully decay back to baseline — not days.
The rolling conditional volatility chart makes this concrete. During the March 2020 selloff, the model correctly elevates estimated volatility above 70% annualized before gradually mean-reverting over the following months. The 2022 Fed tightening cycle produces a second, more sustained elevation, in the 25–40% range. In calmer 2019 and 2023 periods, the model correctly holds volatility close to its historical mean of roughly 15–18% for SPY.
The 10-day forward forecast shows mean-reversion behavior — a property baked into the GARCH structure. If the current conditional variance is above the long-run average, the forecast decays downward toward ω / (1 - α - β) over the horizon. If volatility is currently suppressed, the forecast drifts upward. This unconditional long-run variance is a useful anchor for option pricing and risk budgeting decisions.
4. Use Cases
Position Sizing: Use the GARCH volatility forecast as an inverse denominator for position size. When forecast volatility is elevated, reduce exposure proportionally. This is the core of volatility-targeting strategies used by risk parity and CTA funds.
Options Pricing and IV Comparison: Compare the GARCH-implied volatility forecast against the market's implied volatility (from options prices). A statistically significant divergence between realized GARCH vol and IV can signal relative value trades — buying options when IV is cheap relative to forecast, or selling when it is rich.
Regime Detection: Define volatility regimes by thresholding the conditional vol estimate (e.g., low < 15%, normal 15–25%, high > 25%). Use the regime label as a filter or feature in a broader trading strategy — for example, avoiding mean-reversion strategies during high-volatility regimes.
Risk Management and VaR: Feed the GARCH conditional variance directly into a parametric Value-at-Risk calculation. Unlike historical VaR using a fixed rolling window, GARCH-based VaR adapts dynamically to current market conditions, providing more accurate tail risk estimates during stress periods.
5. Limitations and Edge Cases
Distribution Assumption: The basic GARCH(1,1) with normal errors underestimates tail risk. Financial returns exhibit excess kurtosis — fat tails. Switching to a Student-t or skewed-t distribution (
dist="t"in thearchlibrary) is a straightforward improvement that materially improves out-of-sample performance during crisis episodes.Structural Breaks: GARCH assumes a stationary variance process with a fixed unconditional mean. Structural breaks — like a permanent shift in market microstructure or a change in central bank policy regime — can cause the model to mis-estimate the long-run volatility level. Regime-switching GARCH variants (e.g., Markov-switching GARCH) address this but at significant added complexity.
Forecast Horizon Degradation: GARCH forecasts are most reliable over short horizons (1–10 days). As the forecast horizon extends, all paths converge toward the unconditional variance, and the model loses discriminatory power. Do not over-rely on 60-day GARCH forecasts — they add little beyond the historical mean.
Asymmetry (Leverage Effect): Standard GARCH treats positive and negative shocks of equal magnitude identically. In equity markets, negative shocks typically increase volatility more than positive shocks of the same size (the leverage effect). The EGARCH or GJR-GARCH specifications capture this asymmetry and generally outperform symmetric GARCH on equity data.
Overfitting on Short Windows: The rolling re-estimation loop can overfit on small windows during low-data periods. Keep the minimum estimation window above 120 observations, and monitor the optimizer convergence — failed fits should be handled gracefully (as shown with the
try/exceptblock) rather than silently propagating errors.
Concluding Thoughts
GARCH models occupy a well-earned place in the quantitative practitioner's toolkit precisely because they solve a real, measurable problem: variance in financial markets is not constant, and pretending otherwise leads to poorly calibrated risk estimates and position sizes. The GARCH(1,1) is the workhorse — parsimonious, interpretable, and empirically robust across asset classes and time periods.
The natural next steps from here are meaningful. Swap the normal distribution for Student-t errors and observe the improvement in tail coverage. Implement GJR-GARCH to capture the leverage effect on individual equities. Use the conditional volatility output as a feature in a machine learning classifier for volatility regime prediction. Or integrate the forecast directly into a Kelly-criterion-based position sizing engine. Each of these extensions builds directly on the foundation laid in this article.
If you found this walkthrough useful, more implementations at this depth — covering pairs trading, HMM regime detection, Kalman filters, and options strategies — are published regularly. Follow along to keep building your quant toolkit one rigorous, fully coded piece at a time.
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)