Short-Rate Interest Rate Models › Bond Pricing with Short-Rate Models

Michael BrenndoerferDecember 6, 20258 min read
Quantitative FinanceAll chapters
Part 7 of 15Interest Rate Models and Derivatives
Chapter 38Short-Rate Interest Rate Models › Bond Pricing with Short-Rate Models
Reading section 7 of 13 · Bond Pricing with Short-Rate Models · 8 minSection 7 of 13 · 8 min

Book contents

Section 7 of 13 · 8 min read

Short-Rate Interest Rate Models › Bond Pricing with Short-Rate ModelsOne section · Sections in this chapter7 / 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

Vasicek and CIR models provide closed-form bond pricing formulas. Let's implement these and construct yield curves.

In[16]:
Code
def vasicek_bond_price(r, t, T, kappa, theta, sigma):
    """
    Calculate zero-coupon bond price under Vasicek model.

    Parameters:
    -----------
    r : float or array
        Current short rate
    t : float
        Current time
    T : float
        Maturity time
    kappa, theta, sigma : float
        Vasicek model parameters

    Returns:
    --------
    float or array : Bond price(s)
    """
    tau = T - t
    if tau <= 0:
        return 1.0

    B = (1 - np.exp(-kappa * tau)) / kappa
    A = np.exp(
        (theta - sigma**2 / (2 * kappa**2)) * (B - tau)
        - sigma**2 * B**2 / (4 * kappa)
    )

    return A * np.exp(-B * r)


def cir_bond_price(r, t, T, kappa, theta, sigma):
    """
    Calculate zero-coupon bond price under CIR model.

    Parameters:
    -----------
    r : float or array
        Current short rate
    t : float
        Current time
    T : float
        Maturity time
    kappa, theta, sigma : float
        CIR model parameters

    Returns:
    --------
    float or array : Bond price(s)
    """
    tau = T - t
    if tau <= 0:
        return 1.0

    gamma = np.sqrt(kappa**2 + 2 * sigma**2)
    exp_gamma = np.exp(gamma * tau)

    denominator = (gamma + kappa) * (exp_gamma - 1) + 2 * gamma

    B = 2 * (exp_gamma - 1) / denominator
    A = ((2 * gamma * np.exp((kappa + gamma) * tau / 2)) / denominator) ** (
        2 * kappa * theta / sigma**2
    )

    return A * np.exp(-B * r)

Yield Curve Construction

Let's generate yield curves for different initial short rates.

In[17]:
Code
# Maturities from 0.25 to 30 years
maturities = np.array([0.25, 0.5, 1, 2, 3, 5, 7, 10, 15, 20, 30])
r_scenarios = [0.02, 0.05, 0.08]  # Different starting rates


# Calculate yields for each scenario
def calculate_yields(r, maturities, kappa, theta, sigma, model="vasicek"):
    """Calculate continuously compounded yields for given maturities."""
    yields = np.zeros(len(maturities))
    for i, T in enumerate(maturities):
        if model == "vasicek":
            P = vasicek_bond_price(r, 0, T, kappa, theta, sigma)
        else:
            P = cir_bond_price(r, 0, T, kappa, theta, sigma)
        yields[i] = -np.log(P) / T
    return yields


# Compute yield curves for scenarios
vasicek_yields = np.array(
    [
        calculate_yields(r, maturities, kappa, theta, sigma, "vasicek")
        for r in r_scenarios
    ]
)
cir_yields = np.array(
    [
        calculate_yields(r, maturities, kappa, theta, sigma, "cir")
        for r in r_scenarios
    ]
)
Out[18]:
Visualization
Line plot showing three Vasicek yield curves starting at different rates converging at long maturities.
Vasicek yield curves for three initial rates (2%, 5%, 8%). The curves converge to the long-run mean θ=5% at long maturities, illustrating the effect of mean reversion.
Line plot showing three CIR yield curves starting at different rates converging at long maturities.
CIR yield curves for three initial rates. Like Vasicek, the curves converge to the long-run mean, but the specific shape is influenced by the state-dependent volatility.

The yield curves demonstrate several important features of mean-reverting short-rate models:

  1. Convergence: All curves converge toward a common long-term yield regardless of the initial rate. This reflects the mean-reversion property, where expected rates eventually gravitate toward θ\theta.

  2. Curve shape flexibility: When r(0)<θr(0) < \theta, the yield curve is upward-sloping (normal). When r(0)>θr(0) > \theta, the curve is downward-sloping (inverted). When r(0)=θr(0) = \theta, the curve is relatively flat but shows a slight convexity effect.

  3. Risk premium implicit in long yields: Long-term yields don't equal θ\theta exactly due to the interaction between mean reversion and interest rate volatility, which creates a convexity adjustment in bond prices.

Monte Carlo Bond Pricing

We can also price bonds using Monte Carlo simulation by averaging the discount factor across paths. This approach is more general than the closed-form solutions and extends to models without analytical formulas.

The Monte Carlo estimator for the zero-coupon bond price is:

P^(t,T)=1N∑i=1Nexp⁡(−∑j=0M−1rj(i)Δt)\hat{P}(t,T) = \frac{1}{N}\sum_{i=1}^N \exp\left(-\sum_{j=0}^{M-1} r_j^{(i)} \Delta t\right)

This formula averages the realized discount factors across all simulated paths:

  • NN number of simulation paths
  • MM: number of time steps
  • rj(i)r_j^{(i)}: short rate on path ii at step jj
  • Δt\Delta t: time step size

The inner sum ∑j=0M−1rj(i)Δt\sum_{j=0}^{M-1} r_j^{(i)} \Delta t approximates the integral ∫tTr(s)ds\int_t^T r(s) ds for path ii using a Riemann sum. The exponential of the negative of this integral gives the discount factor for that path. Averaging across all paths produces an unbiased estimator of the bond price, with standard error decreasing as 1/N1/\sqrt{N}.

This approach generalizes to complex interest rate derivatives. For any derivative with payoff h(rT1,rT2,…)h(r_{T_1}, r_{T_2}, \ldots) depending on rates at multiple times, we can estimate the price by simulating paths, computing the payoff on each path, discounting at the realized short-rate integral, and averaging.

In[19]:
Code
def monte_carlo_bond_price(rates, dt, T):
    """
    Calculate bond price using Monte Carlo simulation.

    Parameters:
    -----------
    rates : ndarray
        Simulated rate paths, shape (n_paths, n_steps+1)
    dt : float
        Time step size
    T : float
        Bond maturity

    Returns:
    --------
    float : Estimated bond price
    """
    # Number of steps to maturity
    steps_to_T = int(T / dt)

    # Calculate integral of rates using trapezoidal rule
    rate_integral = np.trapezoid(rates[:, : steps_to_T + 1], dx=dt, axis=1)

    # Discount factor for each path
    discount_factors = np.exp(-rate_integral)

    # Average across paths
    return discount_factors.mean(), discount_factors.std() / np.sqrt(
        len(discount_factors)
    )


# Compare analytical and Monte Carlo prices
test_maturities = [1, 2, 5, 10]
mc_results = []

for T_mat in test_maturities:
    # Analytical prices
    v_ana = vasicek_bond_price(r0, 0, T_mat, kappa, theta, sigma)
    c_ana = cir_bond_price(r0, 0, T_mat, kappa, theta, sigma)

    # Monte Carlo prices
    v_mc, v_se = monte_carlo_bond_price(vasicek_paths, dt, T_mat)
    c_mc, c_se = monte_carlo_bond_price(cir_paths, dt, T_mat)

    mc_results.append((T_mat, v_ana, v_mc, c_ana, c_mc))
Out[20]:
Console
Bond Price Comparison: Analytical vs Monte Carlo
=================================================================
Maturity   Vasicek      MC Vasicek     CIR          MC CIR      
-----------------------------------------------------------------
1          0.973999     0.973864       0.973954     0.973928    
2          0.940067     0.940146       0.939821     0.939827    
5          0.824426     0.826258       0.822950     0.823289    
10         0.647405     0.650255       0.643928     0.644419

The Monte Carlo estimates closely match the analytical formulas, validating our simulation implementation. The small differences arise from simulation error, which decreases with more paths or finer time steps.

Out[21]:
Visualization
Line plot showing Monte Carlo bond price estimates converging to analytical values as path count increases.
Monte Carlo convergence for 5-year bond pricing. The shaded bands show 95% confidence intervals that narrow as the number of paths increases. Both Vasicek and CIR Monte Carlo estimates converge to their respective analytical values.