Interpreting a QuantLib Z-Spread Comparison and Percentage Scaling
Summary
The document presents a question about whether changing when QuantLib’s evaluation date is set affects a bond z-spread calculation. The example constructs a yield curve and a fixed-rate bond, then calculates a present value and a z-spread in two test functions that set the evaluation date at different points. The displayed outputs appear identical, prompting the question about the curve reference date.
The accepted response attributes the apparent discrepancy to inconsistent spread reporting: one test multiplies the z-spread by 100, while the other does not. This is a unit conversion issue when expressing a decimal rate as a percentage, rather than evidence that the evaluation date changed the spread. The exchange provides no further investigation of QuantLib date handling, and the code and outputs alone do not establish whether other date or curve conventions could affect a different calculation.
Key ideas
- The example compares bond z-spread calculations after setting QuantLib’s evaluation date at different points.
- The accepted response identifies inconsistent multiplication by 100 as the source of the reported spread difference.
- A decimal spread and a percentage spread use different scales and should be labeled consistently.
- The answer does not investigate other possible effects of evaluation dates or curve conventions.
Tags
Full text
# TermStructure/Evaluation Dates in Quantlib
# TermStructure/Evaluation Dates in Quantlib
i'm puzzled by the way Quantlib handles the evaluation date in the yield term structure classes. I have the following code as example:
```
import QuantLib as ql
import pandas as pd
#Curve
def build_termstruc(curve_date):
helpers = []
handle = ql.YieldTermStructureHandle()
tenors = [1,3,6,9,12,18,2,3,4,5,6,7,8,9,10,15,20]
swap_rates = [2.75,2.875,2.99,3.12,3.25,3.5,3.61,3.795,3.925,4.025,4.115,4.195,4.265,4.335,4.4,4.555,4.68]
fixing_days = 0
calendar = ql.NullCalendar()
settlement_days = 0
day_counter = ql.Actual360()
index = ql.OvernightIndex("index", settlement_days,ql.CLPCurrency(),calendar,day_counter)
for i in range(len(tenors)):
if i == 0:
helpers += [ql.DepositRateHelper(ql.QuoteHandle(ql.SimpleQuote(swap_rates[i]/100)),
ql.Period(1,ql.Days),
fixing_days,
calendar,
ql.Unadjusted,
False,
ql.Actual360())]
continue
elif i < 6 and i > 0:
period = ql.Period(tenors[i], ql.Months)
frequency = ql.Months
elif i >=6:
period = ql.Period(tenors[i], ql.Years)
frequency = ql.Years
helpers += [ql.OISRateHelper(settlement_days,
period,
ql.QuoteHandle(ql.SimpleQuote(swap_rates[i]/100)),
index,
handle,
False,
0,
ql.Following,
frequency)]
return ql.PiecewiseFlatForward(curve_date, helpers, ql.Actual360())
def build_bond():
#Bond
issue_date = ql.Date(1,6,2015)
maturity_date = ql.Date(1,6,2020)
calendar = ql.NullCalendar()
coupon_day_count = ql.Unadjusted
payment_convention = ql.Following
date_generation = ql.DateGeneration.Forward
month_end = False
settlement_days = 0
int_day_count = ql.Thirty360()
notional = 100
coupons = [4.5/100]
#non static variables
schedule = ql.Schedule (issue_date,
maturity_date,
ql.Period(ql.Semiannual),
calendar,
coupon_day_count,
payment_convention,
date_generation,
month_end)
return ql.FixedRateBond(settlement_days, notional, schedule, coupons, int_day_count)
def test1():
curve_date = ql.Date(14,11,2018)
ql.Settings.instance().evaluationDate = curve_date
ts = build_termstruc(curve_date)
bond = build_bond()
bond_IIR = ql.InterestRate(3.78/100, ql.Actual365Fixed(), ql.Compounded, ql.Annual)
npv = ql.CashFlows.npv(bond.cashflows(), bond_IIR, False)
spread = ql.CashFlows.zSpread(bond.cashflows(), npv, ts, ql.Actual360(), ql.Compounded, ql.Annual, True)*100
print('NPV: ', npv,'Spread: ', spread)
def test2():
curve_date = ql.Date(14,11,2018)
ts = build_termstruc(curve_date)
ql.Settings.instance().evaluationDate = curve_date
bond = build_bond()
bond_IIR = ql.InterestRate(3.78/100, ql.Actual365Fixed(), ql.Compounded, ql.Annual)
npv = ql.CashFlows.npv(bond.cashflows(), bond_IIR, False)
spread = ql.CashFlows.zSpread(bond.cashflows(), npv, ts, ql.Actual360(), ql.Compounded, ql.Annual, True)*100
print('NPV: ', npv,'Spread: ', spread)
if __name__ == '__main__':
test1()
test2()
```
The idea of the code is that the first two methods build a term structure and a bond and the other two (test1 and test2) only change the position where the evaluationDate is set. The result is the following:
```
NPV: 103.15824069057247 Spread: 0.18307250901326033
NPV: 103.15824069057247 Spread: 0.18307250901326033
```
I don't get why i'm getting different z-spreads if im setting the curve reference date.
## Answer by amdopt (score 4, accepted)
https://quant.stackexchange.com/a/42685
Your spread definitions are different. In Def Test2(), it is multiplied by 100. That's the cause of the decimal shift in the spread.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.