Calibration and Parameter Estimation in Models › Calibrating Short-Rate Models to the Yield Curve

Michael BrenndoerferDecember 13, 20258 min read
Quantitative FinanceAll chapters
Part 8 of 15Econometrics and Statistical Modeling
Chapter 45Calibration and Parameter Estimation in Models › Calibrating Short-Rate Models to the Yield Curve
Reading section 7 of 17 · Calibrating Short-Rate Models to the Yield Curve · 8 minSection 7 of 17 · 8 min

Book contents

Section 7 of 17 · 8 min read

Calibration and Parameter Estimation in Models › Calibrating Short-Rate Models to the Yield CurveOne section · Sections in this chapter7 / 17 ↓
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

Short-rate models specify the dynamics of the instantaneous interest rate. For market-consistent pricing, fit the current term structure as closely as the chosen family permits; constant-parameter Vasicek generally cannot match an arbitrary initial curve exactly. In this example the Vasicek coefficients are under the risk-neutral pricing measure QQ, rather than historical estimates under PP. The bond pays one default-free unit at maturity.

The Vasicek model is one of the foundational short-rate models, valued for its analytical tractability and intuitive interpretation. It specifies the evolution of the instantaneous short rate through the following stochastic differential equation:

drt=κ(θ−rt)dt+σdWtdr_t = \kappa(\theta - r_t)dt + \sigma dW_t

where:

  • rtr_t: instantaneous short rate at time tt
  • κ\kappa: mean reversion speed, determining how quickly the rate returns to equilibrium
  • θ\theta: long-term mean rate, the equilibrium level toward which rates are pulled
  • σ\sigma: volatility of the rate, controlling the magnitude of random fluctuations
  • dWtdW_t: standard Brownian motion driving the random component

For κ>0\kappa>0, the drift κ(θ−rt)\kappa(\theta-r_t) pulls the conditional mean toward θ\theta: it is negative above that level and positive below it. Larger κ\kappa gives faster conditional-mean reversion. This is the model's idealization, not a claim that market rates or Gaussian sample paths stay in a bounded range.

Under the Vasicek model, zero-coupon bond prices have a closed-form solution. This analytical tractability is one of the model's key advantages, enabling rapid calibration and pricing:

P(t,T)=A(t,T)e−B(t,T)rtP(t, T) = A(t, T) e^{-B(t, T) r_t}

where:

  • P(t,T)P(t, T): price at time tt of a bond maturing at TT
  • rtr_t: instantaneous short rate at time tt
  • tt: current time
  • TT: maturity time
  • κ,θ,σ\kappa, \theta, \sigma: Vasicek model parameters
  • B(t,T)=1−e−κ(T−t)κB(t, T) = \frac{1 - e^{-\kappa(T-t)}}{\kappa}: duration-like factor that captures how bond prices respond to rate changes
  • A(t,T)=exp⁡[(θ−σ22κ2)(B(t,T)−(T−t))−σ24κB(t,T)2]A(t, T) = \exp\left[\left(\theta - \frac{\sigma^2}{2\kappa^2}\right)(B(t, T) - (T-t)) - \frac{\sigma^2}{4\kappa}B(t, T)^2\right]: exponential-affine prefactor incorporating the expected mean-reverting rate path and variance-induced convexity

The function B(t,T)B(t, T) resembles modified duration in some ways, measuring the sensitivity of the bond price to changes in the short rate. The function A(t,T)A(t, T) incorporates convexity effects and the impact of the long-term mean on bond pricing.

The continuously compounded yield can be derived from the bond price formula:

R(t,T)=−ln⁡P(t,T)T−t=−ln⁡(A(t,T)e−B(t,T)rt)T−t(substitute bond price)=B(t,T)rt−ln⁡A(t,T)T−t(simplify)\begin{aligned} R(t, T) &= -\frac{\ln P(t, T)}{T - t} \\ &= -\frac{\ln\left(A(t, T) e^{-B(t, T) r_t}\right)}{T - t} && \text{(substitute bond price)} \\ &= \frac{B(t, T) r_t - \ln A(t, T)}{T - t} && \text{(simplify)} \end{aligned}

where:

  • R(t,T)R(t, T): continuously compounded yield for maturity TT
  • P(t,T)P(t, T): zero-coupon bond price
  • A(t,T),B(t,T)A(t, T), B(t, T): Vasicek bond price coefficients defined above
  • rtr_t: instantaneous short rate
  • tt: current time
  • TT: maturity time

This relationship translates the pricing-measure parameters into observable yields. For stable numerical evaluation, the code uses the Gaussian integral I=∫0Trs dsI=\int_0^T r_s\,ds. Its mean is m=r0B+θ(T−B)m=r_0B+\theta(T-B) and its variance is v=σ2∫0T[(1−e−κu)/κ]2 duv=\sigma^2\int_0^T[(1-e^{-\kappa u})/\kappa]^2\,du, so log⁡P=−m+v/2\log P=-m+v/2. At κ=0\kappa=0, this becomes −r0T+σ2T3/6-r_0T+\sigma^2T^3/6, with a positive variance correction. Near zero, a series for the integrated variance avoids subtracting large cancelling terms; yields use log prices directly.

In[14]:
Code
from math import expm1, factorial, fsum


def vasicek_log_bond_price(r0, T, kappa, theta, sigma):
    """Log price of a unit zero-coupon bond under risk-neutral Vasicek."""
    if not all(np.isfinite(x) for x in (r0, T, kappa, theta, sigma)):
        raise ValueError("Vasicek inputs must be finite")
    if T <= 0 or kappa < 0 or sigma < 0:
        raise ValueError("Require T > 0, kappa >= 0 and sigma >= 0")
    x = kappa * T
    if not np.isfinite(x):
        raise ValueError("kappa * T is outside the supported range")
    if x < 0.1:
        b = fsum((-x) ** n / factorial(n + 1) for n in range(13))
        one_minus_b = fsum(
            (-1) ** (n + 1) * x**n / factorial(n + 1) for n in range(1, 13)
        )
        j = fsum(
            (-1) ** n * (2 ** (n + 2) - 2) * x**n / (factorial(n + 2) * (n + 3))
            for n in range(13)
        )
    else:
        b = -expm1(-x) / x
        one_minus_b = 1 - b
        b2 = -expm1(-2 * x) / (2 * x)
        j = fsum((1.0, -2 * b, b2)) / x**2
    mean_integral = T * (r0 * b + theta * one_minus_b)
    variance_integral = sigma**2 * T**3 * j
    log_price = -mean_integral + variance_integral / 2
    if not np.isfinite(log_price):
        raise ValueError("Unrepresentable Vasicek log price")
    return log_price


def vasicek_bond_price(r0, T, kappa, theta, sigma):
    """Unit bond price, with an explicit finite-representation check."""
    with np.errstate(over="raise", under="raise"):
        return float(np.exp(vasicek_log_bond_price(r0, T, kappa, theta, sigma)))


def vasicek_yield(r0, T, kappa, theta, sigma):
    """Continuously compounded yield, evaluated directly from log price."""
    return -vasicek_log_bond_price(r0, T, kappa, theta, sigma) / T

Let's create a synthetic market yield curve and calibrate the Vasicek model to it:

In[15]:
Code
# Market yield curve (maturities in years)
maturities = np.array([0.25, 0.5, 1, 2, 3, 5, 7, 10])
market_yields = np.array([0.02, 0.022, 0.025, 0.028, 0.030, 0.033, 0.035, 0.037])


def vasicek_calibration_objective(params, maturities, market_yields):
    """Squared yield errors in basis points."""
    maturities = np.asarray(maturities, dtype=float)
    market_yields = np.asarray(market_yields, dtype=float)
    if (
        maturities.ndim != 1
        or market_yields.ndim != 1
        or maturities.size == 0
        or maturities.shape != market_yields.shape
        or not np.all(np.isfinite(maturities))
        or not np.all(maturities > 0)
        or not np.all(np.isfinite(market_yields))
    ):
        raise ValueError("Require paired finite yields and positive maturities")
    params = np.asarray(params, dtype=float)
    if params.shape != (4,) or not np.all(np.isfinite(params)):
        return np.inf
    r0, kappa, theta, sigma = params
    if kappa < 0 or sigma < 0:
        return np.inf
    try:
        model = np.array(
            [vasicek_yield(r0, T, kappa, theta, sigma) for T in maturities]
        )
    except (ValueError, FloatingPointError, OverflowError):
        return np.inf
    if not np.all(np.isfinite(model)):
        return np.inf
    return float(np.sum(((model - market_yields) * 10000) ** 2))


# Initial guess
x0_vasicek = [0.02, 0.5, 0.03, 0.01]

# Bounds
bounds_vasicek = [(0.001, 0.10), (0.01, 2.0), (0.01, 0.10), (0.001, 0.10)]

# Calibrate
result_vasicek = minimize(
    vasicek_calibration_objective,
    x0_vasicek,
    args=(maturities, market_yields),
    method="L-BFGS-B",
    bounds=bounds_vasicek,
    options={"ftol": 1e-12, "gtol": 1e-6, "maxiter": 2000},
)

require_successful_fit(result_vasicek, "Vasicek calibration")
r0_cal, kappa_cal, theta_cal, sigma_cal = result_vasicek.x
Out[16]:
Console
Vasicek Calibration Results:
----------------------------------------
r0 (short rate):     0.0189 (1.89%)
kappa (mean rev.):   0.4418
theta (long-term):   0.0494 (4.94%)
sigma (volatility):  0.0591

Calibration SSE (bps squared): 112.64440100
RMSE (yield): 3.75 bps
Out[18]:
Visualization
Scatter plot of market yield observations overlaid with the calibrated Vasicek yield curve across maturities from 0.25 to 10 years.
Vasicek fit to a synthetic yield curve. Compare the fitted curve with its target yields and inspect the residuals and optimizer diagnostics before assessing adequacy.
Out[20]:
Visualization
Bar chart of Vasicek model calibration residuals in basis points across maturities from 0.25 to 10 years, showing small positive and negative deviations around zero.
Residuals of this Vasicek fit in basis points. Their size reflects numerical fitting as well as model restrictions; assess scaling, tolerances and starting points before attributing remaining structure to the model family.

The printed values are fitted parameters for this synthetic curve, not a validation of economic realism. Inspect the numerical objective, stopping diagnostics and sensitivity to scaling and starting points before interpreting residual patterns. Constant-parameter Vasicek restricts the shapes it can represent, but the residuals of one finite run also contain optimization error. Time-dependent extensions such as Hull-White can preserve an exact fit to a suitable initial curve.