Building a Risk Framework Including STIR Convexity Adjustments#

Common risk frameworks that I have experienced, used STIRFuture prices to construct IR Curves at the short end. However, they typically do so by implying a direct curve rate from the futures’ prices, adding a convexity adjustment as a parameter, manually.

When portfolio managers want to know their delta sensitivity versus the convexity parameter itself, they can simply add up the number of STIRFuture contracts they have and compare versus swaps. This is trivial to do, but, it does not provide an automatic and complete risk sensitivity framework, or Solver: it requires those extra steps. What this page demonstrates is how to create a Solver for the first 2-years of the Curve including convexity instruments so that risk against IRS (and/or any other relevant Instruments) can be actively calculated.

First we construct a Curve that is used to calculate STIR Future rates. In this framework instrument prices are given in rate terms (i.e. price = 100 - rate).

In [1]: curve_stir = Curve(
   ...:     nodes={
   ...:         dt(2023, 11, 2): 1.0,
   ...:         dt(2024, 3, 20): 1.0,
   ...:         dt(2024, 6, 19): 1.0,
   ...:         dt(2024, 9, 18): 1.0,
   ...:         dt(2024, 12, 18): 1.0,
   ...:         dt(2025, 3, 19): 1.0,
   ...:         dt(2025, 6, 18): 1.0,
   ...:         dt(2025, 9, 17): 1.0,
   ...:         dt(2025, 12, 17): 1.0,
   ...:     },
   ...:     calendar="nyc",
   ...:     convention="act360",
   ...:     id="stir",
   ...: )
   ...: 

Next we define the instruments and construct the risk framework Solver. These Instruments are the next 8 quarterly IMM 3M SOFR futures as of 2nd November 2023.

In [2]: instruments_stir = [
   ...:     STIRFuture(dt(2023, 12, 20), dt(2024, 3, 20), spec="sofr3mf", curves="stir"),
   ...:     STIRFuture(dt(2024, 3, 20), dt(2024, 6, 19), spec="sofr3mf", curves="stir"),
   ...:     STIRFuture(dt(2024, 6, 19), dt(2024, 9, 18), spec="sofr3mf", curves="stir"),
   ...:     STIRFuture(dt(2024, 9, 18), dt(2024, 12, 18), spec="sofr3mf", curves="stir"),
   ...:     STIRFuture(dt(2024, 12, 18), dt(2025, 3, 19), spec="sofr3mf", curves="stir"),
   ...:     STIRFuture(dt(2025, 3, 19), dt(2025, 6, 18), spec="sofr3mf", curves="stir"),
   ...:     STIRFuture(dt(2025, 6, 18), dt(2025, 9, 17), spec="sofr3mf", curves="stir"),
   ...:     STIRFuture(dt(2025, 9, 17), dt(2025, 12, 17), spec="sofr3mf", curves="stir"),
   ...: ]
   ...: 

In [3]: stir_solver = Solver(
   ...:     curves=[curve_stir],
   ...:     instruments=instruments_stir,
   ...:     s=[4.0, 3.75, 3.5, 3.25, 3.15, 3.10, 3.08, 3.07],
   ...:     instrument_labels=["z23", "h24", "m24", "u24", "z24", "h25", "m25", "u25"],
   ...:     id="STIRF"
   ...: )
   ...: 
SUCCESS: `func_tol` reached after 4 iterations (levenberg_marquardt), `f_val`: 8.135658413833101e-17, `time`: 0.0157s

This Solver calculates risk sensitivities against these SOFR future rates. It can be used to risk SOFR futures directly or risk IRS that have been mapped specifically to use the “stir” curve. This is not entirely accurate because IRS should be priced from a convexity adjusted IRS curve.

Consider below creating a long STIR Future position in 1000 lots (at $25 per lot per BP) and analysing the delta risk sensitivity.

In [4]: stirf = STIRFuture(dt(2024, 9, 18), dt(2024, 12, 18), spec="sofr3mf", curves="stir", contracts=1000)

In [5]: stirf.delta(solver=stir_solver)
Out[5]: 
local_ccy                      usd
display_ccy                    usd
type        solver label          
instruments STIRF  z23       -0.00
                   h24       -0.00
                   m24       -0.00
                   u24   -25000.00
                   z24       -0.00
                   h25       -0.00
                   m25       -0.00
                   u25       -0.00

Next consider paying an IRS as measured over the same dates in an equivalent contract notional of 1bn USD.

In [6]: irs = IRS(dt(2024, 9, 18), dt(2024, 12, 18), spec="usd_irs", curves="irs", notional=1e9)

In [7]: irs.delta(curves="stir", solver=stir_solver)
Out[7]: 
local_ccy                     usd
display_ccy                   usd
type        solver label         
instruments STIRF  z23       0.00
                   h24       0.00
                   m24       0.00
                   u24   24238.76
                   z24       0.00
                   h25       0.00
                   m25       0.00
                   u25       0.00

Adding convexity adjustments#

Now that we have a Curve which defines STIR Future prices we can use a Spread to relate these prices to IRS prices and the IRS Curve (technically this Curve does not have to have the same structure as the “stir” Curve but for for purposes of example it inherits it for simplicity’s sake).

In [8]: curve_irs = Curve(
   ...:     nodes={
   ...:         dt(2023, 11, 2): 1.0,
   ...:         dt(2024, 3, 20): 1.0,
   ...:         dt(2024, 6, 19): 1.0,
   ...:         dt(2024, 9, 18): 1.0,
   ...:         dt(2024, 12, 18): 1.0,
   ...:         dt(2025, 3, 19): 1.0,
   ...:         dt(2025, 6, 18): 1.0,
   ...:         dt(2025, 9, 17): 1.0,
   ...:         dt(2025, 12, 17): 1.0,
   ...:     },
   ...:     calendar="nyc",
   ...:     convention="act360",
   ...:     id="irs",
   ...: )
   ...: 

The Instruments are set to be Spreads between the original STIR Futures and an IRS (or potentially an FRA) measured over the same dates.

In [9]: instruments_irs = [
   ...:     Spread(instruments_stir[0], IRS(dt(2023, 12, 20), dt(2024, 3, 20), spec="usd_irs", curves="irs")),
   ...:     Spread(instruments_stir[1], IRS(dt(2024, 3, 20), dt(2024, 6, 19), spec="usd_irs", curves="irs")),
   ...:     Spread(instruments_stir[2], IRS(dt(2024, 6, 19), dt(2024, 9, 18), spec="usd_irs", curves="irs")),
   ...:     Spread(instruments_stir[3], IRS(dt(2024, 9, 18), dt(2024, 12, 18), spec="usd_irs", curves="irs")),
   ...:     Spread(instruments_stir[4], IRS(dt(2024, 12, 18), dt(2025, 3, 19), spec="usd_irs", curves="irs")),
   ...:     Spread(instruments_stir[5], IRS(dt(2025, 3, 19), dt(2025, 6, 18), spec="usd_irs", curves="irs")),
   ...:     Spread(instruments_stir[6], IRS(dt(2025, 6, 18), dt(2025, 9, 17), spec="usd_irs", curves="irs")),
   ...:     Spread(instruments_stir[7], IRS(dt(2025, 9, 17), dt(2025, 12, 17), spec="usd_irs", curves="irs")),
   ...: ]
   ...: 

Finally, we add these into a new dependent Solver (we do not have to create a dependency chain of Solvers we could do this all simultaneously in a single Solver, but it is better elucidated this way). The convexity adjustment rates are shown here beside the s argument. Expressed negatively according to market convention (IRS curve is below the STIR futures curve).

In [10]: full_solver = Solver(
   ....:     pre_solvers=[stir_solver],
   ....:     curves=[curve_irs],
   ....:     instruments=instruments_irs,
   ....:     s=[-0.07, -0.25, -0.5, -0.95, -1.4, -1.8, -2.2, -2.6],
   ....:     instrument_labels=["z23", "h24", "m24", "u24", "z24", "h25", "m25", "u25"],
   ....:     id="Convexity",
   ....: )
   ....: 
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 2.2335740210665174e-13, `time`: 0.0223s

Now we can re-risk the original instruments as part of the extended risk framework.

The STIRFuture does not have any convexity risk. Its risk is expressed relative to other STIRFutures so hedging a STIRFuture with the same STIRFuture is an exact hedge.

In [11]: stirf.delta(solver=full_solver)
Out[11]: 
local_ccy                         usd
display_ccy                       usd
type        solver    label          
instruments STIRF     z23       -0.00
                      h24       -0.00
                      m24       -0.00
                      u24   -25000.00
                      z24       -0.00
                      h25       -0.00
                      m25       -0.00
                      u25       -0.00
            Convexity z23        0.00
                      h24        0.00
                      m24        0.00
                      u24        0.00
                      z24        0.00
                      h25        0.00
                      m25        0.00
                      u25        0.00

The IRS has convexity risk. Hedging an IRS with a STIRFuture constructs a portfolio that has exposure to the movement of the convexity adjustment.

In [12]: irs.delta(solver=full_solver)
Out[12]: 
local_ccy                        usd
display_ccy                      usd
type        solver    label         
instruments STIRF     z23      -0.00
                      h24       0.00
                      m24       0.00
                      u24   24239.88
                      z24       0.00
                      h25      -0.00
                      m25       0.00
                      u25       0.00
            Convexity z23       0.00
                      h24       0.00
                      m24       0.00
                      u24   24239.88
                      z24       0.00
                      h25      -0.00
                      m25      -0.00
                      u25       0.00

We can even combine the instruments into a single Portfolio and observe the combined risk analytics.

In [13]: pf = Portfolio([stirf, irs])

In [14]: pf.delta(solver=full_solver)
Out[14]: 
local_ccy                        usd
display_ccy                      usd
type        solver    label         
instruments STIRF     z23      -0.00
                      h24      -0.00
                      m24       0.00
                      u24    -760.12
                      z24       0.00
                      h25      -0.00
                      m25      -0.00
                      u25       0.00
            Convexity z23       0.00
                      h24       0.00
                      m24       0.00
                      u24   24239.88
                      z24       0.00
                      h25      -0.00
                      m25      -0.00
                      u25       0.00

Sense checking the numbers#

Futures are generally oversold relative to swaps. The STIR Curve is higher than the IRS Curve.

The Portfolio constructed has bought STIR Futures and paid IRS, at a negative spread and thus has positive value as time passes (positive theta). The precise notional of the IRS should be larger if it were to precisely hedge the delta risk of the 1000 lots of the STIR Future. If the market moves and the convexity adjustments move higher (closer towards zero), this portfolio will make MTM gains.

To account for the gain over time (theta value) the Portfolio suffers from negative gamma. If volatility is less than expected over time this will be advantageous. If the volatility is higher and the market movement is significant the loss from gamma will be significant and outweight the value offered by the convexity adjustment.

In [15]: pf.gamma(solver=full_solver)
Out[15]: 
type                                              instruments                                                                                         
solver                                                  STIRF                                         Convexity                                       
label                                                     z23   h24   m24   u24   z24  h25  m25   u25       z23   h24   m24   u24   z24  h25  m25  u25
local_ccy display_ccy type        solver    label                                                                                                     
usd       usd         instruments STIRF     z23          0.00  0.00  0.00 -0.93 -0.00 0.00 0.00 -0.00      0.00  0.00  0.00 -0.93  0.00 0.00 0.00 0.00
                                            h24          0.00  0.00  0.00 -0.61 -0.00 0.00 0.00 -0.00      0.00  0.00  0.00 -0.61  0.00 0.00 0.00 0.00
                                            m24          0.00  0.00  0.00 -0.60 -0.00 0.00 0.00 -0.00      0.00  0.00  0.00 -0.60  0.00 0.00 0.00 0.00
                                            u24         -0.93 -0.61 -0.60 -1.22 -0.01 0.00 0.00 -0.00     -0.93 -0.61 -0.60 -1.22 -0.01 0.00 0.00 0.00
                                            z24         -0.00 -0.00 -0.00 -0.01  0.00 0.00 0.00  0.00     -0.00 -0.00 -0.00 -0.01  0.00 0.00 0.00 0.00
                                            h25          0.00  0.00  0.00  0.00  0.00 0.00 0.00  0.00      0.00  0.00  0.00  0.00  0.00 0.00 0.00 0.00
                                            m25          0.00  0.00  0.00  0.00  0.00 0.00 0.00  0.00      0.00  0.00  0.00  0.00  0.00 0.00 0.00 0.00
                                            u25         -0.00 -0.00 -0.00 -0.00  0.00 0.00 0.00  0.00     -0.00 -0.00 -0.00 -0.00  0.00 0.00 0.00 0.00
                                  Convexity z23          0.00  0.00  0.00 -0.93 -0.00 0.00 0.00 -0.00      0.00  0.00  0.00 -0.93  0.00 0.00 0.00 0.00
                                            h24          0.00  0.00  0.00 -0.61 -0.00 0.00 0.00 -0.00      0.00  0.00  0.00 -0.61  0.00 0.00 0.00 0.00
                                            m24          0.00  0.00  0.00 -0.60 -0.00 0.00 0.00 -0.00      0.00  0.00  0.00 -0.60  0.00 0.00 0.00 0.00
                                            u24         -0.93 -0.61 -0.60 -1.22 -0.01 0.00 0.00 -0.00     -0.93 -0.61 -0.60 -1.22 -0.01 0.00 0.00 0.00
                                            z24          0.00  0.00  0.00 -0.01  0.00 0.00 0.00  0.00      0.00  0.00  0.00 -0.01  0.00 0.00 0.00 0.00
                                            h25          0.00  0.00  0.00  0.00  0.00 0.00 0.00  0.00      0.00  0.00  0.00  0.00  0.00 0.00 0.00 0.00
                                            m25          0.00  0.00  0.00  0.00  0.00 0.00 0.00  0.00      0.00  0.00  0.00  0.00  0.00 0.00 0.00 0.00
                                            u25          0.00  0.00  0.00  0.00  0.00 0.00 0.00  0.00      0.00  0.00  0.00  0.00  0.00 0.00 0.00 0.00