Skip to content
All library documents

Matching QuantLib Bond Prices to Coupon and Curve Conventions

Article Quant Q&A · Author: cpage

Summary

The example explains why a QuantLib price for a fixed-rate bond can differ from a hand calculation. It first checks the generated cash flows and finds that the semiannual coupon schedule is as intended. The initial zero curve, however, interpolates from a zero rate at the valuation date to 4% a year later, so it does not apply a flat 4% rate throughout the first year. Setting all curve nodes to 4% removes that mismatch. The example then aligns the curve’s annual compounding convention with the bond’s semiannual payments, changing the discount factors again.

After these adjustments, the example reports a value close to, but still different from, the stated hand-calculated price. It suggests reviewing the hand calculation as well. The discussion illustrates that cash-flow dates, zero-rate interpolation, day counts, and compounding frequency all affect present value; it does not fully resolve the remaining small discrepancy.

Key ideas

  • Inspect generated cash flows before diagnosing a bond valuation mismatch.
  • A zero-rate curve with a zero-valued initial node will interpolate rates during the first year.
  • Compounding frequency affects discount factors even when the quoted rate is unchanged.
  • Semiannual coupons do not by themselves determine the curve’s compounding convention.
  • A remaining price difference can indicate an issue in the hand calculation or other conventions.

Tags

Full text
# Simple QuantLib Bond Math


# Simple QuantLib Bond Math












I am new to QuantLib and am trying to get it to replicate some simple bond math.

Suppose we have a 5-year bond with annual coupon payments of \$5 and face value of \$100, and interest rate of 4%. Classic calculations yield that the present value of the bond is \$104.45. When I try to do this simple example in QuantLib-Python, I get $104.70--despite my attempts to strip out calendar conventions.

How can I use QuantLib to line up with this simple bond math?

```
from QuantLib import *

# Construct yield curve
calc_date = Date(1, 1, 2017)
Settings.instance().evaluationDate = calc_date

spot_dates = [Date(1,1,2017), Date(1,1,2018), Date(1,1,2027)]
spot_rates = [0.0, 0.04, 0.04]

day_count = SimpleDayCounter()
calendar = NullCalendar()
interpolation = Linear()
compounding = Compounded
compounding_frequency = Annual
spot_curve = ZeroCurve(spot_dates, spot_rates, day_count, calendar, interpolation, compounding, compounding_frequency)

spot_curve_handle = YieldTermStructureHandle(spot_curve)

# Construct bond schedule
issue_date = Date(1, 1, 2017)
maturity_date = Date(1, 1, 2022)
tenor = Period(Semiannual)
calendar = NullCalendar()
business_convention = Unadjusted
date_generation = DateGeneration.Backward
month_end = False

schedule = Schedule(issue_date, maturity_date, tenor, calendar, business_convention, business_convention, date_generation, month_end)

# Create FixedRateBond Object

coupon_rate = 0.05
coupons = [coupon_rate]
settlement_days = 0
face_value = 100

fixed_rate_bond = FixedRateBond(settlement_days,
                                face_value,
                                schedule,
                                coupons,
                                day_count)

# Set Valuation engine
bond_engine = DiscountingBondEngine(spot_curve_handle)
fixed_rate_bond.setPricingEngine(bond_engine)

# Calculate present value
value = fixed_rate_bond.NPV()
print(value)
```

## Answer by Luigi Ballabio (score 13, accepted)

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

To begin with, as Student T suggested, you can check that the cashflows are those you expect:

```
for c in fixed_rate_bond.cashflows():
    print '%20s %12f' % (c.date(), c.amount())

      July 1st, 2017     2.500000
   January 1st, 2018     2.500000
      July 1st, 2018     2.500000
   January 1st, 2019     2.500000
      July 1st, 2019     2.500000
   January 1st, 2020     2.500000
      July 1st, 2020     2.500000
   January 1st, 2021     2.500000
      July 1st, 2021     2.500000
   January 1st, 2022     2.500000
   January 1st, 2022   100.000000
```

They seem ok, so the problem must be in the discount curve. One problem is that you're instantiating it as:

```
spot_dates = [Date(1,1,2017), Date(1,1,2018), Date(1,1,2027)]
spot_rates = [0.0, 0.04, 0.04]
...
```

But that means that zero rates will be interpolated between 0.0 and 0.04 during the first year, and of course that gives you wrong discount factors; so you should use

```
spot_dates = [Date(1,1,2017), Date(1,1,2018), Date(1,1,2027)]
spot_rates = [0.04, 0.04, 0.04]
```

instead. That gives a price of \$104.67, though, which is still not what you want. The next issue is that you're initializing the curve with a 4% rate and an annual compounding frequency; this gives you discount factors equal to $B(T) = 1/(1+R)^T$, which you can verify:

```
for i, c in enumerate(fixed_rate_bond.cashflows()):
    T = day_count.yearFraction(calc_date, c.date())
    B = 1/math.pow(1.04, T)
    print '%20s %12f %12f %12f %12f' % (c.date(), c.amount(), T,
                                        B, spot_curve.discount(c.date()))

      July 1st, 2017     2.500000     0.500000     0.980581     0.980581
   January 1st, 2018     2.500000     1.000000     0.961538     0.961538
      July 1st, 2018     2.500000     1.500000     0.942866     0.942866
   January 1st, 2019     2.500000     2.000000     0.924556     0.924556
      July 1st, 2019     2.500000     2.500000     0.906602     0.906602
   January 1st, 2020     2.500000     3.000000     0.888996     0.888996
      July 1st, 2020     2.500000     3.500000     0.871733     0.871733
   January 1st, 2021     2.500000     4.000000     0.854804     0.854804
      July 1st, 2021     2.500000     4.500000     0.838204     0.838204
   January 1st, 2022     2.500000     5.000000     0.821927     0.821927
   January 1st, 2022   100.000000     5.000000     0.821927     0.821927
```

Since you have semiannual coupons, you probably wanted to use a semiannual compounding frequency, too. That gives you discount factors $B(t) = 1/\left(1+\frac{R}{2}\right)^{2T}$:

```
for i, c in enumerate(fixed_rate_bond.cashflows()):
    T = day_count.yearFraction(calc_date, c.date())
    B = 1/math.pow(1.02, 2*T)
    print '%20s %12f %12f %12f %12f' % (c.date(), c.amount(), T,
                                        B, spot_curve.discount(c.date()))

      July 1st, 2017     2.500000     0.500000     0.980392     0.980392
   January 1st, 2018     2.500000     1.000000     0.961169     0.961169
      July 1st, 2018     2.500000     1.500000     0.942322     0.942322
   January 1st, 2019     2.500000     2.000000     0.923845     0.923845
      July 1st, 2019     2.500000     2.500000     0.905731     0.905731
   January 1st, 2020     2.500000     3.000000     0.887971     0.887971
      July 1st, 2020     2.500000     3.500000     0.870560     0.870560
   January 1st, 2021     2.500000     4.000000     0.853490     0.853490
      July 1st, 2021     2.500000     4.500000     0.836755     0.836755
   January 1st, 2022     2.500000     5.000000     0.820348     0.820348
   January 1st, 2022   100.000000     5.000000     0.820348     0.820348
```

With this further correction, the price is \$104.49, which is still 4 bps higher than you expect. But at this point, you might want to check your classic calculations, too...

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.