Skip to content
All library documents

How QuantLib VanillaSwap Coupons Use Index Fixings

Article Quant Q&A · Author: lieweHenksie

Summary

The note explains why QuantLib floating coupons in a vanilla swap can produce fixing values that differ slightly from values queried directly from the underlying Ibor index. By default, coupon rates are estimated at par from the curve, and the forward-rate dates used for those coupons can differ from the dates implied by the index. The suggested remedy is to enable indexed coupons before constructing the swap, so coupon calculations use the index fixing. The example’s cash-flow table shows small discrepancies on some payment periods and matching values on others.

The note also cautions that querying fixings on schedule dates is only valid when the index has no fixing-day offset. For indices with fixing days, first map each schedule date to the corresponding fixing date. This is a QuantLib implementation detail rather than a general swap-pricing rule; results depend on the index conventions, schedule, and curve setup.

Key ideas

  • QuantLib’s default coupon construction estimates floating rates at par from the curve.
  • Coupon forward dates can differ from those implied by the underlying index.
  • Enabling indexed coupons makes coupons use fixings from the index instance.
  • For indices with fixing-day offsets, query the fixing date associated with each schedule date.

Tags

Full text
# Quantlib: VanillaSwap not using underlying Index fixings correctly


# Quantlib: VanillaSwap not using underlying Index fixings correctly












I am trying to reperform a vanilla swap. The problem is that the vanilla swap object does not seem to be using the exact fixings of the underlying index.

```
import pandas as pd
import QuantLib as ql

calendar = ql.SouthAfrica()

notional = 481000000
fixed_rate = 0.0565
day_count = ql.Actual365Fixed()
settlementDays = 0

# %%
curve=[
('2022/03/04',0.038662937),
('2022/03/07',0.038662937),
('2022/04/04',0.041096174000000006),
('2022/05/04',0.041746752000000005),
('2022/06/06',0.042385545000000004),
('2022/07/04',0.044799741),
('2022/08/04',0.044673466),
('2022/09/06',0.046401243),
('2022/10/04',0.047449248),
('2022/11/04',0.048448323999999994),
('2022/12/05',0.049288543000000004),
('2023/01/04',0.050770971),
('2023/02/06',0.051373198999999994),
('2023/03/06',0.052326463),
('2023/04/04',0.053330200999999994),
('2023/05/08',0.053975877),
('2023/06/06',0.054701456999999995),
('2023/07/04',0.055552298),
('2023/08/04',0.055982044),
('2023/09/05',0.056613829000000004),
('2023/10/04',0.05728623),
('2023/11/06',0.057683783),
('2023/12/04',0.058114147000000005),
('2024/01/04',0.058739921),
('2024/02/06',0.059047349),
('2024/03/04',0.059449816),
('2025/03/04',0.063389835),
('2026/03/04',0.066562792),
('2027/03/04',0.069682955),
('2028/03/06',0.072745578),
('2029/03/05',0.075888138),
('2030/03/04',0.078560623),
('2031/03/04',0.080713083),
('2032/03/04',0.082602893),
('2034/03/06',0.085132561),
('2037/03/04',0.08649955699999999),
('2042/03/04',0.08760520899999999),
('2047/03/04',0.087014442),
('2052/03/04',0.08559789699999999),
('2057/03/04',0.08559789699999999),
('2062/03/04',0.08559789699999999),
('2067/03/04',0.08559789699999999)]

curve_df = pd.DataFrame(curve, columns=['StartDate', 'BidRate'])
curve_df['qlvalDate'] = curve_df.apply(
    lambda row: ql.Date(row['StartDate'], '%Y-%m-%d'), axis=1)

dates = curve_df.qlvalDate.values

rates = curve_df.BidRate.values

# %%
zc = ql.ZeroCurve(
        dates,
        rates,
        ql.Actual365Fixed(),
        calendar,
        ql.Linear(),
        ql.Compounded,
        ql.Semiannual
    )

yts = ql.YieldTermStructureHandle(
    zc
)

discounts = [yts.discount(d) for d in dates]
# %%
inception_date = ql.Date(4, 3, 2022)
valuation_date = ql.Date(4, 3, 2022)
ql.Settings.instance().evaluationDate = valuation_date
maturity_date = ql.Date('2023-12-04', "%Y-%m-%d")
fixed_leg_tenor = ql.Period(3, ql.Months)

# %%

fixed_schedule = ql.Schedule(inception_date, maturity_date,
                             fixed_leg_tenor, calendar,
                             ql.ModifiedFollowing, ql.ModifiedFollowing,
                             ql.DateGeneration.Forward, False)

day_count = ql.Actual365Fixed()
float_spread = 0
jibar3M_index = ql.Jibar(ql.Period(3, ql.Months), yts)
jibar3M_index.addFixing(ql.Date(4, 3, 2022), 0.04217)

fixings = [jibar3M_index.fixing(d) for d in fixed_schedule.dates()]

swap = ql.VanillaSwap(ql.VanillaSwap.Receiver, notional,fixed_schedule,fixed_rate,day_count, fixed_schedule, jibar3M_index, 0, day_count)

cashflows = pd.DataFrame({
    'nominal': cf.nominal(),
    'accrualStartDate': cf.accrualStartDate().ISO(),
    'accrualEndDate': cf.accrualEndDate().ISO(),
    'accrualPeriod': cf.accrualPeriod(),
    'rate': cf.rate(),
    'amount': cf.amount(),
    'forward': cf.indexFixing()
} for cf in map(ql.as_floating_rate_coupon, swap.leg(1)))

cashflows['expectedFixing'] = fixings[:-1]
cashflows['rateDiff*1000'] = (cashflows['expectedFixing']-cashflows['forward'])*1000

cashflows[['accrualStartDate', 'amount', 'indexFixing', 'expectedFixing','rateDiff*1000']]
```

this gives me the below table

| index | accrualStartDate | amount | indexFixing | expectedFixing | rateDiff*1000 |
| 0 | 2022-03-04 | 5223765.424657535 | 0.04217 | 0.04217 | 0.0 |
| 1 | 2022-06-06 | 6012318.654694998 | 0.05013585042525129 | 0.05019999932091297 | 0.06414889566168369 |
| 2 | 2022-09-05 | 6583643.403629088 | 0.05490004437469139 | 0.05490004437469139 | 0.0 |
| 3 | 2022-12-05 | 7327145.736813547 | 0.061100002146100035 | 0.061100002146100035 | 0.0 |
| 4 | 2023-03-06 | 7635693.818594573 | 0.06367293970407391 | 0.06370000036701452 | 0.02706066294061449 |
| 5 | 2023-06-05 | 7882520.176965427 | 0.06573118879149165 | 0.06575001767263097 | 0.01882888113932668 |
| 6 | 2023-09-04 | 8004699.021793886 | 0.06675002040060242 | 0.06675002040060242 | 0.0 |

Where is the difference between my expected fixing, and my cf.indexFixing() coming from?

## Answer by Luigi Ballabio (score 4)

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

That's because the default of the library is to estimate floating-rate coupons at par on the curve. This can cause the end dates of the underlying forward rates to differ from the ones implied by the index.

It's possible to change the default behavior by executing the line:

```
ql.IborCoupon.createIndexedCoupons()
```

before creating the swap. This will cause the coupons to use the fixing from the index instance.

A more detailed explanation is in this video. More on the underlying C++ implementation is in this post in particular, but also in the ones that precede it in the blog.

One last note: the line

```
fixings = [jibar3M_index.fixing(d) for d in fixed_schedule.dates()]
```

works because the JIBAR happens to have 0 fixing days. In general, the fixings should be extracted with

```
fixings = [jibar3M_index.fixing(jibar3M_index.fixingDate(d))
           for d in fixed_schedule.dates()]
```

which takes care of the fixing days, if any.

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.