Pricing SOFR Swaps in QuantLib with Fixings and Overnight Coupons
Summary
The document discusses common issues when valuing SOFR interest rate swaps in QuantLib. One problem is missing historical overnight index fixings: when valuation occurs after a swap has begun, the index needs the past observations required to determine coupons already accruing. The answer illustrates adding historical fixings to the SOFR index before pricing.
It also distinguishes a standard VanillaSwap floating leg from an overnight indexed swap. VanillaSwap creates coupons that reset at the start of each coupon period, while SOFR OIS coupons typically reflect overnight rates compounded or averaged throughout the period. The answer therefore recommends using OvernightIndexedSwap when that is the intended cash-flow convention. The question also mentions a duplicate-pillar error and an inability to value a swap against a later curve date, but the response does not fully diagnose those issues. Correct schedules, index fixings, instrument choice, and valuation-date setup remain important implementation details.
Key ideas
- Historical SOFR fixings are needed to value coupon periods that began before the valuation date.
- QuantLib stores past overnight index observations as fixings on the relevant index.
- VanillaSwap floating coupons fix at the beginning of each coupon period.
- OvernightIndexedSwap is appropriate when the floating leg should reflect overnight SOFR over the coupon period.
- The response does not resolve every reported curve-pillar or valuation-date issue.
Tags
Full text
# QuantLib: Problem with IRS valuation
# QuantLib: Problem with IRS valuation
I would like to value an IRS. My first problem is with "RuntimeError: more than one instrument with pillar". Theres is clearly no overlapping instruments. I can't get the valuation of the floating leg. Also, is there a way to value a swap at e.g. june 2023, when it started in may 2022, if I gave past Sofr ON values and spot rates from june 2023. Normally, there shouldn't be the problem, but I am not sure how QuantLib works. THanks!
Here's the code:
```
import pandas as pd
import datetime as dt
import QuantLib as ql
settlement_days = 2
calendar = ql.UnitedStates(ql.UnitedStates.FederalReserve)
day_count = ql.Actual360()
sofr = ql.Sofr()
curve_dates = ["30.06.2023", "03.07.2023", "05.07.2023", "12.07.2023", "19.07.2023", "07.08.2023", "05.09.2023", "05.10.2023", "06.11.2023",
"05.12.2023", "05.01.2024", "05.02.2024", "05.03.2024", "05.04.2024", "06.05.2024", "05.06.2024", "05.07.2024",
"07.07.2025", "06.07.2026", "05.07.2028", "05.07.2029", "05.07.2030", "07.07.2031",
"05.07.2033", "05.07.2034", "05.07.2035", "07.07.2036", "06.07.2037", "05.07.2039", "05.07.2040",
"05.07.2041", "07.07.2042", "06.07.2043"]
row1 = [0.999576013, 0.999293276, 0.998284236, 0.997276216, 0.994545288, 0.990391441, 0.986043654, 0.981353845]
row2 = [0.977122975, 0.972637548, 0.968188632, 0.964045167, 0.959738687, 0.95554771, 0.951509352, 0.947635722]
row3 = [0.908583534, 0.877579008, 0.823760707, 0.798388971, 0.773888557, 0.749927777]
row4 = [0.703540185, 0.680868756, 0.658502936, 0.636587109, 0.615672676, 0.57654805, 0.558385784]
row5 = [0.541184978, 0.524737797, 0.509183223]
discount_factors = row1 + row2 + row3 + row4 + row5
qlDates = [ql.Date(int(day), int(month), int(year)) for date in curve_dates for day, month, year in [date.split('.')]]
valuationDate = ql.Date(30,6,2023)
ql.Settings.instance().evaluationdate = valuationDate
sofr = ql.Sofr()
SofrCurve = ql.DiscountCurve(qlDates, [1] + discount_factors, day_count, calendar)
SofrCurve.enableExtrapolation()
ValuationCurve = ql.RelinkableYieldTermStructureHandle(SofrCurve)
#ValuationCurve.linkTo(SofrCurve)
sofrIndex = ql.Sofr(ValuationCurve)
swapEngine = ql.DiscountingSwapEngine(ValuationCurve)
fixedRate = 0.0443
Nominal = 3_000_000
forwardStart = ql.Period("2D")
start_date = ql.Date(30,6,2023)
maturity_date = calendar.advance(start_date, 4, ql.Years)
fixed_leg_daycount = ql.Actual360()
fixed_leg_tenor = ql.Period(12, ql.Months)
fixed_schedule = ql.Schedule(start_date, maturity_date,
fixed_leg_tenor, calendar,
ql.ModifiedFollowing, ql.ModifiedFollowing,
ql.DateGeneration.Forward, False)
fixed_leg_daycount = ql.Actual360()
fixed_leg_tenor = ql.Period(12, ql.Months)
fixed_schedule = ql.Schedule(start_date, maturity_date,
fixed_leg_tenor, calendar,
ql.ModifiedFollowing, ql.ModifiedFollowing,
ql.DateGeneration.Forward, False)
float_spread = 0.0
ir_swap = ql.VanillaSwap(ql.VanillaSwap.Payer, Nominal,
fixed_schedule, fixedRate, fixed_leg_daycount, float_schedule, sofrIndex, float_spread, float_leg_daycount)
ir_swap.setPricingEngine(swapEngine)
#Floating Leg
pd.DataFrame(
[(cf.date(), cf.amount()) for cf in ir_swap.leg(1)],
columns=["Date", "Amount"],
index=range(1, len(ir_swap.leg(1))+1)
)
```
OIS swap
```
sofr_curve_log_cubic.nodes()[:10]
((Date(30,6,2023), 1.0),
(Date(4,7,2023), 0.9994349085205639),
(Date(20,9,2023), 0.9882497109370333),
(Date(20,12,2023), 0.9749417559680691),
(Date(20,3,2024), 0.9619089583699006),
(Date(20,6,2024), 0.9494974442774458),
(Date(18,9,2024), 0.9383313017859709),
(Date(18,12,2024), 0.9280793760780555),
(Date(19,3,2025), 0.9187319168325597),
(Date(7,7,2025), 0.9085558942301587))
```
```
today = ql.Date(30, ql.June, 2023)
ql.Settings.instance().evaluationDate = today
sofr_curve_log_cubic = ql.PiecewiseLogCubicDiscount(0, ql.TARGET(),
helpers, ql.Actual360())
sofr_curve_log_cubic.enableExtrapolation()
swapType = ql.OvernightIndexedSwap.Receiver
nominal = 500_000
schedule = ql.MakeSchedule(ql.Date(13,5,2022),
ql.Date(13, 5,2027),
ql.Period('1Y'),
calendar=ql.TARGET())
fixedRate = 0.03
fixedDC = ql.Actual360()
ValuationCurve = ql.RelinkableYieldTermStructureHandle()
ValuationCurve.linkTo(sofr_curve_log_cubic)
overnightIndex = ql.Sofr(ValuationCurve)
# Here are dates and fixings from the start of the swap up until valuation date
for date, fixing in zip(SofrON_qlDates, SofrON_rates):
overnightIndex.addFixing(date, fixing)
ois_swap = ql.OvernightIndexedSwap(swapType,
nominal,
schedule,
fixedRate,
fixedDC,
overnightIndex)
swapEngine = ql.DiscountingSwapEngine(ValuationCurve)
ois_swap.setPricingEngine(swapEngine)
# Unfortunately, the swap is not pricing with the schedule on the valuation date
```
```
## Answer by Luigi Ballabio (score 1)
https://quant.stackexchange.com/a/78713
After you edits, and after initializing `float_schedule` and `float_leg_daycount` (right now you're initializing `fixed_schedule` twice instead) the errors you're getting say that you need past historical fixings. You can store them with
```
sofrIndex.addFixing(Date(30,6,2023), 0.0509)
sofrIndex.addFixing(Date(29,6,2023), 0.0506)
sofrIndex.addFixing(Date(28,6,2023), 0.0506)
```
and so on.
However: if you're using SOFR, you probably don't really want to use `VanillaSwap`, because it creates floating-rate coupons that fix at the beginning of the coupon, and not coupons that average overnight SOFR over the life of the coupon. If that's what you want (as I suspect) use `OvernightIndexedSwap` instead. It takes similar parameters.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.