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