Skip to content
All library documents

Why Zero-Coupon Bond Duration Differs Across QuantLib Bond Types

Article Quant Q&A · Author: TheRealMKS

Summary

The document compares duration and convexity calculated for a zero-coupon bond represented in QuantLib as either a dedicated zero-coupon instrument or a fixed-rate bond with a zero coupon. With the same maturity, settlement date, and zero yield, the reported measures differ slightly. The discussion suggests that the difference may come from how the two bond representations handle compounding frequency and coupon schedules.

An example using another rates library shows that changing a fixed-rate bond’s frequency from semiannual to annual changes its convexity to a value close to the dedicated zero-coupon result, while duration also shifts slightly. This comparison supports the frequency explanation, but the response presents it as a guess rather than a confirmed account of QuantLib’s implementation. The questioner notes that the fixed-rate bond figures agree with Bloomberg, but that observation does not establish which conventions drive the discrepancy. The practical lesson is to check instrument construction, day-count rules, and compounding conventions when comparing bond risk measures across representations or platforms.

Key ideas

  • A zero-coupon bond and a zero-coupon fixed-rate bond can produce slightly different risk measures in QuantLib.
  • The example holds maturity, settlement, and yield constant while changing the bond representation.
  • Changing the fixed-rate bond’s frequency alters the reported convexity and duration.
  • The proposed explanation involving mixed compounding frequencies is tentative and should be checked against library conventions.

Tags

Full text
# Difference in duration between zero 0 rate fixed coupon bond and zero coupon bond in Quantlib


# Difference in duration between zero 0 rate fixed coupon bond and zero coupon bond in Quantlib












I am trying the following example of calculating duration and convexity for a zero coupon bond. For the sake of an easy example, I set the Interest rate to be zero.

In the first attempt, I try using the object `ZeroCouponBond()`, shown in code below:

```
maturity_date = ql.Date(1,6,2025)
settlement_date = ql.Date(20,4,2013)
interest_rate_float = 0
coupon_rate = 0

start_date = settlement_date
schedule = ql.Schedule()

day_counter = ql.ActualActual(ql.ActualActual.Bond)
created_bond = ql.ZeroCouponBond(0, ql.NullCalendar(), float(100), maturity_date)
Interest_Rate = ql.InterestRate(interest_rate_float/100, day_counter, ql.Compounded, 2)

Convexity = ql.BondFunctions.convexity(created_bond, Interest_Rate, settlement_date)
print(Convexity/100)

mod_dur = ql.BondFunctions.duration(created_bond, Interest_Rate, ql.Duration.Modified, settlement_date)
print(mod_dur)

sim_dur = ql.BondFunctions.duration(created_bond, Interest_Rate, ql.Duration.Simple, settlement_date)
print(sim_dur)
```

This gives me the following values:

convexity: 1.5283241884030772

modified duration: 12.115068493150684

simple duration: 12.115068493150684

I repeat this process with a `FixedRateBond()` with coupon rate set to zero, as shown below:

```
maturity_date = ql.Date(1,6,2025)
settlement_date = ql.Date(20,4,2013)
interest_rate_float = 0
coupon_rate = 0

start_date = settlement_date
schedule = ql.Schedule(start_date, maturity_date, ql.Period(ql.Semiannual), ql.NullCalendar(), ql.Unadjusted, ql.Unadjusted, ql.DateGeneration.Backward, True)

day_counter = ql.ActualActual(ql.ActualActual.Bond)
created_bond = ql.FixedRateBond(0, float(100), schedule, [float(coupon_rate/100)], day_counter)
Interest_Rate = ql.InterestRate(interest_rate_float/100, day_counter, ql.Compounded, 2)

Convexity = ql.BondFunctions.convexity(created_bond, Interest_Rate, settlement_date)
print(Convexity/100)

mod_dur = ql.BondFunctions.duration(created_bond, Interest_Rate, ql.Duration.Modified, settlement_date)
print(mod_dur)

sim_dur = ql.BondFunctions.duration(created_bond, Interest_Rate, ql.Duration.Simple, settlement_date)
print(sim_dur)
```

This time, I get the following results:

convexity: 1.528402366863905

modified duration: 12.115384615384615

simple duration: 12.115384615384615

Why is there a difference in the two ways? Am I doing something wrong?

P.S: I checked the results against bloomberg and the values match with those that I get when I use Method 2 (i.e. `FixedRateBond()`)

## Answer by Attack68 (score 1)

https://quant.stackexchange.com/a/79983

My guess is that Quantlib is mixing the stated compounding frequencies.

If I run this,

```
# PYTHON v3.12
from rateslib import *  # v 1.4.0

frb = FixedRateBond(
    effective=dt(2013, 4, 20),
    termination=dt(2025, 6, 1),
    fixed_rate=0.0,
    convention="ActActBond",
    frequency="S",
    modifier="none",
)
frb.convexity(ytm=0, settlement=dt(2013, 4, 20))
# 1.528402366863905
frb.duration(ytm=0, settlement=dt(2013, 4, 20), metric="modified")
# 12.115384615384615
frb.duration(ytm=0, settlement=dt(2013, 4, 20), metric="duration")
# 12.115384615384615
```

whilst,

```

frb = FixedRateBond(
    effective=dt(2013, 4, 20),
    termination=dt(2025, 6, 1),
    fixed_rate=0.0,
    convention="ActActBond",
    frequency="A",    # <- NOTE THIS CHANGE
    modifier="none",
)
frb.convexity(ytm=0, settlement=dt(2013, 4, 20))
# 1.588899530868831
frb.duration(ytm=0, settlement=dt(2013, 4, 20), metric="modified")
# 12.115068493150686
frb.duration(ytm=0, settlement=dt(2013, 4, 20), metric="duration")
# 12.115068493150686
```

You can match these figures against your output and note that in Quantlib the convexity doesn't change from one to the another so that leads to my guess.

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.