4
$\begingroup$

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)
$\endgroup$
  • $\begingroup$ I can't see anything obvious. You should print out all the cashflows and check each one. $\endgroup$ – SmallChess Feb 20 '17 at 6:36
  • $\begingroup$ How would I do that? My hunch is that the yield curve is messing it up. $\endgroup$ – cpage Feb 20 '17 at 15:09
  • 1
    $\begingroup$ 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? $\endgroup$ – Marcino Mar 28 '17 at 11:44
6
$\begingroup$

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

$\endgroup$
  • $\begingroup$ 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). $\endgroup$ – cpage Feb 20 '17 at 18:04
  • $\begingroup$ 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. $\endgroup$ – Luigi Ballabio Feb 21 '17 at 7:58
  • $\begingroup$ 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. $\endgroup$ – Jared Aug 24 '18 at 16:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.