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.
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