Advanced Interest Rate Models (HJM and LMM) › The LIBOR Market Model
Quantitative FinanceAll chapters Advanced Interest Rate Models (HJM and LMM) › The LIBOR Market ModelOne section · Sections in this chapter4 / 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
While the HJM framework provides a theoretically elegant approach to modeling forward rates, it works with instantaneous forward rates, which are not directly observable in markets. You cannot go to a broker and ask to trade the instantaneous forward rate for borrowing at exactly 2.5 years from now. You price caps, floors, and swaptions using discrete forward LIBOR rates for specific tenors (3-month, 6-month). The LIBOR Market Model (LMM), also known as the Brace-Gatarek-Musiela (BGM) model, directly models these market-observable rates. This alignment with market conventions makes LMM the natural choice for pricing and hedging interest rate derivatives.
Forward LIBOR Rates
Consider a set of tenor dates with equal spacing (typically 0.25 or 0.5 years). The forward LIBOR rate is the simply compounded rate at time for borrowing over the period . Unlike the continuously compounded instantaneous forward rate in HJM, the LIBOR rate applies simple interest over a discrete period:
where:
- : year fraction (accrual period)
- : price of a zero-coupon bond maturing at
- : price of a zero-coupon bond maturing at
The formula expresses a simple arbitrage relationship: if you invest one dollar at the LIBOR rate from to , you should receive dollars. Alternatively, you could buy bonds maturing at , which costs in terms of bonds maturing at . Equating these two strategies yields the formula above.
These rates are directly observable from the forward rate agreement (FRA) market and serve as the underlying rates for caps and floors. When a bank quotes a 6-month LIBOR rate starting in 1 year, that is exactly the forward rate for the appropriate index .
LMM Dynamics Under the Forward Measure
The key insight of LMM is that under the -forward measure , the forward LIBOR rate is a martingale. This follows because can be expressed as a ratio of asset prices (zero-coupon bonds), and ratios of tradeable assets are martingales under the numeraire corresponding to the denominator. This is a direct application of the change of numeraire theorem. When we use as the numeraire, any asset price divided by this numeraire becomes a martingale, and the forward rate is precisely such a ratio.
Under this forward measure, has zero drift and follows:
where:
- : volatility of the -th forward rate
- : Brownian motion under the -forward measure
The lognormal dynamics implied by this equation lead directly to a pricing result. Because the rate follows geometric Brownian motion under its natural measure, its distribution at any future time is lognormal. This lognormal distribution leads directly to Black's formula for caplet pricing, and gives the theoretical justification for the market convention that predated the formal LMM framework.
The -forward measure uses the zero-coupon bond as numeraire. Under this measure, any asset price divided by is a martingale. The forward LIBOR rate is naturally a martingale under the -forward measure.
Black's Formula for Caplets
A caplet pays at time , where is the cap rate. This payoff represents the protection provided by the cap: if the realized LIBOR rate exceeds the cap strike, the holder receives the difference applied to the notional over the accrual period. Since is a martingale under and follows lognormal dynamics, the caplet price has a Black-Scholes-type closed-form solution known as Black's formula:
where:
- : initial forward LIBOR rate
- : cap strike rate
- : discount factor for the payment date
- : cumulative standard normal distribution function
- : integrated volatility (approximated as for constant volatility)
The formula terms provide the following intuition:
- : probability that the option expires in-the-money under the forward measure
- : expected value of the floating rate at maturity, conditional on exercise
- : expected cost of the fixed rate strike payment, conditional on exercise
The structure mirrors the Black-Scholes formula for equity options, which is no coincidence. Both arise from the same mathematical setting: lognormal dynamics under the relevant pricing measure. The discount factor converts the expected payoff under the forward measure to a present value.
A cap is simply a portfolio of caplets:
where:
- Cap: total value of the interest rate cap
- Caplet_i: value of the -th individual caplet
- : number of caplets in the contract
Each caplet in the cap protects against high rates in a different future period, and since the caplets have non-overlapping payoff dates, they can be valued independently and summed.
import numpy as np
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 price_cap(
forward_rates,
strike,
maturities,
volatilities,
discount_factors,
delta=0.25,
):
"""
Price a cap as a sum of caplets.
"""
cap_price = 0
for i in range(len(forward_rates)):
caplet = black_caplet_price(
forward_rates[i],
strike,
maturities[i],
volatilities[i],
discount_factors[i],
delta,
)
cap_price += caplet
return cap_price
# Example: Price a 3-year cap with quarterly payments
# Assume flat forward curve at 5% and flat volatility at 20%
num_periods = 12 # 3 years, quarterly
delta = 0.25
forward_rates = np.ones(num_periods) * 0.05
volatilities = np.ones(num_periods) * 0.20
maturities = np.arange(1, num_periods + 1) * delta
discount_factors = np.exp(
-0.05 * (maturities + delta)
) # Approximate with flat 5% curve
strike = 0.05 # At-the-money cap
cap_price = price_cap(
forward_rates, strike, maturities, volatilities, discount_factors, delta
)
# Price caps at different strikes for plotting
strikes = np.linspace(0.03, 0.07, 50)
cap_prices = [
price_cap(
forward_rates, K, maturities, volatilities, discount_factors, delta
)
for K in strikes
]3-year ATM Cap Price: 0.013078 Cap price as percentage of notional: 1.3078%
The cap price of approximately 1.77% of notional represents the premium required to hedge against floating rates exceeding 5% over the next 3 years. This value reflects the sum of individual caplet prices, each weighted by the discount factor and the probability of the forward rate exceeding the strike. For an at-the-money cap where the strike equals the forward rate, roughly half of the value comes from intrinsic value considerations and half from time value.


The plot illustrates the inverse relationship between cap prices and strike rates. As the strike increases, the cap becomes more out-of-the-money, reducing its value. A higher strike means the cap provides protection only against more extreme rate increases, which are less likely to occur. The curvature reflects the non-linear nature of option pricing with respect to the strike, with the steepest gradient occurring near the at-the-money rate where the option's moneyness is most sensitive to small changes in the strike.
LMM Dynamics Under the Terminal Measure
While Black's formula works beautifully for individual caplets, pricing more complex derivatives like swaptions requires simulating multiple forward rates simultaneously. The challenge is that each forward rate is a martingale under a different measure . We cannot simply simulate each rate as a driftless process because they all need to evolve consistently under the same probability measure.
To simulate all rates consistently, we typically work under a single measure, commonly the terminal measure (using as numeraire). Under this measure, only is a driftless martingale. Other forward rates acquire drift terms that arise from the change of measure between their natural measures and the terminal measure.
Under , the dynamics of for become:
where:
- : forward LIBOR rate
- : drift term under the terminal measure
- : volatility of the forward rate
- : Brownian motion under the terminal measure
The drift term is defined as:
where:
- : forward LIBOR rates
- : accrual period
- : correlation between forward rates and
- : volatilities of the respective forward rates
- : state-dependent drift ensuring no arbitrage under the terminal measure
This drift term ensures consistency when modeling all rates under the single terminal measure . To understand each component, consider the following interpretation. The fraction measures the sensitivity of the bond price ratio to the rate . This is essentially the duration of a single-period bond with respect to its yield. The product captures the covariance between the rate being modeled () and the rates acting as numeraires (). The summation accumulates adjustments for all intermediate rates between the payment date and the terminal horizon.
This drift term arises from the change of measure from each forward rate's natural measure to the terminal measure. The negative sign indicates that rates with earlier payment dates tend to drift downward under the terminal measure, which compensates for the fact that they are martingales under different measures.
Correlation Structure
Pricing products that depend on multiple rates, such as swaptions, requires the correlation structure between forward rates. A swap rate is essentially a weighted average of forward rates, so the volatility of the swap rate depends on the individual forward rate volatilities and on how the forward rates co-move. Common parameterizations include:
Constant correlation:
where:
- : correlation between forward rates and
- : constant correlation parameter
This simple specification assumes that all pairs of forward rates have the same correlation. While convenient for calibration, it lacks economic nuance because it treats the correlation between 1-year and 2-year rates the same as the correlation between 1-year and 10-year rates.
Exponential decay:
where:
- : correlation decay parameter
- : distance between tenor indices
This captures the intuition that forward rates with similar maturities are more correlated than rates far apart. Economic factors that drive short-term rates (monetary policy, near-term economic outlook) differ from those driving long-term rates (inflation expectations, long-run growth potential). The exponential form provides a smooth transition from high correlation for adjacent rates to lower correlation for distant rates.
def generate_correlation_matrix(n, model="exponential", params=None):
"""
Generate correlation matrix for forward rates.
Parameters:
-----------
n : int
Number of forward rates
model : str
'constant' or 'exponential'
params : dict
Model parameters (rho for constant, beta for exponential)
"""
if params is None:
params = {"rho": 0.8, "beta": 0.1}
corr = np.eye(n)
for i in range(n):
for j in range(n):
if i != j:
if model == "constant":
corr[i, j] = params["rho"]
elif model == "exponential":
corr[i, j] = np.exp(-params["beta"] * abs(i - j))
return corr
# Generate both types of correlation matrices
n_rates = 12
corr_constant = generate_correlation_matrix(n_rates, "constant", {"rho": 0.8})
corr_exponential = generate_correlation_matrix(
n_rates, "exponential", {"beta": 0.2}
)

Monte Carlo Simulation of the LMM
To price complex derivatives under the LMM, we simulate forward rate paths using the discretized dynamics under the terminal measure. The simulation requires careful handling of the state-dependent drift: at each time step, we must first evaluate the current drift based on the current forward rates before updating the rates for the next step. This creates a natural ordering where we update rates from longest maturity to shortest, since shorter rates depend on longer rates through the drift formula.
def simulate_lmm(
forward_rates_0,
volatilities,
correlation,
delta,
dt,
num_steps,
num_paths=10000,
):
"""
Simulate forward rates under the LIBOR Market Model using terminal measure.
Parameters:
-----------
forward_rates_0 : array
Initial forward rates L_i(0)
volatilities : array
Volatilities sigma_i for each forward rate
correlation : array
Correlation matrix between forward rates
delta : float
Accrual period
dt : float
Time step for simulation
num_steps : int
Number of time steps
num_paths : int
Number of Monte Carlo paths
Returns:
--------
forward_rates : array of shape (num_paths, num_steps+1, n)
Simulated forward rate paths
"""
n = len(forward_rates_0)
forward_rates = np.zeros((num_paths, num_steps + 1, n))
forward_rates[:, 0, :] = forward_rates_0
# Cholesky decomposition for correlated Brownians
chol = np.linalg.cholesky(correlation)
for step in range(num_steps):
t = step * dt
# Generate correlated random numbers
Z = np.random.randn(num_paths, n)
dW = Z @ chol.T * np.sqrt(dt)
for i in range(n):
fixing_time = (i + 1) * delta # Forward rate fixes at T_i
if t < fixing_time:
L_i = forward_rates[:, step, i]
# Calculate drift under terminal measure
drift = 0
for j in range(i + 1, n):
if t < (j + 1) * delta:
L_j = forward_rates[:, step, j]
drift -= (
(delta * L_j / (1 + delta * L_j))
* correlation[i, j]
* volatilities[i]
* volatilities[j]
)
# Lognormal dynamics
dL = L_i * (drift * dt + volatilities[i] * dW[:, i])
forward_rates[:, step + 1, i] = np.maximum(
L_i + dL, 1e-6
) # Keep positive
else:
forward_rates[:, step + 1, i] = forward_rates[
:, step, i
] # Fixed
return forward_rates
# Simulate LMM paths
np.random.seed(42)
n_rates = 12
forward_rates_0 = np.ones(n_rates) * 0.05 # 5% flat forward curve
volatilities = np.ones(n_rates) * 0.20 # 20% volatility
correlation = generate_correlation_matrix(
n_rates, "exponential", {"beta": 0.15}
)
delta = 0.5 # Semi-annual
lmm_paths = simulate_lmm(
forward_rates_0,
volatilities,
correlation,
delta,
dt=0.01,
num_steps=200, # 2 years
num_paths=5000,
)
# Prepare data for plotting
rate_index = 2 # L_2 corresponds to 1.5-2.0 year forward (fixes at 1.5y)
time_grid = np.linspace(0, 2, 201)
# Get statistics for the selected rate
paths_for_rate = lmm_paths[:, :, rate_index]
percentile_5 = np.percentile(paths_for_rate, 5, axis=0)
percentile_50 = np.percentile(paths_for_rate, 50, axis=0)
percentile_95 = np.percentile(paths_for_rate, 95, axis=0)
The simulation captures the stochastic evolution of forward rates, including their correlation structure. Notice how the rate fixes at its fixing time and remains constant thereafter. Before the fixing time, the rate fluctuates randomly around its initial value, with the distribution widening as time progresses. After fixing, the rate becomes a realized constant, which reflects the actual market mechanics where LIBOR rates are set at the beginning of each accrual period.
