Skip to content
All library documents

Matching QuantLib Swap Rates to Curve Nodes

Article Quant Q&A · Author: Fiesteban

Summary

The document investigates why a QuantLib fair rate for a Mexican TIIE swap differs from the market quote used as a curve node. The response identifies two setup issues: the example’s swap start date does not match the stated one-day settlement convention, and the MXN curve’s swap helpers are not configured to discount with the MXN overnight curve. It recommends deriving dates through the relevant calendar and passing the discount curve to the helper in the correct argument position.

After those changes, the reported fair rate matches the input quote. A further caveat concerns QuantLib’s default forward date-generation rule: for some tenors, holiday-adjusted maturity dates can differ from schedules built backward. The response says that if local market conventions require backward generation, that behavior may need library support. The explanation is specific to the supplied curve construction and conventions, so other market setups require their own date and discounting checks.

Key ideas

  • Swap start and maturity dates should follow the market settlement convention and calendar.
  • The discount curve belongs in the swap helper configuration, rather than being attached to the index as shown.
  • Correcting the date setup and helper discounting resolves the reported quote mismatch.
  • QuantLib’s default forward schedule generation can differ from backward schedules around holidays.

Tags

Full text
# QuantLib swap Fair Rate not the same as the constructed curve nodes


# QuantLib swap Fair Rate not the same as the constructed curve nodes












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

## Answer by Luigi Ballabio (score 4)

https://quant.stackexchange.com/a/79235

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.

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.