Building a Conventional Par Tenor Based SOFR Curve#

Many platforms and providers offer straight forward ways of constructing IR curves from market data. Without introducing features such as meeting dates or turns we can replicate some of those simpler models here. See the image below which contains the market data we will use. This consists of purely par tenor IRSs.

SOFR Curve

We can replicate the input data for the Curve in a table as follows:

In [1]: data = DataFrame({
   ...:     "Term": ["1W", "2W", "3W", "1M", "2M", "3M", "4M", "5M", "6M", "7M", "8M", "9M", "10M", "11M", "12M", "18M", "2Y", "3Y", "4Y", "5Y", "6Y", "7Y", "8Y", "9Y", "10Y", "12Y", "15Y", "20Y", "25Y", "30Y", "40Y"],
   ...:     "Rate": [5.309,5.312,5.314,5.318,5.351,5.382,5.410,5.435,5.452,5.467,5.471,5.470,5.467,5.457,5.445,5.208,4.990,4.650,4.458,4.352,4.291,4.250,4.224,4.210,4.201,4.198,4.199,4.153,4.047,3.941,3.719],
   ...: })
   ...: 

In [2]: data["Termination"] = [add_tenor(dt(2023, 9, 29), _, "F", "nyc") for _ in data["Term"]]

In [3]: with option_context("display.float_format", lambda x: '%.6f' % x):
   ...:     print(data)
   ...: 
   Term     Rate Termination
0    1W 5.309000  2023-10-06
1    2W 5.312000  2023-10-13
2    3W 5.314000  2023-10-20
3    1M 5.318000  2023-10-30
4    2M 5.351000  2023-11-29
5    3M 5.382000  2023-12-29
6    4M 5.410000  2024-01-29
7    5M 5.435000  2024-02-29
8    6M 5.452000  2024-04-01
9    7M 5.467000  2024-04-29
10   8M 5.471000  2024-05-29
11   9M 5.470000  2024-07-01
12  10M 5.467000  2024-07-29
13  11M 5.457000  2024-08-29
14  12M 5.445000  2024-09-30
15  18M 5.208000  2025-03-31
16   2Y 4.990000  2025-09-29
17   3Y 4.650000  2026-09-29
18   4Y 4.458000  2027-09-29
19   5Y 4.352000  2028-09-29
20   6Y 4.291000  2029-10-01
21   7Y 4.250000  2030-09-30
22   8Y 4.224000  2031-09-29
23   9Y 4.210000  2032-09-29
24  10Y 4.201000  2033-09-29
25  12Y 4.198000  2035-10-01
26  15Y 4.199000  2038-09-29
27  20Y 4.153000  2043-09-29
28  25Y 4.047000  2048-09-29
29  30Y 3.941000  2053-09-29
30  40Y 3.719000  2063-10-01

We will configure DF nodes dates to be on the termination date of the swaps:

In [4]: sofr = Curve(
   ...:     id="sofr",
   ...:     convention="Act360",
   ...:     calendar="nyc",
   ...:     modifier="MF",
   ...:     interpolation="log_linear",
   ...:     nodes={
   ...:         **{dt(2023, 9, 27): 1.0},  # <- this is today's DF,
   ...:         **{_: 1.0 for _ in data["Termination"]},
   ...:     }
   ...: )
   ...: 

Now we will calibrate the curve to the given swap market prices, using a global Solver, passing in the calibrating instruments and rates.

In [5]: solver = Solver(
   ...:     curves=[sofr],
   ...:     instruments=[IRS(dt(2023, 9, 29), _, spec="usd_irs", curves="sofr") for _ in data["Termination"]],
   ...:     s=data["Rate"],
   ...:     instrument_labels=data["Term"],
   ...:     id="us_rates",
   ...: )
   ...: 
SUCCESS: `func_tol` reached after 6 iterations (levenberg_marquardt), `f_val`: 9.762925922627622e-12, `time`: 0.1804s

In [6]: data["DF"] = [float(sofr[_]) for _ in data["Termination"]]

In [7]: with option_context("display.float_format", lambda x: '%.6f' % x):
   ...:     print(data)
   ...: 
   Term     Rate Termination       DF
0    1W 5.309000  2023-10-06 0.998674
1    2W 5.312000  2023-10-13 0.997644
2    3W 5.314000  2023-10-20 0.996616
3    1M 5.318000  2023-10-30 0.995148
4    2M 5.351000  2023-11-29 0.990722
5    3M 5.382000  2023-12-29 0.986287
6    4M 5.410000  2024-01-29 0.981707
7    5M 5.435000  2024-02-29 0.977135
8    6M 5.452000  2024-04-01 0.972460
9    7M 5.467000  2024-04-29 0.968382
10   8M 5.471000  2024-05-29 0.964102
11   9M 5.470000  2024-07-01 0.959468
12  10M 5.467000  2024-07-29 0.955590
13  11M 5.457000  2024-08-29 0.951393
14  12M 5.445000  2024-09-30 0.947131
15  18M 5.208000  2025-03-31 0.924982
16   2Y 4.990000  2025-09-29 0.905822
17   3Y 4.650000  2026-09-29 0.871147
18   4Y 4.458000  2027-09-29 0.838553
19   5Y 4.352000  2028-09-29 0.806694
20   6Y 4.291000  2029-10-01 0.775460
21   7Y 4.250000  2030-09-30 0.745580
22   8Y 4.224000  2031-09-29 0.716532
23   9Y 4.210000  2032-09-29 0.688022
24  10Y 4.201000  2033-09-29 0.660553
25  12Y 4.198000  2035-10-01 0.607796
26  15Y 4.199000  2038-09-29 0.536326
27  20Y 4.153000  2043-09-29 0.440813
28  25Y 4.047000  2048-09-29 0.373075
29  30Y 3.941000  2053-09-29 0.321250
30  40Y 3.719000  2063-10-01 0.252888

Next we will create an IRS and generate the metrics for npv, delta (DV01), gamma and analytic delta (PV01).

In [8]: irs = IRS(
   ...:     effective=dt(2023, 11, 21),
   ...:     termination=dt(2025, 2, 21),
   ...:     notional=-100e6,
   ...:     fixed_rate=5.40,
   ...:     curves="sofr",
   ...:     spec="usd_irs",
   ...: )
   ...: 

In [9]: irs.npv(solver=solver)
Out[9]: <Dual: 219549.730569, (sofr0, sofr1, sofr2, ...), [0.0, 0.0, 0.0, ...]>

In [10]: irs.delta(solver=solver).sum()
Out[10]: 
local_ccy  display_ccy
usd        usd           -11904.15
dtype: float64

In [11]: irs.gamma(solver=solver).sum().sum()
Out[11]: 2.9082212288716685

In [12]: irs.analytic_delta(curve=sofr)
Out[12]: <Dual: -11945.028320, (sofr0, sofr1, sofr2, ...), [-0.0, -0.0, -0.0, ...]>

Finally we can double check the cashflows and cashflows_table of the swap.

In [13]: irs.cashflows_table(solver=solver)
Out[13]: 
local_ccy            USD
collateral_ccy       NaN
payment                 
2024-02-23      -8936.52
2025-02-25     245715.92

In [14]: irs.cashflows(solver=solver)
Out[14]: 
               Type   Period  Ccy  Acc Start    Acc End    Payment Convention  DCF      Notional   DF Collateral  Rate  Spread    Cashflow         NPV  FX Rate     NPV Ccy
leg1 0  FixedPeriod     Stub  USD 2023-11-21 2024-02-21 2024-02-23     act360 0.26 -100000000.00 0.98       None  5.40     NaN  1380000.00  1349664.73     1.00  1349664.73
     1  FixedPeriod  Regular  USD 2024-02-21 2025-02-21 2025-02-25     act360 1.02 -100000000.00 0.93       None  5.40     NaN  5490000.00  5100650.57     1.00  5100650.57
leg2 0  FloatPeriod     Stub  USD 2023-11-21 2024-02-21 2024-02-23     act360 0.26  100000000.00 0.98       None  5.43    0.00 -1388936.52 -1358404.81     1.00 -1358404.81
     1  FloatPeriod  Regular  USD 2024-02-21 2025-02-21 2025-02-25     act360 1.02  100000000.00 0.93       None  5.16    0.00 -5244284.08 -4872360.76     1.00 -4872360.76