Volatility
Black–Scholes assumes one constant volatility. Markets disagree: options at different strikes trade as if volatility were different. This chapter reads volatility off option prices, builds a model where volatility itself is random, and prices a contract on volatility directly.
10.1Implied Volatility & the Smile
Traders quote options not by price but by implied volatility: the volatility that, plugged into Black–Scholes, reproduces the market price. If Black–Scholes were right, every strike would give the same number. In real markets it doesn't: plotted against strike, implied volatility forms a smile or a downward-sloping skew. Here the "market" is Merton's jump model (section 8.1), and you can see which shapes jumps produce.
Predict first: In a model with occasional downward jumps, implied volatility at low strikes, compared with high strikes, will be…
Set the jump intensity, the average jump size and its spread. Left: implied volatility against strike. Right: the probability distribution of the stock at maturity that the option prices imply, against the lognormal curve Black–Scholes would use.
- $C_{\text{market}}(K)$
- The observed call price at strike $K$: here computed from the jump model.
- $\sigma_{\text{imp}}(K)$
- Implied volatility: the unique $\sigma$ that makes the Black–Scholes price match. It is found numerically, because Black–Scholes can't be solved for $\sigma$ in closed form.
- smile, skew
- The shape of $\sigma_{\text{imp}}$ against $K$. Equity markets show a downward skew: low strikes (crash protection) have higher implied volatility.
- $q(K)$
- The risk-neutral density of $S_T$: how likely each final price is, as priced by the market. The second derivative of call prices in the strike recovers it (Breeden–Litzenberger).
In plain words: implied volatility is a unit of price. It lets you compare options across strikes on one scale. A flat line would mean the market agrees with Black–Scholes' bell-curve world. A skew means the market's distribution of future prices has a fatter left tail than the lognormal: you can see it directly on the right. Every shape of the smile corresponds to a shape of the distribution, and the second derivative of prices across strikes turns one into the other. Try positive jumps: the skew flips. Try symmetric jumps (mean 0): the skew becomes a smile.
The smile is how the market says "Black–Scholes is wrong" while still using its formula as a quoting convention. Downward jumps give a skew, symmetric jumps a smile, and the size of the effect fades with maturity, because many jumps average out (section 8.1). Real equity skews are also steep at long maturities, which pure jump models struggle with. That is one reason stochastic volatility (next section) is needed too.
Going deeper: the precise statement
Breeden–Litzenberger. Since $C(K) = e^{-rT}\mathbb E^Q[(S_T - K)^+]$, differentiating twice in $K$ gives $\partial_{KK}C = e^{-rT}q(K)$. Call prices across all strikes therefore determine the entire risk-neutral distribution at that maturity.
Local volatility (Dupire). Given call prices across strikes and maturities, there is exactly one diffusion $dS = rS\,dt + \sigma_{\text{loc}}(S, t)S\,dW$ that reproduces all of them: $\sigma_{\text{loc}}^2(K, T) = \dfrac{2\,(\partial_T C + rK\,\partial_K C)}{K^2\,\partial_{KK}C}$. It fits today's smile exactly, but it tends to predict future smiles that flatten, unlike real markets.
Arbitrage bounds. A smile must keep call prices decreasing and convex in $K$ (no butterfly arbitrage) and increasing in $T$ (no calendar arbitrage). Otherwise $q$ would be negative somewhere.
References: D. Breeden & R. Litzenberger, "Prices of state-contingent claims implicit in option prices", J. Business 51 (1978), 621–651. B. Dupire, "Pricing with a smile", Risk 7 (1994), 18–20. J. Gatheral, The Volatility Surface (Wiley, 2006).
Show the code (Python + NumPy)
import numpy as np
from math import erf, exp, log, sqrt, factorial
N = lambda x: 0.5 * (1 + erf(x / sqrt(2)))
def bs_call(S, K, T, r, s):
d1 = (log(S / K) + (r + s * s / 2) * T) / (s * sqrt(T))
return S * N(d1) - K * exp(-r * T) * N(d1 - s * sqrt(T))
def merton_call(S, K, T, r, s, lam, mu, dj): # average of BS prices over the number of jumps
k = exp(mu + dj * dj / 2) - 1
return sum(exp(-lam * (1 + k) * T) * (lam * (1 + k) * T) ** n / factorial(n)
* bs_call(S, K, T, r - lam * k + n * log(1 + k) / T, sqrt(s * s + n * dj * dj / T)) for n in range(60))
def implied_vol(price, S, K, T, r, lo=1e-4, hi=3.0):
for _ in range(80): # bisection: call prices rise with volatility
mid = (lo + hi) / 2
lo, hi = (mid, hi) if bs_call(S, K, T, r, mid) < price else (lo, mid)
return (lo + hi) / 2
S, T, r, s, lam, mu, dj = 100.0, 0.5, 0.02, 0.15, 1.0, -0.10, 0.10 # one jump a year, 10% down on average
for K in (80, 90, 100, 110, 120):
print(f"K = {K}: implied volatility {implied_vol(merton_call(S, K, T, r, s, lam, mu, dj), S, K, T, r):.4f}")
Ks = np.arange(40, 201, 0.5) # Breeden-Litzenberger: density = e^{rT} C''(K)
C = np.array([merton_call(S, K, T, r, s, lam, mu, dj) for K in Ks])
density = exp(r * T) * np.diff(C, 2) / 0.5 ** 2
print(f"density integrates to {density.sum() * 0.5:.4f}")
Output when we ran it:
K = 80: implied volatility 0.2333
K = 90: implied volatility 0.2134
K = 100: implied volatility 0.1968
K = 110: implied volatility 0.1858
K = 120: implied volatility 0.1796
density integrates to 1.0000
10.2Stochastic Volatility (Heston)
Real volatility changes over time: calm stretches, then turbulent ones (you saw this "volatility clustering" in real data, section 11.3). The Heston model lets the variance $v$ follow its own random process (a CIR process, section 5.2), correlated with the stock. It is the most widely used stochastic-volatility model because, despite being random, it still has an almost-closed-form price.
Predict first: Make the correlation ρ between the price and its volatility strongly negative. The smile will…
Drag the correlation ρ and the volatility of volatility ξ. Left: one simulated price path and its volatility. Right: the implied-volatility smile from the exact Fourier price (line), confirmed by 20,000 simulated paths (dots).
- $v_t$
- The instantaneous variance. Its square root is the volatility. Here it starts at $v_0 = 0.04$ (20% volatility).
- $\kappa,\ \theta$
- Variance is pulled back toward its long-run level $\theta = 0.04$ at speed $\kappa = 2$ (half-life about 4 months).
- $\xi$
- The volatility of volatility: how wildly $v$ itself moves. It curves the smile.
- $\rho$
- The correlation between price and variance moves (section 4.4). It tilts the smile.
- Fourier price
- Heston's model has a known characteristic function for $\ln S_T$. An integral of it gives the exact call price, with no simulation needed.
In plain words: two random processes, tied together. When ρ is negative, bad days for the stock tend to be days when volatility jumps. That is the leverage effect seen in equity markets. The result is a distribution with a fat left tail, and the market prices that as a skew. The volatility of volatility ξ decides how fat both tails get, so it controls the smile's curvature. Heston reproduces the qualitative shape of equity skews with five parameters, and it prices vanilla options in milliseconds, which is why it is a standard first model to calibrate.
ρ sets the skew and ξ the curvature. Stochastic volatility explains skews that persist at long maturities, where jumps have averaged out. Real desks go further: stochastic local volatility combines Heston with Dupire's local volatility so that the model fits today's smile exactly and still has realistic dynamics.
Going deeper: the precise statement
Pricing PDE. Itô's lemma for two variables (section 4.4) and a hedge in the stock and one other option give $\partial_tV + \tfrac12vS^2V_{SS} + \rho\xi vSV_{Sv} + \tfrac12\xi^2vV_{vv} + rSV_S + \kappa(\theta - v)V_v - rV = 0$ (with zero market price of volatility risk). Volatility itself can't be traded, so the model is incomplete: the stock alone can't hedge an option.
Characteristic function. $\mathbb E[e^{iu\ln S_T}] = \exp\big(C(u,T) + D(u,T)\,v_0 + iu\ln S_0\big)$ with explicit $C$ and $D$. The price is $C = S_0P_1 - Ke^{-rT}P_2$ with $P_j = \tfrac12 + \tfrac1\pi\int_0^\infty \operatorname{Re}[\cdots]\,du$. The "little Heston trap" form, used here, avoids branch-cut errors in the complex logarithm.
Feller condition. If $2\kappa\theta \ge \xi^2$, the variance never reaches 0 (section 5.2's CIR result). Calibrated equity parameters often violate it, which is why simulations need careful schemes (full truncation here, or Andersen's QE scheme).
References: S. Heston, "A closed-form solution for options with stochastic volatility with applications to bond and currency options", Review of Financial Studies 6 (1993), 327–343. H. Albrecher et al., "The little Heston trap", Wilmott Magazine (2007). Gatheral, The Volatility Surface, ch. 2.
Show the code (Python + NumPy)
import numpy as np
from math import erf, exp, log, sqrt, pi
T, r, v0, kap, th, xi, rho, S0, K = 1.0, 0.02, 0.04, 2.0, 0.04, 0.5, -0.7, 100.0, 90.0
def cf(u): # characteristic function of ln S_T
b = kap - rho * xi * 1j * u
d = np.sqrt(b ** 2 + xi ** 2 * (1j * u + u ** 2)); g = (b - d) / (b + d); e = np.exp(-d * T)
C = r * 1j * u * T + kap * th / xi ** 2 * ((b - d) * T - 2 * np.log((1 - g * e) / (1 - g)))
return np.exp(C + (b - d) / xi ** 2 * (1 - e) / (1 - g * e) * v0 + 1j * u * log(S0))
u = np.linspace(1e-6, 200, 4001)
P1 = 0.5 + np.trapz(np.real(np.exp(-1j * u * log(K)) * cf(u - 1j) / (1j * u * cf(-1j))), u) / pi
P2 = 0.5 + np.trapz(np.real(np.exp(-1j * u * log(K)) * cf(u) / (1j * u)), u) / pi
exact = S0 * P1 - K * exp(-r * T) * P2
rng, n, steps = np.random.default_rng(30), 100_000, 200 # Monte Carlo, full-truncation Euler
dt, lnS, v = T / steps, np.full(n, log(S0)), np.full(n, v0)
for _ in range(steps):
z1 = rng.normal(size=n); z2 = rho * z1 + sqrt(1 - rho ** 2) * rng.normal(size=n); vp = np.maximum(v, 0)
lnS += (r - vp / 2) * dt + np.sqrt(vp * dt) * z1
v += kap * (th - vp) * dt + xi * np.sqrt(vp * dt) * z2
pay = exp(-r * T) * np.maximum(np.exp(lnS) - K, 0)
print(f"Heston call K = {K}: Fourier {exact:.4f} Monte Carlo {pay.mean():.4f} +- {pay.std() / sqrt(n):.4f}")
Output when we ran it:
Heston call K = 90.0: Fourier 15.0921 Monte Carlo 15.0726 +- 0.0414
10.3Variance Swaps
A variance swap is a bet on volatility itself: at maturity it pays the realized variance of the stock minus a fixed strike agreed today. What is the fair strike? Remarkably, it can be replicated with ordinary options, a strip of puts and calls at every strike, each held in proportion to $1/K^2$. The reason is Itô's lemma applied to $\ln S$.
Predict first: In the strip of options that replicates a variance swap, each option is held in proportion to…
Change the number of options in the strip and today's volatility. Left: what each option contributes (puts below the current price, calls above). Right: realized variance over 4,000 simulated years of the Heston model, against the fair strike and the strip's value.
- $\frac1T\int_0^T\sigma_t^2\,dt$
- Realized variance: the average squared volatility over the life of the swap. In practice it is measured as the average squared daily log return, which is section 2.1's quadratic variation.
- $\int_0^T dS_t/S_t$
- The gains from continuously holding one dollar's worth of stock: a trading strategy with zero value under risk-neutral pricing (when $r = 0$).
- $\ln(S_T/S_0)$
- A "log contract": a payoff that can be built from a strip of options.
- $P(K),\ C(K)$
- Put and call prices at strike $K$ today. Out-of-the-money options are used: puts below today's price, calls above.
- $K_{\text{var}}$
- The fair variance strike: the value that makes the swap worth zero today.
In plain words: Itô's lemma says $d\ln S = dS/S - \tfrac12\sigma^2dt$. The "$-\tfrac12\sigma^2dt$" correction is exactly the variance you want. So variance is twice the difference between holding the stock dynamically and holding a log contract statically. The log contract's payoff is curved, and any curved payoff can be built from options with weights equal to its curvature. For $\ln K$ that curvature is $1/K^2$. So one static basket of options plus simple stock trading locks in realized variance, whatever the volatility does, in any diffusion model. More strikes make the strip more accurate. Any single year's realized variance is highly uncertain (right panel), but the average matches the strike.
Variance can be bought and sold like any asset, and its fair price is a weighted sum of option prices. That makes volatility tradable and gives a model-free benchmark, the idea behind the VIX index, which is computed from exactly this 1/K² strip of S&P 500 options. The replication needs continuous paths: jumps add an error term, one reason variance swaps were capped after the 2008 crash.
Going deeper: the precise statement
Spanning formula. Any twice-differentiable payoff $f(S_T)$ can be written, for any $F$, as $f(F) + f'(F)(S_T - F) + \int_0^F f''(K)(K - S_T)^+dK + \int_F^\infty f''(K)(S_T - K)^+dK$ (Carr & Madan). With $f = -\ln$, $f''(K) = 1/K^2$.
Assumptions. The replication holds exactly for continuous price paths with any stochastic volatility. With jumps, realized variance differs from the log-contract value by a term of order $\sum_{\text{jumps}}\big(J^2 - 2(e^J - 1 - J)\big)$, which is positive for crashes: realized variance then exceeds what the strip pays, so the strip underprices the swap. Discretely sampled realized variance and interest rates add small corrections.
Volatility swaps. A swap on volatility $\sqrt{\text{variance}}$ is not replicable by a static strip. By Jensen's inequality its fair strike is below $\sqrt{K_{\text{var}}}$: a convexity adjustment.
References: K. Demeterfi, E. Derman, M. Kamal & J. Zou, "A guide to volatility and variance swaps", J. Derivatives 6 (1999), 9–32. P. Carr & D. Madan, "Towards a theory of volatility trading", in Volatility (Risk Books, 1998). Gatheral, The Volatility Surface, ch. 11.
Show the code (Python + NumPy)
import numpy as np
from math import exp, sqrt
T, v0, kap, th, xi, rho = 1.0, 0.04, 2.0, 0.04, 0.5, -0.7 # Heston, r = 0
fair = th + (v0 - th) * (1 - exp(-kap * T)) / (kap * T) # E[(1/T) * integral of v dt]
rng, n, days = np.random.default_rng(31), 20_000, 252
dt, v, realized = T / days, np.full(n, v0), np.zeros(n)
for _ in range(days):
z1 = rng.normal(size=n); z2 = rho * z1 + sqrt(1 - rho ** 2) * rng.normal(size=n); vp = np.maximum(v, 0)
ret = -vp / 2 * dt + np.sqrt(vp * dt) * z1 # daily log return
realized += ret ** 2
v += kap * (th - vp) * dt + xi * np.sqrt(vp * dt) * z2
realized /= T
print(f"fair variance strike {fair:.5f} average realized {realized.mean():.5f} +- {realized.std() / sqrt(n):.5f}")
print(f"realized variance ranges from {np.percentile(realized, 5):.4f} to {np.percentile(realized, 95):.4f} (5th-95th percentile)")
Output when we ran it:
fair variance strike 0.04000 average realized 0.03973 +- 0.00022
realized variance ranges from 0.0094 to 0.1019 (5th-95th percentile)
Chapter recap
Quick questions on this chapter's big ideas. Each answer is explained as soon as you choose.
1. Implied volatility is…
2. In the Heston model, which parameter mainly tilts the smile into a skew?
3. A variance swap can be replicated by holding options at every strike in proportion to…