MultiCsaCurves have discontinuous derivatives#

This documentation page is written to exemplify the discontinuous nature of the MultiCsaCurve. Even though the AD of rateslib still functions, the definition of an intrinsic MultiCsaCurve forces discontinuity.

To set the stage, consider the absolute value function, \(abs(x)\). This function has a continuous and calculable derivative at every point except zero.

In [1]: x_plus_1 = Dual(1.0, ["x"], [])

In [2]: abs(x_plus_1)
Out[2]: <Dual: 1.000000, (x), [1.0]>

In [3]: gradient(abs(x_plus_1))
Out[3]: array([1.])

In [4]: x_minus_1 = Dual(-1.0, ["x"], [])

In [5]: abs(x_minus_1)
Out[5]: <Dual: 1.000000, (x), [-1.0]>

In [6]: gradient(abs(x_minus_1))
Out[6]: array([-1.])

At the point zero rateslib still returns a result (which contains a true derivative from one side, but a false derivative from the other side). Users are expected to know that automatic differentiation, calculated in this way, does not work. The abs function is not AD safe over its complete domain.

In [7]: x_zero = Dual(0.0, ["x"], [])

In [8]: abs(x_zero)
Out[8]: <Dual: -0.000000, (x), [-1.0]>

In [9]: gradient(abs(x_zero))
Out[9]: array([-1.])

An intrinsic MultiCsaCurve has the same properties. It has real, calculable derivatives everywhere, except when there is a crossover point from one CTD currency to another, this is a point without a valid derivative.

Build a MultiCsaCurve with equal collateral currencies#

This section will build a MultiCsaCurve, for which the CTD currency is not distinct. Either of the two collateral currencies have exactly the same ‘cheapness’. Either is valid as the CTD.

In [10]: eur = Curve({dt(2000, 1, 1): 1.0, dt(2005, 1, 1): 1.0, dt(2010, 1, 1): 1.0})

In [11]: eurusd = Curve({dt(2000, 1, 1): 1.0, dt(2005, 1, 1): 1.0, dt(2010, 1, 1): 1.0})

In [12]: usd = Curve({dt(2000, 1, 1): 1.0, dt(2005, 1, 1): 1.0, dt(2010, 1, 1): 1.0})

In [13]: fxf = FXForwards(
   ....:     fx_rates=FXRates({"eurusd": 1.1}, settlement=dt(2000, 1, 1)),
   ....:     fx_curves={"eureur": eur, "eurusd": eurusd, "usdusd": usd},
   ....: )
   ....: 

In [14]: solver = Solver(
   ....:     curves=[eur, eurusd, usd],
   ....:     instruments=[
   ....:         IRS(dt(2000, 1, 1), "5y", spec="eur_irs", curves=eur),
   ....:         IRS(dt(2000, 1, 1), "10y", spec="eur_irs", curves=eur),
   ....:         IRS(dt(2000, 1, 1), "5y", spec="usd_irs", curves=usd),
   ....:         IRS(dt(2000, 1, 1), "10y", spec="usd_irs", curves=usd),
   ....:         XCS(dt(2000, 1, 1), "5y", spec="eurusd_xcs", curves=[eur, eurusd, usd, usd]),
   ....:         XCS(dt(2000, 1, 1), "10y", spec="eurusd_xcs", curves=[eur, eurusd, usd, usd]),
   ....:     ],
   ....:     s=[1.0, 1.5, 1.0, 1.5, 0.0, 0.0],  # <-- local ccy rates are same and no xccy basis
   ....:     instrument_labels=["5yEur", "10yEur", "5yUsd", "10yUsd", "5yXcy", "10yXcy"],
   ....:     fx=fxf,
   ....: )
   ....: 
SUCCESS: `func_tol` reached after 6 iterations (levenberg_marquardt), `f_val`: 1.9706528790705603e-13, `time`: 0.1488s

With the market setup, create the intrinsic MultiCsaCurve. This curve discounts EUR cashflows with the cheapest to deliver of EUR and USD collateral.

In [15]: multi_csa = fxf.curve(cashflow="eur", collateral=("eur", "usd"))

In [16]: type(multi_csa)
Out[16]: rateslib.curves.curves.MultiCsaCurve

What happens to risk and NPV when the market moves?#

Setup the base case for comparison. Below, an IRS is created and its NPV and risk sensitivities to the above calibrating instruments are stated.

In [17]: irs =  IRS(dt(2000, 1, 1), "10y", spec="eur_irs", curves=[eur, multi_csa], fixed_rate=2.0)

In [18]: irs.npv(solver=solver)
Out[18]: <Dual: -47355.037429, (7aed20, 7aed21, 7aed22, ...), [946208.0, -69869.0, -1078055.6, ...]>

In [19]: irs.delta(solver=solver)
Out[19]: 
local_ccy                    eur
display_ccy                  eur
type        solver label        
instruments ca65b_ 5yEur   11.56
                   10yEur 961.08
                   5yUsd   -0.18
                   10yUsd   0.00
                   5yXcy   13.59
                   10yXcy  -0.03
fx          fx     eurusd  -0.00

Now we will make USD collateral more expensive to deliver by 20bps. EUR deliverance is unchanged.

In [20]: solver.s = [1.0, 1.5, 1.0, 1.5, -20.0, -20.0]

In [21]: solver.iterate()
SUCCESS: `func_tol` reached after 6 iterations (levenberg_marquardt), `f_val`: 9.5499366802011e-16, `time`: 0.1379s

And re-evaluate the risk metrics and NPV. The NPV is broadly unchanged.

In [22]: irs.npv(solver=solver)
Out[22]: <Dual: -47354.991891, (7aed20, 7aed21, 7aed22, ...), [973000.0, -98027.6, -1078055.6, ...]>

In [23]: irs.delta(solver=solver)
Out[23]: 
local_ccy                    eur
display_ccy                  eur
type        solver label        
instruments ca65b_ 5yEur   11.38
                   10yEur 961.08
                   5yUsd    0.00
                   10yUsd  -0.00
                   5yXcy    0.00
                   10yXcy   0.00
fx          fx     eurusd   0.00

Instead of making the USD collateral more expensive relative to EUR it could be made 20bps cheaper. The impacts for this are also shown.

In [24]: solver.s = [1.0, 1.5, 1.0, 1.5, 20.0, 20.0]

In [25]: solver.iterate()
SUCCESS: `func_tol` reached after 6 iterations (levenberg_marquardt), `f_val`: 2.726164774852884e-15, `time`: 0.1405s
In [26]: irs.npv(solver=solver)
Out[26]: <Dual: -47088.693056, (7aed20, 7aed21, 7aed22, ...), [939776.5, -77240.0, -1062111.7, ...]>

In [27]: irs.delta(solver=solver)
Out[27]: 
local_ccy                    eur
display_ccy                  eur
type        solver label        
instruments ca65b_ 5yEur   16.41
                   10yEur 945.70
                   5yUsd   -0.19
                   10yUsd   0.02
                   5yXcy   14.45
                   10yXcy  -1.19
fx          fx     eurusd   0.00

By analysing these results it is clear that risk sensitivities do not always explain the NPV changes given market movements of these instruments.