I am tring to reconcile SOFR OIS swap cash flows coming out of QuantLib. I input some SOFR OIS swap rates, create a discount curve and then try and price the swaps with the discount curve in order to check the NPVs are 0. The NPV that QuantLib calculates is zero but when I try and calculate the cashflows manually they arent quite. Just wondering what I am doing wrong.
import QuantLib as ql
from pandas import DataFrame
import pandas as pd
import numpy as np
import datetime
swap_periods =[
ql.Period(1,ql.Weeks),ql.Period(2,ql.Weeks),ql.Period(1,ql.Months),ql.Period(2,ql.Months),ql.Period(3,ql.Months),
ql.Period(4,ql.Months),ql.Period(5,ql.Months),ql.Period(6,ql.Months),ql.Period(7,ql.Months),ql.Period(8,ql.Months),
ql.Period(9,ql.Months),ql.Period(10,ql.Months),ql.Period(11,ql.Months),ql.Period(1,ql.Years),
ql.Period(2,ql.Years),ql.Period(3,ql.Years),ql.Period(4,ql.Years),ql.Period(5,ql.Years),ql.Period(6,ql.Years),
ql.Period(7,ql.Years),ql.Period(8,ql.Years),ql.Period(9,ql.Years),ql.Period(10,ql.Years),ql.Period(12,ql.Years),
ql.Period(15,ql.Years),ql.Period(20,ql.Years),ql.Period(25,ql.Years),ql.Period(30,ql.Years)]
swap_rates = [5.071,5.074,5.0803,5.1855,5.228,5.263,5.2929,5.3116,5.3234,5.3189,5.312,5.2915,5.2682,5.2401,4.604,4.156,3.883,
3.719,3.615,3.542,3.493,3.462,3.438,3.418,3.404,3.344,3.237,3.138]
""" Parameter Setup """
calc_date = ql.Date(23,6,2023)
ql.Settings.instance().evaluationDate = calc_date
calendar = ql.UnitedStates()
business_convention = ql.ModifiedFollowing #A business day convention whereby payment days that fall on a holiday or a Saturday or a Sunday roll forward to the next target business day
day_count = ql.Actual360()
coupon_frequency = ql.Annual
settlement_days = 2 #t+2 settlement convention for SOFR swaps
""" SwapRateHelper """
sofr = ql.Sofr()
rate_helpers = []
for rate,tenor in list(zip(swap_rates,swap_periods)):
helper = ql.OISRateHelper(settlement_days, tenor, ql.QuoteHandle(ql.SimpleQuote(rate/100.0)),sofr)
rate_helpers.append(helper)
sofrCurve = ql.PiecewiseCubicZero(calc_date,rate_helpers,day_count)
valuation_Curve = ql.YieldTermStructureHandle(sofrCurve)
sofrIndex = ql.Sofr(valuation_Curve)
swapEngine = ql.DiscountingSwapEngine(valuation_Curve)
effective_date = calendar.advance(calc_date, settlement_days, ql.Days)
notional = 10_000_000
ois_swaps = []
for rate,tenor in list(zip(swap_rates,swap_periods)):
fixedRate = rate/100
oisSwap = ql.MakeOIS(tenor, sofrIndex, fixedRate, nominal=notional)
oisSwap.setPricingEngine(swapEngine)
ois_swaps.append(oisSwap)
def leg_information(effective_date, day_count,oisSwap, leg_type, sofrCurve):
leg_df=pd.DataFrame(columns=['date','yearfrac','CF','discountFactor','PV','totalPV'])
cumSum_pv= 0
leg = oisSwap.leg(0) if leg_type == "fixed" else oisSwap.leg(1)
for index, cf in enumerate(leg):
yearfrac = day_count.yearFraction(effective_date,cf.date())
df = sofrCurve.discount(yearfrac)
pv = df * cf.amount()
cumSum_pv += pv
row={'date':datetime.datetime(cf.date().year(), cf.date().month(), cf.date().dayOfMonth()),'yearfrac':yearfrac, 'CF':cf.amount() ,'discountFactor':df,'PV':pv,'totalPV':cumSum_pv}
leg_df.loc[index]=row
return leg_df
fixed_leg = leg_information(effective_date, day_count,ois_swaps[14], 'fixed', sofrCurve)
float_leg = leg_information(effective_date, day_count,ois_swaps[14], 'Float', sofrCurve)
print(fixed_leg)
print(float_leg)
print(ois_swaps[14].NPV())
If I compare the final totalPV in each leg there is a difference but the NPV is very small. I must be doing something wrong when manually calculating. Thanks for any input.