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 bootstrapSee 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
| Property | Type | Description |
|---|---|---|
.converged | bool | Whether calibration converged |
.iterations | int | Number of iterations performed |
.objective | float | Final objective function value (sum of squared residuals) |
.residuals | list[float] | Per-instrument residuals (target - computed) |
.jacobian | numpy.ndarray | 2D Jacobian matrix, shape (n_instruments, n_vars) |
.condition_number | float | Condition number of the Jacobian |
.status | str | Status message describing termination reason |
.time_seconds | float | Calibration wall-clock time in seconds |
.algorithm | str | Algorithm 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) # TrueSolver
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
| Name | Type | Default | Description |
|---|---|---|---|
curves | list[DiscountCurve | LineCurve] | required | Curves to calibrate (node values are adjusted) |
instruments | list[tuple | dict] | required | Calibration instruments with targets (see Instruments Format) |
weights | list[float] | None | None | Per-instrument weights (uniform if None) |
func_tol | float | 1e-14 | Function tolerance for convergence |
grad_tol | float | 1e-14 | Gradient tolerance for convergence |
max_iter | int | 100 | Maximum iterations |
ini_lambda | tuple[float, float, float] | None | None | LM damping parameters (initial, factor_up, factor_down) |
pre_solvers | list[Solver] | None | None | Solvers to run first (staged calibration for multi-curve frameworks) |
fx_rates | FXRates | None | None | FX rates for cross-currency calibration |
algorithm | str | "levenberg_marquardt" | Optimization algorithm: "levenberg_marquardt", "gauss_newton", "gradient_descent" |
learning_rate | float | None | None | Learning rate for gradient descent (default 1.0 with Armijo backtracking) |
See DiscountCurve, LineCurve for curves. See FXRates for cross-currency calibration.
Methods
| Method | Returns | Description |
|---|---|---|
.iterate() | SolverResult | Run calibration, returns result with convergence info |
.get_curve(index=0) | Curve | Get calibrated curve by index |
.get_curve_by_id(curve_id) | Curve | Get calibrated curve by its id string |
.delta(instrument, result) | DataFrame | Bucket-level delta risk (columns: instrument_label, tenor, sensitivity, currency) |
.gamma(instrument, result) | DataFrame | Cross-gamma matrix as labeled DataFrame |
.to_json() | str | Serialize solver state to JSON |
.from_json(s) (classmethod) | Solver | Deserialize 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 # Truebootstrap
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
| Name | Type | Default | Description |
|---|---|---|---|
curves | list[Curve] | required | Curves to calibrate |
instruments | list[dict] | required | Instruments in dict format (see keys below) |
target_curve_idx | int | required | Index of curve to calibrate in curves list |
tol | float | None | None | Root-finding tolerance (default: 1e-12) |
max_outer | int | None | None | Maximum outer iterations for global interpolators (default: 10) |
max_brent | int | None | None | Maximum Brent iterations per node (default: 100) |
bracket_low | float | None | None | Lower bracket for Brent search (default: -0.05) |
bracket_high | float | None | None | Upper bracket for Brent search (default: 0.30) |
The instruments list uses dict format with keys:
| Key | Type | Required | Description |
|---|---|---|---|
instrument | object | yes | Instrument's inner Rust binding (e.g., irs._inner) |
target | float | yes | Target rate to calibrate to |
disc_curve_idx | int | yes | Index of discount curve in curves list |
forecast_curve_idx | int | no | Index of forecast curve (defaults to disc_curve_idx) |
label | str | no | Instrument 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-6See 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").