Advanced Interest Rate Models (HJM and LMM) › Calibration to Market Data
Quantitative FinanceAll chapters Advanced Interest Rate Models (HJM and LMM) › Calibration to Market DataOne section · Sections in this chapter8 / 13 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Calibration is the process of choosing model parameters (volatilities, correlations) so that the model reproduces observed market prices. For interest rate models, calibration typically targets cap/floor volatilities and swaption volatilities. The goal is to find parameters that allow the model to price liquid instruments consistently with the market while maintaining reasonable behavior for illiquid or exotic instruments. This balancing act is central to practical derivatives pricing.
Cap Volatility Calibration
Market caps are quoted in terms of implied volatility. Given a quoted cap volatility, we can back out the price using Black's formula, then calibrate the LMM volatilities to match these prices. The calibration process inverts the pricing formula: rather than using volatility to find price, we use price to find volatility.
The caplet volatility structure in LMM has several common parameterizations. A popular choice is the piecewise-constant volatility, where each forward rate has a constant volatility that can differ across maturities. More sophisticated approaches use time-varying or maturity-dependent volatility functions that can capture features like the volatility "hump" often observed at intermediate maturities.
import numpy as np
from scipy.optimize import minimize
from scipy.stats import norm
def black_caplet_price(
forward_rate, strike, maturity, volatility, discount_factor, delta=0.25
):
"""
Price a caplet using Black's formula.
Parameters:
-----------
forward_rate : float
Current forward LIBOR rate L_i(0)
strike : float
Cap strike rate K
maturity : float
Time to caplet fixing date T_i
volatility : float
Forward rate volatility sigma_i
discount_factor : float
P(0, T_{i+1}) discount factor to payment date
delta : float
Accrual period (default 0.25 for quarterly)
Returns:
--------
price : float
Caplet price
"""
if maturity <= 0:
return max(forward_rate - strike, 0) * delta * discount_factor
v = volatility * np.sqrt(maturity)
d1 = (np.log(forward_rate / strike) + 0.5 * v**2) / v
d2 = d1 - v
price = (
delta
* discount_factor
* (forward_rate * norm.cdf(d1) - strike * norm.cdf(d2))
)
return price
def calibrate_cap_volatilities(
market_cap_vols, forward_rates, maturities, discount_factors, delta
):
"""
Calibrate LMM volatilities to match market cap implied volatilities.
Parameters:
-----------
market_cap_vols : array
Market-quoted cap volatilities for each maturity
forward_rates : array
Forward LIBOR rates
maturities : array
Cap maturities
discount_factors : array
Discount factors for payment dates
delta : float
Accrual period
Returns:
--------
calibrated_vols : array
Calibrated forward rate volatilities
"""
n = len(forward_rates)
def objective(vols):
"""Sum of squared errors between model and market cap prices."""
total_error = 0
for i in range(n):
model_price = black_caplet_price(
forward_rates[i],
forward_rates[i], # ATM strike
maturities[i],
vols[i],
discount_factors[i],
delta,
)
market_price = black_caplet_price(
forward_rates[i],
forward_rates[i],
maturities[i],
market_cap_vols[i],
discount_factors[i],
delta,
)
total_error += (model_price - market_price) ** 2
return total_error
# Initial guess: market vols
x0 = market_cap_vols.copy()
# Bounds: positive volatilities
bounds = [(0.01, 1.0)] * n
result = minimize(objective, x0, method="L-BFGS-B", bounds=bounds)
return result.x
# Example: Calibrate to a typical cap volatility term structure
# Market often shows a "hump" in cap volatilities
market_maturities = np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])
market_cap_vols = np.array(
[0.18, 0.22, 0.24, 0.25, 0.24, 0.23, 0.22, 0.21]
) # Typical hump shape
forward_rates_calib = np.ones(len(market_maturities)) * 0.05
discount_factors_calib = np.exp(-0.05 * (market_maturities + 0.25))
calibrated_vols = calibrate_cap_volatilities(
market_cap_vols,
forward_rates_calib,
market_maturities,
discount_factors_calib,
delta=0.25,
)
Calibration Results: 0.5y: Market = 18.0%, Model = 18.0% 1.0y: Market = 22.0%, Model = 22.0% 1.5y: Market = 24.0%, Model = 24.0% 2.0y: Market = 25.0%, Model = 25.0% 2.5y: Market = 24.0%, Model = 24.0% 3.0y: Market = 23.0%, Model = 23.0% 3.5y: Market = 22.0%, Model = 22.0% 4.0y: Market = 21.0%, Model = 21.0%
The calibration results demonstrate the model's ability to fit the term structure of volatility. The calibrated forward rate volatilities track the market cap volatilities, capturing the hump shape around the 2-year mark that is characteristic of interest rate markets. This hump reflects the market's view that rates are most uncertain at intermediate horizons: near-term rates are anchored by current monetary policy, while very long-term rates are stabilized by mean reversion.
Swaption Volatility Surface Calibration
The swaption market provides a rich volatility surface across different expiries and swap tenors. Calibrating the LMM to this surface is more challenging because swaption volatility depends on both individual forward rate volatilities and their correlations. Unlike cap calibration, where each caplet can be calibrated independently, swaption calibration requires simultaneously fitting multiple interdependent parameters.
A typical swaption volatility surface (often called a "vol cube" when strikes are included) shows variation across:
- Expiry: Time until the option expires
- Tenor: Length of the underlying swap
- Strike: Often expressed as offset from ATM
# Create a synthetic swaption volatility surface
expiries = np.array([0.5, 1, 2, 3, 5]) # Option expiries
tenors = np.array([1, 2, 3, 5, 7, 10]) # Swap tenors
# Typical swaption vol surface (ATM)
swaption_vol_surface = np.array(
[
[0.25, 0.24, 0.23, 0.21, 0.19, 0.17], # 6m expiry
[0.24, 0.23, 0.22, 0.20, 0.18, 0.16], # 1y expiry
[0.22, 0.21, 0.20, 0.18, 0.17, 0.15], # 2y expiry
[0.20, 0.19, 0.18, 0.17, 0.16, 0.14], # 3y expiry
[0.18, 0.17, 0.16, 0.15, 0.14, 0.13], # 5y expiry
]
)

Joint Calibration Challenge
Calibrating the LMM to both caps and swaptions simultaneously is challenging because caps constrain individual forward rate volatilities while swaptions constrain both volatilities and correlations. A model that fits cap volatilities perfectly may still misprice swaptions if the correlation structure is wrong. Conversely, adjusting correlations to fit swaptions may introduce inconsistencies with cap prices. A common approach involves:
- Stage 1: Calibrate forward rate volatilities to the cap volatility curve
- Stage 2: Calibrate correlations to match swaption volatilities, given the volatilities from Stage 1
More sophisticated approaches use global optimization to calibrate volatilities and correlations jointly, treating the entire parameter set as a single optimization problem. This avoids the suboptimality that can arise from sequential calibration but requires more computational resources and careful attention to the objective function weighting.
def approximate_swaption_vol(
forward_vols, correlation, delta, start_idx, end_idx, expiry
):
"""
Approximate swaption volatility from forward rate volatilities and correlations.
Uses the "frozen" approximation where weights are computed at time 0.
"""
n_rates = end_idx - start_idx
# Equal weights approximation (simplified)
weights = np.ones(n_rates) / n_rates
# Calculate approximate swaption variance
variance = 0
for i in range(n_rates):
for j in range(n_rates):
idx_i = start_idx + i
idx_j = start_idx + j
if idx_i < len(forward_vols) and idx_j < len(forward_vols):
variance += (
weights[i]
* weights[j]
* correlation[idx_i, idx_j]
* forward_vols[idx_i]
* forward_vols[idx_j]
* expiry
)
return np.sqrt(variance / expiry) if expiry > 0 else 0
def calibrate_correlation(
forward_vols, target_swaption_vols, expiries, tenors, delta
):
"""
Calibrate correlation parameter to match swaption volatilities.
Uses exponential correlation structure.
"""
n = len(forward_vols)
def objective(beta):
"""Sum of squared errors for swaption vols."""
beta = beta[0]
if beta <= 0:
return 1e10
corr = generate_correlation_matrix(n, "exponential", {"beta": beta})
total_error = 0
for i, expiry in enumerate(expiries[:3]): # Use first 3 expiries
for j, tenor in enumerate(tenors[:3]): # Use first 3 tenors
start_idx = int(expiry / delta)
end_idx = int((expiry + tenor) / delta)
if end_idx <= n:
model_vol = approximate_swaption_vol(
forward_vols, corr, delta, start_idx, end_idx, expiry
)
target_vol = target_swaption_vols[i, j]
total_error += (model_vol - target_vol) ** 2
return total_error
result = minimize(
objective, [0.2], method="Nelder-Mead", bounds=[(0.01, 1.0)]
)
return result.x[0]
# Calibrate correlation to swaption surface
# Extend forward vols to cover swaption maturities
extended_forward_vols = np.concatenate(
[calibrated_vols, np.ones(12) * calibrated_vols[-1]]
)
calibrated_beta = calibrate_correlation(
extended_forward_vols, swaption_vol_surface, expiries, tenors, delta=0.25
)
# Generate calibrated correlation matrix for comparison
n = len(extended_forward_vols)
calibrated_corr = generate_correlation_matrix(
n, "exponential", {"beta": calibrated_beta}
)
# Calculate comparison values
comparison_data = []
delta_val = 0.25
for i, expiry in enumerate(expiries[:3]):
for j, tenor in enumerate(tenors[:3]):
start_idx = int(expiry / delta_val)
end_idx = int((expiry + tenor) / delta_val)
if end_idx <= n:
model_vol = approximate_swaption_vol(
extended_forward_vols,
calibrated_corr,
delta_val,
start_idx,
end_idx,
expiry,
)
market_vol = swaption_vol_surface[i, j]
comparison_data.append((expiry, tenor, market_vol, model_vol))Calibrated correlation decay parameter (beta): 0.0100 Swaption Vol Comparison (Expiry x Tenor): Market vs Model: 0.5y x 1y: Market = 25.0%, Model = 23.9% 0.5y x 2y: Market = 24.0%, Model = 22.3% 0.5y x 3y: Market = 23.0%, Model = 21.6% 1.0y x 1y: Market = 24.0%, Model = 22.4% 1.0y x 2y: Market = 23.0%, Model = 21.5% 1.0y x 3y: Market = 22.0%, Model = 21.1% 2.0y x 1y: Market = 22.0%, Model = 20.9% 2.0y x 2y: Market = 21.0%, Model = 20.7% 2.0y x 3y: Market = 20.0%, Model = 20.6%
The calibration shows that while we can match some swaptions well, perfect fit across the entire surface requires more sophisticated volatility and correlation parameterizations. This is an active area of research and practical development. In production systems, you often use more flexible correlation structures (such as angle-based parameterizations) and volatility functions that depend on both time and maturity to achieve better fits.
