Pricing Mechanisms#

This guide is aimed at users who are not completely new to rateslib and who have a little experience already building Instruments, Curves and Solvers are are familiar with some of its basic mechanics already.

Summary#

Rateslib’s API design for valuing and obtaining risk sensitivities of Instruments follows the first two pillars of its design philosophy:

  • Maximise flexibility : minimise user input,

  • Prioritise risk sensitivities above valuation.

This means the arguments required for the Instrument.npv(), Instrument.delta() and Instrument.gamma() are the same and optionally require:

curves, solver, fx, base, local

When calculating risk metrics a solver, which contains derivative mapping information, is required. However, when calculating value, it is sufficient to just provide curves. In this case, and if the curves do not contain AD then the calculation might be upto 300% faster.

Since these arguments are optional and can be inferred from each other it is important to understand the combination that can produce results. There are two sections on this page which discuss these combinations.

  1. How solver, fx, base and local interact? I.e. what currency will results be displayed in?

  2. How curves, solver and Instruments interact? I.e. which Curves will be used to price which Instruments?

How do solver, fx, base and local interact?#

One of the most important aspects to keep track of when valuing Instrument.npv() is that of the currency in which it is displayed. This is the base currency it is displayed in. base does not need to be explicitly set to get the results one expects.

The local argument

local can, at any time, be set to True and this will return a dict containing a currency key and a value. By using this we keep track of the currency of each Leg of the Instrument. This is important for risk sensitivities and is used internally, especially for multi-currency instruments.

In [1]: curve = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.96}, id="curve")

In [2]: fxr = FXRates({"usdeur": 0.9, "gbpusd": 1.25}, base="gbp", settlement=dt(2022, 1, 3))

In [3]: fxf = FXForwards(
   ...:     fx_rates=fxr,
   ...:     fx_curves={"usdusd": curve, "eureur": curve, "gbpgbp": curve, "eurusd": curve, "gbpusd": curve},
   ...:     base="eur",
   ...: )
   ...: 

In [4]: solver = Solver(
   ...:     curves=[curve],
   ...:     instruments=[IRS(dt(2022, 1, 1), "1y", "a", curves=curve)],
   ...:     s=[4.109589041095898],
   ...:     fx=fxf,
   ...: )
   ...: 
SUCCESS: `func_tol` reached after 0 iterations (levenberg_marquardt), `f_val`: 0.0, `time`: 0.0003s

The below shows the use of the local argument to get the PV of both Legs on this XCS separately in each currency. When specifying a base and setting local to False the PV of the Legs are aggregated and converted to the given currency.

In [5]: nxcs = XCS(dt(2022, 2, 1), "6M", "A", currency="eur", leg2_currency="usd", leg2_mtm=False)

In [6]: nxcs.npv(curves=[curve]*4, fx=fxf, local=True)
Out[6]: 
{'usd': <Dual: -4.962793, (fx_gbpusd, fx_usdeur, curve1, ...), [0.0, 5.5, 251.5, ...]>,
 'eur': <Dual: 4.466513, (curve1, curve0), [-226.4, 221.8]>}

In [7]: nxcs.npv(curves=[curve]*4, fx=fxf, base="usd")
Out[7]: <Dual: 0.000000, (fx_gbpusd, fx_usdeur, curve1, ...), [0.0, 0.0, 0.0, ...]>

What is best practice?#

For single currency Instruments, if you want to return an npv value in its local currency then you do not need to supply base or fx arguments. However, to be explicit, base can also be specified.

In [8]: irs = IRS(dt(2022, 2, 1), "6M", "A", currency="usd", fixed_rate=4.0, curves=curve)

In [9]: irs.npv(solver=solver)              # USD is local currency default, solver.fx.base is EUR.
Out[9]: <Dual: 330.405115, (curve1, curve0), [-514447.8, 494200.3]>

In [10]: irs.npv(solver=solver, base="usd")  # USD is explicit, solver.fx.base is EUR.
Out[10]: <Dual: 330.405115, (curve1, curve0), [-514447.8, 494200.3]>

To calculate a value in another non-local currency supply an fx object and specify the base. It is not good practice to supply fx as numeric since this can result in errors (if the exchange rate is given the wrong way round (human error)) and it does not preserve AD or any FX sensitivities. base is inferred from the fx object so the following are all equivalent. fx objects are commonly inherited from solvers.

In [11]: irs.npv(fx=fxr)                     # GBP is fx's base currency
Out[11]: <Dual: 264.324092, (curve1, curve0, fx_gbpusd, ...), [-411558.2, 395360.2, -211.5, ...]>

In [12]: irs.npv(fx=fxr, base="gbp")         # GBP is explicitly specified
Out[12]: <Dual: 264.324092, (curve1, curve0, fx_gbpusd, ...), [-411558.2, 395360.2, -211.5, ...]>

In [13]: irs.npv(fx=fxr, base=fxr.base)      # GBP is fx's base currency
Out[13]: <Dual: 264.324092, (curve1, curve0, fx_gbpusd, ...), [-411558.2, 395360.2, -211.5, ...]>

In [14]: irs.npv(solver=solver, base="gbp")  # GBP is explicitly specified
Out[14]: <Dual: 264.324092, (fx_gbpusd, fx_usdeur, curve1, ...), [-211.5, 0.0, -411558.2, ...]>

For multi-currency Instruments, which include FXSwaps, FXExchanges and XCSs, these instruments typically rely on an FXForwards object to value correctly, in which case that will be supplied either via solver or via the fx argument. base can be set explicitly, or set as the same as fx.base, or it will be taken as the local Leg1 currency.

Technical rules#

If base is not given it will be inferred from one of two objects;

  • either it will be inferred from the provided fx object,

  • or it will be inferred from the Leg or from Leg1 of an Instrument.

base will not be inherited from a second layer inherited object. I.e. base will not be set equal to the base currency of the solver.fx associated object.

Inheritance map for base
Possible argument combinations supplied and rateslib return.#

Case and Output

base

fx

solver with fx

solver without fx

base is explicit

Returns if currency and base are available in fx object, otherwise raises.

X

X

Returns and warns about best practice.

X

(numeric)

Returns if currency and base are available in fx object, otherwise raises.

X

X

Returns if currency and base are available in fx object, otherwise raises. Will warn if fx and solver.fx are not the same object.

X

X

X

Returns if base aligns with local currency, else raises.

X

Returns if base aligns with local currency, else raises.

X

X

base is inferred and logic reverts to above cases.

Returns inferring base from fx object.

<-

X

Returns inferring base from fx object. Warns if fx and solver.fx are not the same object.

<-

X

X

Returns inferring base from fx object.

<-

X

X

Returns inferring base as Leg or Leg1 local currency.

(local)

X

Returns inferring base as Leg or Leg1 local currency.

(local)

X

Returns inferring base as Leg or Leg1 local currency.

(local)

Examples#

We continue the examples above using the USD IRS created and consider possible npvs:

In [15]: def npv(irs, curves=NoInput(0), solver=NoInput(0), fx=NoInput(0), base=NoInput(0)):
   ....:    try:
   ....:       _ = irs.npv(curves, solver, fx, base)
   ....:    except Exception as e:
   ....:       _ = str(e)
   ....:    return _
   ....: 
# The following are all explicit EUR output
In [16]: npv(irs, base="eur")          # Error since no conversion rate available.
Out[16]: "`base` (eur) cannot be requested without supplying `fx` as a valid FXRates or FXForwards object to convert to currency (usd).\nIf you are using a `Solver` with multi-currency instruments have you forgotten to attach the FXForwards in the solver's `fx` argument?"

In [17]: npv(irs, base="eur", fx=fxr)  # Takes 0.9 FX rate from object.
Out[17]: <Dual: 297.364604, (curve1, curve0, fx_gbpusd, ...), [-463003.0, 444780.2, 0.0, ...]>

In [18]: npv(irs, base="eur", fx=2.0)  # UserWarning and no fx Dual sensitivities.
Out[18]: <Dual: 660.810231, (curve1, curve0), [-1028895.5, 988400.5]>

In [19]: npv(irs, base="eur", solver=solver)  # Takes 0.95 FX rates from solver.fx
Out[19]: <Dual: 297.364604, (fx_gbpusd, fx_usdeur, curve1, ...), [0.0, 330.4, -463003.0, ...]>

In [20]: npv(irs, base="eur", fx=fxr, solver=solver)  # Takes 0.9 FX rate from fx
Out[20]: <Dual: 297.364604, (curve1, curve0, fx_gbpusd, ...), [-463003.0, 444780.2, 0.0, ...]>

# The following infer the base
In [21]: npv(irs)                         # Base is inferred as local currency: USD
Out[21]: <Dual: 330.405115, (curve1, curve0), [-514447.8, 494200.3]>

In [22]: npv(irs, fx=fxr)                 # Base is inferred from fx: GBP
Out[22]: <Dual: 264.324092, (curve1, curve0, fx_gbpusd, ...), [-411558.2, 395360.2, -211.5, ...]>

In [23]: npv(irs, fx=fxr, base=fxr.base)  # Base is explicit from fx: GBP
Out[23]: <Dual: 264.324092, (curve1, curve0, fx_gbpusd, ...), [-411558.2, 395360.2, -211.5, ...]>

In [24]: npv(irs, fx=fxr, solver=solver)  # Base is inferred from fx: GBP. UserWarning for different fx objects
Out[24]: <Dual: 264.324092, (curve1, curve0, fx_gbpusd, ...), [-411558.2, 395360.2, -211.5, ...]>

In [25]: npv(irs, solver=solver)          # Base is inferred as local currency: USD
Out[25]: <Dual: 330.405115, (curve1, curve0), [-514447.8, 494200.3]>

In [26]: npv(irs, solver=solver, fx=solver.fx)  # Base is inferred from solver.fx: EUR
Out[26]: <Dual: 264.324092, (fx_gbpusd, fx_usdeur, curve1, ...), [-211.5, 0.0, -411558.2, ...]>

How curves, solver and Instruments interact?#

The pricing mechanisms in rateslib require Instruments and Curves. FX objects (usually FXForwards) may also be required (for multi-currency instruments), and these are all often interdependent and calibrated by a Solver.

Since Instruments are separate objects to Curves and Solvers, when pricing them it requires a mapping to link them all together. This leads to…

Three different modes of initialising an Instrument:

  1. Dynamic - Price Time Mapping: this means an Instrument is initialised without any curves and these must be provided later at price time, usually inside a function call.

    In [27]: instrument = IRS(dt(2022, 1, 1), "10Y", "A", fixed_rate=2.5)
    
    In [28]: curve = Curve({dt(2022, 1, 1): 1.0, dt(2032, 1, 1): 0.85})
    
    In [29]: instrument.npv(curves=curve)
    Out[29]: -82171.04166115227
    
    In [30]: instrument.rate(curves=curve)
    Out[30]: 1.6151376354769178
    
  2. Explicit - Immediate Mapping: this means an Instrument is initialised with curves and this object will be used if no Curves are provided at price time. The Curves must already exist when initialising the Instrument.

    In [31]: curve = Curve({dt(2022, 1, 1): 1.0, dt(2032, 1, 1): 0.85})
    
    In [32]: instrument = IRS(dt(2022, 1, 1), "10Y", "A", fixed_rate=2.5, curves=curve)
    
    In [33]: instrument.npv()
    Out[33]: -82171.04166115227
    
    In [34]: instrument.rate()
    Out[34]: 1.6151376354769178
    
  3. Indirect - String id Mapping: this means an Instrument is initialised with curves that contain lookup information to collect the Curves at price time from a solver.

    In [35]: instrument = IRS(dt(2022, 1, 1), "10Y", "A", fixed_rate=2.5, curves="curve-id")
    
    In [36]: curve = Curve({dt(2022, 1, 1): 1.0, dt(2032, 1, 1): 0.85}, id="curve-id")
    
    In [37]: solver = Solver(
       ....:     curves=[curve],
       ....:     instruments=[IRS(dt(2022, 1, 1), "10Y", "A", curves=curve)],
       ....:     s=[1.6151376354769178]
       ....: )
       ....: 
    SUCCESS: `func_tol` reached after 0 iterations (levenberg_marquardt), `f_val`: 1.779867417404908e-29, `time`: 0.0015s
    
    In [38]: instrument.npv(solver=solver)
    Out[38]: <Dual: -82171.041661, (curve-id1, curve-id0), [-1146523.3, 892373.8]>
    
    In [39]: instrument.rate(solver=solver)
    Out[39]: <Dual: 1.615138, (curve-id1, curve-id0), [-11.8, 10.0]>
    

Then, for price time, this then also leads to the following cases…

Two modes of pricing an Instrument:

  1. Direct Curves Override: if curves are given dynamically these are used regardless of which initialisation mode was used for the Instrument.

    In [40]: curve = Curve({dt(2022, 1, 1): 1.0, dt(2032, 1, 1): 0.85})
    
    In [41]: irs = IRS(dt(2022, 1, 1), "10Y", "A", curves=curve)
    
    In [42]: other_curve = Curve({dt(2022, 1, 1): 1.0, dt(2032, 1, 1): 0.85})
    
    In [43]: irs.npv(curves=other_curve)  # other_curve overrides the initialised curve
    Out[43]: -2.9103830456733704e-11
    
    In [44]: irs.rate(curves=other_curve)  # other_curve overrides the initialised curve
    Out[44]: 1.6151376354769178
    
  2. With Default Initialisation: if curves at price time are not provided then those specified at initialisation are used.

    1. As Objects: if Curves were specified these are used directly (see 2. above)

    2. From String id with Solver: if curves are not objects, but strings, then a solver must be supplied to extract the Curves from (see 3. above).

In the unusual combination that curves are given directly in combination with a solver, and those curves do not form part of the solver’s curve collection, then depending upon the rateslib options configured, then errors or warnings might be raised or this might be ignored.

What is best practice?#

Amongst the variety of input pricing methods there is a recommended way of working. This is to use method 3) and to initialise Instruments with a defined curves argument as string id s. This does not impede dynamic pricing if curves are constructed and supplied later directly to pricing methods. The curves attribute on the Instrument is instructive of its pricing intent.

In [45]: irs = IRS(
   ....:     effective=dt(2022, 1, 1),
   ....:     termination="6m",
   ....:     frequency="Q",
   ....:     currency="usd",
   ....:     notional=500e6,
   ....:     fixed_rate=2.0,
   ....:     curves="sofr",  # or ["sofr", "sofr"] for forecasting and discounting
   ....: )
   ....: 

In [46]: irs.curves
Out[46]: 'sofr'

At any point a Curve could be constructed and used for dynamic pricing, even if its id does not match the instrument initialisation. This is usually used in sampling or scenario analysis.

In [47]: curve = Curve(
   ....:     nodes={dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.98},
   ....:     id="not_sofr"
   ....: )
   ....: 

In [48]: irs.rate(curve)
Out[48]: 1.9975948370062162

Why is this best practice?#

The reasons that this is best practice are:

  • It provides more flexibility when working with multiple different curve models and multiple Solver s. Instruments do not need to be re-initialised just to extract alternate valuations or alternate risk sensitivities.

  • It provides more flexibility since only Instruments constructed in this manner can be directly added to the Portfolio class. It also extends the Spread and Fly classes to allow Instruments which do not share the same Curves.

  • It removes the need to externally keep track of the necessary pricing curves needed for each instrument created, which is often four curves for two legs.

  • It creates redundancy by avoiding programmatic errors when curves are overwritten and object oriented associations are silently broken, which can occur when using the other methods.

  • It is anticipated that this mechanism is the one most future proofed when rateslib is extended for server-client-api transfer via JSON or otherwise.

Multiple curve model Solvers#

Consider two different curve models, a log-linear one and a log-cubic spline, which we calibrate with the same instruments.

In [49]: instruments = [
   ....:     IRS(dt(2022, 1, 1), "4m", "Q", curves="sofr"),
   ....:     IRS(dt(2022, 1, 1), "8m", "Q", curves="sofr"),
   ....: ]
   ....: 

In [50]: s = [1.85, 2.10]

In [51]: ll_curve = Curve(
   ....:     nodes={
   ....:         dt(2022, 1, 1): 1.0,
   ....:         dt(2022, 5, 1): 1.0,
   ....:         dt(2022, 9, 3): 1.0
   ....:     },
   ....:     interpolation="log_linear",
   ....:     id="sofr"
   ....: )
   ....: 

In [52]: lc_curve = Curve(
   ....:     nodes={
   ....:         dt(2022, 1, 1): 1.0,
   ....:         dt(2022, 5, 1): 1.0,
   ....:         dt(2022, 9, 3): 1.0
   ....:     },
   ....:     t=[dt(2022, 1, 1), dt(2022, 1, 1), dt(2022, 1, 1), dt(2022, 1, 1),
   ....:        dt(2022, 5, 1),
   ....:        dt(2022, 9, 3), dt(2022, 9, 3), dt(2022, 9, 3), dt(2022, 9, 3)],
   ....:     id="sofr",
   ....: )
   ....: 

In [53]: ll_solver = Solver(curves=[ll_curve], instruments=instruments, s=s, instrument_labels=["4m", "8m"], id="sofr")
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 1.0687768947368452e-16, `time`: 0.0041s

In [54]: lc_solver = Solver(curves=[lc_curve], instruments=instruments, s=s, instrument_labels=["4m", "8m"], id="sofr")
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 1.2711948339281648e-16, `time`: 0.0038s

In [55]: ll_curve.plot("1D", comparators=[lc_curve], labels=["LL Curve", "LC Curve"])
Out[55]: 
(<Figure size 640x480 with 1 Axes>,
 <Axes: >,
 [<matplotlib.lines.Line2D at 0x7f0a9f1c6210>,
  <matplotlib.lines.Line2D at 0x7f0a9ef28410>])

(Source code, png, hires.png, pdf)

_images/g_mechanisms-1.png

Since the irs instrument was initialised indirectly with string id s we can supply the Solver s as pricing parameters and the curves named “sofr” in each of them will be looked up and used to price the irs.

In [56]: irs.rate(solver=ll_solver)
Out[56]: <Dual: 2.017016, (sofr0, sofr1, sofr2), [200.1, -103.5, -98.7]>

In [57]: irs.rate(solver=lc_solver)
Out[57]: <Dual: 1.984736, (sofr0, sofr1, sofr2), [220.2, -143.1, -79.1]>

The Dual datatypes already hint at different risk sensitivities of the instrument under the different curve model solvers. For good order we can display the delta risks.

In [58]: irs.delta(solver=ll_solver)
Out[58]: 
local_ccy                     usd
display_ccy                   usd
type        solver label         
instruments sofr   4m     8341.36
                   8m    16622.06

In [59]: irs.delta(solver=lc_solver)
Out[59]: 
local_ccy                     usd
display_ccy                   usd
type        solver label         
instruments sofr   4m    11573.70
                   8m    13397.65

The programmatic errors avoided are as follows:

In [60]: try:
   ....:     irs.delta(curves=ll_curve, solver=lc_solver)
   ....: except Exception as e:
   ....:     print(e)
   ....: 
A curve has been supplied, as part of ``curves``, which has the same `id` ('sofr'),
as one of the curves available as part of the Solver's collection but is not the same object.
This is ambiguous and cannot price.
Either refactor the arguments as follows:
1) remove the conflicting curve: [curves=[..], solver=<Solver>] -> [curves=None, solver=<Solver>]
2) change the `id` of the supplied curve and ensure the rateslib.defaults option 'curve_not_in_solver' is set to 'ignore'.
   This will remove the ability to accurately price risk metrics.

Using a Portfolio#

We can consider creating another Solver for the ESTR curve which extends the SOFR solver.

In [61]: instruments = [
   ....:     IRS(dt(2022, 1, 1), "3m", "Q", curves="estr"),
   ....:     IRS(dt(2022, 1, 1), "9m", "Q", curves="estr"),
   ....: ]
   ....: 

In [62]: s = [0.75, 1.65]

In [63]: ll_curve = Curve(
   ....:     nodes={
   ....:         dt(2022, 1, 1): 1.0,
   ....:         dt(2022, 4, 1): 1.0,
   ....:         dt(2022, 10, 1): 1.0
   ....:     },
   ....:     interpolation="log_linear",
   ....:     id="estr",
   ....: )
   ....: 

In [64]: combined_solver = Solver(
   ....:     curves=[ll_curve],
   ....:     instruments=instruments,
   ....:     s=s,
   ....:     instrument_labels=["3m", "9m"],
   ....:     pre_solvers=[ll_solver],
   ....:     id="estr"
   ....: )
   ....: 
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 1.237437454456797e-15, `time`: 0.0032s

Now we create another IRS and add it to a Portfolio

In [65]: irs2 = IRS(
   ....:     effective=dt(2022, 1, 1),
   ....:     termination="6m",
   ....:     frequency="Q",
   ....:     currency="eur",
   ....:     notional=-300e6,
   ....:     fixed_rate=1.0,
   ....:     curves="estr",
   ....: )
   ....: 

In [66]: pf = Portfolio([irs, irs2])

In [67]: pf.npv(solver=combined_solver, local=True)
Out[67]: 
{'usd': <Dual: 42456.377860, (sofr0, sofr1, sofr2), [499326346.8, -258138138.0, -246219539.0]>,
 'eur': <Dual: -638082.239972, (estr0, estr1, estr2), [-299965158.7, 151143418.5, 150334069.1]>}

In [68]: pf.delta(solver=combined_solver)
Out[68]: 
local_ccy                      eur      usd
display_ccy                    eur      usd
type        solver label                   
instruments sofr   4m         0.00  8341.36
                   8m         0.00 16622.06
            estr   3m     -3741.35     0.00
                   9m    -11247.59     0.00

In [69]: pf.gamma(solver=combined_solver)
Out[69]: 
type                                           instruments                
solver                                                sofr       estr     
label                                                   4m    8m   3m   9m
local_ccy display_ccy type        solver label                            
eur       eur         instruments sofr   4m           0.00  0.00 0.00 0.00
                                         8m           0.00  0.00 0.00 0.00
                                  estr   3m           0.00  0.00 0.14 0.28
                                         9m           0.00  0.00 0.28 0.44
usd       usd         instruments sofr   4m          -0.32 -0.50 0.00 0.00
                                         8m          -0.50 -0.52 0.00 0.00
                                  estr   3m           0.00  0.00 0.00 0.00
                                         9m           0.00  0.00 0.00 0.00

Warnings#

Silently breaking object associations#

Warning

There is no redundancy for breaking object oriented associations when an Instrument is initialised with curves as objects.

When an Instrument is created with a direct object association to Curves which have already been constructed. These will then be used by default when pricing.

In [70]: curve = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.98})

In [71]: irs = IRS(dt(2022, 1, 1), "6m", "Q", currency="usd", fixed_rate=2.0, curves=curve)

In [72]: irs.rate()
Out[72]: 1.9975948370062162

In [73]: irs.npv()
Out[73]: -12.0008132132225

If the object is overwritten, or is recreated (say, as a new Curve) the results will not be as expected.

In [74]: curve = "bad_object"  # overwrite the curve variable but the object still exists.

In [75]: irs.rate()
Out[75]: 1.9975948370062162

It is required to update objects instead of recreating them. The documentation for FXForwards.update() also elaborates on this point.

Disassociated objects#

Warning

Combining curves and solver that are not associated is bad practice. There are options for trying to avoid this behaviour.

Consider the below example, which includes two Curve s and a Solver. One Curve, labelled “ibor”, is independent, the other, labelled “rfr”, is associated with the Solver, since it has been iteratively solved.

In [76]: rfr_curve = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.98}, id="rfr")

In [77]: ibor_curve = Curve({dt(2022, 1, 1): 1.0, dt(2023, 1, 1): 0.97}, id="ibor")

In [78]: solver = Solver(
   ....:     curves=[rfr_curve],
   ....:     instruments=[(Value(dt(2023, 1, 1)), ("rfr",), {})],
   ....:     s=[0.9825]
   ....: )
   ....: 
SUCCESS: `func_tol` reached after 8 iterations (levenberg_marquardt), `f_val`: 2.3630340650323354e-14, `time`: 0.0012s

When the option curve_not_in_solver is set to “ignore” the independent Curve and a disassociated Solver can be provided to a pricing method and the output returns. It uses the curve and, effectively, ignores the disassociated solver.

In [79]: irs = IRS(dt(2022, 1, 1), dt(2023, 1, 1), "A")

In [80]: defaults.curve_not_in_solver = "ignore"

In [81]: irs.rate(ibor_curve, solver)
Out[81]: 3.050416607823765

In the above the solver is not used for pricing, since it is decoupled from ibor_curve. It is technically an error to list it as an argument.

Setting the option to “warn” or “raise” enforces a UserWarning or a ValueError when this behaviour is detected.

In [82]: defaults.curve_not_in_solver = "raise"

In [83]: try:
   ....:     irs.rate(ibor_curve, solver)
   ....: except Exception as e:
   ....:     print(e)
   ....: 
`curve` must be in `solver`.

When referencing objects by id s this becomes immediately apparent since, the below will always fail regardless of the configurable option (the solver does not contain the requested curve and therefore cannot fulfill the request).

In [84]: defaults.curve_not_in_solver = "ignore"

In [85]: try:
   ....:     irs.rate("ibor", solver)
   ....: except Exception as e:
   ....:     print(e)
   ....: 
`curves` must contain str curve `id` s existing in `solver` (or its associated `pre_solvers`)