Introduction

On October 19, 1987, the Dow Jones fell 22 % in a single day. The day after, it swung violently again. The following weeks stayed turbulent. Weeks later, calm returned — and held.

That is volatility clustering: large price changes tend to be followed by more large changes, and small ones by small ones. The market does not simply flip between risky and safe at random. Turbulence has memory.

Plain statistical models miss this entirely. If you estimate a single standard deviation for the whole series, you assume every day is equally risky. That assumption is wrong — and dangerously so for anyone pricing options, computing capital reserves, or managing a portfolio.

In 1982 the economist Robert F. Engle published the ARCH model (Autoregressive Conditional Heteroskedasticity), which let volatility vary over time and depend on past shocks. His student Tim Bollerslev extended it in 1986 into GARCH (Generalized ARCH), the version used in virtually every quantitative finance desk today. Engle received the 2003 Nobel Prize in Economics for this work.

The core idea is elegant: yesterday's squared surprise, together with yesterday's estimated variance, predicts today's variance. Calm breeds calm; storms breed storms — until the process reverts toward its long-run average.

Try It

The chart below simulates a GARCH(1,1) process. Each bar is one time step: the height shows the return (price change) and the orange curve tracks the conditional standard deviation — the model's live forecast of how wild the next step might be.

<!-- GARCH(1,1) volatility clustering demo -->
<div class="controls">
  <button id="btn-run" type="button">Run</button>
  <button id="btn-shock" type="button">Shock</button>
  <button id="btn-reset" type="button" class="ghost">Reset</button>
  <span class="label-vol">Current vol (σ): <strong id="cur-vol">—</strong></span>
</div>
<canvas id="chart" width="560" height="260"></canvas>
<p class="hint" id="hint-msg">Press Run to simulate returns, or Shock to inject a big move.</p>
/* chart and controls layout */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
#chart { width: 100%; height: auto; display: block; border: 1px solid #dce4ed; border-radius: 8px; background: #f7f9fb; }
.label-vol { font-size: .85rem; color: #555; margin-left: auto; }
.hint { font-size: .82rem; color: #666; margin: .5rem 0 0; min-height: 1.3em; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// GARCH(1,1) parameters — GARCH(1,1)
const OMEGA = 0.05;  // baseline variance (long-run floor)
const ALPHA = 0.10;  // ARCH coefficient — reaction to last shock
const BETA  = 0.85;  // GARCH coefficient — persistence of last variance
// stationarity holds because alpha + beta = 0.95
const MAX_BARS = 80; // max bars shown at once

// simulation state
let returns = [];
let variances = [];
let running = false;
let animId = null;

function garchStep(prevVar, prevEps) {
  // GARCH recursion: sigma_t^2 = omega + alpha * eps_{t-1}^2 + beta * sigma_{t-1}^2
  return OMEGA + ALPHA * prevEps * prevEps + BETA * prevVar;
}

function randn() {
  // standard normal via Box-Muller
  let u = 0, v = 0;
  while (u === 0) u = Math.random();
  while (v === 0) v = Math.random();
  return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}

function initSeries() {
  // initialize from long-run (unconditional) variance
  const longRunVar = OMEGA / (1 - ALPHA - BETA);
  returns = [0];
  variances = [longRunVar];
}

function step(shockOverride) {
  const n = returns.length;
  const prevVar = variances[n - 1];
  const prevEps = returns[n - 1];
  const newVar = garchStep(prevVar, prevEps);
  const sigma = Math.sqrt(newVar);
  const z = (shockOverride !== undefined) ? shockOverride : randn();
  const r = sigma * z;
  returns.push(r);
  variances.push(newVar);
  if (returns.length > MAX_BARS) {
    returns.shift();
    variances.shift();
  }
}

function draw() {
  const canvas = document.getElementById('chart');
  const ctx = canvas.getContext('2d');
  const W = canvas.width, H = canvas.height;
  const pad = { top: 16, right: 12, bottom: 20, left: 32 };
  ctx.clearRect(0, 0, W, H);

  // find y-axis scale from data
  const maxAbs = Math.max(...returns.map(Math.abs), 0.5);
  const maxSig = Math.max(...variances.map(v => Math.sqrt(v)), 0.3);
  const scaleY = (Math.max(maxAbs, maxSig * 3) + 0.05);
  const innerW = W - pad.left - pad.right;
  const innerH = H - pad.top - pad.bottom;
  const midY = pad.top + innerH / 2;
  const toY = (v) => midY - (v / scaleY) * (innerH / 2);
  const barW = Math.max(4, Math.floor(innerW / MAX_BARS) - 1);

  // draw zero line
  ctx.strokeStyle = '#c0cad4';
  ctx.lineWidth = 1;
  ctx.beginPath(); ctx.moveTo(pad.left, midY); ctx.lineTo(W - pad.right, midY); ctx.stroke();

  // draw return bars (blue positive, red negative)
  for (let i = 0; i < returns.length; i++) {
    const x = pad.left + i * (innerW / MAX_BARS);
    const r = returns[i];
    ctx.fillStyle = r >= 0 ? '#4a8fc4' : '#c94040';
    const barTop = r >= 0 ? toY(r) : midY;
    const barH = Math.abs(toY(r) - midY);
    ctx.fillRect(x, barTop, barW, Math.max(1, barH));
  }

  // draw orange conditional-sigma curve
  ctx.strokeStyle = '#e07a10';
  ctx.lineWidth = 2;
  ctx.lineJoin = 'round';
  ctx.beginPath();
  for (let i = 0; i < variances.length; i++) {
    const x = pad.left + i * (innerW / MAX_BARS) + barW / 2;
    const sig = Math.sqrt(variances[i]);
    const y = toY(sig);
    i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
  }
  ctx.stroke();

  // legend top-left
  const lx = pad.left + 4, ly = pad.top + 4;
  ctx.font = '11px system-ui, sans-serif';
  ctx.fillStyle = '#4a8fc4';
  ctx.fillRect(lx, ly, 10, 10);
  ctx.fillStyle = '#333';
  ctx.fillText('Return', lx + 13, ly + 9);
  ctx.strokeStyle = '#e07a10';
  ctx.lineWidth = 2;
  ctx.beginPath(); ctx.moveTo(lx + 70, ly + 5); ctx.lineTo(lx + 80, ly + 5); ctx.stroke();
  ctx.fillStyle = '#333';
  ctx.fillText('Vol (σ)', lx + 83, ly + 9);

  // update the current-vol readout
  const lastSig = Math.sqrt(variances[variances.length - 1]);
  document.getElementById('cur-vol').textContent = lastSig.toFixed(3);
}

// animation loop — one GARCH step per frame
function runLoop() {
  if (!running) return;
  step();
  draw();
  document.getElementById('hint-msg').textContent = i18nFmt('Step %n% — watch the orange curve spike after a large bar.', { n: returns.length });
  animId = setTimeout(runLoop, 80);
}

document.getElementById('btn-run').addEventListener('click', () => {
  running = !running;
  document.getElementById('btn-run').textContent = running ? 'Pause' : 'Run';
  if (running) runLoop();
  else { clearTimeout(animId); document.getElementById('hint-msg').textContent = 'Paused. Press Run to continue or Shock to inject a large move.'; }
});

document.getElementById('btn-shock').addEventListener('click', () => {
  running = false;
  clearTimeout(animId);
  document.getElementById('btn-run').textContent = 'Run';
  // inject a large ±4-sigma return
  step(4.0 * (Math.random() > 0.5 ? 1 : -1));
  draw();
  document.getElementById('hint-msg').textContent = 'Big move injected — watch the orange volatility forecast rise and decay.';
});

document.getElementById('btn-reset').addEventListener('click', () => {
  running = false;
  clearTimeout(animId);
  document.getElementById('btn-run').textContent = 'Run';
  initSeries();
  draw();
  document.getElementById('hint-msg').textContent = 'Press Run to simulate returns, or Shock to inject a big move.';
});

initSeries();
draw();
Result
Console

Click Shock to inject a large return and watch the orange forecast spike, then slowly decay. Click Run to generate a long random path and observe how bursts of large returns cluster together — even though the underlying random draws are independent. That is the memory GARCH encodes.

The Real Complexity

GARCH(1,1) has just three parameters — ω\omega, α\alpha, β\beta — and one recursion:

σt2=ω+α εt−12+β σt−12\sigma_t^2 = \omega + \alpha \, \varepsilon_{t-1}^2 + \beta \, \sigma_{t-1}^2

where εt−1=rt−1−μ\varepsilon_{t-1} = r_{t-1} - \mu is yesterday's demeaned return (the shock) and σt2\sigma_t^2 is today's conditional variance. The return itself is:

rt=μ+σt⋅zt,zt∼N(0,1)r_t = \mu + \sigma_t \cdot z_t, \quad z_t \sim \mathcal{N}(0, 1)

What each term does:

  • ω\omega — the baseline. When everything else is zero, variance reverts here.
  • α εt−12\alpha \, \varepsilon_{t-1}^2 — the ARCH term. A large shock yesterday (ε2\varepsilon^2 large) pushes today's variance up.
  • β σt−12\beta \, \sigma_{t-1}^2 — the GARCH term. Yesterday's already-elevated variance carries forward.

Stationarity requires α+β<1\alpha + \beta < 1. Then the unconditional (long-run) variance is σ∞2=ω/(1−α−β)\sigma^2_{\infty} = \omega / (1 - \alpha - \beta) and the process is mean-reverting. In real markets α+β\alpha + \beta is often 0.97–0.99: shocks decay slowly, which is why turbulent periods last weeks, not days.

Estimation is done by maximum likelihood. Given a return series r1,…,rTr_1, \dots, r_T, the log-likelihood is:

ℓ=−12∑t=1T[log⁡(2π)+log⁡σt2+εt2σt2]\ell = -\frac{1}{2} \sum_{t=1}^{T} \left[ \log(2\pi) + \log \sigma_t^2 + \frac{\varepsilon_t^2}{\sigma_t^2} \right]

Maximizing over (ω,α,β)(\omega, \alpha, \beta) via gradient descent or quasi-Newton methods gives the fitted model. The recursion is not analytically solvable in general — you walk forward from an initial σ12\sigma_1^2 and let the series teach the parameters.

Extensions abound: EGARCH adds asymmetry (bad news raises volatility more than good news of the same size — the leverage effect); GJR-GARCH adds an indicator term; multivariate DCC-GARCH models correlations across assets. But the intuition lives entirely in the scalar GARCH(1,1). See also Bayesian inference for an alternative way to fit models with memory.

Where It Matters

Anywhere that risk is priced or managed, a volatility model is underneath:

  • Option pricing: the Black–Scholes formula needs a volatility input. GARCH provides a time-varying one that matches the implied volatility smile far better than a constant.
  • Value at Risk (VaR): regulators under Basel II/III require banks to estimate the loss that will not be exceeded on 99 % of trading days. A GARCH-based VaR adapts when markets get rough; a static estimate does not.
  • Portfolio risk: mean–variance optimization is only as good as its covariance matrix. DCC-GARCH provides correlations that shift during crises (assets that seem uncorrelated in calm periods often move together in crashes).
  • Algorithmic trading: many strategies scale position size inversely to the current GARCH volatility estimate, automatically pulling back when turbulence spikes.
  • Central banks and regulators: monitoring systemic risk requires tracking when volatility across many markets rises simultaneously — a multivariate GARCH signal.

The insight generalizes far beyond finance: any time series where the variance of the noise is itself variable and autocorrelated — electricity prices, internet traffic, seismic activity — is a candidate for GARCH-type modeling. The math of volatility clustering is not limited to stock markets.

Conclusion

GARCH is a lesson in finding the right abstraction. The raw observation — that big market moves cluster — was known long before Engle. What was missing was a model that could encode that memory, estimate it from data, and forecast it one step ahead.

The GARCH(1,1) recursion does all three with just three numbers: a floor, a reaction speed, and a persistence. Together they produce the alternating calm and stormy regimes that anyone who has watched financial markets recognizes — not as mystical market moods, but as the output of a simple, mean-reverting variance process.

Next time you see a volatility index spike and linger for weeks, you are watching α\alpha and β\beta at work. And if you want to predict when the storm will pass, the same model gives you the answer — in expectation, at least. See Bayesian inference for a complementary view of how uncertainty about parameters is handled when the data is limited.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/garch-volatility/Content licensed under CC BY-NC 4.0.