Why Rebuilt Zero Curves Can Misprice Par Bonds
Summary
The document investigates why a zero curve reconstructed from a bootstrapped par-bond curve can price a bond away from par. The example builds a QuantLib curve using log-cubic interpolation on discount factors, converts curve rates into spot rates at the curve dates, and then constructs a second curve using linear interpolation on those rates. The reconstructed curve produces a small pricing discrepancy for a bond whose coupon matches an input par rate.
The accepted explanation points to differences in interpolation and cash-flow dates. The original curve has nodes at dates that do not coincide with every coupon payment date, so the rebuilt curve interpolates between nodes differently. The example also shows that one input maturity is a year and a half, affecting the node schedule. Reconstructing discount factors and using log-cubic discount interpolation brings the price back close to par in this case. This illustrates sensitivity to curve representation and interpolation; it does not establish that one interpolation method is universally preferable or eliminate other day-count and schedule conventions as possible sources of differences.
Key ideas
- A curve rebuilt with a different interpolation method can produce different bond prices.
- Par bond cash-flow dates may not coincide with the zero-curve nodes, requiring interpolation.
- The example’s initial curve interpolates log discount factors, while the reconstructed curve interpolates spot rates linearly.
- Rebuilding from discount factors with log-cubic interpolation restores the example’s price to approximately par.
- Maturity schedules, day-count conventions, and payment dates affect curve construction and valuation.
Tags
Full text
# Zero Curve from a par curve curve QuantLib
# Zero Curve from a par curve curve QuantLib
I'm trying to understand why pricing a par bond with zero curve, contracted from par bonds themselves doesn't give me par. (Based on examples here)
```
import QuantLib as ql
bond_maturities = [ql.Period(6*i, ql.Months) for i in range(3,21)]
bond_rates = [5.75, 6.0, 6.25, 6.5, 6.75, 6.80, 7.00, 7.1, 7.15,
7.2, 7.3, 7.35, 7.4, 7.5, 7.6, 7.6, 7.7, 7.8]
calc_date = ql.Date(15, 1, 2015)
ql.Settings.instance().evaluationDate = calc_date
calendar = ql.UnitedStates()
bussiness_convention = ql.Unadjusted
day_count = ql.ActualActual(ql.ActualActual.Bond) #My fix relative to the source
end_of_month = True
settlement_days = 0
face_amount = 100
coupon_frequency = ql.Period(ql.Semiannual)
settlement_days = 0
bond_helpers = []
for r, m in zip(bond_rates, bond_maturities):
termination_date = calc_date + m
schedule = ql.Schedule(calc_date,
termination_date,
coupon_frequency,
calendar,
bussiness_convention,
bussiness_convention,
ql.DateGeneration.Backward,
end_of_month)
helper = ql.FixedRateBondHelper(ql.QuoteHandle(ql.SimpleQuote(face_amount)),
settlement_days,
face_amount,
schedule,
[r/100.0],
day_count,
bussiness_convention,
)
bond_helpers.append(helper)
yieldcurve = ql.PiecewiseLogCubicDiscount(calc_date, bond_helpers, day_count)
yieldCurveHandle = ql.YieldTermStructureHandle(yieldcurve)
```
Now if I check the price of a bond with a yield from the input I should have par
```
termination_date = calc_date + bond_maturities[1] # 2Y bond
schedule = ql.Schedule(calc_date, termination_date, coupon_frequency, calendar,
bussiness_convention, bussiness_convention, ql.DateGeneration.Backward,
end_of_month)
coupons = [bond_rates[1] / 100]
fixed_rate_bond = ql.FixedRateBond(settlement_days, face_amount, schedule, coupons, day_count)
bond_engine = ql.DiscountingBondEngine(yieldCurveHandle)
fixed_rate_bond.setPricingEngine(bond_engine)
fixed_rate_bond.dirtyPrice()
```
Indeed
```
100.00000000001226
```
Now, I want to extract zero curve from the curve object
```
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)
```
From this curve I want to build a zero curve (based on example here)
```
spotCurve = ql.ZeroCurve(yieldcurve.dates(), spots, day_count, calendar, ql.Linear(),
ql.Compounded, ql.Semiannual)
spotCurveHandle = ql.YieldTermStructureHandle(spotCurve)
```
I expect the price of the bond still be par, but
```
bond_engine_spot = ql.DiscountingBondEngine(spotCurveHandle)
fixed_rate_bond.setPricingEngine(bond_engine_spot)
fixed_rate_bond.dirtyPrice()
```
```
100.0999007315603
```
Any ideas what I'm missing here?
## Answer by David Duarte (score 1, accepted)
https://quant.stackexchange.com/a/51664
I believe one the problems is the interpolation, because the way you have it set up, you don't have a curve node for each of the cash flows of the bond you are pricing.
The curve you built initially uses LogCubic interpolation on discount factors and the one you are reconstructing uses linear interpolation on spot rates.
Notice that...
```
for cf in fixed_rate_bond.cashflows():
print(cf.date(), cf.amount())
```
will give you:
```
July 15th, 2015 3.0000000000000027
January 15th, 2016 3.0000000000000027
July 15th, 2016 3.0000000000000027
January 17th, 2017 3.0000000000000027
January 17th, 2017 100.0
```
But if you check you curve dates, you have: `spotCurve.dates()`
(Date(15,1,2015), Date(15,7,2016), Date(15,1,2017), Date(15,7,2017),
(...)
So you will be interpolating the discount factor/zero rate for 15.07.2015 and 15.01.2016.
The problem is actually the way you defined `bond_maturities` since the first period will have "1Y6M"
Incidentally, if you try to rebuild it with discount factors, even the way you have it set up but without linear interpolation on zero rates, it will be closer.
```
dfs = [spotCurve.discount(dt) for dt in spotCurve.dates()]
spotCurve = ql.MonotonicLogCubicDiscountCurve(spotCurve.dates(), dfs, day_count)
spotCurveHandle = ql.YieldTermStructureHandle(spotCurve)
bond_engine_spot = ql.DiscountingBondEngine(spotCurveHandle)
fixed_rate_bond.setPricingEngine(bond_engine_spot)
fixed_rate_bond.dirtyPrice()
```
which outputs: 100.00000000001228Shown 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.