Term Structure of Interest Rates › Bootstrapping the Zero Curve
Quantitative FinanceAll chapters Term Structure of Interest Rates › Bootstrapping the Zero CurveOne section · Sections in this chapter3 / 8 ↓
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
Direct single-payment prices do not cover every maturity needed for a curve. Long-dated zeros do exist, including Treasury STRIPS, but coupon-bearing instruments often supply additional pricing information. Bootstrapping is one way to extract maturity-specific discount factors from those prices.
A coupon bond's yield to maturity is the single internal rate of return that discounts its payment schedule to its price under a stated convention. It summarizes several cash-flow dates through a nonlinear pricing equation; it is not a simple average of their zero rates. We can work backward from coupon bond prices sequentially, or use a fitted curve model.
The Bootstrapping Algorithm
Bootstrapping extracts zero-coupon rates or discount factors from instrument prices in maturity order. Each step uses previously determined nodes and a stated interpolation rule for any missing payment dates, then solves the new maturity's pricing equation.
With suitable instruments and a local interpolation rule, each step can leave one new scalar unknown. Earlier coupon-date discounts must already be known or supplied by that rule. When interpolation depends on the candidate maturity rate, solve the resulting scalar equation rather than treating those intermediate discounts as fixed.
The algorithm works as follows:
- Start with an instrument that isolates one discount factor, such as a zero-coupon bond. The shortest coupon bond alone may not identify discounts for all its payment dates.
- For each successive maturity, discount earlier cash flows using solved nodes and the chosen interpolation rule.
- Solve for the new rate that reproduces the instrument's price, checking that the implied discount factor is positive.
- Repeat for the remaining instruments, with consistent currency, credit and quoting conventions.
Consider a 2-year annual coupon bond with face value $100, annual coupon payment dollars, and price . Here is a cash amount: a 5% annual coupon on $100 face gives . If we know the 1-year zero rate , we can solve for :
This equation says that the bond's price equals the present value of all its cash flows. The first coupon payment at year 1 is discounted at the known 1-year rate. The final payment (coupon plus principal) at year 2 is discounted at the unknown 2-year rate.
Rearranging step by step to isolate the unknown rate:
where:
- : market price of the 2-year bond
- : annual coupon payment
- : previously bootstrapped 1-year zero rate
- : unknown 2-year zero rate we're solving for
The numerator is the residual price after subtracting the first coupon's present value. It must be positive for a finite real zero rate when the final cash flow is positive. Dividing by that final payment gives its discount factor; the logarithm then recovers the rate. If the residual is nonpositive, check the data, assumptions and instrument suitability rather than taking its logarithm.
Worked Example: Bootstrapping from Bond Prices
Let's bootstrap a zero curve from the following bonds, all with $100 face value and annual coupon payments:
| Maturity | Coupon Rate | Price |
|---|---|---|
| 1 year | 0% | $96.15 |
| 2 years | 5% | $101.50 |
| 3 years | 6% | $103.00 |
Step 1: 1-year zero rate
The 1-year bond is a zero-coupon bond, so we can extract its rate directly using the fundamental zero-rate formula:
Step 2: 2-year zero rate
The 2-year bond pays $5 at year 1 and $105 at year 2. We know the 1-year rate, so we can discount the first coupon and solve for the 2-year rate:
Step 3: 3-year zero rate
The 3-year bond pays $6 at years 1 and 2, and $106 at year 3. Using the rates already determined, we can discount the intermediate coupons and solve for the remaining unknown:
We retain unrounded discount factors throughout the calculation and round only the displayed results. The bootstrapped zero curve is:
| Maturity | Zero Rate |
|---|---|
| 1 year | 3.93% |
| 2 years | 4.12% |
| 3 years | 4.83% |



Python Implementation of Bootstrapping
import numpy as np
def bootstrap_zero_curve(maturities, coupon_rates, prices, face_value=100):
"""
Bootstrap zero rates from coupon bond prices.
Parameters:
-----------
maturities : array-like
Bond maturities in years (assumed to be 1, 2, 3, ...)
coupon_rates : array-like
Annual coupon rates as decimals
prices : array-like
Bond prices
face_value : float
Face value of bonds
Returns:
--------
zero_rates : ndarray
Continuously compounded zero rates
discount_factors : ndarray
Discount factors for each maturity
Raises:
-------
ValueError
If a final payment, residual price after known cash flows, or implied
discount factor is nonfinite or nonpositive.
"""
n = len(maturities)
zero_rates = np.zeros(n)
discount_factors = np.zeros(n)
for i in range(n):
maturity = maturities[i]
coupon = coupon_rates[i] * face_value
price = prices[i]
# Calculate PV of known cash flows
pv_known = 0
for j in range(i):
pv_known += coupon * discount_factors[j]
# Final cash flow
final_cf = face_value + coupon
if not np.isfinite(final_cf) or final_cf <= 0:
raise ValueError("Final cash flow must be finite and positive.")
# Solve for discount factor
residual = price - pv_known
if not np.isfinite(residual) or residual <= 0:
raise ValueError(
"Residual price must be finite and positive; "
"check quotes and valuation assumptions."
)
df = residual / final_cf
if not np.isfinite(df) or df <= 0:
raise ValueError(
"Implied discount factor must be finite and positive; "
"check quotes and valuation assumptions."
)
discount_factors[i] = df
# Convert to zero rate
zero_rates[i] = -np.log(df) / maturity
return zero_rates, discount_factors# Bond data from our example
maturities = np.array([1, 2, 3])
coupon_rates = np.array([0.00, 0.05, 0.06])
prices = np.array([96.15, 101.50, 103.00])
# Bootstrap the zero curve
zero_rates, discount_factors = bootstrap_zero_curve(maturities, coupon_rates, prices)Bootstrapped Zero Curve ---------------------------------------- Maturity Discount Factor Zero Rate ---------------------------------------- 1 yr 0.9615 3.93% 2 yr 0.9209 4.12% 3 yr 0.8651 4.83%
The bootstrapped rates (3.93%, 4.12%, 4.83%) match our manual calculations exactly. The discount factors (0.9615, 0.9209, 0.8651) show how $1 received in the future is worth progressively less as the maturity increases. A dollar in 3 years is worth only about 87 cents today.
Handling Intermediate Maturities
Real-world bootstrapping must handle bonds that don't mature at convenient annual intervals. When you need a discount factor for a maturity that falls between bootstrapped points, you interpolate. Common interpolation methods include:
- Linear interpolation on zero rates: Simple but can produce non-smooth forward rates
- Log-linear interpolation on discount factors: Preserves positive factors between positive endpoints; the implied interval forward rate is positive, zero, or negative according to whether the factors decline, stay constant, or increase
- Cubic spline interpolation: Produces smooth curves but may oscillate
from scipy.interpolate import CubicSpline
def interpolate_zero_curve(
known_maturities, known_rates, target_maturities, method="cubic"
):
"""
Interpolate within the known maturity range.
Outside it, linear interpolation holds endpoint rates constant,
while the cubic branch extrapolates its end polynomial.
"""
if method == "linear":
return np.interp(target_maturities, known_maturities, known_rates)
elif method == "cubic":
cs = CubicSpline(known_maturities, known_rates)
return cs(target_maturities)
else:
raise ValueError(f"Unknown method: {method}")
# Create a fine grid of maturities
fine_maturities = np.linspace(0.5, 3.5, 100)
# Add an illustrative zero-year anchor equal to the one-year rate.
# This is a boundary assumption, not another observed instrument.
extended_maturities = np.concatenate([[0], maturities])
extended_rates = np.concatenate(
[[zero_rates[0]], zero_rates]
) # Equal endpoint anchors do not force a flat cubic between them.
# Interpolate
interpolated_rates = interpolate_zero_curve(
extended_maturities, extended_rates, fine_maturities, method="cubic"
)
Within its supported range, interpolation supplies discounts for intermediate payment dates. Outside that range, choose and justify an extrapolation policy. Pricing derivatives with uncertain payments can also require volatility, credit or other model inputs beyond this curve.