I've been using QuantLib for constructing a yield curve and pricing a bond. I am wondering if I'm using the correct method to create my yield term structure (yts) for the pricing process.

Here is the reproducible example :

import QuantLib as ql
import math

calculation_date = ql.Date().todaysDate()

ql.Settings.instance().evaluationDate = calculation_date
yts = ql.RelinkableYieldTermStructureHandle()

index = ql.OvernightIndex("USD Overnight Index", 0, ql.USDCurrency(), ql.UnitedStates(ql.UnitedStates.Settlement), ql.Actual360(),yts)

swaps = {
    ql.Period("1W"): 0.05064,
    ql.Period("2W"): 0.05067,
    ql.Period("3W"): 0.05072,
    ql.Period("1M"): 0.051021000000000004,
    ql.Period("2M"): 0.051391,
    ql.Period("3M"): 0.051745,
    ql.Period("4M"): 0.05194,
    ql.Period("5M"): 0.051980000000000005,
    ql.Period("6M"): 0.051820000000000005,
    ql.Period("7M"): 0.051584000000000005,
    ql.Period("8M"): 0.05131,
    ql.Period("9M"): 0.050924,
    ql.Period("10M"): 0.050603999999999996,
    ql.Period("11M"): 0.050121,
    ql.Period("12M"): 0.049550000000000004,
    ql.Period("18M"): 0.04558500000000001,
    ql.Period("2Y"): 0.042630999999999995,
    ql.Period("3Y"): 0.038952,
    ql.Period("4Y"): 0.036976,
    ql.Period("5Y"): 0.035919,
    ql.Period("6Y"): 0.03535,
    ql.Period("7Y"): 0.034998,
    ql.Period("8Y"): 0.034808,
    ql.Period("9Y"): 0.034738000000000005,
    ql.Period("10Y"): 0.034712,
    ql.Period("12Y"): 0.034801,
    ql.Period("15Y"): 0.034923,
    ql.Period("20Y"): 0.034662,
    ql.Period("25Y"): 0.03375,
    ql.Period("30Y"): 0.032826,
    ql.Period("40Y"): 0.030834999999999998,
    ql.Period("50Y"): 0.02896

rate_helpers = []

for tenor, rate in swaps.items():
    helper = ql.OISRateHelper(2, tenor, ql.QuoteHandle(ql.SimpleQuote(rate)), index)

curve = ql.PiecewiseFlatForward(calculation_date, rate_helpers, ql.Actual360())

index = index.clone(yts)

engine = ql.DiscountingSwapEngine(yts)

print("maturity |  market  |  model  |  zero rate  |  discount factor |  present value")
for tenor, rate in swaps.items():
    ois_swap = ql.MakeOIS(tenor, index, rate)
    pv = ois_swap.NPV()
    fair_rate = ois_swap.fairRate()
    maturity_date = ois_swap.maturityDate()
    discount_factor = curve.discount(maturity_date)
    zero_rate = curve.zeroRate(maturity_date, ql.Actual365Fixed() , ql.Continuous).rate()
    print(f"   {tenor}    | {rate*100:.6f} | {fair_rate*100:.6f} | {zero_rate*100:.6f} | {discount_factor:.6f} | {pv:.6f}")

issue_date = ql.Date(12,1,2022)
maturity_date = ql.Date(12,1,2027)
coupon_frequency = ql.Period(ql.Semiannual)
calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
date_generation = ql.DateGeneration.Backward
coupon_rate = 4.550000/100
day_count = ql.Thirty360(ql.Thirty360.USA)
spread = ql.SimpleQuote(89.965 / 10000.0)

schedule = ql.Schedule( issue_date,

bond = ql.FixedRateBond(2, 100, schedule, [coupon_rate], day_count)

spread_handle = ql.QuoteHandle(spread)
spreaded_curve = ql.ZeroSpreadedTermStructure(yts, spread_handle)
spreaded_curve_handle = ql.YieldTermStructureHandle(spreaded_curve)
print(f"NPV {bond.NPV()} vs dirty price {bond.dirtyPrice()} - clean price {bond.cleanPrice()}")

I'm using the yield term structure (yts) linked to the curve (curve) constructed using ql.PiecewiseFlatForward.

I'm wondering if it is correct to use the yts which links to the forward curve to price the bond.

Or, do I need to build a zero-coupon curve for pricing? If so, how would I build and use this zero-coupon curve?

I've noticed that QuantLib allows the conversion from forward rates to zero rates using the zeroRate() function. Is this function enough to derive the correct zero-coupon rates from the forward rates for bond pricing, or is a more explicit construction of a zero-coupon curve necessary?

Any guidance or examples would be greatly appreciated. Thanks!

  • $\begingroup$ I think that ql.DiscountingBondEngine() does the trick for me but I would appreciate confirmation. $\endgroup$
    – TourEiffel
    May 30, 2023 at 15:50

1 Answer 1


What curve to use has little to do with QuantLib itself and more to do with how you're modelling credit risk for your bond.

Bootstrapping over OIS rates, whether using QuantLib or not, gives you a risk-free rate, which can in fact also give you zero rates (by integrating the forwards) and, when using QuantLib, can in fact be passed to DiscountingBondEngine. But it's probably the wrong curve to use for discounting because it's risk-free.

What risky curve to use depends on the data you have available. You can fit one over quoted bond prices (see for example QuantLib's FittedBondDiscountCurve), or you can add a z-spread over the risk-free curve to add credit risk (with ZeroSpreadedTermStructure in QuantLib), or you can interpolate zero-rates or discount factors coming from some other desk (ZeroCurve or DiscountCurve, respectively).

It's a modelling choice, though, and depends on your context. Looking up how to use the corresponding class is probably the easy part.

  • $\begingroup$ Currently I add I-Spread : spreaded_curve = ql.ZeroSpreadedTermStructure(yts, spread_handle). Im pretty sure its the correct way to do but I wanted confirmation. $\endgroup$
    – TourEiffel
    May 31, 2023 at 11:46
  • 1
    $\begingroup$ "I-Spread" is usually the difference between a bond's yield and linearly interpolated rates. If you run an I-spread quote through any other calculation, you may not reproduce the intended price, although the difference would be small. $\endgroup$ Jun 1, 2023 at 11:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.