3
$\begingroup$

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

  1. Curve Date: 2021-09-13
  2. Valuation (Settlement) Date: 2021-09-20
  3. CSA Collateral Crncy: EUR
  4. Use OIS DC Stripping: Yes

Curves

  1. EUR OIS ESTR (discounting), Mid, Piecewise Linear Interpolation
  2. EUR vs. 6m (projecting), Mid, Piecewise Linear Interpolation

Swap

  1. Vanilla EUR 5y receiver IRS
  2. 10mm EUR,
  3. Effective date : 2021-09-20 (5d)
  4. 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:

  1. 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)
  2. Why am I getting the above error?
  3. 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

$\endgroup$
3
  • $\begingroup$ So, after some checking, it appears the error lies in the handler building using FRAs in the Euribor6M curve. Anyone more familiar than me with this who can spot where it fails? $\endgroup$
    – gussilago
    Sep 19 at 12:29
  • $\begingroup$ Would it be because of the last FRA starting at month 18 with a 6-month period (24) and hence colliding with the 2Y-swap? $\endgroup$
    – gussilago
    Sep 19 at 12:35
  • $\begingroup$ Hey, I have been trying to replicate your code with the FRA fix, but it keep failing with the error 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$
    – kismsu
    Nov 10 at 22:00
2
$\begingroup$

So, the error lied as commented on the collision on the 2y-point between FRA and swap.

By adding this line to the FRA-section:

elif 'M' in tenor:
    fra_start = int(tenor[0:tenor.find('M')]) - 6
    helpers.append( ql.FraRateHelper(ql.QuoteHandle(ql.SimpleQuote(rate/100)),fra_start, euribor6M) )

to take into account the period of the FRA (correct me if I'm wrong here guys!) I managed to end up with a similar fair rate as Bloomberg:

fairRate = spot5YearSwap.fairRate()
print(f"Fair swap rate: {fairRate:.6%}")

>> Fair swap rate: -0.280942%
$\endgroup$

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.