1
$\begingroup$

I generated/calibrated a SOFR Curve using Quantlib Python and would like to know why when repricing the swaps have non-zero NPVs. Appreciate any assistance. Thank you.

Parameters

mktDate = ql.Date(8,3,2021)
ql.Settings.instance().evaluationDate = mktDate
Settlement = 2
Calendar = ql.UnitedStates()
DayCount = ql.Actual360()

OIS Curve Helper

oisHelper = []
for quote in marketQuotes:
    oisHelper.append(ql.OISRateHelper(Settlement, ql.Period(quote[0]), 
ql.QuoteHandle(ql.SimpleQuote(quote[1]/100)), ql.Sofr()))

Curve Specification

sofrCurve = ql.PiecewiseLinearZero(Settlement, Calendar, oisHelper, DayCount)
valCurve = ql.YieldTermStructureHandle(sofrCurve)
sofrIndex = ql.Sofr(valCurve)
swapEngine = ql.DiscountingSwapEngine(valCurve)

Reprice Quotes

for quote in marketQuotes:
    start = Calendar.advance(mktDate, Settlement, ql.Days)
    schedule = ql.MakeSchedule(start, Calendar.advance(start, ql.Period(quote[0])), ql.Period('1Y'), calendar = Calendar)
    fixedRate = quote[1]/100
    oisSwap = ql.OvernightIndexedSwap(
        ql.OvernightIndexedSwap.Receiver, 
        1E6, 
        schedule, 
        fixedRate, 
        DayCount,
        sofrIndex)
    oisSwap.setPricingEngine(swapEngine)
    print(quote, round(oisSwap.NPV(),3)) 

Output

('1W', 0.01982) 0.0
('2W', 0.02394) -0.0
('3W', 0.02503) -0.0
('1M', 0.02897) -0.0
('3M', 0.037) 0.0
('4M', 0.041) -0.0
('5M', 0.043) 0.0
('6M', 0.04597) 0.0
('7M', 0.04797) 0.0
('8M', 0.04997) -0.0
('9M', 0.05197) 0.0
('10M', 0.0535) 0.0
('11M', 0.055) 0.0
('1Y', 0.0565) -0.0
('15M', 0.06) -0.0
('18M', 0.069) 0.003
('21M', 0.083) 0.004
('2Y', 0.10403) 0.0
('3Y', 0.27409) 0.049
('4Y', 0.50109) -0.0
('5Y', 0.718) -0.0
('6Y', 0.90703) 0.0
('7Y', 1.066) -0.0
('8Y', 1.19203) 0.83
('9Y', 1.29306) 0.521
('10Y', 1.37903) -0.0
('12Y', 1.51294) -0.0
('15Y', 1.63591) -0.0
('20Y', 1.72494) 0.766
('25Y', 1.75318) 1.322
('30Y', 1.76979) -0.0
('40Y', 1.71094) 0.0
('50Y', 1.63649) -0.0
$\endgroup$
0
$\begingroup$

Appears to work when using ql.MakeOIS. Still need to understand why there is a discrepancy. Any thoughts?

Reprice Quotes

for quote in marketQuotes:
    swapTenor = ql.Period(quote[0])
    fixedRate = quote[1]/100
    oisSwap = ql.MakeOIS(swapTenor, sofrIndex, fixedRate, nominal=1E6)
    print(quote, round(oisSwap.NPV(),3)) 

Output

('1W', 0.01982) 0.0
('2W', 0.02394) 0.0
('3W', 0.02503) 0.0
('1M', 0.02897) 0.0
('3M', 0.037) -0.0
('4M', 0.041) 0.0
('5M', 0.043) 0.0
('6M', 0.04597) 0.0
('7M', 0.04797) 0.0
('8M', 0.04997) 0.0
('9M', 0.05197) -0.0
('10M', 0.0535) -0.0
('11M', 0.055) -0.0
('1Y', 0.0565) 0.0
('15M', 0.06) 0.0
('18M', 0.069) 0.0
('21M', 0.083) -0.0
('2Y', 0.10403) -0.0
('3Y', 0.27409) -0.0
('4Y', 0.50109) 0.0
('5Y', 0.718) 0.0
('6Y', 0.90703) -0.0
('7Y', 1.066) 0.0
('8Y', 1.19203) -0.0
('9Y', 1.29306) -0.0
('10Y', 1.37903) 0.0
('12Y', 1.51294) 0.0
('15Y', 1.63591) 0.0
('20Y', 1.72494) 0.0
('25Y', 1.75318) 0.0
('30Y', 1.76979) 0.0
('40Y', 1.71094) 0.0
('50Y', 1.63649) 0.0
$\endgroup$
2
0
$\begingroup$

The difference probably comes from not having exactly the same conventions. When you use ql.MakeOIS all the conventions will come from the ql.Sofr index, but when you build the Instrument manually with ql.OvernightIndexedSwap you are entering all the convention by hand, namely for the schedule.

The ql.MakeSchedule class has many more parameters which mostly default to None:

  • convention=None
  • terminalDateConvention=None,
  • rule=None
  • forwards=False
  • backwards=False,
  • endOfMonth=None
  • firstDate=None
  • nextToLastDate=None
$\endgroup$
2
  • $\begingroup$ Thank you David. Appreciate the feedback. I will test each parameter. $\endgroup$ – lee lo Apr 1 at 9:12
  • $\begingroup$ Still struggling. I tried various configurations with the parameters with no luck. $\endgroup$ – lee lo Apr 1 at 12:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.