Skip to content
All library documents

Bucketed Swap Delta from Zero Rates with Separate Curves

Article Quant Q&A · Author: arthurspooner

Summary

The document explains how to calculate an interest rate swap’s bucketed delta in rateslib when the available market inputs are zero rates rather than quoted calibration swaps. It treats zero-rate observations as pseudo-instruments that calibrate the curve and define the risk buckets. The solver’s calibration also supplies the Jacobian needed to translate curve changes into instrument-level sensitivities.

The example constructs a curve from discount-factor nodes, calibrates it to continuously compounded zero rates, then calculates swap delta. To separate forecasting and discounting risk, it builds distinct curves and calibrates each to its own rate observations; it also illustrates extracting risk by swap leg. The reported outputs demonstrate tenor-level contributions across curves. The author cautions that analytic PV01 formulas may not match directly because their forward-rate periods differ from the par-tenor buckets. The example is specific to the library’s stated setup and conventions.

Key ideas

  • Bucketed delta uses calibration instruments to define risk buckets and provide the required curve Jacobian.
  • Zero-rate observations can be represented by pseudo-instruments when calibrating a curve.
  • Separate forecasting and discounting curves allow their swap risk contributions to be measured independently.
  • Solver delta can be applied to an instrument or to leg-level present values.
  • Analytic PV01 comparisons require matching tenor and forward-period conventions.

Tags

Full text
# How do you simply get delta in rateslib?


# How do you simply get delta in rateslib?












I'm simply trying to create an IRS and get its delta, based on two curves (a discounting curve and a projection curve).

I have pasted the code from https://rateslib.com/py/en/2.0.x/j_delta.html# below, but I'm not quite sure what it's doing.

It seems to be calibrating a curve to swap rates? In my case, I have the actual zero-coupon rates, so I believe I need to use a LineCurve object instead and simply give it the rates?

Secondarily, it then feeds the same Solver it used to calibrate the curves to the call to the .delta() method and apparently computes a delta to the calibrating instruments, but, again, I have no calibrating instruments, I simply have a curve to which I want the delta (i.e. dv01, what happens if rates move up by 1bps, and i would like to see the contributions from the 2 different curves across different tenors).

```
usd_curve = Curve(
    nodes={
        dt(2022, 1, 1): 1.0,
        dt(2022, 2, 1): 1.0,
        dt(2022, 4, 1): 1.0,
        dt(2023, 1, 1): 1.0,
     },
    id="sofr",
)

instruments = [
    IRS(dt(2022, 1, 1), "1m", "A", curves="sofr"),
    IRS(dt(2022, 1, 1), "3m", "A", curves="sofr"),
    IRS(dt(2022, 1, 1), "1y", "A", curves="sofr"),
]

usd_solver = Solver(
    curves=[usd_curve],
    id="usd_sofr",
    instruments=instruments,
    s=[2.5, 3.25, 4.0],
    instrument_labels=["1m", "3m", "1y"],
)

SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 2.479770306840636e-13, `time`: 0.0036s

irs = IRS(
    effective=dt(2022, 1, 1),
    termination="6m",
    frequency="A",
    currency="usd",
)

irs.curves = "sofr"

irs.delta(solver=usd_solver)
```

## Answer by Attack68 (score 2, accepted)

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

There are a number of things going on here;

- You have some continuously compounded zero rates and you want to construct a Curve from that. This tutorial page specifically discusses that.

- You are trying to calculate bucketed delta. Bucketed delta requires calibrating instruments for two reasons; first those instruments define the buckets you want to measure risk to, and secondly the calibration process determines the gradients (i.e. Jacobian) that is needed for risk calculations.

- You claim that you do not have calibrating instruments, but you do: essentially they are zero coupon rate pseudo instruments.

Putting all of the above together you get the delta to a swap as follows:

```
from rateslib import *   #  <- rateslib 2.5.1, python 3.12

# DEFINE YOUR CURVE
curve = Curve(
    interpolation="log_linear",
    convention="act365f",
    calendar="nyc",
    id="v",
    nodes={
        dt(2026, 1, 19): 1.0,
        dt(2027, 1, 19): 1.0,
        dt(2028, 1, 19): 1.0,
    }
)
# CALIBRATE CURVE TO YOUR ZERO RATES
solver = Solver(
    curves=[curve],
    s=[1.98, 2.25],  #  <-  Zero rates used
    instruments=[
        Value(dt(2027, 1, 19), curves="v", metric="cc_zero_rate"),
        Value(dt(2028, 1, 19), curves="v", metric="cc_zero_rate"),
    ]
)
```

Now create an IRS and get some delta:

```
irs = IRS(dt(2026, 3, 6), "1y", spec="usd_irs", curves="v")
df = irs.delta(solver=solver)
# local_ccy                       usd
# display_ccy                     usd
# type        solver label           
# instruments 7524e_ 1y     75.077054
#                    2y     26.595562
```

The `Solver` contains the curve mappings so it knows that `curve` is labelled with `id='v'` and it can use this on the `irs`.

Two Curves

Now you want to measure risk as forecasting and discounting separately. There are a number of ways to do this. The easiest is to just create the same Curve two times with identifiers to allow calculations to be separate.

```
# DEFINE ADDITIONAL FORECASTING CURVE
curve_fore = Curve(
    interpolation="log_linear",
    convention="act360",
    calendar="nyc",
    id="f",
    nodes={
        dt(2026, 1, 19): 1.0,
        dt(2027, 1, 19): 1.0,
        dt(2028, 1, 19): 1.0,
    }
)
# CALIBRATE TO YOUR ZERO RATES
solver = Solver(
    curves=[curve, curve_fore],
    s=[1.98, 2.25] * 2,  #  <-  Zero rates used, same for both curves
    instruments=[
        Value(dt(2027, 1, 19), curves="v", metric="cc_zero_rate"),
        Value(dt(2028, 1, 19), curves="v", metric="cc_zero_rate"),
        Value(dt(2027, 1, 19), curves="f", metric="cc_zero_rate"),
        Value(dt(2028, 1, 19), curves="f", metric="cc_zero_rate"),
    ],
    instrument_labels=["1y_disc", "2y_disc", "1y_fore", "2y_fore"]
)

# Give the IRS an off market fixed rate to create some discounting risk
irs = IRS(dt(2026, 3, 6), "1y", spec="usd_irs", curves=["f", "v"], fixed_rate=4.00)
df = irs.delta(solver=solver)
# local_ccy                         usd
# display_ccy                       usd
# type        solver label             
# instruments 32484_ 1y_disc   1.679549
#                    2y_disc   0.533190
#                    1y_fore  75.077054
#                    2y_fore  26.595562
```

Legs

You can pass an NPV to the `Solver` for it to determine `delta` provided that that NPV contains derivative information, i.e. it is a Dual type. Therefore you can also obtain delta information for each Leg.

```
df1 = solver.delta(npv={"usd": irs.leg1.npv(rate_curve=curve_fore, disc_curve=curve)})
# local_ccy                        usd
# display_ccy                      usd
# type        solver label            
# instruments f113f_ 1y_disc  3.484922
#                    2y_disc  1.106325
#                    1y_fore  0.000000
#                    2y_fore  0.000000
df2 = solver.delta(npv={"usd": irs.leg2.npv(rate_curve=curve_fore, disc_curve=curve)})
# local_ccy                         usd
# display_ccy                       usd
# type        solver label             
# instruments f113f_ 1y_disc  -1.805374
#                    2y_disc  -0.573134
#                    1y_fore  75.077054
#                    2y_fore  26.595562
```

Compare these values with the analytic formulae I provided in interest rate swap: PV01 vs DV01. I'm interested to see how close they are: although actually the formulae given are for contigous forward rate periods and not par tenor periods and so they are not immediately directly comparable.

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.