Building a Zero Curve from Bond Prices with QuantLib
Summary
The document explains how to use QuantLib’s FixedRateBondHelper to bootstrap a discount curve from bond market data. It distinguishes the bond’s coupon, which defines its scheduled cash flows, from its quoted price, which calibrates the curve. Short-maturity deposits can be included as deposit helpers, while longer-maturity bonds use their schedules and coupon rates. The example uses quoted bond prices and shows curve nodes at instrument maturity dates.
The answers also clarify that a helper price is generally supplied as a clean price, and that settlement conventions, calendars, and schedule construction can shift dates. One example suggests par-priced bonds with yield used as coupon as a rough approximation, but this is not a substitute for using actual bond terms and prices. Results depend on matching day-count rules, payment dates, settlement details, and market conventions to the instruments being modeled.
Key ideas
- Bond helper coupons specify contractual cash flows; they are not the bond’s yield to maturity.
- The quoted bond price is the market input used to calibrate the curve, typically as a clean price.
- Short-dated instruments can be represented with deposit helpers and longer bonds with fixed-rate bond helpers.
- Curve node dates reflect helper maturity and the conventions used to build schedules and adjust dates.
- A par bond with yield substituted as coupon is only described as a rough approximation.
Tags
Full text
# parameter in FixedRateBondHelper of quantlib
# parameter in FixedRateBondHelper of quantlib
I'm working with 10 bonds with different maturity and want to get the zero curve. I tried the quantlib. However, I cannot understand the parameter in FixedRateBondHelper. Here is my code:
```
def bootstrap(ytm_data):
calc_date = ql.Date(13, 4, 2020)
ql.Settings.instance().evaluationDate = calc_date
ytm_data['ytm'] = (1+ytm_data['ytm']/2).pow(2)-1 # annulize YTM
calendar = ql.UnitedStates()
bussiness_convention = ql.Unadjusted
#day_count_bill = ql.Actual360()
day_count = ql.ActualActual(ql.ActualActual.Bond)
end_of_month = False
settlement_days = 2
face_amount = 100
coupon_frequency = ql.UnitedStates.GovernmentBond
depo_helper = []
bond_helper = []
for date,rate,quote in ytm_data.values.tolist():
timedelta = date - calc_date
period = ql.Period('%dd'%timedelta)
if timedelta<365:
depo_helper.append(ql.DepositRateHelper(ql.QuoteHandle(ql.SimpleQuote(rate)), period, settlement_days, calendar, bussiness_convention, end_of_month, day_count))
if timedelta>365:
schedule = ql.Schedule(calc_date, date,
coupon_frequency,
calendar,
bussiness_convention,
bussiness_convention,
ql.DateGeneration.Backward,
end_of_month)
helper = ql.FixedRateBondHelper(ql.QuoteHandle(ql.SimpleQuote(quote)),
settlement_days,
face_amount,
schedule,
[rate],
day_count,
bussiness_convention,
)
bond_helper.append(helper)
rate_helper = depo_helper + bond_helper
yieldcurve = ql.PiecewiseLogCubicDiscount(calc_date,
rate_helper,
day_count)
spots = []
tenors = []
for d in yieldcurve.dates():
yrs = day_count.yearFraction(calc_date, d)
compounding = ql.Compounded
freq = ql.Semiannual
zero_rate = yieldcurve.zeroRate(yrs, compounding, freq)
tenors.append(yrs)
eq_rate = zero_rate.equivalentRate(day_count,
compounding,
freq,
calc_date,
d).rate()
spots.append(eq_rate)
return spots,tenors,yieldcurve
```
Here is the market data Here is ytm_data
I have calculate YTM of each bonds. Here is the question:
- what's the data should I put into the ql.QuoteHandle(ql.SimpleQuote(rate)). I just use annulized YTM as rate
- what's the [rate] in ql.FixedRateBondHelper, the coupon rate our YTM rate.
- What's price should I put into ql.QuoteHandle(ql.SimpleQuote(quote)), clean price or full price.
- after calculation, the node in yieldcurve has a different datetime with original market data. Why does this happen.
I'm quit new to quantlib. Thanks in advance!
## Answer by David Duarte (score 2)
https://quant.stackexchange.com/a/54535
The FixedRateBondHelper class in python has the following constructor:
```
ql.FixedRateBondHelper(
price, settlementDays, faceAmount, schedule, coupons,
dayCounter, paymentConv=Following, redemption=100.0,
issueDate=Date(), paymentCalendar=Calendar(),
exCouponPeriod=Period(), exCouponCalendar=Calendar(),
exCouponConvention=Unadjusted, exCouponEndOfMonth=False,
useCleanPrice=True
)
```
- The first argumento should be the price.
- You should input the coupon.
- You should input the coupon. Alternatively, you could use a price of 100 and use the YTM as the coupon, which would be a pretty good approximation.
- Unless your calendar and conventions move the dates to business days, the days should match.
Try this simplified example:
```
import QuantLib as ql
calc_date = ql.Date(13, 4, 2020)
ql.Settings.instance().evaluationDate = calc_date
data = [
('17-03-2020', '12-05-2020', 0, 99.99),
('09-01-2020', '09-07-2020', 0, 99.97),
('10-10-2019', '08-10-2020', 0, 99.94),
('26-03-2020', '25-03-2021', 0, 99.88),
('31-03-2020', '31-03-2022', 0.375, 100.28),
('15-04-2020', '15-04-2023', 0.25, 99.88),
('31-03-2020', '31-03-2025', 0.5, 100.4),
('18-02-2020', '31-03-2027', 0.625, 100.15),
('18-02-2020', '15-02-2030', 1.5, 107.25),
('18-02-2020', '15-02-2050', 2.0, 115.95),
]
helpers = []
day_count = ql.ActualActual(ql.ActualActual.Bond)
for issue_date, maturity, coupon, price in data:
price = ql.QuoteHandle(ql.SimpleQuote(price))
issue_date = ql.Date(issue_date, '%d-%m-%Y')
maturity = ql.Date(maturity, '%d-%m-%Y')
schedule = ql.MakeSchedule(issue_date, maturity, ql.Period(ql.Semiannual))
helper = ql.FixedRateBondHelper(price, 2, 100, schedule, [coupon / 100], day_count)
helpers.append(helper)
yieldcurve = ql.PiecewiseLogCubicDiscount(calc_date, helpers, day_count)
```
If you check the curve nodes, they should match you instrument maturity dates...
```
yieldcurve.nodes()
```
((Date(13,4,2020), 1.0), (Date(12,5,2020), 0.999892874058905), (Date(9,7,2020), 0.9996928754839124), (Date(8,10,2020), 0.9993928776218975), (Date(25,3,2021), 0.9987928818978993), (Date(31,3,2022), 0.9954638937930196), (Date(15,4,2023), 0.9913234618735143), (Date(31,3,2025), 0.9794138780398628), (Date(31,3,2027), 0.9586836389927256), (Date(15,2,2030), 0.9291051282013438), (Date(15,2,2050), 0.6518640785683848))
## Answer by gregV (score 0)
https://quant.stackexchange.com/a/75517
Another example using FixedRateBondHelper on some US treasury data and following the code above:
```
import QuantLib as ql
calc_date = ql.Date(5, 5, 2023)
ql.Settings.instance().evaluationDate = calc_date
settlement_days = 1
face_amount = 100
data = [
('31-08-2021', '31-08-2023', 0.125, 98.43537),
('30-09-2021', '30-09-2023', 0.25, 98.13213),
('01-11-2021', '31-10-2023', 0.375, 97.82426),
('30-11-2021', '30-11-2023', 0.5, 97.56833),
('31-12-2021', '31-12-2023', 0.75, 97.36831),
('31-01-2022', '31-01-2024', 0.875, 97.16839),
('28-02-2022', '29-02-2024', 1.5, 97.35896),
('31-03-2022', '31-03-2024', 2.25, 97.79973),
('02-05-2022', '30-04-2024', 2.5, 97.88095),
('31-05-2022', '31-05-2024', 2.5, 97.77523),
('30-06-2022', '30-06-2024', 3.0, 98.2304),
('01-08-2022', '31-07-2024', 3.0, 98.21679),
('31-08-2022', '31-08-2024', 3.25, 98.53124),
('30-09-2022', '30-09-2024', 4.25, 99.87726),
('31-10-2022', '31-10-2024', 4.375, 100.14486),
('30-11-2022', '30-11-2024', 4.5, 100.42819),
('03-01-2023', '31-12-2024', 4.25, 100.1332),
('31-01-2023', '31-01-2025', 4.125, 100.0258),
('28-02-2023', '28-02-2025', 4.625, 101.04361),
('15-03-2022', '15-03-2025', 1.75, 95.98058),
]
helpers = []
for issue_date, maturity, coupon, price in data:
price = ql.QuoteHandle(ql.SimpleQuote(price))
issue_date = ql.Date(issue_date, '%d-%m-%Y')
maturity = ql.Date(maturity, '%d-%m-%Y')
schedule = ql.MakeSchedule(issue_date, maturity, ql.Period(ql.Semiannual))
day_count = ql.ActualActual(ql.ActualActual.Bond, schedule)
helper = ql.FixedRateBondHelper(price, 1, 100, schedule, [coupon / 100], day_count)
helpers.append(helper)
yc = ql.PiecewiseLogCubicDiscount(calc_date, helpers, day_count)
```Shown 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.