Reconciling QuantLib Zero Rates, Forwards, and Discount Factors
Summary
The document explains why rates and discount factors derived from a QuantLib zero-coupon curve may differ from input deposit yields. The example builds a curve from deposit rate helpers and compares calculated forward rates and discount factors with quoted yields. The mismatch arises from inconsistent date and rate conventions in the comparison, rather than necessarily from a faulty curve.
Deposit helpers with settlement days define rates over periods beginning after the evaluation date. Curve queries should therefore use the corresponding spot and maturity dates. Discount calculations must also use the curve’s day-count convention: the example uses Thirty/365, so dividing elapsed days by 365 gives the wrong year fraction. Finally, continuous compounding produces a different quoted rate from the simple convention used for deposits; matching the original inputs requires the simple convention and its corresponding discount formula. The explanation applies to the conventions and setup shown, and careful alignment is needed when adapting it to other instruments or curves.
Key ideas
- Deposit rate helpers with settlement days start their accrual periods after the evaluation date.
- Curve rate queries should use the spot and maturity dates defined by the helpers.
- Discount factors depend on the day-count year fraction, which may differ from elapsed days divided by 365.
- Continuous and simple compounding produce different rates from the same discount factor.
- To compare curve outputs with deposit quotes, align dates, day counts, and compounding conventions.
Tags
Full text
# Yield to Maturity (YTM) to Zero Coupon Yield Curve (ZCYC) Forward Rate and Discount Factor mismatch in QuantLib
# Yield to Maturity (YTM) to Zero Coupon Yield Curve (ZCYC) Forward Rate and Discount Factor mismatch in QuantLib
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!
## Answer by Luigi Ballabio (score 1)
https://quant.stackexchange.com/a/79038
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)`.Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.