# The effect of today's fixing on bootstrapping

Hello again.

Today’s post is based on a question by Steve Hsieh on the QuantLib mailing list and an issue by Marcin Rybacki on GitHub. Thanks to both!

## The effect of today’s fixing on bootstrapping

The purpose of this notebook is to highlight an effect that might not be obvious.

```
import QuantLib as ql
import pandas as pd
today = ql.Date(23, ql.January, 2024)
ql.Settings.instance().evaluationDate = today
ql.IborCoupon.createIndexedCoupons()
```

## Setting up

Let’s say we have the usual dual-curve setup for pricing fixed vs floater swaps. I’ll gloss over the mechanics of creating the discount and forecast curves; it’s described elsewhere.

For brevity, I’ll use just a handful of OIS to create a sample discount curve.

```
helpers = [
ql.OISRateHelper(
2,
tenor,
ql.QuoteHandle(ql.SimpleQuote(quote / 100.0)),
ql.Estr(),
paymentFrequency=ql.Annual,
)
for tenor, quote in [
(ql.Period("1y"), 3.995),
(ql.Period("5y"), 4.135),
(ql.Period("10y"), 4.372),
(ql.Period("20y"), 4.798),
]
]
discount_curve = ql.PiecewiseLogCubicDiscount(
0, ql.TARGET(), helpers, ql.Actual360()
)
discount_handle = ql.YieldTermStructureHandle(discount_curve)
```

Next, we create the forecast curve for the floating index; in this case, 6-months Euribor.

```
quoted_swap_data = [
(ql.Period("1y"), 3.96),
(ql.Period("2y"), 4.001),
(ql.Period("3y"), 4.055),
(ql.Period("5y"), 4.175),
(ql.Period("7y"), 4.304),
(ql.Period("10y"), 4.499),
(ql.Period("12y"), 4.611),
(ql.Period("15y"), 4.741),
(ql.Period("20y"), 4.846),
]
```

```
index = ql.Euribor(ql.Period(6, ql.Months))
settlement_days = 2
calendar = ql.TARGET()
fixed_frequency = ql.Annual
fixed_convention = ql.Unadjusted
fixed_day_count = ql.Thirty360(ql.Thirty360.BondBasis)
helpers = [
ql.SwapRateHelper(
ql.QuoteHandle(ql.SimpleQuote(quote / 100.0)),
tenor,
calendar,
fixed_frequency,
fixed_convention,
fixed_day_count,
index,
ql.QuoteHandle(),
ql.Period(0, ql.Days),
discount_handle,
)
for tenor, quote in quoted_swap_data
]
euribor_curve = ql.PiecewiseFlatForward(0, ql.TARGET(), helpers, ql.Actual360())
```

Here are the resulting forward rates:

```
df = pd.DataFrame(euribor_curve.nodes(), columns=["date", "rate"])
df.style.format({"rate": "{:.6%}"})
```

date | rate | |
---|---|---|

0 | January 23rd, 2024 | 3.797969% |

1 | January 27th, 2025 | 3.797969% |

2 | January 26th, 2026 | 3.919519% |

3 | January 27th, 2027 | 4.039856% |

4 | January 25th, 2029 | 4.218121% |

5 | January 27th, 2031 | 4.499489% |

6 | January 25th, 2034 | 4.884324% |

7 | January 25th, 2036 | 5.149876% |

8 | January 26th, 2039 | 5.275125% |

9 | January 27th, 2044 | 5.163086% |

We’re now able to create an instance of `Euribor6M`

that can forecast
future fixings—or today’s fixing, if we don’t have already stored it in
the library.

```
euribor_handle = ql.YieldTermStructureHandle(euribor_curve)
euribor = ql.Euribor6M(euribor_handle)
```

```
euribor.fixing(today)
```

```
0.03834665129363748
```

## Pricing a sample swap

Now I’ll create a sample swap and price it using the discount and forecast curves I created. I’ll have it start in the past, so I’ll have to store the fixing for the current coupon (which was in the past and can’t be read off the forecast curve.)

```
start_date = today - ql.Period(21, ql.Months)
end_date = start_date + ql.Period(15, ql.Years)
fixed_schedule = ql.Schedule(
start_date,
end_date,
ql.Period(fixed_frequency),
calendar,
fixed_convention,
fixed_convention,
ql.DateGeneration.Forward,
False,
)
float_schedule = ql.Schedule(
start_date,
end_date,
euribor.tenor(),
calendar,
euribor.businessDayConvention(),
euribor.businessDayConvention(),
ql.DateGeneration.Forward,
False,
)
swap = ql.VanillaSwap(
ql.Swap.Payer,
1_000_000,
fixed_schedule,
0.04,
fixed_day_count,
float_schedule,
euribor,
0.0,
euribor.dayCounter(),
)
swap.setPricingEngine(ql.DiscountingSwapEngine(discount_handle))
euribor.addFixing(ql.Date(19, 10, 2023), 0.0413)
swap.NPV()
```

```
47643.03343425237
```

## A surprising effect

Now, if we’re pricing a number of swaps with different schedules, it’s not very convenient to figure out what past fixings we need to store. It’s easier to store the whole history of the index for the past year or so—and if we already have it in our systems, we’ll probably add today’s fixing as well.

```
euribor.addFixing(today, 0.0394)
```

At this point, if we ask the index for today’s fixing, it will return the stored value.

```
euribor.fixing(today)
```

```
0.0394
```

(A note: the full signature of the `fixing`

method is

```
Real fixing(const Date& fixingDate, bool forecastTodaysFixing = false)
```

so we can still read the corresponding rate off the curve, if we need it for comparison.)

```
euribor.fixing(today, True)
```

```
0.03729528572216041
```

Our sample swap, though, doesn’t use today’s fixing to determine its coupons, so its price shouldn’t change—right?

```
swap.NPV()
```

```
46857.318298378086
```

## What happened?

True, the swap doesn’t use today’s fixing directly. But storing it
causes the forecast curve to recalculate, because it is used by the
quoted swaps over which we’re bootstrapping it. *Their* first coupon is
now determined, and the rest of the curve has to change so that their
fair rate still corresponds to the quoted one.

To check this, we can ask the curve for its nodes again and compare the result with what we have already stored in the data frame:

```
df["new rate"] = [r for n, r in euribor_curve.nodes()]
df.style.format({"rate": "{:.6%}", "new rate": "{:.6%}"})
```

date | rate | new rate | |
---|---|---|---|

0 | January 23rd, 2024 | 3.797969% | 3.694805% |

1 | January 27th, 2025 | 3.797969% | 3.694805% |

2 | January 26th, 2026 | 3.919519% | 3.919519% |

3 | January 27th, 2027 | 4.039856% | 4.039856% |

4 | January 25th, 2029 | 4.218121% | 4.218121% |

5 | January 27th, 2031 | 4.499489% | 4.499489% |

6 | January 25th, 2034 | 4.884324% | 4.884324% |

7 | January 25th, 2036 | 5.149876% | 5.149876% |

8 | January 26th, 2039 | 5.275125% | 5.275125% |

9 | January 27th, 2044 | 5.163086% | 5.163086% |

We can see the difference in the first year of the curve. The rates from the second year onwards are not modified; intuitively, that’s because both the old and the new curve, by construction, give the same value to the first two floating coupons (those corresponding to a 1-year swap) so the rest of the curve doesn’t need to change.

However, this change is enough to modify our forecast of the next coupon of our sample swap, and therefore its total NPV.

## Is this desirable?

Well, it might be unexpected or confusing at first, but I don’t see a use case for not including the fixing effect. On the one hand, once it’s available, we can assume that the quoted swap rates make use of the information, and therefore we need it as well; and on the other hand, ignoring the fixing during bootstrapping and including it when pricing would cause quoted swaps to no longer price at 0—which is obviously undesirable. All in all, I think including the effect is the right thing to do.