Hull-White Model
One-factor Gaussian short-rate model, src/models/hullwhite/:
\[ dr_t = \bigl(\theta(t) - \alpha r_t\bigr)dt + \sigma(t)\,dW_t . \]
API
pub struct HullWhite<'a, T: Scalar> {
alpha: T,
curve: &'a dyn InterestRatesTermStructure<T>,
calibration_quality: Option<HullWhiteCalibrationQuality>,
vol_func: Option<HullWhiteTimeDependentVolatility<T>>,
}
let mut hw = HullWhite::new(alpha, &sofr_curve).with_constant_volatility(0.01);
| Method | Formula |
|---|---|
B(t, T) | \(\frac{1-e^{-\alpha(T-t)}}{\alpha}\) |
A(t, T, sigma, curve) | \(\frac{P(0,T)}{P(0,t)}\exp\!\bigl(B\,f(0,t) - \frac{\sigma^2}{4\alpha}(1-e^{-2\alpha t})B^2\bigr)\) |
zcb_price(r_t, t, T, sigma, curve) | \(A(t,T)\,e^{-B(t,T)r_t}\) |
zcb_price_volatility(sigma, t, T) | \(\sigma B(t,T)\sqrt{\frac{1-e^{-2\alpha t}}{2\alpha}}\) |
theta(t, sigma, curve) | drift fitted to the initial curve |
caplet_price(strike, t, S, sigma, curve) | \((1+\tau K)\,\text{BondPut}(t,S,\frac1{1+\tau K})\) |
swaption_price(strike, t_option, &[(pay_time, accrual)], sigma, curve) | Jamshidian decomposition |
bond_put_price, bond_call_price | zero-coupon bond options |
The closed forms are shared with ClosedFormHullWhiteCapletPricer, ClosedFormHullWhiteCapPricer and ClosedFormHullWhiteSwaptionPricer (Caps and Floors, Swaptions).
Calibration
hw.calibrate("e_ids, "e_store, &curve, Level::Mid)?;
hw.calibrate_with_configuration(&config, &constructed_store, "e_store, &curve, Level::Mid)?;
ModelCalibrationConfiguration (JSON in examples/hullwhite/data/hw_calibration.json):
{
"source": { "Surface": { "market_index": "SOFR" } },
"quote_ids": [
"CapletFloorlet_USD_SOFR_3M_3M_Absolute_0.045_Straddle_Black",
"CapletFloorlet_USD_SOFR_3M_6M_Absolute_0.045_Straddle_Black",
"CapletFloorlet_USD_SOFR_3M_1Y_Absolute_0.045_Straddle_Black"
],
"strike": "Atm",
"alpha": 0.1
}
Algorithm, per calibration quote in expiry order:
- Parse the identifier to get expiry \(T_i\), index tenor and strike;
strike: "Atm"replaces the quoted strike with the forward. - Read the Black (or Normal) vol from the surface/cube and compute the market caplet/swaption price.
- Solve by bisection for the piecewise-constant \(\sigmai\) on \([T{i-1},T_i]\) such that the Hull-White price matches, keeping earlier pillars fixed.
Results are kept in HullWhiteCalibrationQuality { records: Vec<HullWhiteCalibrationRecord> }, each record holding identifier, expiry, t, big_t, market_vol, market_price, model_price, calibrated_sigma, forward_rate, effective_strike. HullWhiteTimeDependentVolatility::new(schedule).with_pillar_labels().with_ift_sensitivities() exposes the sigma pillars as labelled AD leaves so downstream prices carry sensitivities to the calibration quotes.
cargo run -p hullwhite bootstraps SOFR, builds the caplet surface, calibrates and prints a quality table (expiry, t, market vol, model implied vol, market price, model price, error) followed by ATM cap prices built from the calibrated model, then simulates paths using examples/hullwhite/data/simulation.json.
Simulation
{
"market_index": "SOFR",
"model": {
"HullWhite": {
"alpha": 0.1,
"volatility": {
"Calibrated": {
"source": { "Surface": { "market_index": "SOFR" } },
"quote_ids": ["..."],
"strike": "Atm",
"alpha": 0.1
}
}
}
},
"n_paths": 1000,
"seed": 42,
"horizon": "5Y",
"frequency": "Monthly"
}
SimulationBuilder calibrates (if Calibrated), then evolves \(r\) exactly on the date grid with the Gaussian transition \(r_{t+\Delta} = r_t e^{-\alpha\Delta} + \int\theta + \sigma\sqrt{\frac{1-e^{-2\alpha\Delta}}{2\alpha}}Z\). Discount factors along a path are zcb_price(r_t, t, T). In the XVA engine the same calibrated schedule is transferred to an LGM model via LgmRateModel::calibrated (LGM).