Matching OIS Curve Rates to Quoted Swap Rates
Summary
The document investigates why a two-month euro short-term rate OIS quote differs from a rate queried from a curve bootstrapped with dated overnight indexed swaps. The accepted explanation is that the floating leg compounds the overnight rate daily, while the market quote is the simple fixed rate. Querying a simple forward rate produces the quoted level in the example.
The answer cautions that a single forward rate is not generally the quoted swap rate when the fixed leg has multiple coupons: the fair quote applies across the entire fixed leg. It recommends constructing the corresponding OIS and reading its fair rate. A second answer presents a separate curve and swap implementation as a benchmark and reports matching rates, though it does not explain the original discrepancy. The example is specific to the stated instruments, dates, and conventions; curve setup and contract details still need to match the market instrument.
Key ideas
- OIS floating legs accrue daily compounded overnight rates, while the fixed quote is simple.
- The compounding convention used for a curve query can explain a mismatch with a market quote.
- For swaps with multiple fixed coupons, the fair rate applies across the leg and is not one interval forward rate.
- Pricing the matching OIS and requesting its fair rate is a more general way to obtain the quote.
Tags
Full text
# Why is there a difference between curve swap rate and market swap rate?
# Why is there a difference between curve swap rate and market swap rate?
I am constructing an ESTR curve from inter-meeting OIS swaps (tickers EESFXA Curncy in BBG) as per below
```
import QuantLib as ql
ql.Settings.instance().evaluationDate = ql.Date(13, 12, 2024)
swap_data = [(ql.Date(13,12,2024), ql.Date(18,12,2024), 0.03167), (ql.Date(18,12,2024), ql.Date(5,2,2025), 0.02917), (ql.Date(5,2,2025), ql.Date(12,3,2025), 0.02627), (ql.Date(12,3,2025), ql.Date(23,4,2025), 0.02303)]
helpers = []
for row in swap_data:
helpers.append(
ql.DatedOISRateHelper(
row[0],
row[1],
ql.QuoteHandle(ql.SimpleQuote(row[2])),
ql.Estr()
)
)
day_count = ql.Actual360()
curve = ql.PiecewiseFlatForward(ql.Date(13, 12, 2024), helpers, day_count)
curve.enableExtrapolation()
```
When I then try to query the curve as follows
```
today = ql.Date(13, 12, 2024)
tgt = ql.TARGET()
spot = tgt.advance(today, ql.Period(2, ql.Days))
end_date = tgt.advance(spot, ql.Period('2m'))
curve.forwardRate(spot, end_date, ql.Actual360(), ql.Compounded, ql.Daily).rate()
```
I get the number to be ~2.8598
The market however prices the 2 month ESTR swap (ticker EESWEB Curncy in BBG) for those dates at ~2.8667
Why does this discrepancy appear?
## Answer by Luigi Ballabio (score 4, accepted)
https://quant.stackexchange.com/a/81408
The floating-rate leg of an OIS pays daily-compounded ESTR. The fixed-rate leg, which is the one quoted by the market, pays a simple rate. Asking for that rate gives you what you expect:
```
print(curve.forwardRate(spot, end_date, ql.Actual360(), ql.Simple).rate())
0.028667328384901276
```
However, I would be careful about asking the curve for the rate. For one thing, it wouldn't work when the fixed leg has more than one coupon; in that case, the quoted rate is not easily calculated, because it's the rate that all fixed coupons should pay for the swap to be fair, and it doesn't correspond to any one forward rate on the curve. Instead, I would instantiate the corresponding OIS and ask it for its fair rate (there's a `fairRate` method for that).
## Answer by Attack68 (score 3)
https://quant.stackexchange.com/a/81399
Its pretty easy to build a log-linearly interpolated curve in something like Excel or even on paper for these very short dated, separable and contiguous instruments. If you are uncertain if you have configured it correctly in Quantlib do a benchmark.
Here is benchmark from `rateslib`:
```
from rateslib import * # Python 3.12, rateslib 1.6.0
curve = Curve(
nodes={
dt(2024, 12, 13): 1.0,
dt(2024, 12, 18): 1.0,
dt(2025, 2, 5): 1.0,
dt(2025, 3, 16): 1.0,
},
convention="act360",
calendar="tgt",
id="estr"
)
solver = Solver(
curves=[curve],
instruments=[
IRS(dt(2024, 12, 13), dt(2024, 12, 18), spec="eur_irs", curves="estr"),
IRS(dt(2024, 12, 18), dt(2025, 2, 5), spec="eur_irs", curves="estr"),
IRS(dt(2025, 2, 5), dt(2025, 3, 12), spec="eur_irs", curves="estr")
],
s=[3.167, 2.917, 2.627]
)
```
With the results:
```
curve.rate(dt(2024, 12, 17), "2m") # 2.866733
IRS(dt(2024, 12, 17), "2m", spec="eur_irs", curves="estr").rate(solver=solver) # 2.866733
```
No idea how you are getting 2.8598.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.