Calibration and Parameter Estimation in Models › Calibrating Short-Rate Models to the Yield Curve
Quantitative FinanceAll chapters 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 , rather than historical estimates under . 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:
where:
- : instantaneous short rate at time
- : mean reversion speed, determining how quickly the rate returns to equilibrium
- : long-term mean rate, the equilibrium level toward which rates are pulled
- : volatility of the rate, controlling the magnitude of random fluctuations
- : standard Brownian motion driving the random component
For , the drift pulls the conditional mean toward : it is negative above that level and positive below it. Larger 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:
where:
- : price at time of a bond maturing at
- : instantaneous short rate at time
- : current time
- : maturity time
- : Vasicek model parameters
- : duration-like factor that captures how bond prices respond to rate changes
- : exponential-affine prefactor incorporating the expected mean-reverting rate path and variance-induced convexity
The function resembles modified duration in some ways, measuring the sensitivity of the bond price to changes in the short rate. The function 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:
where:
- : continuously compounded yield for maturity
- : zero-coupon bond price
- : Vasicek bond price coefficients defined above
- : instantaneous short rate
- : current time
- : maturity time
This relationship translates the pricing-measure parameters into observable yields. For stable numerical evaluation, the code uses the Gaussian integral . Its mean is and its variance is , so . At , this becomes , with a positive variance correction. Near zero, a series for the integrated variance avoids subtracting large cancelling terms; yields use log prices directly.
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) / TLet's create a synthetic market yield curve and calibrate the Vasicek model to it:
# 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.xVasicek 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


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.