Calibration
Calibrate single and multi-curve frameworks using the Solver with Levenberg-Marquardt, Gauss-Newton, and gradient descent algorithms.
Single-Curve Calibration
Calibrate a single USD SOFR DiscountCurve using the default Levenberg-Marquardt algorithm. Define curve nodes at instrument maturity dates with initial discount factors of 1.0, then let the solver find the correct values.
import datetime
from vade import DiscountCurve, IRS, Solver
effective = datetime.date(2025, 6, 16)
nodes = {
effective: 1.0,
datetime.date(2026, 6, 16): 1.0,
datetime.date(2027, 6, 16): 1.0,
datetime.date(2028, 6, 16): 1.0,
datetime.date(2030, 6, 16): 1.0,
datetime.date(2035, 6, 16): 1.0,
}
curve = DiscountCurve(
nodes, interpolation="log_linear", convention="act360", id="sofr"
)
irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360")
irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360")
irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360")
irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360")
irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360")
instruments = [
(irs_1y, 4.00),
(irs_2y, 3.85),
(irs_3y, 3.75),
(irs_5y, 3.70),
(irs_10y, 3.85),
]
solver = Solver(curves=[curve], instruments=instruments)
result = solver.iterate()
result.converged # True
result.iterations > 0 # TrueSolver Diagnostics
Inspect calibration quality through the SolverResult diagnostics. Residuals show per-instrument calibration error and the Jacobian reveals sensitivity structure.
import datetime
from vade import DiscountCurve, IRS, Solver
effective = datetime.date(2025, 6, 16)
nodes = {
effective: 1.0,
datetime.date(2026, 6, 16): 1.0,
datetime.date(2027, 6, 16): 1.0,
datetime.date(2028, 6, 16): 1.0,
datetime.date(2030, 6, 16): 1.0,
datetime.date(2035, 6, 16): 1.0,
}
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360", id="sofr")
irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360")
irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360")
irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360")
irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360")
irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360")
solver = Solver(
curves=[curve],
instruments=[(irs_1y, 4.00), (irs_2y, 3.85), (irs_3y, 3.75), (irs_5y, 3.70), (irs_10y, 3.85)],
)
result = solver.iterate()
result.objective < 1e-10 # True
max(abs(r) for r in result.residuals) < 1e-8 # True
result.jacobian.shape # (5, 5)
result.condition_number > 0 # True
calibrated = solver.get_curve(0)
float(calibrated.discount_factor(datetime.date(2026, 6, 16))) < 1.0 # True
float(calibrated.discount_factor(datetime.date(2035, 6, 16))) > 0.5 # True
float(calibrated.zero_rate(effective, datetime.date(2026, 6, 16))) > 0.03 # TrueMulti-Curve Calibration
Separate OIS discounting from forward projection using pre_solvers. The inner solver calibrates the OIS discount curve, and the outer solver calibrates the forward curve while using the pre-calibrated discount curve for present-value calculations.
import datetime
from vade import DiscountCurve, IRS, Solver
effective = datetime.date(2025, 6, 16)
# Stage 1: OIS discount curve (slightly lower rates than Term SOFR)
disc_nodes = {
effective: 1.0,
datetime.date(2026, 6, 16): 1.0,
datetime.date(2027, 6, 16): 1.0,
datetime.date(2028, 6, 16): 1.0,
}
disc_curve = DiscountCurve(
disc_nodes, interpolation="log_linear", convention="act360", id="ois_disc"
)
ois_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=3.90, convention="act360", float_convention="act360")
ois_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360")
ois_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.65, convention="act360", float_convention="act360")
inner_solver = Solver(
curves=[disc_curve],
instruments=[(ois_1y, 3.90), (ois_2y, 3.75), (ois_3y, 3.65)],
)
# Stage 2: Forward projection curve
fwd_nodes = {
effective: 1.0,
datetime.date(2026, 6, 16): 1.0,
datetime.date(2027, 6, 16): 1.0,
datetime.date(2028, 6, 16): 1.0,
}
fwd_curve = DiscountCurve(
fwd_nodes, interpolation="log_linear", convention="act360", id="sofr_fwd"
)
fwd_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360")
fwd_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360")
fwd_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360")
outer_solver = Solver(
curves=[fwd_curve],
instruments=[(fwd_1y, 4.00), (fwd_2y, 3.85), (fwd_3y, 3.75)],
pre_solvers=[inner_solver],
)
mc_result = outer_solver.iterate()
mc_result.converged # True
mc_result.iterations > 0 # TrueThe dict instrument format gives explicit control over which curves are used for discounting and forecasting. Use disc_curve_idx and forecast_curve_idx to map each instrument to the correct curve when a single solver holds multiple curves.
import datetime
from vade import DiscountCurve, IRS, Solver
effective = datetime.date(2025, 6, 16)
disc_curve = DiscountCurve(
{effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0},
interpolation="log_linear", convention="act360", id="disc",
)
fwd_curve = DiscountCurve(
{effective: 1.0, datetime.date(2026, 6, 16): 1.0, datetime.date(2027, 6, 16): 1.0},
interpolation="log_linear", convention="act360", id="fwd",
)
irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360")
irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360")
instruments = [
{"instrument": irs_1y, "target": 4.00, "disc_curve_idx": 0, "forecast_curve_idx": 1, "label": "1Y_IRS", "currency": "USD"},
{"instrument": irs_2y, "target": 3.85, "disc_curve_idx": 0, "forecast_curve_idx": 1, "label": "2Y_IRS", "currency": "USD"},
]
solver = Solver(curves=[disc_curve, fwd_curve], instruments=instruments)
result = solver.iterate()
result.converged # TrueAlgorithm Comparison
Compare Levenberg-Marquardt, Gauss-Newton, and gradient descent on the same single-curve calibration problem. Each algorithm needs a fresh curve since the solver mutates node values in place.
import datetime
from vade import DiscountCurve, IRS, Solver
effective = datetime.date(2025, 6, 16)
irs_1y = IRS(effective=effective, termination="1Y", frequency="a", fixed_rate=4.00, convention="act360", float_convention="act360")
irs_2y = IRS(effective=effective, termination="2Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360")
irs_3y = IRS(effective=effective, termination="3Y", frequency="a", fixed_rate=3.75, convention="act360", float_convention="act360")
irs_5y = IRS(effective=effective, termination="5Y", frequency="a", fixed_rate=3.70, convention="act360", float_convention="act360")
irs_10y = IRS(effective=effective, termination="10Y", frequency="a", fixed_rate=3.85, convention="act360", float_convention="act360")
instruments = [
(irs_1y, 4.00),
(irs_2y, 3.85),
(irs_3y, 3.75),
(irs_5y, 3.70),
(irs_10y, 3.85),
]
def make_curve():
return DiscountCurve(
{
effective: 1.0,
datetime.date(2026, 6, 16): 1.0,
datetime.date(2027, 6, 16): 1.0,
datetime.date(2028, 6, 16): 1.0,
datetime.date(2030, 6, 16): 1.0,
datetime.date(2035, 6, 16): 1.0,
},
interpolation="log_linear",
convention="act360",
id="sofr",
)
# Levenberg-Marquardt (default)
solver_lm = Solver(curves=[make_curve()], instruments=instruments)
result_lm = solver_lm.iterate()
result_lm.converged # True
result_lm.algorithm # 'levenberg_marquardt'
# Gauss-Newton
solver_gn = Solver(curves=[make_curve()], instruments=instruments, algorithm="gauss_newton")
result_gn = solver_gn.iterate()
result_gn.converged # True
result_gn.algorithm # 'gauss_newton'
# Gradient Descent
solver_gd = Solver(curves=[make_curve()], instruments=instruments, algorithm="gradient_descent")
result_gd = solver_gd.iterate()
result_gd.converged # True
result_gd.algorithm # 'gradient_descent'
result_gd.iterations >= result_lm.iterations # TrueNext Steps
- Risk -- compute delta, gamma, and bucket-level sensitivities from calibrated curves
- Quick Start -- see the full pipeline in one workflow
Serialization
All major curve types and the [Solver](../api/solver.md#solver) support JSON
Rates
Vade's rates analytics cover interest rate curve construction, instrument pricing, risk sensitivities, and volatility products. Build discount and forward curves from market data, price linear and non-linear derivatives, and compute AD-powered greeks -- all from a single calibrated curve object.