Skip to content
All library documents

Building a Zero Curve from Bond Prices with QuantLib

Article Quant Q&A · Author: Gao Haocheng

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.