Solving Curves with a Dependency Chain#

In real trading environments market-makers and brokers are typically responsible for creating and maintaining their own curves. Normally different teams are responsible for the pricing of different instruments. Some instruments and curves are only dependent upon their local environment, for which a team will have complete visibility. Other instruments might be dependent upon pre-requisite markets to price instruments exactly.

A good example is local currency interest rate curves defined by IRS prices, and cross-currency curves defined by XCS prices but dependent upon FX rates and local currency IRS curves.

Below, for demonstration purposes we present a minimalist example of a dependency chain constructing EUR and USD interest curves independently and then using them to price a cross-currency model.

Curves in dependency chain

The EUR Curve#

The EUR curve is configured and constructed by the EUR IRS team. That team can determine that model in any way they choose.

In the below configuration they have a log linear curve with nodes on 1st May and 1st Jan next year, and these are calibrated exactly with 4M and 1Y swaps whose values are 2% and 2.5%.

In [1]: eureur = Curve(
   ...:     nodes={
   ...:         dt(2022, 1, 1): 1.0,
   ...:         dt(2022, 5, 1): 1.0,
   ...:         dt(2023, 1, 1): 1.0,
   ...:     },
   ...:     convention="act360",
   ...:     calendar="tgt",
   ...:     interpolation="log_linear",
   ...:     id="eureur",
   ...: )
   ...: 

In [2]: eur_kws = dict(
   ...:     effective=dt(2022, 1, 3),
   ...:     spec="eur_irs",
   ...:     curves="eureur",
   ...: )
   ...: 

In [3]: eur_solver = Solver(
   ...:     curves=[eureur],
   ...:     instruments=[
   ...:         IRS(**eur_kws, termination="4M"),
   ...:         IRS(**eur_kws, termination="1Y"),
   ...:     ],
   ...:     s=[2.0, 2.5],
   ...:     id="eur"
   ...: )
   ...: 
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 1.1522476203957725e-18, `time`: 0.0021s

The USD Curve#

The same for the USD IRS team. Notice that this model is slightly different for purposes of example. They have a node at the 7M point and also a 7M swap.

In [4]: usdusd = Curve(
   ...:     nodes={
   ...:         dt(2022, 1, 1): 1.0,
   ...:         dt(2022, 8, 1): 1.0,
   ...:         dt(2023, 1, 1): 1.0,
   ...:     },
   ...:     convention="act360",
   ...:     calendar="nyc",
   ...:     interpolation="log_linear",
   ...:     id="usdusd",
   ...: )
   ...: 

In [5]: usd_kws = dict(
   ...:     effective=dt(2022, 1, 3),
   ...:     spec="usd_irs",
   ...:     curves="usdusd",
   ...: )
   ...: 

In [6]: usd_solver = Solver(
   ...:     curves=[usdusd],
   ...:     instruments=[
   ...:         IRS(**usd_kws, termination="7M"),
   ...:         IRS(**usd_kws, termination="1Y"),
   ...:     ],
   ...:     s=[4.0, 4.8],
   ...:     id="usd"
   ...: )
   ...: 
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 8.95311645138308e-13, `time`: 0.0024s

The XCS Curve#

The XCS team are then able to rely on these curves, trusting a construction from their colleagues. The configuration of the XCS curves is freely chosen by this team. In the configuration the the only linking arguments are the pre_solver argument and the string id curves references in the instrument initialisation. An FXForwards object is also created from all the constructed curves to price forward FX rates for the instruments.

In [7]: fxr = FXRates({"eurusd": 1.10}, settlement=dt(2022, 1, 3))

In [8]: eurusd = Curve(
   ...:     nodes={
   ...:         dt(2022, 1, 1): 1.0,
   ...:         dt(2022, 5, 1): 1.0,
   ...:         dt(2022, 9, 1): 1.0,
   ...:         dt(2023, 1, 1): 1.0,
   ...:     },
   ...:     convention="act360",
   ...:     calendar=None,
   ...:     interpolation="log_linear",
   ...:     id="eurusd",
   ...: )
   ...: 

In [9]: fxf = FXForwards(
   ...:     fx_rates=fxr,
   ...:     fx_curves={
   ...:         "usdusd": usdusd,
   ...:         "eureur": eureur,
   ...:         "eurusd": eurusd,
   ...:     }
   ...: )
   ...: 

In [10]: xcs_kws = dict(
   ....:     effective=dt(2022, 1, 3),
   ....:     spec="eurusd_xcs",
   ....:     curves=["eureur", "eurusd", "usdusd", "usdusd"]
   ....: )
   ....: 

In [11]: xcs_solver = Solver(
   ....:     pre_solvers=[eur_solver, usd_solver],
   ....:     fx=fxf,
   ....:     curves=[eurusd],
   ....:     instruments=[
   ....:         XCS(**xcs_kws, termination="4m"),
   ....:         XCS(**xcs_kws, termination="8m"),
   ....:         XCS(**xcs_kws, termination="1y"),
   ....:     ],
   ....:     s=[-5.0, -6.5, -11.0],
   ....:     id="eur/usd",
   ....: )
   ....: 
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 1.0572444443750555e-20, `time`: 0.0384s

Back to the EUR Team#

If the EUR team would now need to value and risk an arbitrary swap they are able to do that within their own local model.

In [12]: irs = IRS(**eur_kws, termination="9M", fixed_rate=1.15, notional=100e6)

In [13]: irs.npv(solver=eur_solver)
Out[13]: <Dual: 939767.076623, (eureur0, eureur1, eureur2), [98315131.2, -34950220.0, -64240777.6]>

In [14]: irs.delta(solver=eur_solver)
Out[14]: 
local_ccy                    eur
display_ccy                  eur
type        solver label        
instruments eur    eur0  1231.09
                   eur1  6115.57

Since their curves are used within the XCS framework this will give precisely the same result if it taken from their model.

In [15]: irs.npv(solver=xcs_solver)
Out[15]: <Dual: 939767.076623, (eureur0, eureur1, eureur2), [98315131.2, -34950220.0, -64240777.6]>

In [16]: irs.delta(solver=xcs_solver)
Out[16]: 
local_ccy                        eur
display_ccy                      eur
type        solver  label           
instruments eur     eur0     1231.09
                    eur1     6115.57
            usd     usd0        0.00
                    usd1        0.00
            eur/usd eur/usd0    0.00
                    eur/usd1    0.00
                    eur/usd2    0.00
fx          fx      eurusd      0.00

This framework can advance the cause of the EUR team if the swap is collateralised in another currency. For this, the XCS model is definitely required and can be referred to directly:

In [17]: irs.curves = ["eureur", "eurusd"]  # <- changing to a USD CSA for this swap.

In [18]: irs.npv(solver=xcs_solver)
Out[18]: <Dual: 940325.640976, (eureur0, eureur1, eureur2, ...), [98373566.3, -35314844.4, -64892849.8, ...]>

In [19]: irs.delta(solver=xcs_solver)
Out[19]: 
local_ccy                        eur
display_ccy                      eur
type        solver  label           
instruments eur     eur0     1231.82
                    eur1     6119.22
            usd     usd0       -0.02
                    usd1       -0.03
            eur/usd eur/usd0   -0.35
                    eur/usd1  -48.84
                    eur/usd2  -22.46
fx          fx      eurusd     -0.00

Thus the framework is completely consistent and customisable for all teams to use as required.