Saturday, October 27, 2018

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



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


No comments:

Post a Comment

technique - How credible is wikipedia?

I understand that this question relates more to wikipedia than it does writing but... If I was going to use wikipedia for a source for a res...