API Reference

Solver

Multi-curve calibration solver with Levenberg-Marquardt, Gauss-Newton, and gradient descent algorithms, plus iterative bootstrap.

All types are available via flat import:

from vade import Solver, SolverResult
from vade.solver import bootstrap

See Conventions for all accepted string parameter values.

See also: Calibration Guide for multi-curve calibration walkthroughs. | Bootstrap & Parametric Guide for bootstrap vs optimizer comparison. | Risk Guide for solver-based delta and gamma computation.

Contents: SolverResult | Solver | bootstrap

SolverResult

Calibration result with convergence diagnostics and Jacobian. Rust-backed.

Returned by Solver.iterate() -- not constructed directly.

Properties

PropertyTypeDescription
.convergedboolWhether calibration converged
.iterationsintNumber of iterations performed
.objectivefloatFinal objective function value (sum of squared residuals)
.residualslist[float]Per-instrument residuals (target - computed)
.jacobiannumpy.ndarray2D Jacobian matrix, shape (n_instruments, n_vars)
.condition_numberfloatCondition number of the Jacobian
.statusstrStatus message describing termination reason
.time_secondsfloatCalibration wall-clock time in seconds
.algorithmstrAlgorithm used (e.g., "levenberg_marquardt")

Example

import datetime
from vade import IRS, DiscountCurve, Solver

nodes = {
    datetime.date(2024, 1, 1): 1.0,
    datetime.date(2025, 1, 1): 1.0,
    datetime.date(2026, 1, 1): 1.0,
}
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f", id="usd")

irs_1y = IRS(effective=datetime.date(2024, 1, 1), termination="1Y", frequency="a", fixed_rate=3.0, convention="act365f", float_convention="act365f")
irs_2y = IRS(effective=datetime.date(2024, 1, 1), termination="2Y", frequency="a", fixed_rate=3.5, convention="act365f", float_convention="act365f")

solver = Solver(curves=[curve], instruments=[(irs_1y, 3.0), (irs_2y, 3.5)])
result = solver.iterate()

result.converged  # True
result.iterations > 0  # True
result.algorithm  # 'levenberg_marquardt'
result.objective < 1e-10  # True
len(result.residuals)  # 2
result.jacobian.shape  # (2, 2)
result.condition_number > 0  # True
isinstance(result.time_seconds, float)  # True
isinstance(result.status, str)  # True

Solver

Multi-curve calibration solver with selectable optimization algorithm. Rust-backed.

Constructor

Solver(
    *,
    curves,
    instruments,
    weights=None,
    func_tol=1e-14,
    grad_tol=1e-14,
    max_iter=100,
    ini_lambda=None,
    pre_solvers=None,
    fx_rates=None,
    algorithm="levenberg_marquardt",
    learning_rate=None,
)

Parameters

NameTypeDefaultDescription
curveslist[DiscountCurve | LineCurve]requiredCurves to calibrate (node values are adjusted)
instrumentslist[tuple | dict]requiredCalibration instruments with targets (see Instruments Format)
weightslist[float] | NoneNonePer-instrument weights (uniform if None)
func_tolfloat1e-14Function tolerance for convergence
grad_tolfloat1e-14Gradient tolerance for convergence
max_iterint100Maximum iterations
ini_lambdatuple[float, float, float] | NoneNoneLM damping parameters (initial, factor_up, factor_down)
pre_solverslist[Solver] | NoneNoneSolvers to run first (staged calibration for multi-curve frameworks)
fx_ratesFXRates | NoneNoneFX rates for cross-currency calibration
algorithmstr"levenberg_marquardt"Optimization algorithm: "levenberg_marquardt", "gauss_newton", "gradient_descent"
learning_ratefloat | NoneNoneLearning rate for gradient descent (default 1.0 with Armijo backtracking)

See DiscountCurve, LineCurve for curves. See FXRates for cross-currency calibration.

Methods

MethodReturnsDescription
.iterate()SolverResultRun calibration, returns result with convergence info
.get_curve(index=0)CurveGet calibrated curve by index
.get_curve_by_id(curve_id)CurveGet calibrated curve by its id string
.delta(instrument, result)DataFrameBucket-level delta risk (columns: instrument_label, tenor, sensitivity, currency)
.gamma(instrument, result)DataFrameCross-gamma matrix as labeled DataFrame
.to_json()strSerialize solver state to JSON
.from_json(s) (classmethod)SolverDeserialize from JSON

Instruments Format

The instruments parameter accepts three formats:

Simple 2-tuple -- single-curve calibration:

instruments = [(irs_1y, 3.50), (irs_2y, 3.75), (irs_5y, 4.00)]

Extended 5-tuple -- multi-curve with labels (required for .delta() and .gamma()):

instruments = [
    (irs_1y, 3.50, 0, "1Y_IRS", "USD"),
    (irs_2y, 3.75, 0, "2Y_IRS", "USD"),
]

Fields: (instrument, target, curve_idx, label, currency).

Dict format -- multi-curve with explicit disc/forecast curve indices:

instruments = [
    {
        "instrument": irs,
        "target": 3.50,
        "disc_curve_idx": 0,
        "forecast_curve_idx": 1,
        "label": "1Y",
        "currency": "USD",
    }
]

Keys: instrument (required), target (required), disc_curve_idx (required), forecast_curve_idx (optional, defaults to disc_curve_idx), label (optional), currency (optional).

The dict format also supports leg2_disc_curve_idx and leg2_forecast_curve_idx for cross-currency swaps with four curves.

Example

import datetime
from vade import IRS, DiscountCurve, Solver

nodes = {
    datetime.date(2024, 1, 1): 1.0,
    datetime.date(2025, 1, 1): 1.0,
    datetime.date(2026, 1, 1): 1.0,
    datetime.date(2027, 1, 1): 1.0,
}
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f", id="usd")

irs_1y = IRS(effective=datetime.date(2024, 1, 1), termination="1Y", frequency="a", fixed_rate=3.0, convention="act365f", float_convention="act365f")
irs_2y = IRS(effective=datetime.date(2024, 1, 1), termination="2Y", frequency="a", fixed_rate=3.2, convention="act365f", float_convention="act365f")
irs_3y = IRS(effective=datetime.date(2024, 1, 1), termination="3Y", frequency="a", fixed_rate=3.4, convention="act365f", float_convention="act365f")

solver = Solver(curves=[curve], instruments=[(irs_1y, 3.0), (irs_2y, 3.2), (irs_3y, 3.4)])
result = solver.iterate()
result.converged  # True

calibrated = solver.get_curve(0)
df_1y = calibrated.discount_factor(datetime.date(2025, 1, 1))
df_1y > 0.95  # True
df_1y < 1.0  # True

bootstrap

Iterative bootstrap calibration using node-by-node Brent root-finding. Module-level function.

from vade.solver import bootstrap

result = bootstrap(
    curves,
    instruments,
    target_curve_idx,
    tol=None,
    max_outer=None,
    max_brent=None,
    bracket_low=None,
    bracket_high=None,
)

Parameters

NameTypeDefaultDescription
curveslist[Curve]requiredCurves to calibrate
instrumentslist[dict]requiredInstruments in dict format (see keys below)
target_curve_idxintrequiredIndex of curve to calibrate in curves list
tolfloat | NoneNoneRoot-finding tolerance (default: 1e-12)
max_outerint | NoneNoneMaximum outer iterations for global interpolators (default: 10)
max_brentint | NoneNoneMaximum Brent iterations per node (default: 100)
bracket_lowfloat | NoneNoneLower bracket for Brent search (default: -0.05)
bracket_highfloat | NoneNoneUpper bracket for Brent search (default: 0.30)

The instruments list uses dict format with keys:

KeyTypeRequiredDescription
instrumentobjectyesInstrument's inner Rust binding (e.g., irs._inner)
targetfloatyesTarget rate to calibrate to
disc_curve_idxintyesIndex of discount curve in curves list
forecast_curve_idxintnoIndex of forecast curve (defaults to disc_curve_idx)
labelstrnoInstrument label for diagnostics

Returns: calibrated curve (same type as curves[target_curve_idx]).

Note: The number of instruments must equal the number of calibratable nodes on the target curve (all nodes except the anchor node at the curve's start date). Each instrument calibrates one node, ordered by maturity.

Note: Unlike Solver, bootstrap requires the instrument's internal Rust binding via ._inner. The Solver wrapper handles this conversion automatically, but bootstrap operates at the lower level.

When to use bootstrap() vs Solver: bootstrap() performs node-by-node Brent root-finding -- fast and exact for sequential calibration where each instrument maps to one curve node. Use Solver when instruments share nodes, when you need Levenberg-Marquardt optimization, or when calibrating multiple curves simultaneously.

Example

import datetime
from vade import DiscountCurve, Deposit, IRS
from vade.solver import bootstrap

effective = datetime.date(2025, 6, 16)

# Curve with one anchor node (effective=1.0) plus one node per instrument
curve = DiscountCurve(
    {
        effective: 1.0,
        datetime.date(2025, 12, 16): 1.0,
        datetime.date(2026, 6, 16): 1.0,
        datetime.date(2027, 6, 16): 1.0,
    },
    interpolation="log_linear",
    convention="act360",
)

# Instruments with rate=0.0 / fixed_rate=0.0 (targets come from instruments dict)
dep_6m = Deposit(effective=effective, termination="6m", rate=0.0, convention="act360")
irs_1y = IRS(effective=effective, termination="1y", frequency="a", fixed_rate=0.0,
             convention="act360", float_convention="act360")
irs_2y = IRS(effective=effective, termination="2y", frequency="a", fixed_rate=0.0,
             convention="act360", float_convention="act360")

# Use ._inner for Rust-level instrument bindings
# bracket_low/bracket_high set for DF-space search (not rate-space)
calibrated = bootstrap(
    curves=[curve],
    instruments=[
        {"instrument": dep_6m._inner, "target": 0.0425, "disc_curve_idx": 0, "label": "6M Depo"},
        {"instrument": irs_1y._inner, "target": 4.00, "disc_curve_idx": 0, "label": "1Y IRS"},
        {"instrument": irs_2y._inner, "target": 3.85, "disc_curve_idx": 0, "label": "2Y IRS"},
    ],
    target_curve_idx=0,
    bracket_low=0.5,
    bracket_high=1.5,
)

# Verify calibration: discount factors decrease with maturity
df_6m = calibrated.discount_factor(datetime.date(2025, 12, 16))
df_1y = calibrated.discount_factor(datetime.date(2026, 6, 16))
df_2y = calibrated.discount_factor(datetime.date(2027, 6, 16))
assert 0.95 < float(df_6m) < 1.0
assert 0.90 < float(df_1y) < 1.0
assert 0.85 < float(df_2y) < 0.98
assert float(df_6m) > float(df_1y) > float(df_2y)

# Repricing verification: calibrated curve reproduces target rates
repriced_6m = float(dep_6m.rate(calibrated))
assert abs(repriced_6m - 0.0425) < 1e-6

See also: Bootstrap vs Parametric Calibration for a full walkthrough comparing bootstrap and Solver approaches.

For most calibration workflows, prefer Solver which provides richer diagnostics (convergence info, Jacobian, delta/gamma risk). Bootstrap is useful when you need node-by-node stripping with local interpolators (e.g., "flat_forward").

On this page