Skip to content
All library documents

Matching QuantLib OIS Curve Calibration and Swap Pricing

Article Quant Q&A · Author: TourEiffel

Summary

The document investigates why rates produced by a QuantLib bootstrap from USD SOFR overnight indexed swaps differ from the market quotes. The question includes a rate table, instrument conventions, and code that builds the curve with OISRateHelper, then compares those quotes with fair rates from swaps made using MakeVanillaSwap.

The accepted answer identifies an instrument mismatch: MakeVanillaSwap constructs a vanilla swap that pays LIBOR against fixed, while the bootstrap helpers represent OIS instruments. To compare like with like, use SwapRateHelper when the market instruments are vanilla swaps, or construct an OvernightIndexedSwap to check an OIS curve. The displayed discrepancies therefore do not establish that the bootstrap itself is wrong; they can arise because the comparison instrument differs from the calibration instrument. The answer does not examine every convention in the setup, so matching instrument type alone may not resolve any remaining convention or curve configuration differences.

Key ideas

  • A calibration curve should be checked using the same instrument type used to build it.
  • MakeVanillaSwap constructs a vanilla swap, which does not match the OIS instruments represented by OISRateHelper.
  • Use SwapRateHelper for vanilla swap quotes or an OvernightIndexedSwap for OIS checks.
  • A mismatch in fair rates can reflect different instrument definitions, and other convention differences may still need review.

Tags

Full text
# BootStrap with quantlib USD SOFR (vs. FIXED RATE) swap curve


# BootStrap with quantlib USD SOFR (vs. FIXED RATE) swap curve












I struggle to understand why my market rates does not match my bootstrap model. So I wonder why the spread is that high between market & model.

```
maturity |  market  |   model
   1W    | 0.050640 | 0.050626
   2W    | 0.050670 | 0.050631
   3W    | 0.050720 | 0.050655
   1M    | 0.051021 | 0.050916
   2M    | 0.051391 | 0.051178
   3M    | 0.051745 | 0.051415
   4M    | 0.051940 | 0.051493
   5M    | 0.051980 | 0.051424
   6M    | 0.051820 | 0.051149
   7M    | 0.051584 | 0.050821
   8M    | 0.051310 | 0.050454
   9M    | 0.050924 | 0.049979
   10M    | 0.050604 | 0.049582
   11M    | 0.050121 | 0.049026
   12M    | 0.049550 | 0.048386
   18M    | 0.045585 | 0.044806
   2Y    | 0.042631 | 0.041774
   3Y    | 0.038952 | 0.038230
   4Y    | 0.036976 | 0.036321
   5Y    | 0.035919 | 0.035297
   6Y    | 0.035350 | 0.034745
   7Y    | 0.034998 | 0.034403
   8Y    | 0.034808 | 0.034219
   9Y    | 0.034738 | 0.034151
   10Y    | 0.034712 | 0.034125
   12Y    | 0.034801 | 0.034210
   15Y    | 0.034923 | 0.034327
   20Y    | 0.034662 | 0.034075
   25Y    | 0.033750 | 0.033193
   30Y    | 0.032826 | 0.032298
   40Y    | 0.030835 | 0.030369
   50Y    | 0.028960 | 0.028548
```

My curve only contain swaps.

Fixed leg :

- Discounting OIS

- Settlement T+2 Days

- Term 2 Week

- Day Count ACT/360

- Pay Freq Annual

- Bus Adj ModifiedFollowing

- Adjust Accrl and Pay Dates

- Roll Conv Backward (EOM)

- Calc Cal FD

- Pay Delay 2 Business Days

Float Leg

- Day Count ACT/360

- Pay Freq Annual

- Index SOFRRATE Index

- Reset Freq Daily

- Bus Adj ModifiedFollowing

```
self.swaps =   {Period("1W"): 0.05064, Period("2W"): 0.05067, Period("3W"): 0.05072, Period("1M"): 0.051021000000000004, Period("2M"): 0.051391, Period("3M"): 0.051745, Period("4M"): 0.05194, Period("5M"): 0.051980000000000005, Period("6M"): 0.051820000000000005, Period("7M"): 0.051584000000000005, Period("8M"): 0.05131, Period("9M"): 0.050924, Period("10M"): 0.050603999999999996, Period("11M"): 0.050121, Period("12M"): 0.049550000000000004, Period("18M"): 0.04558500000000001, Period("2Y"): 0.042630999999999995, Period("3Y"): 0.038952, Period("4Y"): 0.036976, Period("5Y"): 0.035919, Period("6Y"): 0.03535, Period("7Y"): 0.034998, Period("8Y"): 0.034808, Period("9Y"): 
0.034738000000000005, Period("10Y"): 0.034712, Period("12Y"): 0.034801, Period("15Y"): 0.034923, Period("20Y"): 0.034662, Period("25Y"): 0.03375, Period("30Y"): 0.032826, Period("40Y"): 0.030834999999999998, Period("50Y"): 0.02896}
```

Below is how I use `OISRateHelper`

```
rate_helpers = []
for tenor, rate in self.swaps.items():
            helper = ql.OISRateHelper(2, tenor, ql.QuoteHandle(ql.SimpleQuote(rate)), self.swap_underlying)
            rate_helpers.append(helper)
```

And below is how I compare new rates to given rates :

```
self.curve = ql.PiecewiseSplineCubicDiscount(calculation_date, rate_helpers, self.swap_day_count_conv)
yts = ql.YieldTermStructureHandle(self.curve)

# Link index to discount curve
index = index.clone(yts)

# Create engine with yield term structure
engine = ql.DiscountingSwapEngine(yts)

# Check the swaps reprice

print("maturity |  market  |   model")
for tenor, rate in self.swaps.items():
    swap = ql.MakeVanillaSwap(tenor,
                            index, 0.01,
                            ql.Period('0D'),
                            fixedLegTenor=ql.Period('2D'),
                            fixedLegDayCount=self.swap_day_count_conv,
                            fixedLegCalendar=ql.UnitedStates(ql.UnitedStates.GovernmentBond),
                            floatingLegCalendar=ql.UnitedStates(ql.UnitedStates.GovernmentBond),
                            pricingEngine=engine)

    print(f"   {tenor}    | {rate:.6f} | {swap.fairRate():.6f}")
```

Also note that :

```
self.swap_underlying = ql.OvernightIndex("USD Overnight Index", 2, ql.USDCurrency(), ql.UnitedStates(ql.UnitedStates.Settlement), ql.Actual360())

self.swap_day_count_conv = ql.Actual360()
```

Did I miss something? Is the implementation I made correct? Are there any discrepancies in the parameters?

Note Curve description :

Swap description :

## Answer by Luigi Ballabio (score 3, accepted)

https://quant.stackexchange.com/a/75657

By using `ql.MakeVanillaSwap`, you're creating a swap that pays LIBOR vs fixed, not an OIS like the ones you used to bootstrap the curve. If you actually want to use vanilla swaps, you need to use `SwapRateHelper`, not `OISRateHelper`. If you do want to use OIS instead, you'll have to use `OvernightIndexedSwap` to build the swap and retrieve the fair rate.

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.