0
$\begingroup$

I have a set of YTM data for various tenors, and I'm constructing a ZCYC using the QuantLib library. However, when I calculate the forward rates and discount factors from the ZCYC and compare them with the corresponding YTMs, I'm noticing a mismatch after the fourth or fifth decimal point.

import QuantLib as ql
import pandas as pd 
# Input YTM data
quotes = [
    (1, ql.Period(1, ql.Days), 5.2300),
    (1, ql.Period(1, ql.Months), 5.2310),
    (1, ql.Period(2, ql.Months), 5.2250),
    (1, ql.Period(3, ql.Months), 5.2270),
    (1, ql.Period(6, ql.Months), 5.2280),
    (1, ql.Period(1, ql.Years), 5.2200),
    (1, ql.Period(2, ql.Years), 5.2300),
    (1, ql.Period(3, ql.Years), 5.2400),
    (1, ql.Period(4, ql.Years), 5.2500),
    (1, ql.Period(5, ql.Years), 5.2600),
]

# Set evaluation date
evaluation_date = ql.Date(2, 5, 2011)
ql.Settings.instance().evaluationDate = evaluation_date

# Set day count convention and calendar
day_count = ql.Thirty365()
calendar = ql.NullCalendar()

# Create rate helpers
rate_helpers = []
for settlement_days, period, rate in quotes:
    quote_handle = ql.SimpleQuote(rate / 100)
    helper = ql.DepositRateHelper(
        ql.QuoteHandle(quote_handle),
        period,
        settlement_days,
        ql.NullCalendar(),
        ql.Unadjusted,
        False,
        day_count,
    )
    rate_helpers.append(helper)

# Construct the yield curve
zyc_curve = ql.PiecewiseLogLinearDiscount(evaluation_date, rate_helpers, day_count)


scheduledates = [calendar.advance(evaluation_date,i[1]) for i in quotes]
units = [date-evaluation_date for date in scheduledates]

forwardrates = []
discount_factors = []
for adate in scheduledates:
    fr = zyc_curve.forwardRate(evaluation_date,adate,day_count,ql.Continuous)
    discount_factors.append(fr.discountFactor(evaluation_date,adate))
    forwardrates.append(fr.rate())
zcyc_data = pd.DataFrame({"units":units,"discount_factors":discount_factors,"forwardrates":forwardrates})
print(zcyc_data)

units   discount_factors    forwardrates
0   1   0.99985673  0.05229625
1   31  0.99571868  0.05220126
2   61  0.99148291  0.05203423
3   92  0.98727404  0.05194205
4   184 0.97486342  0.05162297
5   366 0.95103047  0.05090652
6   731 0.90647191  0.04977953
7   1096 0.86575243 0.04871949
8   1461 0.82839858 0.04771890
9   1827 0.79401037 0.04677247

> for 61 days if we check
import numpy as np
dicountfactor = round(np.exp(-(61/365) * (0.05203423)),8)

> I am getting 0.99134157 which does not tallying with 0.99148291


Expected output

Days YTM     ZCYC      DF
            
1   5.2300  5.2296  0.99985673
30  5.2310  5.2198  0.99571894
60  5.2250  5.2027  0.99148408
90  5.2270  5.1936  0.98727551
180 5.2280  5.1617  0.97486629
365 5.2200  5.0883  0.95038986
730 5.2300  4.9742  0.90530443
1095 5.2400 4.8668  0.86415425
1460 5.2500 4.7655  0.82644658
1825 5.2600 4.6698  0.79176551


I've tried adjusting the day count convention and other parameters in my code, but the mismatch persists. I'm wondering if there's something I'm missing or if there's a specific consideration I need to take into account when comparing YTMs with ZCYC forward rates and discount factors in QuantLib.

Could someone please provide insights or guidance on how to reconcile this discrepancy and ensure accuracy in the calculations?

Thanks in advance for your help!

$\endgroup$

1 Answer 1

1
$\begingroup$

You're getting some of the conventions wrong.

First, the dates at which you ask the curve for the rates. You write

scheduledates = [calendar.advance(evaluation_date,i[1]) for i in quotes]

but in your quotes, and therefore in your DepositRateHelper instances, you have 1 settlement day. This means that the underlying rate is not the forward between, for instance, the evaluation date and the evaluation date + 2 months; instead, it is the rate between the evaluation date + 1 day and the evaluation date + 1 day + 2 months. The code above needs to be something like:

scheduledates = []
units = []
for i in quotes:
    start_date = calendar.advance(evaluation_date, i[0], ql.Days)
    maturity_date = calendar.advance(start_date, i[1])
    scheduledates.append(maturity_date)
    units.append(maturity_date - start_date)

By the way, you can check the dates above by printing zyc_curve.nodes(); you'll see that they match the updated calculation.

In the same way, the call to zyc_curve.forwardRate needs to take the settlement days into account, so you need something like:

for i in quotes:
    spot_date = calendar.advance(evaluation_date, i[0], ql.Days)
    maturity_date = calendar.advance(spot_date, i[1])
    fr = zyc_curve.forwardRate(spot_date, maturity_date, day_count, ql.Continuous)
    discount_factors.append(fr.discountFactor(spot_date, maturity_date))
    forwardrates.append(fr.rate())

This gives you:

   units  discount_factors  forwardrates
0      1          0.999857      0.052296
1     31          0.995719      0.052198
2     61          0.991484      0.052027
3     92          0.987276      0.051936
4    184          0.974866      0.051617
5    366          0.951036      0.050901
6    731          0.906481      0.049774
7   1096          0.865766      0.048714
8   1461          0.828416      0.047714
9   1827          0.794031      0.046767

Second, when you try to reproduce the discount, you're using

dicountfactor = round(np.exp(-(61/365) * (0.05203423)),8)

but in the calculations above, you used day_count = ql.Thirty365() which means you can't simply divide the number of days by 365 (that would be the Actual/365 convention); the formula is more complex. You can check the difference:

spot_date = calendar.advance(evaluation_date, 1, ql.Days)
maturity_date = scheduledates[2]

print(maturity_date - spot_date)
61

print((maturity_date - spot_date)/365)
0.16712328767123288

print(day_count.yearFraction(spot_date, maturity_date))
0.1643835616438356

The discount factor is therefore:

dicountfactor = round(
    np.exp(-day_count.yearFraction(spot_date, scheduledates[2]) * forwardrates[2]), 8
)
print(dicountfactor)

which gives you 0.9914841, the same as in the table.

Finally, if you want to get back the original input rates, you can replace ql.Continuous with ql.Simple in the call to zyc_curve.forwardRate (because deposit rates are simple rates). This will give you

   units  discount_factors  forwardrates
0      1          0.999857       0.05230
1     31          0.995719       0.05231
2     61          0.991484       0.05225
3     92          0.987276       0.05227
4    184          0.974866       0.05228
5    366          0.951036       0.05220
6    731          0.906481       0.05230
7   1096          0.865766       0.05240
8   1461          0.828416       0.05250
9   1827          0.794031       0.05260

In this case, if you want to reproduce the discount, the formula would be D = 1/(1 + R * T).

$\endgroup$
2
  • $\begingroup$ This said, you might also want to check if those rates are indeed deposit rates. That's not what one usually calls YTM. $\endgroup$ Commented Apr 18 at 20:23
  • $\begingroup$ Thanks for your explaination this gave me more understanding. also i have gone through your quantlib cookbook chapter Constructing a yield curve. where you merged the depo_helpers with bond_helpers. $\endgroup$ Commented Apr 22 at 2:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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