Skip to content

Commit

Permalink
DOC: convexity cookbook (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
attack68 authored Nov 2, 2023
1 parent 12bc031 commit 1ab4242
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/source/g_cookbook.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ into any one category.
z_dependencychain.rst
z_turns.rst
z_stubs.rst
z_convexityrisk.rst
1 change: 1 addition & 0 deletions docs/source/i_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,4 @@ Cookbook
z_dependencychain.rst
z_turns.rst
z_stubs.rst
z_convexityrisk.rst
186 changes: 186 additions & 0 deletions docs/source/z_convexityrisk.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
.. _conv-risk-doc:

.. ipython:: python
:suppress:
from rateslib.curves import *
from rateslib.instruments import *
import matplotlib.pyplot as plt
from datetime import datetime as dt
import numpy as np
from pandas import DataFrame, option_context
Building a Risk Framework Including STIR Convexity Adjustments
****************************************************************

Common risk frameworks that I have experienced, used :class:`~rateslib.instruments.STIRFuture`
prices to construct IR *Curves* at the short end. However they typically did so by implying a
direct curve rate from the future's price, and then 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 a *complete* risk sensitivity framework, or
:class:`~rateslib.solver.Solver` - it requires thos extra steps. What this page shows is how to
create a *Solver* for the first 2-years of the
*Curve* including convexity instruments so that risk against :class:`~rateslib.instruments.IRS`
(and/or any other relevant *Instruments*) can be actively calculated.

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

.. ipython:: python
curve_stir = Curve(
nodes={
dt(2023, 11, 2): 1.0,
# dt(2023, 12, 20): 1.0, need only the maturity of each STIRF
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*.

.. ipython:: python
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"),
]
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"
)
This *Solver* calculates risk sensitivities against SOFR future rates. It can be used
to risk SOFR futures directly or risk :class:`~rateslib.instruments.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.

.. ipython:: python
stirf = STIRFuture(dt(2024, 9, 18), dt(2024, 12, 18), spec="sofr3mf", curves="stir", contracts=1000)
stirf.delta(solver=stir_solver)
.. ipython:: python
irs = IRS(dt(2024, 9, 18), dt(2024, 12, 18), spec="usd_irs", curves="irs", notional=1e9)
irs.delta(curves="stir", solver=stir_solver)
Adding convexity adjustments
------------------------------

Now that we have a *Curve* which defines *STIR Future* prices we can use a
:class:`~rateslib.instruments.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).

.. ipython:: python
curve_irs = Curve(
nodes={
dt(2023, 11, 2): 1.0,
# dt(2023, 12, 20): 1.0, need only the maturty of each STIRF
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.

.. ipython:: python
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).

.. ipython:: python
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",
)
Now we can re-risk the original instruments as part of the extended risk framework.

.. ipython:: python
stirf.delta(solver=full_solver)
.. ipython:: python
irs.delta(solver=full_solver)
We can even combine the instruments into a single :class:`~rateslib.instruments.Portfolio`
and observe the combined risk analytics.

.. ipython:: python
pf = Portfolio([stirf, irs])
pf.delta(solver=full_solver)
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.

.. ipython:: python
pf.gamma(solver=full_solver)

0 comments on commit 1ab4242

Please sign in to comment.