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.