Diagnosing SOFR Swap Fair-Rate Differences in QuantLib
Summary
The document presents a QuantLib setup for building a USD OIS discount curve and a SOFR projection curve from deposit, futures, and swap quotes, then pricing swaps at matching quoted tenors. The author reports small differences between the calculated fair rates and the input swap rates, with the displayed discrepancies varying across maturities.
The code and output illustrate a curve-construction and instrument-convention debugging problem, but the document does not include an answer or identify the cause of the differences. The setup includes separate discounting and projection curves, futures helpers, swap schedules, day counts, settlement conventions, and interpolation choices; any of these assumptions or mismatches may affect calibration and valuation. The reported comparison is therefore diagnostic evidence rather than a general result, and the example is incomplete as a reproducible solution because some valuation variables are not defined in the excerpt.
Key ideas
- The example bootstraps separate OIS discounting and SOFR projection curves from market instruments.
- It compares swap fair rates at selected tenors with the quotes used as curve inputs.
- Small rate discrepancies are reported, but the document does not resolve their source.
- Curve helpers, schedules, day counts, settlement dates, and interpolation choices can affect the comparison.
Tags
Full text
# Quantlib: SOFR IRS Fair Rate not the same as inputs
# Quantlib: SOFR IRS Fair Rate not the same as inputs
I am making an OIS curve and a SOFR curve with bloomberg quotes
When I valuate a SOFR swap in the same tenors as the SOFR inputs, the FairRates are not the same as the quotes I used to create the curve. Can somenone please help me?
```
# -*- coding: utf-8 -*
import QuantLib as ql
import numpy as np
from datetime import datetime
s = datetime.now()
# EvaluationDate
ql.Settings.instance().evaluationDate = ql.Date(29,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(6)
# 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,
ql.YieldTermStructureHandle(),
False, 2, ql.ModifiedFollowing,
ql.Annual, calendar)
for n, unit in swaps.keys()]
## helpers merge
hlprUSDOIS = depositHelpers + OISHelpers
crvUSDOIS = ql.PiecewiseLogLinearDiscount(0, calendar,
hlprUSDOIS, ql.Actual360())
crvUSDOIS.enableExtrapolation()
discount_curve = ql.RelinkableYieldTermStructureHandle()
discount_curve.linkTo(crvUSDOIS)
#%% SOFR
######################## SOFR Construction ######################
calendar = ql.UnitedStates(6)
# 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_sofr = [tenor2ql[ten[-1]] for ten in str_tenor]
period_sofr = [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_sofr[0], tenor_sofr[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_sofr)
swaps = {}
for i in range(1, n):
swaps[(period_sofr[i], tenor_sofr[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,
ql.QuoteHandle(ql.SimpleQuote(0.0)))
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)
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, calendar, hlprSOFR,
# ql.Actual360())
crvSOFR = ql.PiecewiseCubicZero(0, calendar, hlprSOFR, ql.Actual360())
crvSOFR.enableExtrapolation()
crv_usdswp = ql.RelinkableYieldTermStructureHandle()
crv_usdswp.linkTo(crvSOFR)
###### SWAP VALUATION #######
for i in range(len(tenor_sofr)):
legDC = ql.Actual360()
start = ql.Date(1,5,2024)
maturity = ql.UnitedStates(6).advance(start, ql.Period(period_sofr[i], tenor_sofr[i]))
cal = ql.UnitedStates(6)
convention = ql.ModifiedFollowing
termDateConvention = ql.ModifiedFollowing
rule = ql.DateGeneration.Backward
isEndOfMonth = False
cpn_tenor = ql.Period('1Y')
ibor = ql.Sofr(crv_usdswp)
fixfltSchdl = ql.Schedule(start, maturity, cpn_tenor, cal, convention,
termDateConvention, rule, isEndOfMonth)
swap = ql.VanillaSwap(typ, notional, fixfltSchdl, rate, legDC, fixfltSchdl,
ibor, 0, legDC)
discount_engine = ql.DiscountingSwapEngine(discount_curve)
swap.setPricingEngine(discount_engine)
print((np.round(swap.fairRate(),6)-data_nonfut[i])*10000)
```
The Output is the difference in basis points between the SOFR fair rates and the inputs:
0.0 -0.059999999999990616 -0.07000000000000062 -0.010000000000010001 0.010000000000010001 0.020000000000020002 0.030000000000030003 0.020000000000020002 0.030000000000030003 0.050000000000050004 0.049999999999980616 0.059999999999990616 0.07000000000000062 0.07000000000000062 0.07000000000000062Shown 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.