QuantLib Treasury Curve Bootstrapping: Supply Bond Prices to Helpers
Summary
The document examines a failed attempt to construct a US Treasury zero-coupon curve in QuantLib using deposit and fixed-rate bond helpers. The key diagnostic is to build the curve from deposit helpers alone: this isolates the source of the reported root-bracketing error to the bond instruments rather than the short-term deposits.
The explanation identifies a mismatch in the bond-helper inputs. A FixedRateBondHelper expects a market price as well as the bond’s coupon rate, but the example feeds the same spot-rate value into both roles. That makes the calibration inconsistent and prevents the solver from finding a root. The answer demonstrates that changing the price input allows execution, but the illustrative prices are hypothetical and do not constitute market data. The document does not provide a complete methodology for selecting Treasury instruments, stripping coupons, or validating the resulting curve, so a usable curve still requires appropriate observed prices and conventions.
Key ideas
- Build the curve with deposit helpers alone to check whether the error originates in the bond helpers.
- A fixed-rate bond helper requires a bond price and coupon rate as separate inputs.
- Using spot-rate values as both prices and coupon rates creates inconsistent calibration inputs.
- The example’s replacement prices are hypothetical and should not be treated as observed Treasury prices.
Tags
Full text
# How to bootstrap the zero coupon curve for US treasuries
# How to bootstrap the zero coupon curve for US treasuries
Here is my understanding of the process:
- Capture price of most recently sold gov security at each tenor of the curve (reference treasuryDirect)
- For coupon paying securities, (i.e. tenor>2yr) you must strip the interest gained from coupon from the price and recalc the security
- Calculate yield for each tenor in the new curve using zero coupon prices
- Interpolate the curve for 360 nodes (representing months up to 30 years)
What am I doing wrong?
Hello, I need help trying to construct a zero coupon curve using treasury spot rates, I am following the cookbook on quantlib but struggling to understand why I am throwing the error:
```
_QuantLib.YieldTermStructure_zeroRate(self, *args) RuntimeError: 1st iteration: failed at 4th alive instrument, pillar February 3rd, 2025, maturity February 3rd, 2025, reference date February 2nd, 2024: root not bracketed: f[0.593053,1.59535] -> [-6.243439e+01,-1.681992e+02]
```
Code:
```
import QuantLib as ql
# Spot rates provided
spot_rates = [5.49, 5.51, 5.43, 5.42, 5.22, 4.81, 4.36, 4.14, 3.99, 4.02, 4.03, 4.33, 4.22] # in percent
# Corresponding tenors in months and years for deposits and bonds
deposit_tenors_months = [1, 3, 6]
bond_tenors_years = [1, 2, 3, 5, 7, 10, 20, 30]
# Convert annual spot rates to QuantLib QuoteHandle objects and adjust for percentage
spot_rate_handles = [ql.QuoteHandle(ql.SimpleQuote(rate / 100.0)) for rate in spot_rates]
# Assuming today's date is the settlement date
settlement_date = ql.Date(2, 2, 2024)
ql.Settings.instance().evaluationDate = settlement_date
# Define day count and calendar for deposits
calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
day_count_deposit = ql.Actual360()
# Deposit rate helpers
deposit_helpers = [
ql.DepositRateHelper(spot_rate_handles[i],
ql.Period(tenors, ql.Months),
2, # fixing days
calendar,
ql.ModifiedFollowing,
False,
day_count_deposit)
for i, tenors in enumerate(deposit_tenors_months)
]
# Day count for bonds
day_count_bond = ql.ActualActual(ql.ActualActual.ISDA)
# Bond rate helpers
bond_helpers = []
for i, tenors in enumerate(bond_tenors_years, start=3): # Starting from the 4th element in spot_rates
maturity_date = settlement_date + ql.Period(tenors, ql.Years)
schedule = ql.Schedule(settlement_date,
maturity_date,
ql.Period(ql.Annual),
calendar,
ql.Unadjusted,
ql.Unadjusted,
ql.DateGeneration.Backward,
False)
bond_helpers.append(
ql.FixedRateBondHelper(spot_rate_handles[i],
3, # settlement days
100.0, # face amount
schedule,
[spot_rates[i] / 100.0], # coupon rate
day_count_bond,
ql.ModifiedFollowing,
100.0) # redemption
)
# Combine deposit and bond helpers
rate_helpers = deposit_helpers + bond_helpers
# Construct the curve
curve = ql.PiecewiseLogCubicDiscount(settlement_date,
rate_helpers,
ql.ActualActual(ql.ActualActual.ISDA))
# Extract zero rates for months 1-360
zero_rates = []
for month in range(1, 361):
date = settlement_date + ql.Period(month, ql.Months)
yrs = curve.dayCounter().yearFraction(settlement_date, date)
zero_rate = curve.zeroRate(yrs, ql.Compounded, ql.Annual).rate()
zero_rates.append(zero_rate)
# Print some of the zero rates
print("Zero Rates for the first 12 months:")
for month, rate in enumerate(zero_rates[:12], start=1):
print(f"Month {month}: {rate*100:.2f}%")
```
## Answer by Xiarpedia (score 2)
https://quant.stackexchange.com/a/78239
By executing the following code:
```
# Construct the curve
curve = ql.PiecewiseLogCubicDiscount(
settlement_date, deposit_helpers, ql.ActualActual(ql.ActualActual.ISDA)
)
date, rates = zip(*curve.nodes())
```
I concluded that the deposits did not cause this issue. Thus moving onwards with the FixedRateBondHelpers, it is expecting prices and coupon rates as input. However, those two variables are set equally as of now, causing the error as the solver can not find a solution. If we hypothetically set the following price:
```
prices= [
ql.QuoteHandle(ql.SimpleQuote(rate+90)) for rate in spot_rates
]
```
and in the for loop use:
```
bond_helpers.append(
ql.FixedRateBondHelper(
prices[i],
3, # settlement days
100.0, # face amount
schedule,
[spot_rates[i] / 100.0], # coupon rate
day_count_bond,
ql.ModifiedFollowing,
100.0,
) # redemption
)
```
The code will execute and find a solution. Here are a few links that I found useful when investigating FixedRateBondHelper:
- Documentation
- Example usage of FixedRateBondHelperShown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.