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 uniqueid
. EachCurve
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 forinstruments
.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 callingiterate()
.Each instrument provided to
instruments
must have itscurves
andmetric
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’srate`()
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:
- 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
Return the error in calibrating instruments, including
pre_solvers
, scaled to the risk representation factor.Objective function scalar value of the solver;
1d array of mid-market rates of each calibrating instrument with given curves, size (m,).
1d array of curve node variables for each ordered curve, size (n,).
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
andself.weights
.
- r#
1d array of mid-market rates of each calibrating instrument with given curves, size (m,).
Depends on
self.pre_curves
andself.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
andself.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 theSolver
. 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 frompre_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 toInstruments
. When constructing and pricing multi-currency instruments it is likely that theSolver
used is associated with anFXForwards
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 directfx
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 theSolver
.
- 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 frompre_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 SolverExamples
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.