0
$\begingroup$

I'm trying to understand why pricing a par bond with zero curve, contracted from par bonds themselves doesn't give me par. (Based on examples here)

import QuantLib as ql

bond_maturities = [ql.Period(6*i, ql.Months) for i in range(3,21)]
bond_rates = [5.75, 6.0, 6.25, 6.5, 6.75, 6.80, 7.00, 7.1, 7.15,
              7.2, 7.3, 7.35, 7.4, 7.5, 7.6, 7.6, 7.7, 7.8]

calc_date = ql.Date(15, 1, 2015)
ql.Settings.instance().evaluationDate = calc_date

calendar = ql.UnitedStates()
bussiness_convention = ql.Unadjusted
day_count = ql.ActualActual(ql.ActualActual.Bond) #My fix relative to the source
end_of_month = True
settlement_days = 0
face_amount = 100
coupon_frequency = ql.Period(ql.Semiannual)
settlement_days = 0

bond_helpers = []
for r, m in zip(bond_rates, bond_maturities):
    termination_date = calc_date + m
    schedule = ql.Schedule(calc_date,
                   termination_date,
                   coupon_frequency,
                   calendar,
                   bussiness_convention,
                   bussiness_convention,
                   ql.DateGeneration.Backward,
                   end_of_month)

    helper = ql.FixedRateBondHelper(ql.QuoteHandle(ql.SimpleQuote(face_amount)),
                                        settlement_days,
                                        face_amount,
                                        schedule,
                                        [r/100.0],
                                        day_count,
                                        bussiness_convention,
                                        )
    bond_helpers.append(helper)

yieldcurve = ql.PiecewiseLogCubicDiscount(calc_date, bond_helpers, day_count)
yieldCurveHandle  = ql.YieldTermStructureHandle(yieldcurve)

Now if I check the price of a bond with a yield from the input I should have par

termination_date = calc_date + bond_maturities[1] # 2Y bond
schedule = ql.Schedule(calc_date, termination_date, coupon_frequency, calendar,
                       bussiness_convention, bussiness_convention, ql.DateGeneration.Backward,
                       end_of_month)
coupons = [bond_rates[1] / 100]
fixed_rate_bond = ql.FixedRateBond(settlement_days, face_amount, schedule, coupons, day_count)

bond_engine = ql.DiscountingBondEngine(yieldCurveHandle)
fixed_rate_bond.setPricingEngine(bond_engine)
fixed_rate_bond.dirtyPrice()

Indeed

100.00000000001226

Now, I want to extract zero curve from the curve object

spots = []
tenors = []
for d in yieldcurve.dates():
    yrs = day_count.yearFraction(calc_date, d)
    compounding = ql.Compounded
    freq = ql.Semiannual
    zero_rate = yieldcurve.zeroRate(yrs, compounding, freq)
    tenors.append(yrs)
    eq_rate = zero_rate.equivalentRate(day_count,
                                       compounding,
                                       freq,
                                       calc_date,
                                       d).rate()
    spots.append(eq_rate)

From this curve I want to build a zero curve (based on example here)

spotCurve = ql.ZeroCurve(yieldcurve.dates(), spots, day_count, calendar, ql.Linear(),
                         ql.Compounded, ql.Semiannual)
spotCurveHandle = ql.YieldTermStructureHandle(spotCurve)

I expect the price of the bond still be par, but

bond_engine_spot = ql.DiscountingBondEngine(spotCurveHandle)
fixed_rate_bond.setPricingEngine(bond_engine_spot)
fixed_rate_bond.dirtyPrice()
100.0999007315603

Any ideas what I'm missing here?

$\endgroup$
1
$\begingroup$

I believe one the problems is the interpolation, because the way you have it set up, you don't have a curve node for each of the cash flows of the bond you are pricing.

The curve you built initially uses LogCubic interpolation on discount factors and the one you are reconstructing uses linear interpolation on spot rates.

Notice that...

for cf in fixed_rate_bond.cashflows():
    print(cf.date(), cf.amount())

will give you:

July 15th, 2015 3.0000000000000027
January 15th, 2016 3.0000000000000027
July 15th, 2016 3.0000000000000027
January 17th, 2017 3.0000000000000027
January 17th, 2017 100.0

But if you check you curve dates, you have: spotCurve.dates()

(Date(15,1,2015), Date(15,7,2016), Date(15,1,2017), Date(15,7,2017),

(...)

So you will be interpolating the discount factor/zero rate for 15.07.2015 and 15.01.2016.

The problem is actually the way you defined bond_maturities since the first period will have "1Y6M"

Incidentally, if you try to rebuild it with discount factors, even the way you have it set up but without linear interpolation on zero rates, it will be closer.

dfs = [spotCurve.discount(dt) for dt in spotCurve.dates()]
spotCurve = ql.MonotonicLogCubicDiscountCurve(spotCurve.dates(), dfs, day_count)
spotCurveHandle = ql.YieldTermStructureHandle(spotCurve)
bond_engine_spot = ql.DiscountingBondEngine(spotCurveHandle)
fixed_rate_bond.setPricingEngine(bond_engine_spot)
fixed_rate_bond.dirtyPrice()

which outputs: 100.00000000001228

| improve this answer | |
$\endgroup$
  • $\begingroup$ Thanks for comments. I've extracted more points from curve and it worked. Do you know if can do MonotonicLogCubicDiscountCurve from rates input? $\endgroup$ – kismsu Mar 16 at 12:06
  • $\begingroup$ Yes, you can use the ql.MonotonicCubicZeroCurve or LogCubicZeroCurve classes $\endgroup$ – David Duarte Mar 16 at 12:34

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.