A smile in four numbers
Interest-rate and FX options trade across many strikes, and desks need a smooth, arbitrage-aware way to interpolate and extrapolate the quoted volatilities. SABR (Hagan, Kumar, Lesniewski, Woodward, 2002) is the market standard. It models a forward F with its own random volatility alpha: dF = alpha F^beta dW and d alpha = nu alpha dZ with correlation rho between the shocks. Four parameters shape one expiry's smile: alpha sets the overall level, beta how volatility depends on the price level (0 normal, 1 lognormal), rho the tilt (skew) and nu the curvature (how much the wings rise).
# The four parameters of one expiry, for a rate forward of 3%
f, t = 0.03, 1.0 # forward rate and expiry in years
alpha, beta, rho, nu = 0.035, 0.5, -0.3, 0.4
print(round(alpha / f ** (1 - beta), 4)) # 0.2021 alpha / f^(1-beta) is roughly the at-the-money volatility (before the correction)Hagan's formula
Hagan's asymptotic expansion gives the Black implied volatility without simulation. At the money (K = F) it is alpha / F^(1-beta) times a time correction 1 + (...) * T, where the bracket combines three effects: the beta curvature of the backbone, the rho * nu skew term and the nu^2 smile term. Away from the money the same correction multiplies two more factors: a denominator in the log-moneyness L = ln(F/K) and the ratio z / chi(z), with z = (nu/alpha) (FK)^((1-beta)/2) L and chi(z) = ln((sqrt(1 - 2 rho z + z^2) + z - rho) / (1 - rho)). As z approaches 0 the ratio tends to 1, so the formula joins the at-the-money value smoothly, but computing 0 / 0 directly would fail: treat tiny z as a limit.
# The pieces of Hagan's formula: the time correction, the at-the-money volatility and z / chi(z)
import math
def sabr_correction(f, k, t, alpha, beta, rho, nu):
fk = f * k
return 1 + ((1 - beta) ** 2 / 24 * alpha ** 2 / fk ** (1 - beta) + rho * beta * nu * alpha / (4 * fk ** ((1 - beta) / 2)) + (2 - 3 * rho ** 2) * nu ** 2 / 24) * t
def sabr_atm_vol(f, t, alpha, beta, rho, nu):
return alpha / f ** (1 - beta) * sabr_correction(f, f, t, alpha, beta, rho, nu)
def z_over_chi(z, rho):
if abs(z) < 1e-7:
return 1.0
return z / math.log((math.sqrt(1 - 2 * rho * z + z ** 2) + z - rho) / (1 - rho))
print(round(sabr_atm_vol(0.03, 1.0, 0.035, 0.5, -0.3, 0.4), 6)) # 0.203877 the at-the-money volatility
print(round(z_over_chi(0.5, -0.3), 6), z_over_chi(0.0, -0.3)) # 1.103683 1.0 near 1, and exactly 1.0 at z = 0Reading the smile, and fitting it
Plot the implied volatility against strike and the parameters have clear signatures. A negative rho lifts the low strikes and lowers the high ones (downward skew, typical of equities and many rates); rho = 0 gives a symmetric smile. A larger nu raises both wings. beta moves the backbone: how much the at-the-money volatility changes as the forward moves. Practitioners fix beta by convention or from history, then calibrate the rest to the quoted volatilities of each expiry (next topic), often solving for alpha from the at-the-money quote so it matches exactly. Two cautions: the formula is an approximation that degrades for long expiries and very low strikes, where it can imply negative probability densities.
# The smile for three values of rho: a negative rho lifts the low strikes (downward skew)
import math
def sabr_vol(f, k, t, alpha, beta, rho, nu):
fk = f * k
log = math.log(f / k)
mid = fk ** ((1 - beta) / 2)
z = nu / alpha * mid * log
ratio = 1.0 if abs(z) < 1e-7 else z / math.log((math.sqrt(1 - 2 * rho * z + z ** 2) + z - rho) / (1 - rho))
denominator = mid * (1 + (1 - beta) ** 2 / 24 * log ** 2 + (1 - beta) ** 4 / 1920 * log ** 4)
bracket = 1 + ((1 - beta) ** 2 / 24 * alpha ** 2 / fk ** (1 - beta) + rho * beta * nu * alpha / (4 * mid) + (2 - 3 * rho ** 2) * nu ** 2 / 24) * t
return alpha / denominator * ratio * bracket
for rho in (-0.5, 0.0, 0.5):
print(rho, [round(sabr_vol(0.03, k, 1.0, 0.035, 0.5, rho, 0.4), 4) for k in (0.02, 0.03, 0.04)])
# one line per rho: the first value (low strike) is the highest when rho is negative and the lowest when rho is positive# A free sanity check: with beta = 1 and nu = 0 SABR is Black-Scholes, so the at-the-money volatility is alpha at every expiry
def atm(f, t, alpha, beta, rho, nu):
fk = f * f
bracket = 1 + ((1 - beta) ** 2 / 24 * alpha ** 2 / fk ** (1 - beta) + rho * beta * nu * alpha / (4 * fk ** ((1 - beta) / 2)) + (2 - 3 * rho ** 2) * nu ** 2 / 24) * t
return alpha / f ** (1 - beta) * bracket
print(atm(100.0, 0.5, 0.2, 1.0, -0.3, 0.0), atm(100.0, 5.0, 0.2, 1.0, -0.3, 0.0)) # 0.2 0.2rho strictly inside -1 and 1, nu not huge), and refit each expiry separately and smoothly over time. For low or negative rates use the normal (Bachelier) version of the formula, which does not break down near zero.- SABR gives each expiry a smile from four parameters: alpha (level), beta (backbone), rho (skew) and nu (curvature).
- Hagan's formula = prefactor x
z / chi(z)x time correction; at the money it reduces toalpha / F^(1-beta)times the correction, andz / chi(z)tends to 1 asztends to 0. - A negative rho tilts the smile down, a larger nu raises the wings; fit each expiry separately, check for arbitrage at the wings, and use the normal version near zero rates.
Next up: model calibration, fitting model parameters to market prices.
Lab · what's graded
- Add the time correction with
sabr_correction25 pts - Read the money point with
sabr_atm_vol25 pts - Tame the 0 / 0 in
z_over_chi25 pts - Draw the smile with
sabr_implied_vol25 pts