# How to bootstrap the zero coupon curve for US treasuries

Here is my understanding of the process:

1. Capture price of most recently sold gov security at each tenor of the curve (reference treasuryDirect)
2. For coupon paying securities, (i.e. tenor>2yr) you must strip the interest gained from coupon from the price and recalc the security
3. Calculate yield for each tenor in the new curve using zero coupon prices
4. 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.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}%")

• In your code you have 13 spot rates and 11 tenors for instruments. This didnt work for me, I omitted the last 2 spot rates.
– Attack68
Commented Feb 5 at 13:00
• @Attack68 ahh sorry I missed this as I wrote it at 2AM. I did correct the code to ensure that the yields and tenors are the same length but somehow am still getting the same error. Were you able to generate the zero curve after omitting the last 2 spot rates? Commented Feb 6 at 8:35
• Yes, in my own software, not in QuantLib, I don't understand the QuantLib error.
– Attack68
Commented Feb 6 at 8:44
• Thanks for editing the error message, it is much more understandable now. (Basically the iteration did not converge). Commented Feb 7 at 12:46
• You may wan to change your methodology to reflect better the practical behavior of U.S. treasury debt. Recall that the U.S. treasury seldom taps existing issues, unlike some other countries treasuries, but rather sells at par new issues with maturities close to already outstanding ("off-the-run") issues, but different coupons. The off-the-run treasuries form a curve,. The most recently issued ("on-the-run") treasuries usually trade a little special versus the off-the-run curve because of technical reasons that are unique to each issue and don't really create a curve or a spread with a (cont) Commented Feb 7 at 17:18

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: