I am using QuantLib to calculate the SOFR index for a bond's (ISIN : US025816CL12) cash flows. My objective is to understand how QuantLib computes the SOFR fixing rate for future dates. Here is my code:
SOFRindex = ql.OvernightIndex("SOFR", 1, ql.USDCurrency(), calendar, day_count, discounting_term_structure)
bond = ql.FloatingRateBond(0,faceValue, schedule, SOFRindex, day_count,spreads=[spread])
#display bond cashflows
for i, cashflow in enumerate(bond.cashflows()):
d = cashflow.date()
try:
result = SOFRindex.fixing(d, True)
print(f"Index Value for {d} is {round(result*100,2)}")
except Exception as e:
print(f"Index Value for {d} is No Data")
The above code works, but I'm not quite sure how QuantLib is determining the SOFR rates for future dates. The output is as follows:
Index Value for August 4th, 2023 is 5.17
Index Value for November 6th, 2023 is 5.17
Index Value for February 5th, 2024 is 4.96
Index Value for May 6th, 2024 is 4.57
Index Value for August 5th, 2024 is 3.93
Index Value for November 4th, 2024 is 3.93
Index Value for February 4th, 2025 is 3.33
Index Value for May 5th, 2025 is 3.33
Index Value for August 4th, 2025 is 3.06
Index Value for November 4th, 2025 is 3.06
Index Value for February 4th, 2026 is 3.06
Index Value for May 4th, 2026 is 3.06
Index Value for August 4th, 2026 is 2.96
Index Value for November 4th, 2026 is 2.96
Using Bloomberg we can see that the SOFR Secured Overnight Financing Rate Compounded Index gives a rate of 5.10860 for the period from he 05/04/2023 to 08/04/2023 using the backward convention real date are 05/02/2023 and 08/02/2023. As we can see this does not match the 5.17.
To show that Bloomberg is relevant and correct we can take an example from the past :
start date : 11/04/2022 --> Backward convention : 11/02/2022
end date : 02/06/2023 --> Backward convention : 02/02/2023
Gives a rate of 4.73020 which does match with 4.730198
So I believe that I am missing something but I don't know what yet.
Is the below correct ?
SOFRindex = ql.OvernightIndex("SOFR", 1, ql.USDCurrency(), calendar, day_count, discounting_term_structure)
Because I am not quite sure that this use forward rates for futures date.
Full code is just below and can be used as a reproducible example
import QuantLib as ql
import datetime
calculation_date = ql.Date().todaysDate() #ql.Date(15, 6, 2023)
ql.Settings.instance().evaluationDate = calculation_date
yts = ql.RelinkableYieldTermStructureHandle()
index_for_curve_s490 = ql.OvernightIndex("USD Overnight Index", 0, ql.USDCurrency(), ql.UnitedStates(ql.UnitedStates.Settlement), ql.Actual360(),yts)
swaps = {
ql.Period("1W"): 0.050881,
ql.Period("2W"): 0.050907,
ql.Period("3W"): 0.050901,
ql.Period("1M"): 0.050985,
ql.Period("2M"): 0.05155,
ql.Period("3M"): 0.05202,
ql.Period("4M"): 0.052316,
ql.Period("5M"): 0.052405,
ql.Period("6M"): 0.052419,
ql.Period("7M"): 0.052346,
ql.Period("8M"): 0.052213,
ql.Period("9M"): 0.052052,
ql.Period("10M"): 0.051765,
ql.Period("11M"): 0.051434,
ql.Period("12M"): 0.051021,
ql.Period("18M"): 0.047224,
ql.Period("2Y"): 0.044145,
ql.Period("3Y"): 0.03992,
ql.Period("4Y"): 0.037565,
ql.Period("5Y"): 0.036239,
ql.Period("6Y"): 0.035464,
ql.Period("7Y"): 0.034974,
ql.Period("8Y"): 0.034677,
ql.Period("9Y"): 0.034518,
ql.Period("10Y"): 0.03442,
ql.Period("12Y"): 0.034378,
ql.Period("15Y"): 0.03437,
ql.Period("20Y"): 0.033933,
ql.Period("25Y"): 0.032933,
ql.Period("30Y"): 0.031949,
ql.Period("40Y"): 0.029842,
ql.Period("50Y"): 0.02773,
}
rate_helpers = []
for tenor, rate in swaps.items():
helper = ql.OISRateHelper(2, tenor, ql.QuoteHandle(ql.SimpleQuote(rate)), index_for_curve_s490)
rate_helpers.append(helper)
curve = ql.PiecewiseFlatForward(calculation_date, rate_helpers, ql.Actual360())
yts.linkTo(curve)
spread = 65/10000
#spread = 0
settlementDays = 2
faceValue = 100
compounding = ql.Compounded
pricingDate = calculation_date + ql.Period(f"{settlementDays}D")
issueDate = ql.Date(4, 11, 2021)
maturityDate = ql.Date(4, 11, 2026)
tenor = ql.Period("3M")
calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
day_count = ql.Actual360()
coupon_rate = 0.05729695699595476
schedule = ql.Schedule( pricingDate,
maturityDate,
tenor,
calendar,
ql.Unadjusted,
ql.Unadjusted,
ql.DateGeneration.Backward,
True)
flat_forward = ql.FlatForward(2,
calendar,
coupon_rate,
ql.Thirty360(ql.Thirty360.USA),
compounding)
discounting_term_structure = ql.RelinkableYieldTermStructureHandle(curve)
index_term_structure = ql.RelinkableYieldTermStructureHandle(flat_forward)
SOFRindex = ql.OvernightIndex("SOFR", 1, ql.USDCurrency(), calendar, day_count, discounting_term_structure)
#index = index_for_curve_s490.clone(yts)
bond = ql.FloatingRateBond(2,faceValue, schedule, SOFRindex, day_count,spreads=[spread])
#display bond cashflows start date and end date
for i, cashflow in enumerate(bond.cashflows()):
d = cashflow.date()
try:
result = SOFRindex.fixing(d, True)
print(f"Accrual Start {d} : {round(result*100,2)}")
except Exception as e:
print(f" {d} No Data")
#print(f"Coupon for {d} is {round(cashflow.amount(),2)}")
pricing_engine = ql.DiscountingBondEngine(discounting_term_structure)
bond.setPricingEngine(pricing_engine)
print(f"My Calculated Price is {bond.cleanPrice()} and the bloomberg price is 99.13")
issueDate
inschedule
instead ofpricingDate
and then to add a fixing for the date of the first period since it is in the paste. (If I set this fixing point as 0% then I am close to what bloomberg price, but if I put real fixing rate, I have a wide spread between me and bloomberg. Do you have any thought on what is happening ? $\endgroup$