Solver#

class rateslib.solver.Solver(curves=(), instruments=(), s=[], weights=NoInput.blank, algorithm=NoInput.blank, fx=NoInput.blank, instrument_labels=NoInput.blank, id=NoInput.blank, pre_solvers=(), max_iter=100, func_tol=1e-11, conv_tol=1e-14, ini_lambda=NoInput.blank, callback=NoInput.blank)#

Bases: Gradients

A numerical solver to determine node values on multiple curves simultaneously.

Parameters:
  • curves (sequence) – Sequence of Curve objects where each curve has been individually configured for its node dates and interpolation structures, and has a unique id. Each Curve will be dynamically updated by the Solver.

  • instruments (sequence) – Sequence of calibrating instrument specifications that will be used by the solver to determine the solved curves. See notes.

  • s (sequence) – Sequence of objective rates that each solved calibrating instrument will solve to. Must have the same length and order as instruments.

  • weights (sequence, optional) – The weights that should be used within the objective function when determining the loss function associated with each calibrating instrument. Should be of same length as instruments. If not given defaults to all ones.

  • algorithm (str in {"levenberg_marquardt", "gauss_newton", "gradient_descent"}) – The optimisation algorithm to use when solving curves via iterate().

  • fx (FXForwards, optional) – The FXForwards object used in FX rate calculations for instruments.

  • instrument_labels (list of str, optional) – The names of the calibrating instruments which will be used in delta risk outputs.

  • id (str, optional) – The identifier used to denote the instance and attribute risk factors.

  • pre_solvers (list,) – A collection of Solver s that have already determined curves to which this instance has a dependency. Used for aggregation of risk sensitivities.

  • max_iter (int) – The maximum number of iterations to perform.

  • func_tol (float) – The tolerance to determine convergence if the objective function is lower than a specific value. Defaults to 1e-12.

  • conv_tol (float) – The tolerance to determine convergence if successive objective function values are similar. Defaults to 1e-17.

  • ini_lambda (3-tuple of float, optional) – Parameters to control the Levenberg-Marquardt algorithm, defined as the initial lambda value, the scaling factor for a successful iteration and the scaling factor for an unsuccessful iteration. Defaults to (1000, 0.25, 2).

  • callback (callable, optional) – Is called after each iteration. Used for debugging or optimisation.

Notes

Once initialised the Solver will numerically determine and set all of the relevant DF node values on each Curve simultaneously by calling iterate().

Each instrument provided to instruments must have its curves and metric preset at initialisation, and can then be used directly (as shown in some examples).

If the Curves and/or metric are not preset then the Solver instruments can be given as a tuple where the second and third items are a tuple and dict representing positional and keyword arguments passed to the instrument’s rate`() method. Usually using the keyword arguments is more explicit. An example is:

  • (FixedRateBond([args]), (), {“curves”: bond_curve, “metric”: “ytm”}),

Examples

See the documentation user guide here.

curves#
Type:

dict

instruments#
Type:

sequence

weights#
Type:

sequence

s#
Type:

sequence

algorithm#
Type:

str

fx#
Type:

FXForwards

id#
Type:

str

tol#
Type:

float

max_iter#
Type:

int

n#

The total number of curve variables to solve for.

Type:

int

m#

The total number of calibrating instruments provided to the Solver.

Type:

int

W#

A diagonal array constructed from weights.

Type:

2d array

variables#

List of variable name tags used in extracting derivatives automatically.

Type:

list[str]

instrument_labels#

List of calibrating instrument names for delta risk visualization.

Type:

list[str]

pre_solvers#
Type:

list

pre_variables#

List of variable name tags used in extracting derivatives automatically.

Type:

list[str]

pre_m#

The total number of calibrating instruments provided to the Solver including those in pre-solvers

Type:

int

pre_n#

The total number of curve variables solved for, including those in pre-solvers.

Type:

int

Attributes Summary

error

Return the error in calibrating instruments, including pre_solvers, scaled to the risk representation factor.

g

Objective function scalar value of the solver;

r

1d array of mid-market rates of each calibrating instrument with given curves, size (m,).

r_pre

v

1d array of curve node variables for each ordered curve, size (n,).

x

1d array of error in each calibrating instrument rate, of size (m,).

Methods Summary

delta(npv[, base, fx])

Calculate the delta risk sensitivity of an instrument's NPV to the calibrating instruments of the Solver, and to FX rates.

gamma(npv[, base, fx])

Calculate the cross-gamma risk sensitivity of an instrument's NPV to the calibrating instruments of the Solver.

iterate()

Solve the DF node values and update all the curves.

jacobian(solver)

Calculate the Jacobian with respect to another Solver's instruments.

market_movements(solver)

Determine market movements between the Solver's instrument rates and those rates priced from a second Solver.

Attributes Documentation

error#

Return the error in calibrating instruments, including pre_solvers, scaled to the risk representation factor.

Return type:

Series

g#

Objective function scalar value of the solver;

\[g = \mathbf{(r-S)^{T}W(r-S)}\]

Depends on self.x and self.weights.

r#

1d array of mid-market rates of each calibrating instrument with given curves, size (m,).

Depends on self.pre_curves and self.instruments.

r_pre#
v#

1d array of curve node variables for each ordered curve, size (n,).

Depends on self.curves.

x#

1d array of error in each calibrating instrument rate, of size (m,).

\[\mathbf{x} = \mathbf{r-S}\]

Depends on self.r and self.s.

Methods Documentation

delta(npv, base=NoInput.blank, fx=NoInput.blank)#

Calculate the delta risk sensitivity of an instrument’s NPV to the calibrating instruments of the Solver, and to FX rates.

Parameters:
  • npv (Dual,) – The NPV of the instrument or composition of instruments to risk.

  • base (str, optional) – The currency (3-digit code) to report risk metrics in. If not given will default to the local currency of the cashflows.

  • fx (FXRates, FXForwards, optional) – The FX object to use to convert risk metrics. If needed but not given will default to the fx object associated with the Solver. It is not recommended to use this argument with multi-currency instruments, see notes.

Return type:

DataFrame

Notes

Output Structure

Note

Instrument values are scaled to 1bp (1/10000th of a unit) when they are rate based. FX values are scaled to pips (1/10000th of an FX rate unit).

The output DataFrame has the following structure:

  • A 3-level index by ‘type’, ‘solver’, and ‘label’;

    • type is either ‘instruments’ or ‘fx’, and fx exposures are only calculated and displayed in some cases where genuine FX exposure arises.

    • solver lists the different solver id s to identify between different instruments in dependency chains from pre_solvers.

    • label lists the given instrument names in each solver using the instrument_labels.

  • A 2-level column header index by ‘local_ccy’ and ‘display_ccy’;

    • local_ccy displays the currency for which cashflows are payable, and therefore the local currency risk sensitivity amount.

    • display_ccy displays the currency which the local currency risk sensitivity has been converted to via an FX transformation.

Converting a delta from a local currency to another base currency also introduces FX risk to the NPV of the instrument, which is included in the output.

Best Practice

The fx option is provided to allow tactical and fast conversion of delta risks to Instruments. When constructing and pricing multi-currency instruments it is likely that the Solver used is associated with an FXForwards object to consistently produce FX forward rates within an aribitrage free framework. In that case it is more consistent to re-use those FX associations. If such an association exists and a direct fx object is supplied a warning may be emitted if they are not the same object.

gamma(npv, base=NoInput.blank, fx=NoInput.blank)#

Calculate the cross-gamma risk sensitivity of an instrument’s NPV to the calibrating instruments of the Solver.

Parameters:
  • npv (Dual2,) – The NPV of the instrument or composition of instruments to risk.

  • base (str, optional) – The currency (3-digit code) to report risk metrics in. If not given will default to the local currency of the cashflows.

  • fx (FXRates, FXForwards, optional) – The FX object to use to convert risk metrics. If needed but not given will default to the fx object associated with the Solver.

Return type:

DataFrame

Notes

Note

Instrument values are scaled to 1bp (1/10000th of a unit) when they are rate based.

FX values are scaled to pips (1/10000th of an FX unit).

The output DataFrame has the following structure:

  • A 5-level index by ‘local_ccy’, ‘display_ccy’, ‘type’, ‘solver’, and ‘label’;

    • local_ccy displays the currency for which cashflows are payable, and therefore the local currency risk sensitivity amount.

    • display_ccy displays the currency which the local currency risk sensitivity has been converted to via an FX transformation.

    • type is either ‘instruments’ or ‘fx’, and fx exposures are only calculated and displayed in some cases where genuine FX exposure arises.

    • solver lists the different solver id s to identify between different instruments in dependency chains from pre_solvers.

    • label lists the given instrument names in each solver using the instrument_labels.

  • A 3-level column header index using the last three levels of the above;

Converting a gamma/delta from a local currency to another base currency also introduces FX risk to the NPV of the instrument, which is included in the output.

Examples

This example replicates the analytical calculations demonstrated in Pricing and Trading Interest Rate Derivatives (2022), derived from first principles. The results are stated in the cross-gamma grid in figure 22.1.

In [1]: curve_r = Curve(
   ...:     nodes={
   ...:         dt(2022, 1, 1): 1.0,
   ...:         dt(2023, 1, 1): 0.99,
   ...:         dt(2024, 1, 1): 0.98,
   ...:         dt(2025, 1, 1): 0.97,
   ...:         dt(2026, 1, 1): 0.96,
   ...:         dt(2027, 1, 1): 0.95,
   ...:     },
   ...:     id="r"
   ...: )
   ...: 

In [2]: curve_z = Curve(
   ...:     nodes={
   ...:         dt(2022, 1, 1): 1.0,
   ...:         dt(2023, 1, 1): 0.99,
   ...:         dt(2024, 1, 1): 0.98,
   ...:         dt(2025, 1, 1): 0.97,
   ...:         dt(2026, 1, 1): 0.96,
   ...:         dt(2027, 1, 1): 0.95,
   ...:     },
   ...:     id="z"
   ...: )
   ...: 

In [3]: curve_s = Curve(
   ...:     nodes={
   ...:         dt(2022, 1, 1): 1.0,
   ...:         dt(2023, 1, 1): 0.99,
   ...:         dt(2024, 1, 1): 0.98,
   ...:         dt(2025, 1, 1): 0.97,
   ...:         dt(2026, 1, 1): 0.96,
   ...:         dt(2027, 1, 1): 0.95,
   ...:     },
   ...:     id="s"
   ...: )
   ...: 

In [4]: args = dict(termination="1Y", frequency="A", fixing_method="ibor", leg2_fixing_method="ibor")

In [5]: instruments = [
   ...:     SBS(dt(2022, 1, 1), curves=["r", "s", "s", "s"], **args),
   ...:     SBS(dt(2023, 1, 1), curves=["r", "s", "s", "s"], **args),
   ...:     SBS(dt(2024, 1, 1), curves=["r", "s", "s", "s"], **args),
   ...:     SBS(dt(2025, 1, 1), curves=["r", "s", "s", "s"], **args),
   ...:     SBS(dt(2026, 1, 1), curves=["r", "s", "s", "s"], **args),
   ...:     SBS(dt(2022, 1, 1), curves=["r", "s", "z", "s"], **args),
   ...:     SBS(dt(2023, 1, 1), curves=["r", "s", "z", "s"], **args),
   ...:     SBS(dt(2024, 1, 1), curves=["r", "s", "z", "s"], **args),
   ...:     SBS(dt(2025, 1, 1), curves=["r", "s", "z", "s"], **args),
   ...:     SBS(dt(2026, 1, 1), curves=["r", "s", "z", "s"], **args),
   ...:     IRS(dt(2022, 1, 1), "1Y", "A", curves=["r", "s"], leg2_fixing_method="ibor"),
   ...:     IRS(dt(2023, 1, 1), "1Y", "A", curves=["r", "s"], leg2_fixing_method="ibor"),
   ...:     IRS(dt(2024, 1, 1), "1Y", "A", curves=["r", "s"], leg2_fixing_method="ibor"),
   ...:     IRS(dt(2025, 1, 1), "1Y", "A", curves=["r", "s"], leg2_fixing_method="ibor"),
   ...:     IRS(dt(2026, 1, 1), "1Y", "A", curves=["r", "s"], leg2_fixing_method="ibor"),
   ...: ]
   ...: 

In [6]: solver = Solver(
   ...:     curves=[curve_r, curve_s, curve_z],
   ...:     instruments=instruments,
   ...:     s=[0.]*5 + [0.]*5 + [1.5]*5,
   ...:     id="sonia",
   ...:     instrument_labels=[
   ...:         "s1", "s2", "s3", "s4", "s5",
   ...:         "z1", "z2", "z3", "z4", "z5",
   ...:         "r1", "r2", "r3", "r4", "r5",
   ...:     ],
   ...: )
   ...: 
SUCCESS: `func_tol` reached after 5 iterations (levenberg_marquardt), `f_val`: 2.4563064998532004e-14, `time`: 0.0658s

In [7]: irs = IRS(dt(2022, 1, 1), "5Y", "A", notional=-8.3e8, curves=["z", "s"], leg2_fixing_method="ibor", fixed_rate=25.0)

In [8]: irs.delta(solver=solver)
Out[8]: 
local_ccy                       usd
display_ccy                     usd
type        solver label           
instruments sonia  s1     -94442.25
                   s2     -75096.12
                   s3     -56083.86
                   s4     -37006.71
                   s5     -18514.47
                   z1     -82885.27
                   z2     -81643.61
                   z3     -80637.57
                   z4     -79212.55
                   z5     -78025.91
                   r1    -177327.52
                   r2    -156739.73
                   r3    -136721.43
                   r4    -116219.27
                   r5     -96540.38

In [9]: irs.gamma(solver=solver)
Out[9]: 
type                                           instruments                                                                                    
solver                                               sonia                                                                                    
label                                                   s1    s2    s3    s4    s5    z1    z2    z3    z4    z5    r1    r2    r3    r4    r5
local_ccy display_ccy type        solver label                                                                                                
usd       usd         instruments sonia  s1          18.86  7.50  5.60  3.70  1.85  8.28  8.15  8.05  7.91  7.79 27.14 15.65 13.65 11.61  9.64
                                         s2           7.50 14.99  5.60  3.70  1.85  0.05  8.15  8.05  7.91  7.79  7.55 23.14 13.65 11.61  9.64
                                         s3           5.60  5.60 11.22  3.71  1.85  0.00  0.04  8.08  7.93  7.81  5.60  5.65 19.30 11.64  9.67
                                         s4           3.70  3.70  3.71  7.38  1.85 -0.00  0.00  0.04  7.91  7.79  3.70  3.70  3.75 15.29  9.64
                                         s5           1.85  1.85  1.85  1.85  3.70  0.00  0.00  0.00  0.04  7.84  1.85  1.85  1.85  1.89 11.53
                                         z1           8.28  0.05  0.00 -0.00  0.00 -0.00  0.00  0.00 -0.00  0.00  8.28  0.05  0.00 -0.00  0.00
                                         z2           8.15  8.15  0.04  0.00  0.00  0.00  0.00  0.00 -0.00  0.00  8.15  8.15  0.04 -0.00  0.00
                                         z3           8.05  8.05  8.08  0.04  0.00  0.00  0.00  0.00 -0.00  0.00  8.05  8.05  8.08  0.04  0.00
                                         z4           7.91  7.91  7.93  7.91  0.04 -0.00 -0.00 -0.00 -0.00 -0.00  7.91  7.91  7.93  7.91  0.04
                                         z5           7.79  7.79  7.81  7.79  7.84  0.00  0.00  0.00 -0.00 -0.00  7.79  7.79  7.81  7.79  7.84
                                         r1          27.14  7.55  5.60  3.70  1.85  8.28  8.15  8.05  7.91  7.79 35.42 15.70 13.65 11.61  9.64
                                         r2          15.65 23.14  5.65  3.70  1.85  0.05  8.15  8.05  7.91  7.79 15.70 31.30 13.70 11.61  9.64
                                         r3          13.65 13.65 19.30  3.75  1.85  0.00  0.04  8.08  7.93  7.81 13.65 13.70 27.37 11.68  9.67
                                         r4          11.61 11.61 11.64 15.29  1.89 -0.00  0.00  0.04  7.91  7.79 11.61 11.61 11.68 23.20  9.68
                                         r5           9.64  9.64  9.67  9.64 11.53  0.00  0.00  0.00  0.04  7.84  9.64  9.64  9.67  9.68 19.37
iterate()#

Solve the DF node values and update all the curves.

This method uses a gradient based optimisation routine, to solve for all the curve variables, \(\mathbf{v}\), as follows,

\[\mathbf{v} = \underset{\mathbf{v}}{\mathrm{argmin}} \;\; f(\mathbf{v}) = \underset{\mathbf{v}}{\mathrm{argmin}} \;\; (\mathbf{r(v)} - \mathbf{S})\mathbf{W}(\mathbf{r(v)} - \mathbf{S})^\mathbf{T}\]

where \(\mathbf{r}\) are the mid-market rates of the calibrating instruments, \(\mathbf{S}\) are the observed and target rates, and \(\mathbf{W}\) is the diagonal array of weights.

Return type:

None

jacobian(solver)#

Calculate the Jacobian with respect to another Solver’s instruments.

Parameters:

solver (Solver) – The other Solver for which the Jacobian is to be determined.

Return type:

DataFrame

Notes

This Jacobian converts risk sensitivities expressed in the underlying Solver’s instruments to the instruments in the other solver.

Warning

A Jacobian transformation is only possible between Solvers whose instruments are associated with Curves with string ID mappings (which is best practice and demonstrated HERE XXX). This allows two different Solvers to contain their own Curves (which may or may not be equivalent models), and for the instrument rates of one Solver to be evaluated by the Curves present in another Solver

Examples

This example creates a Jacobian transformation between par tenor IRS and forward tenor IRS. These models are completely consistent and lossless.

In [1]: par_curve = Curve(
   ...:     nodes={
   ...:         dt(2022, 1, 1): 1.0,
   ...:         dt(2023, 1, 1): 1.0,
   ...:         dt(2024, 1, 1): 1.0,
   ...:         dt(2025, 1, 1): 1.0,
   ...:     },
   ...:     id="curve",
   ...: )
   ...: 

In [2]: par_instruments = [
   ...:     IRS(dt(2022, 1, 1), "1Y", "A", curves="curve"),
   ...:     IRS(dt(2022, 1, 1), "2Y", "A", curves="curve"),
   ...:     IRS(dt(2022, 1, 1), "3Y", "A", curves="curve"),
   ...: ]
   ...: 

In [3]: par_solver = Solver(
   ...:     curves=[par_curve],
   ...:     instruments=par_instruments,
   ...:     s=[1.21, 1.635, 1.99],
   ...:     id="par_solver",
   ...:     instrument_labels=["1Y", "2Y", "3Y"],
   ...: )
   ...: 
SUCCESS: `func_tol` reached after 4 iterations (levenberg_marquardt), `f_val`: 3.701224165929206e-13, `time`: 0.0058s

In [4]: fwd_curve = Curve(
   ...:     nodes={
   ...:         dt(2022, 1, 1): 1.0,
   ...:         dt(2023, 1, 1): 1.0,
   ...:         dt(2024, 1, 1): 1.0,
   ...:         dt(2025, 1, 1): 1.0,
   ...:     },
   ...:     id="curve"
   ...: )
   ...: 

In [5]: fwd_instruments = [
   ...:     IRS(dt(2022, 1, 1), "1Y", "A", curves="curve"),
   ...:     IRS(dt(2023, 1, 1), "1Y", "A", curves="curve"),
   ...:     IRS(dt(2024, 1, 1), "1Y", "A", curves="curve"),
   ...: ]
   ...: 

In [6]: s_fwd = [float(_.rate(solver=par_solver)) for _ in fwd_instruments]

In [7]: fwd_solver = Solver(
   ...:     curves=[fwd_curve],
   ...:     instruments=fwd_instruments,
   ...:     s=s_fwd,
   ...:     id="fwd_solver",
   ...:     instrument_labels=["1Y", "1Y1Y", "2Y1Y"],
   ...: )
   ...: 
SUCCESS: `func_tol` reached after 4 iterations (levenberg_marquardt), `f_val`: 6.935721736838484e-15, `time`: 0.0036s

In [8]: par_solver.jacobian(fwd_solver)
Out[8]: 
                par_solver           
                        1Y    2Y   3Y
fwd_solver 1Y         1.00  0.51 0.34
           1Y1Y       0.00  0.49 0.33
           2Y1Y       0.00 -0.00 0.32
market_movements(solver)#

Determine market movements between the Solver’s instrument rates and those rates priced from a second Solver.

Parameters:

solver (Solver) – The other Solver whose Curves are to be used for measuring the final instrument rates of the existing Solver’s instruments.

Return type:

DataFrame

Notes

Warning

Market movement calculations are only possible between Solvers whose instruments are associated with Curves with string ID mappings (which is best practice and demonstrated HERE XXX). This allows two different Solvers to contain their own Curves (which may or may not be equivalent models), and for the instrument rates of one Solver to be evaluated by the Curves present in another Solver.