# 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()
date_generation = DateGeneration.Backward
month_end = False

# 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)

• I can't see anything obvious. You should print out all the cashflows and check each one. Commented Feb 20, 2017 at 6:36
• How would I do that? My hunch is that the yield curve is messing it up. Commented Feb 20, 2017 at 15:09
• 104.45 is the price of the bond with annual coupons but your code prices the bond assuming semi-annual coupons. Isn't that where the difference comes from? Commented Mar 28, 2017 at 11:44

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...

• Wow thank you, Luigi. Suppose I wanted to change the evaluation date. If I change calc date, the price still corresponds to Date(1,1,2017). If I comment out Settings.instance().evaluationDate = calc_date the program doesn't break. How do I find the NPV of the bond in Date(1,1,2018). Commented Feb 20, 2017 at 18:04
• The yield curve is still discounting to its first date (1/1/17). You can rebuild the curve, or you can use dirtyPrice instead of NPV. The former will take evaluation date and settlement days into account. Commented Feb 21, 2017 at 7:58
• Luigi is correct: cpage has calculated \$104.45 for the price of the bond with annual compounding. This answer's final result of \$104.49 is exact for a bond with semi-annual compounding. Commented Aug 24, 2018 at 16:41
• the tensor is incorrect, it should be "tenor = Period(Annual)",and i get a result of 104.4518223310162.
– 唐寿权
Commented Nov 20, 2019 at 3:06