Advanced Interest Rate Models (HJM and LMM) › The LIBOR Market Model

Michael BrenndoerferDecember 7, 202517 min read
Quantitative FinanceAll chapters
Part 7 of 15Interest Rate Models and Derivatives
Chapter 39Advanced Interest Rate Models (HJM and LMM) › The LIBOR Market Model
Reading section 4 of 13 · The LIBOR Market Model · 17 minSection 4 of 13 · 17 min

Book contents

Section 4 of 13 · 17 min read

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 T0<T1<⋯<TnT_0 < T_1 < \cdots < T_n with equal spacing δ=Ti+1−Ti\delta = T_{i+1} - T_i (typically 0.25 or 0.5 years). The forward LIBOR rate Li(t)L_i(t) is the simply compounded rate at time tt for borrowing over the period [Ti,Ti+1][T_i, T_{i+1}]. Unlike the continuously compounded instantaneous forward rate in HJM, the LIBOR rate applies simple interest over a discrete period:

Li(t)=1δ(P(t,Ti)P(t,Ti+1)−1)L_i(t) = \frac{1}{\delta}\left(\frac{P(t, T_i)}{P(t, T_{i+1})} - 1\right)

where:

  • δ=Ti+1−Ti\delta = T_{i+1} - T_i: year fraction (accrual period)
  • P(t,Ti)P(t, T_i): price of a zero-coupon bond maturing at TiT_i
  • P(t,Ti+1)P(t, T_{i+1}): price of a zero-coupon bond maturing at Ti+1T_{i+1}

The formula expresses a simple arbitrage relationship: if you invest one dollar at the LIBOR rate from TiT_i to Ti+1T_{i+1}, you should receive 1+δLi(t)1 + \delta L_i(t) dollars. Alternatively, you could buy 1/P(t,Ti+1)1/P(t, T_{i+1}) bonds maturing at Ti+1T_{i+1}, which costs P(t,Ti)/P(t,Ti+1)P(t, T_i)/P(t, T_{i+1}) in terms of bonds maturing at TiT_i. 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 Li(t)L_i(t) for the appropriate index ii.

LMM Dynamics Under the Forward Measure

The key insight of LMM is that under the Ti+1T_{i+1}-forward measure QTi+1\mathbb{Q}^{T_{i+1}}, the forward LIBOR rate Li(t)L_i(t) is a martingale. This follows because Li(t)L_i(t) 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 P(t,Ti+1)P(t, T_{i+1}) 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, Li(t)L_i(t) has zero drift and follows:

dLi(t)Li(t)=σi(t) dWiTi+1(t)\frac{dL_i(t)}{L_i(t)} = \sigma_i(t) \, dW_i^{T_{i+1}}(t)

where:

  • σi(t)\sigma_i(t): volatility of the ii-th forward rate
  • WiTi+1(t)W_i^{T_{i+1}}(t): Brownian motion under the Ti+1T_{i+1}-forward measure QTi+1\mathbb{Q}^{T_{i+1}}

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.

Forward Measure

The TT-forward measure uses the zero-coupon bond P(t,T)P(t, T) as numeraire. Under this measure, any asset price divided by P(t,T)P(t, T) is a martingale. The forward LIBOR rate Li(t)L_i(t) is naturally a martingale under the Ti+1T_{i+1}-forward measure.

Black's Formula for Caplets

A caplet pays δmax⁡(Li(Ti)−K,0)\delta \max(L_i(T_i) - K, 0) at time Ti+1T_{i+1}, where KK 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 Li(t)L_i(t) is a martingale under QTi+1\mathbb{Q}^{T_{i+1}} and follows lognormal dynamics, the caplet price has a Black-Scholes-type closed-form solution known as Black's formula:

Capleti=δ⋅P(0,Ti+1)⋅[Li(0)N(d1)−KN(d2)]\text{Caplet}_i = \delta \cdot P(0, T_{i+1}) \cdot [L_i(0) N(d_1) - K N(d_2)]

where:

  • Li(0)L_i(0): initial forward LIBOR rate
  • KK: cap strike rate
  • P(0,Ti+1)P(0, T_{i+1}): discount factor for the payment date
  • N(⋅)N(\cdot): cumulative standard normal distribution function
  • d1=ln⁡(Li(0)/K)+12vi2vid_1 = \frac{\ln(L_i(0)/K) + \frac{1}{2}v_i^2}{v_i}
  • d2=d1−vid_2 = d_1 - v_i
  • vi=∫0Tiσi(t)2 dtv_i = \sqrt{\int_0^{T_i} \sigma_i(t)^2 \, dt}: integrated volatility (approximated as σiTi\sigma_i \sqrt{T_i} for constant volatility)

The formula terms provide the following intuition:

  • N(d2)N(d_2): probability that the option expires in-the-money under the forward measure
  • Li(0)N(d1)L_i(0) N(d_1): expected value of the floating rate at maturity, conditional on exercise
  • KN(d2)K N(d_2): 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 P(0,Ti+1)P(0, T_{i+1}) converts the expected payoff under the forward measure to a present value.

A cap is simply a portfolio of caplets:

Cap=∑i=0n−1Capleti\text{Cap} = \sum_{i=0}^{n-1} \text{Caplet}_i

where:

  • Cap: total value of the interest rate cap
  • Caplet_i: value of the ii-th individual caplet
  • nn: 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.

In[7]:
Code
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
]
Out[8]:
Console
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.

Out[9]:
Visualization
Line plot showing caplet payoff as a function of LIBOR rate, with zero payoff below strike and linear increase above it.
Payoff diagram for a standard interest rate caplet with strike $K=5\%$. The non-linear payoff profile $\max(L-K, 0)$ demonstrates the option's protection against rising rates, acting effectively as an insurance policy for floating-rate borrowers.
Out[10]:
Visualization
Line plot showing cap price decreasing as strike rate increases.
Pricing profile of a 3-year cap across varying strike rates. The inverse relationship illustrates the premium reduction as the option moves out-of-the-money, with the curve's convexity highlighting the sensitivity of protection costs to the chosen strike level.

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 Li(t)L_i(t) is a martingale under a different measure QTi+1\mathbb{Q}^{T_{i+1}}. 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 QTn\mathbb{Q}^{T_n} (using P(t,Tn)P(t, T_n) as numeraire). Under this measure, only Ln−1(t)L_{n-1}(t) 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 QTn\mathbb{Q}^{T_n}, the dynamics of Li(t)L_i(t) for i<n−1i < n-1 become:

dLi(t)Li(t)=μi(t) dt+σi(t) dWiTn(t)\frac{dL_i(t)}{L_i(t)} = \mu_i(t) \, dt + \sigma_i(t) \, dW_i^{T_n}(t)

where:

  • Li(t)L_i(t): forward LIBOR rate
  • μi(t)\mu_i(t): drift term under the terminal measure
  • σi(t)\sigma_i(t): volatility of the forward rate
  • WiTn(t)W_i^{T_n}(t): Brownian motion under the terminal measure QTn\mathbb{Q}^{T_n}

The drift term is defined as:

μi(t)=−∑j=i+1n−1δLj(t)1+δLj(t)ρijσi(t)σj(t)\mu_i(t) = -\sum_{j=i+1}^{n-1} \frac{\delta L_j(t)}{1 + \delta L_j(t)} \rho_{ij} \sigma_i(t) \sigma_j(t)

where:

  • Li(t),Lj(t)L_i(t), L_j(t): forward LIBOR rates
  • δ\delta: accrual period
  • ρij\rho_{ij}: correlation between forward rates LiL_i and LjL_j
  • σi(t),σj(t)\sigma_i(t), \sigma_j(t): volatilities of the respective forward rates
  • μi(t)\mu_i(t): state-dependent drift ensuring no arbitrage under the terminal measure

This drift term ensures consistency when modeling all rates under the single terminal measure QTn\mathbb{Q}^{T_n}. To understand each component, consider the following interpretation. The fraction δLj(t)1+δLj(t)\frac{\delta L_j(t)}{1 + \delta L_j(t)} measures the sensitivity of the bond price ratio P(t,Tj)/P(t,Tj+1)P(t, T_j)/P(t, T_{j+1}) to the rate LjL_j. This is essentially the duration of a single-period bond with respect to its yield. The product ρijσi(t)σj(t)\rho_{ij} \sigma_i(t) \sigma_j(t) captures the covariance between the rate being modeled (LiL_i) and the rates acting as numeraires (LjL_j). 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 ρij\rho_{ij} 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:

ρij=ρfor all i≠j\rho_{ij} = \rho \quad \text{for all } i \neq j

where:

  • ρij\rho_{ij}: correlation between forward rates LiL_i and LjL_j
  • ρ\rho: 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:

ρij=e−β∣i−j∣\rho_{ij} = e^{-\beta |i - j|}

where:

  • β\beta: correlation decay parameter
  • ∣i−j∣|i - j|: 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.

In[11]:
Code
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}
)
Out[12]:
Visualization
Twelve-by-twelve heatmap with values of 1 on the diagonal and a uniform correlation of 0.8 for every off-diagonal forward-rate pair.
Correlation matrix for the Constant Correlation model ($\rho=0.8$). The uniform off-diagonal color indicates that all forward rate pairs are assumed to move together with the same degree of dependence, regardless of their maturity distance.
Twelve-by-twelve heatmap with values of 1 on the diagonal and bands that decline in correlation as the distance between forward-rate indices increases.
Correlation matrix for the Exponential Decay model ($\beta=0.2$). The gradient from the diagonal outward illustrates the realistic property that adjacent rates are highly correlated while rates with widely separated maturities evolve more independently.

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.

In[13]:
Code
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)
Out[14]:
Visualization
Line chart of the median simulated 1.5y-2.0y forward rate with a 5th-to-95th percentile band. The distribution widens until the vertical fixing line at 1.5 years and remains unchanged afterward; a horizontal reference marks the initial 5% rate.
Cross-path distribution of the 1.5y-2.0y forward LIBOR rate under the LMM. The 5th-95th percentile band widens until the 1.5-year fixing date, after which every simulated path stops evolving and the median and percentile boundaries remain flat.

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.

Out[15]:
Visualization
Line plot showing median LMM forward curves at multiple time steps, with each successive curve shifted and shortened as rates fix.
Snapshots of the forward curve evolution in the LMM. The colored lines show the median term structure at progressive time steps, showing how the curve reshapes and shortens as near-term rates expire.