0
$\begingroup$

I'm having trouble getting the same nodes when evaluating Fair Rates for a Mexican TIIE swap.

I think my problem is in the MXNOIS curve creation, but I'm not sure.
For evaluating, I am creating the MXNOIS curve using both USDOIS curve and USDSOFR curve.

¿Does Anyone know where the problem is?

import QuantLib as ql
import numpy as np


# EvaluationDate
ql.Settings.instance().evaluationDate = ql.Date(24,4,2024)


# USD OIS Construction

tenor2ql = {'B': ql.Days, 'D': ql.Days, 'M': ql.Months, 'W': ql.Weeks, 
            'Y': ql.Years, 'L': ql.Weeks}

# market calendar
calendar = ql.UnitedStates(1)

# input data
str_tenors = ['1B', '1W', '2W', '3W', '1M', '2M', '3M', '4M', '5M', '6M', '9M',
              '12M', '18M', '2Y', '3Y', '4Y', '5Y', '7Y', '10Y', '12Y', '15Y',
              '20Y', '25Y', '30Y', '40Y']

tenor =[tenor2ql[ten[-1]] for ten in str_tenors]
period = [1, 1, 2, 3, 1, 2, 3, 4,  
          5, 6, 9, 12, 18, 2, 3, 4,
          5, 7, 10, 12, 15, 20, 25, 30,
          40]
data = [0.0533, 0.053302, 0.053308, 0.05332, 0.053362, 0.053377,
        0.053385, 0.053279, 0.053173, 0.052985, 0.05246, 0.051868,
        0.04978 , 0.048395, 0.046192, 0.04474 , 0.043821, 0.04291,
        0.042347, 0.042266, 0.042207, 0.041692, 0.040661, 0.039605,
        0.037396]

# Deposit rates
deposits = {(period[0], tenor[0]): data[0]}

# Swap rates
n = len(period)
swaps = {}
for i in range(1,n):
    swaps[(period[i], tenor[i])] = data[i]
    
# Rate Quantlib.Quote objects
## desposits
for n, unit in deposits.keys():
    deposits[(n, unit)] = ql.SimpleQuote(deposits[(n, unit)]) #Tasa cero para el primer tenor
## swap rates
for n, unit in swaps.keys():
    swaps[(n, unit)] = ql.SimpleQuote(swaps[(n, unit)]) # SimpleQuote es el formato de rates en quantlib
    
# Rate helpers deposits
dayCounter = ql.Actual360()
settlementDays = 2
## deposits
depositHelpers = [ql.DepositRateHelper(ql.QuoteHandle(deposits[(n, unit)]),
                                       ql.Period(int(n), unit), 
                                       settlementDays,
                                       calendar, 
                                       ql.ModifiedFollowing, 
                                       False, 
                                       dayCounter) 
                  for (n, unit) in deposits.keys()]
## swap rates
OIS_Index = ql.FedFunds()
OISHelpers = [ql.OISRateHelper(settlementDays, ql.Period(int(n), unit),
                               ql.QuoteHandle(swaps[(n,unit)]),
                               OIS_Index) 
              for n, unit in swaps.keys()]
## helpers merge
hlprUSDOIS = depositHelpers + OISHelpers

crvUSDOIS = ql.PiecewiseLogLinearDiscount(0, ql.UnitedStates(1), 
                                          hlprUSDOIS, ql.Actual360())

crvUSDOIS.enableExtrapolation()
discount_curve = ql.RelinkableYieldTermStructureHandle()
discount_curve.linkTo(crvUSDOIS)

######################## SOFR Construction ######################


# settlement date
dt_settlement = calendar.advance(
        ql.Settings.instance().evaluationDate, ql.Period('2D'))

# input data
str_tenor = ['1B', '%2Y', '%3Y', '%4Y', '%5Y', '%6Y', '%7Y', '%8Y', '%9Y',
             '%10Y', '%12Y', '%15Y', '%20Y', '%25Y', '%30Y']
tenor = [tenor2ql[ten[-1]] for ten in str_tenor]
period = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 20, 25, 30]
data_nonfut = [0.0531, 0.048523, 0.046355, 0.04497 , 0.044095, 0.04357,
               0.043216, 0.042959, 0.042792, 0.042682, 0.042614, 0.042585,
               0.042118, 0.041112, 0.04007 ]
data_fut = [0.05345, 0.05265, 0.05095, 0.04915, 0.0473]

# IborIndex
swapIndex = ql.Sofr()

# Deposit rates
deposits = {(period[0], tenor[0]): data_nonfut[0]}

# Futures rates
n_fut = len(data_fut)
imm = ql.IMM.nextDate(dt_settlement)
imm = dt_settlement
futures = {}
for i in range(n_fut):
    imm = ql.IMM.nextDate(imm)
    futures[imm] = 100 - data_fut[i]*100  

# Swap rates
n = len(period)
swaps = {}
for i in range(1, n):
    swaps[(period[i], tenor[i])] = data_nonfut[i]
    
# Rate Qauntlib.Quote objects
## desposits

for n, unit in deposits.keys():
    deposits[(n, unit)] = ql.SimpleQuote(deposits[(n, unit)])
## futures
for d in futures.keys():
    futures[d] = futures[d]
## swap rates
for n, unit in swaps.keys():
    swaps[(n, unit)] = ql.SimpleQuote(swaps[(n, unit)])
    
# Rate helpers deposits
dayCounter = ql.Actual360()
settlementDays = 2
## deposits
depositHelpers = [ql.DepositRateHelper(ql.QuoteHandle(deposits[(n, unit)]),
                                       ql.Period(int(n), unit), 
                                       settlementDays,
                                       calendar, 
                                       ql.ModifiedFollowing, 
                                       False, 
                                       dayCounter) 
                  for n, unit in deposits.keys()]
## futures
months = 3
futuresHelpers = [ql.FuturesRateHelper(
    ql.QuoteHandle(ql.SimpleQuote(futures[d])), 
    d, months, calendar, 
    ql.ModifiedFollowing, True, dayCounter
    ) 
    for d in futures.keys()
]

## swap rates
fixedLegFrequency = ql.Annual
fixedLegAdjustment = ql.ModifiedFollowing
fixedLegDayCounter = ql.Actual360()
## swaphelper
swapHelpers = [ql.SwapRateHelper(
    ql.QuoteHandle(swaps[(n,unit)]),
    ql.Period(int(n), unit), 
    calendar,
    fixedLegFrequency, 
    fixedLegAdjustment,
    fixedLegDayCounter, 
    swapIndex, 
    ql.QuoteHandle(), 
    ql.Period(2, ql.Days),
    discount_curve
    )
    for n, unit in swaps.keys()
]

## helpers merge
hlprSOFR = depositHelpers + futuresHelpers + swapHelpers

# Bootstrap
crvSOFR = ql.PiecewiseNaturalLogCubicDiscount(0, ql.UnitedStates(1), 
                                              hlprSOFR, 
                                              ql.Actual360())
crvSOFR.enableExtrapolation()

crv_usdswp = ql.RelinkableYieldTermStructureHandle()
crv_usdswp.linkTo(crvSOFR)

########################## MXN_OIS Construction ############################
# data
def f(x):
    if x[-1]=='L':
        return int(x[1:-1])*4
    else:
        return int(x[1:-1])

# Calendars
calendar_mx = ql.Mexico()

spotfx = 17.0628

# TIIE Quotes 

data_tiie = [0.11245, 0.1042005, 0.10025, 0.0978005, 0.09640125, 
             0.0952005, 0.09489, 0.0954, 0.09543, 0.095305]
str_tenors_tiie = ['%1L', '%26L', '%39L', '%52L', '%65L', 
                   '%91L', '%130L', '%195L', '%260L', '%390L']

tenors_tiie = [tenor2ql[ten[-1]] for ten in str_tenors_tiie]
tiie_period = [f(t) for t in str_tenors_tiie]

# Fwd Quotes

str_tenors_fwds = ['%3M', '%6M', '%9M', '%1Y']
fwds_period = [3, 6, 9, 13]
data_fwds = [0.25095, 0.50925, 0.7563 , 1.0026 ]


# Basis Quotes
basis_period = [2, 3, 6, 9, 13, 26, 39, 52, 65, 91, 130, 195, 260, 390]
data_basis = [-0.00170628, 0.00464522, 0.00443489, 0.00400474, 0.00303945,
              0.002, 0.0023, 0.00275, 0.00285, 0.0031, 0.00335, 0.00286, 
              0.0019925, 0.0019]


tenor = ql.EveryFourthWeek

# Basis swaps
basis_usdmxn = {}
n_basis = len(basis_period)

for i in range(1, n_basis):
    basis_usdmxn[(basis_period[i], tenor)] = data_basis[i]

# Forward Points
fwdpts = {}
n_fwds = len(fwds_period)

for i in range(n_fwds):
    fwdpts[(fwds_period[i], tenor)] = data_fwds[i]

# Deposit rates
deposits = {(tiie_period[0], tenors_tiie[0]): data_tiie[0]}

# TIIE Swap rates
swaps_tiie = {}
n_tiie = len(tiie_period)

for i in range(1, n_tiie):
    swaps_tiie[(tiie_period[i], tenors_tiie[i])] = data_tiie[i]

# Qauntlib.Quote objects
for n, unit in basis_usdmxn.keys():
    basis_usdmxn[(n, unit)] = ql.SimpleQuote(basis_usdmxn[(n, unit)])

for n, unit in fwdpts.keys():
    fwdpts[(n, unit)] = ql.SimpleQuote(fwdpts[(n, unit)])

for n, unit in deposits.keys():
    deposits[(n, unit)] = ql.SimpleQuote(deposits[(n, unit)])

for n, unit in swaps_tiie.keys():
    swaps_tiie[(n, unit)] = ql.SimpleQuote(swaps_tiie[(n, unit)])
    
# Deposit rate helper
dayCounter = ql.Actual360()
settlementDays = 1
depositHelpers = [ql.DepositRateHelper(ql.QuoteHandle(deposits[(n, unit)]),
                                       ql.Period(int(n), ql.Weeks), 
                                       settlementDays, calendar_mx, 
                                       ql.Following, False,  dayCounter)
                  for n, unit in deposits.keys()]

# FX Forwards helper
fxSwapHelper = [ql.FxSwapRateHelper(ql.QuoteHandle(fwdpts[(n,u)]),
                                    ql.QuoteHandle(ql.SimpleQuote(spotfx)),
                                    ql.Period(int(n*4), ql.Weeks), 2,
                                    calendar_mx, ql.Following,
                                    False, True, discount_curve) 
                for n,u in fwdpts.keys()]

# Swap rate helpers
settlementDays = 2
fixedLegFrequency = ql.EveryFourthWeek
fixedLegAdjustment = ql.Following
fixedLegDayCounter = ql.Actual360()

# SOFR #
fxIborIndex = ql.Sofr(crv_usdswp)



swapHelpers = [ql.SwapRateHelper(ql.QuoteHandle(swaps_tiie[(n, unit)]),
                                 ql.Period(int(n), ql.Weeks), 
                                 calendar_mx,
                                 fixedLegFrequency, 
                                 fixedLegAdjustment,
                                 fixedLegDayCounter, 
                                 fxIborIndex, 
                                 ql.QuoteHandle(
                                     basis_usdmxn[(n/4, tenor)]), 
                                 ql.Period(0, ql.Days))
               for n, unit in swaps_tiie.keys()]

# Rate helpers merge
hlprMXNOIS = depositHelpers + fxSwapHelper + swapHelpers

# Bootstrap
crvMXNOIS = ql.PiecewiseNaturalLogCubicDiscount(0, ql.Mexico(), 
                                                hlprMXNOIS, 
                                                ql.Actual360())
crvMXNOIS.enableExtrapolation()

crv_mxnois = ql.RelinkableYieldTermStructureHandle()
crv_mxnois.linkTo(crvMXNOIS)



########################## MXNTIIE Construction #############################

# Data
str_tenors = ['%1L', '%3L', '%6L', '%9L', '%13L', '%26L', '%39L', '%52L', 
              '%65L', '%91L', '%130L', '%195L', '%260L', '%390L']
period = [f(t) for t in str_tenors]
tenors = [tenor2ql[t[-1]] for t in str_tenors]
data = [0.11245, 0.1123025, 0.11165, 0.11065, 0.10935, 0.1042005, 0.10025,
        0.0978005, 0.09640125, 0.0952005, 0.09489, 0.0954, 0.09543, 0.095305]


# Deposits

deposits = {(period[0], tenors[0]): data[0]}

swaps = {}
for i in range(1, len(str_tenors)):
    swaps[(period[i], tenors[i])] = data[i]


# Rate Qauntlib.Quote objects
## desposits
for n, unit in deposits.keys():
    deposits[(n, unit)] = ql.SimpleQuote(deposits[(n, unit)])
## swap rates
for n, unit in swaps.keys():
    swaps[(n, unit)] = ql.SimpleQuote(swaps[(n, unit)])
    
# Deposit rate helpers
dayCounter = ql.Actual360()
settlementDays = 1
depositHelpers = [ql.DepositRateHelper(
        ql.QuoteHandle(deposits[(n, unit)]), ql.Period(n, unit), 
        settlementDays, calendar_mx, ql.Following, False, dayCounter)
    for n, unit in deposits.keys()]

# Swap rate helpers
settlementDays = 1
fixedLegFrequency = ql.EveryFourthWeek
fixedLegAdjustment = ql.Following
fixedLegDayCounter = ql.Actual360()
ibor_MXNTIIE = ql.IborIndex('TIIE',
                            ql.Period(13), settlementDays, 
                            ql.MXNCurrency(), calendar_mx,
                            ql.Following, False, ql.Actual360(), 
                            crv_mxnois)

swapHelpers = [ql.SwapRateHelper(ql.QuoteHandle(swaps[(n,unit)]),
                                 ql.Period(int(n), unit), calendar_mx,
                                 fixedLegFrequency, fixedLegAdjustment,
                                 fixedLegDayCounter, ibor_MXNTIIE)
               for n, unit in swaps.keys()]

# helpers merge
hlprTIIE = depositHelpers + swapHelpers


# Curve creation
crvTIIE = ql.PiecewiseNaturalLogCubicDiscount(0, ql.Mexico(), hlprTIIE, 
                                               ql.Actual360())
crvTIIE.enableExtrapolation()




########################## Ibor TIIE #######################

ibor_tiie_crv = ql.RelinkableYieldTermStructureHandle()
ibor_tiie_crv.linkTo(crvTIIE)

ibor_tiie = ql.IborIndex('TIIE',
              ql.Period(13),
              1,
              ql.MXNCurrency(),
              ql.Mexico(),
              ql.Following,
              False,
              ql.Actual360(),
              ibor_tiie_crv)

#%%
######################## SWAP VALUATION #################################

# Swap starts tomorrow as TIIE swaps have +1 day settlement
# We will evaluate a 91 Lunar months swap
start = ql.Date(26,4,2024)
maturity = start + 91*28

notional = 100_000_000
rate = .0950
typ = -1
rule = ql.DateGeneration.Backward

cal = ql.Mexico()
legDC = ql.Actual360()
cpn_tenor = ql.Period(13)
convention = ibor_tiie.businessDayConvention()
termDateConvention = ibor_tiie.businessDayConvention()
rule = rule
isEndOfMonth = False
    
# fix-float leg schedules
fixfltSchdl = ql.Schedule(start, maturity, cpn_tenor, cal, convention,
                        termDateConvention, rule, isEndOfMonth)

# swap
swap = ql.VanillaSwap(typ, notional, fixfltSchdl, rate, legDC, fixfltSchdl,
                      ibor_tiie, 0, legDC)


### Discount Engine ###
discount_engine = ql.DiscountingSwapEngine(crv_mxnois)

swap.setPricingEngine(discount_engine)

print('Node for 91L Tenor:   ', 9.52005)
print('Calculated 91L Tenor: ', np.round(swap.fairRate()*100,6))

I am valuating a 91 lunar month swap (2548 days) that pays every 28 days
The Output is:
Node for 91L Tenor: 9.52005
Calculated 91L Tenor: 9.515049

$\endgroup$

1 Answer 1

4
$\begingroup$

Three things.

First, you set the evaluation date as

ql.Settings.instance().evaluationDate = ql.Date(24,4,2024)

and then at the end you write

# Swap starts tomorrow as TIIE swaps have +1 day settlement
# We will evaluate a 91 Lunar months swap
start = ql.Date(26,4,2024)
maturity = start + 91*28

but with 1 day settlement, the start should be the 25th. You also need to be careful of holidays when you set the maturity. I'd write something like

cal = ql.Mexico()
start = cal.advance(ql.Date(24,4,2024), 1, ql.Days)
maturity = cal.advance(start, 91*4, ql.Weeks)

Second, you set up the swap helpers as:

ibor_MXNTIIE = ql.IborIndex('TIIE',
                            ql.Period(13), settlementDays, 
                            ql.MXNCurrency(), calendar_mx,
                            ql.Following, False, ql.Actual360(), 
                            crv_mxnois)

swapHelpers = [ql.SwapRateHelper(ql.QuoteHandle(swaps[(n,unit)]),
                                 ql.Period(int(n), unit), calendar_mx,
                                 fixedLegFrequency, fixedLegAdjustment,
                                 fixedLegDayCounter, ibor_MXNTIIE)
               for n, unit in swaps.keys()]

but this doesn't cause them to use the MXNOIS curve for discounting. The curve needs to be passed to the helper, not the index. Unfortunately there are a couple of default arguments in the way, so the correct way is:

ibor_MXNTIIE = ql.IborIndex('TIIE',
                            ql.Period(13), settlementDays, 
                            ql.MXNCurrency(), calendar_mx,
                            ql.Following, False, ql.Actual360())

swapHelpers = [ql.SwapRateHelper(ql.QuoteHandle(swaps[(n,unit)]),
                                 ql.Period(int(n), unit), calendar_mx,
                                 fixedLegFrequency, fixedLegAdjustment,
                                 fixedLegDayCounter, ibor_MXNTIIE,
                                 ql.QuoteHandle(), ql.Period(),
                                 crv_mxnois)
               for n, unit in swaps.keys()]

this gives you matching rates.

Third, however: the swap helpers build their swaps with rule = ql.DateGeneration.Forward, not rule = ql.DateGeneration.Backward. For some tenors (not this one) this can cause discrepancies if the maturity needs to be adjusted for a holiday. If the convention for MXN swaps is indeed to be built backwards, please open an issue on GitHub so we can add that parameter.

$\endgroup$
3
  • $\begingroup$ Maybe it would be a useful exercise for some student to just makr reference implementations for various currencies swap curves and onclude in the distro. $\endgroup$ Commented May 3 at 15:34
  • 1
    $\begingroup$ Agreed. If I only had some students :) $\endgroup$ Commented May 6 at 21:17
  • $\begingroup$ Thanks, it works now. I am having the same trouble with the SOFR swaps. The fair rate is nt the same as the inputs $\endgroup$
    – Fiesteban
    Commented May 7 at 19:23

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.