I am just starting to use Quantlib, and want to try and replicate the SWPM-functionality in Bloomberg, and price a vanilla 5Y EUR OIS. Below is the overall swap data used in BBG:
Overall settings
- Curve Date: 2021-09-13
- Valuation (Settlement) Date: 2021-09-20
- CSA Collateral Crncy: EUR
- Use OIS DC Stripping: Yes
Curves
- EUR OIS ESTR (discounting), Mid, Piecewise Linear Interpolation
- EUR vs. 6m (projecting), Mid, Piecewise Linear Interpolation
Swap
- Vanilla EUR 5y receiver IRS
- 10mm EUR,
- Effective date : 2021-09-20 (5d)
- Maturity: 2026-09-20 (5y)
BBG solves the two legs with a NPV = 70,789.04 and a fixed coupon of -0.280922
Now, my code are found below (executable) - but obviously I get an error, or else I would not ask for your help!
Code:
import QuantLib as ql
""" General settings """
calendar = ql.TARGET()
todaysDate = ql.Date(13, ql.September, 2021)
ql.Settings.instance().evaluationDate = todaysDate
fixingDays = 5
settlementDate = calendar.advance(todaysDate, fixingDays, ql.Days)
# must be a business day
settlementDate = calendar.adjust(settlementDate)
depositDayCounter = ql.Actual360()
swFixedLegFrequency = ql.Annual
termStructureDayCounter = ql.Actual365Fixed()
print("Today: %s " % todaysDate)
print("Settlement date: %s " % settlementDate)
""" Quotes """
estr_rates = """
1D -0.571
1W -0.571
2W -0.5707
1M -0.571
2M -0.57075
3M -0.571
4M -0.57105
5M -0.5705
6M -0.5703
7M -0.56895
8M -0.56795
9M -0.56695
10M -0.567
11M -0.56525
12M -0.56455
18M -0.55815
2Y -0.55213
3Y -0.51495
4Y -0.47592
5Y -0.4216
6Y -0.37382
7Y -0.31379
8Y -0.2502
9Y -0.18776
10Y -0.12588
11Y -0.06616
12Y -0.00469
15Y 0.13036
20Y 0.24826
25Y 0.27148
30Y 0.25117
40Y 0.19365
50Y 0.14543
"""
euribor_6m_rates = """
6M -0.52
7M -0.512
8M -0.507
9M -0.5
10M -0.492
11M -0.486
12M -0.474
13M -0.472
14M -0.467
15M -0.462
16M -0.455
17M -0.448
18M -0.44
2Y -0.4598
3Y -0.4032
4Y -0.3485
5Y -0.2825
6Y -0.2249
7Y -0.1605
8Y -0.094
9Y -0.029
10Y 0.0328
11Y 0.0918
12Y 0.1468
15Y 0.2778
20Y 0.384
25Y 0.3998
30Y 0.3743
40Y 0.3053
50Y 0.245
"""
euribor_data = {line.split('\t')[0] : float(line.split('\t')[-1]) for line in euribor_6m_rates.splitlines() if line.strip()}
estr_data = {line.split('\t')[0] : float(line.split('\t')[-1]) for line in estr_rates.splitlines() if line.strip()}
# /*********************
# *** RATE HELPERS ***
# *********************/
eonia = ql.Eonia()
helpers = []
for tenor, rate in estr_data.items():
if tenor == '1D':
helpers.append(ql.DepositRateHelper(rate / 100, eonia))
else:
helpers.append( ql.OISRateHelper(2, ql.Period(tenor), ql.QuoteHandle(ql.SimpleQuote(rate/100)), eonia) )
# /*********************
# ** CURVE BUILDING **
# *********************/
# /*********************
# ** ESTR CURVE **
# *********************/
estrTermStructure = ql.PiecewiseLogCubicDiscount(todaysDate, helpers, termStructureDayCounter)
estrTermStructure.enableExtrapolation()
# // the one used for discounting cash flows
discountingTermStructure = ql.RelinkableYieldTermStructureHandle()
# /*********************
# ** EURIBOR 6M **
# *********************/
euribor6M = ql.Euribor6M()
helpers = []
for tenor, rate in euribor_data.items():
if tenor == '6M':
helpers.append( ql.DepositRateHelper(ql.QuoteHandle(ql.SimpleQuote(rate/100)),ql.Period(tenor), 3, calendar, ql.Following, False, depositDayCounter) )
elif 'M' in tenor:
helpers.append( ql.FraRateHelper(ql.QuoteHandle(ql.SimpleQuote(rate/100)),int(tenor[0:tenor.find('M')]), euribor6M) )
else:
helpers.append( ql.SwapRateHelper(ql.QuoteHandle(ql.SimpleQuote(rate/100)),ql.Period(tenor), calendar, swFixedLegFrequency, ql.Unadjusted, ql.Thirty360(ql.Thirty360.BondBasis),euribor6M, ql.QuoteHandle(), ql.Period(0, ql.Days), discountingTermStructure) )
euribor6MTermStructure = ql.PiecewiseLogCubicDiscount(settlementDate, helpers, termStructureDayCounter)
# // the one used for forward rate forecasting
forecastingTermStructure = ql.RelinkableYieldTermStructureHandle()
# /*********************
# ** Swap **
# *********************/
nominal = 1000000.0
#fixed leg
fixedLegFrequency = ql.Annual
fixedLegConvention = ql.ModifiedFollowing
fixedLegDayCounter = ql.Thirty360(ql.Thirty360.BondBasis)
fixedRate = 0.007
firstFixDate = ql.Date(20,9,2022)
#floating leg
floatingLegDayCounter = ql.Actual360()
floatingLegFrequency = ql.Semiannual
floatingLegConvention = ql.ModifiedFollowing
euriborIndex = ql.Euribor6M(forecastingTermStructure)
spread = 0.0
lengthInYears = 5
swapType = ql.VanillaSwap.Receiver
maturity = ql.Date(20, ql.September, 2026) #settlementDate + lengthInYears*12
fixedSchedule = ql.Schedule(settlementDate, maturity,
ql.Period(fixedLegFrequency),
calendar, fixedLegConvention,
fixedLegConvention,
ql.DateGeneration.Backward, False, firstFixDate)
"""
list(fixedSchedule)
[Date(20,9,2021), << is this the start of the period?
Date(20,9,2022), << this one should be first payment date
Date(20,9,2023),
Date(20,9,2024),
Date(22,9,2025),
Date(21,9,2026)]
"""
floatSchedule = ql.Schedule(settlementDate, maturity,
ql.Period(floatingLegFrequency),
calendar, floatingLegConvention,
floatingLegConvention,
ql.DateGeneration.Backward, False)
"""
list(floatSchedule)
[Date(20,9,2021), << is this the start of the period?
Date(21,3,2022),
Date(20,9,2022),
Date(20,3,2023),
Date(20,9,2023),
Date(20,3,2024),
Date(20,9,2024),
Date(20,3,2025),
Date(22,9,2025),
Date(20,3,2026),
Date(21,9,2026)]
"""
forecastingTermStructure.linkTo(euribor6MTermStructure)
discountingTermStructure.linkTo(estrTermStructure)
spot5YearSwap = ql.VanillaSwap(swapType, nominal,
fixedSchedule, fixedRate, fixedLegDayCounter,
floatSchedule, euriborIndex, spread,
floatingLegDayCounter)
# and then the discount curve for the engine:
swapEngine = ql.DiscountingSwapEngine(discountingTermStructure)
spot5YearSwap.setPricingEngine(swapEngine)
NPV = spot5YearSwap.NPV()
fairSpread = spot5YearSwap.fairSpread()
fairRate = spot5YearSwap.fairRate()
Now running this, I get the following error:
RuntimeError: 2nd leg: more than one instrument with pillar September 15th, 2023
Which I have found somewhere that it might have to do with the building of the curves (using same tenor twice), but I can't figure out where.
The questions I'd like to get an answer to is:
- Is it legit to use Eonia() and respective Helpers building my "ESTR"/OIS-curve? Or how should I go ahead with this? Looking at ECB, the Eonia-methodology from Oct 2019 seems to adjust for future ESTR-transition, but is this reflected in Quantlib? (See here: Eonia/ESTR-transition)
- Why am I getting the above error?
- Am I constructing my schedules for my swap correctly - BBG-dates and Python are aligned, yet my schedules show the first and last date?
How far off am I for getting this working?
Since I am this new to this, and I can't really find any "fresh" examples of this anywhere, I thought it could be a good question in this forum - especially considering EONIA is out, and ESTR will be the new standard going forward.
Best,
/N
RuntimeError: 2nd leg: 1st iteration: failed at 1st alive instrument, pillar March 16th, 2022, maturity March 16th, 2022, reference date September 20th, 2021: negative time (-0.0109589) given
. Have you seen something like that? $\endgroup$