Choosing Discount Curves for Bond Pricing in QuantLib
Summary
The document explains that a yield curve bootstrapped from overnight indexed swap quotes can provide discount factors and zero rates, so it can be used by a bond pricing engine. Converting forward rates to zero rates is not the central issue: QuantLib’s term structure supports these representations and the pricing engine can use it directly.
The key modelling choice is whether the curve reflects the bond’s credit risk. An OIS curve represents risk-free discounting, which may be unsuitable for a risky bond. Alternatives include fitting a discount curve to bond prices, applying a z-spread to the risk-free curve, or building a curve from supplied zero rates or discount factors. The appropriate choice depends on available market data and the modelling context; the document gives no numerical comparison or validation of the example’s particular bond price.
Key ideas
- An OIS curve can supply discount factors and zero rates for bond pricing.
- A separate zero-coupon curve is not inherently required just because the bootstrapped curve is represented using forwards.
- Risk-free OIS discounting may not capture a risky bond’s credit risk.
- Credit risk can be represented with a fitted bond curve, a z-spread, or externally supplied zero rates or discount factors.
- Curve choice is a modelling decision guided by the data and context.
Tags
Full text
# Using Yield Term Structure for Bond Pricing in QuantLib: Is a Zero-Coupon Curve Necessary?
# Using Yield Term Structure for Bond Pricing in QuantLib: Is a Zero-Coupon Curve Necessary?
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)
rate_helpers.append(helper)
curve = ql.PiecewiseFlatForward(calculation_date, rate_helpers, ql.Actual360())
yts.linkTo(curve)
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,
maturity_date,
coupon_frequency,
calendar,
ql.Unadjusted,
ql.Unadjusted,
date_generation,
False)
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)
bond.setPricingEngine(ql.DiscountingBondEngine(spreaded_curve_handle))
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!
## Answer by Luigi Ballabio (score 2, accepted)
https://quant.stackexchange.com/a/75720
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.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.