Esc

    The API is under testing and may change before release.

    Calibrate Hull-White and pin it

    You'll fit Hull-White's volatility to the swaptions a callable bond could be called into, get back a market with the fit pinned, and price the bond on it.

    Before you start

    • helixio installed: see installing.
    • The callable bond's terms, a curve, and one at-the-money normal vol per call date.
    • A mean reversion. The fit holds it fixed; see Hull-White calibration for how to pick one.

    Steps

    1. Book the callable: a fixed-rate bond and its call schedule.
    2. Mark the curve, read the bond's strip with models.strip_from_callable, and quote one vol per strip point with with_flat_swaption_vol.
    3. Fit and pin in one call: calibration.hull_white_pinned(market, callable, mean_reversion) returns the pinned market and the fit.
    4. Price the callable on the pinned market.

    The comments in the program match these steps.

    Rust
    use chrono::NaiveDate;
    use helixrs::{
        AnyInstrument, Callability, CallableBond, CollateralSpec, Currency, DayCountFraction,
        FixedRateBond, Frequency, Locator, MarketContext, QuoteSheet, SigmaShape, calibrate_and_pin,
        hull_white_mean_reversion_label, hull_white_sigma_label, strip_from_callable,
    };
    
    type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
    
    fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    fn main() -> Outcome<()> {
        let close = date((2026, 10, 1));
        let maturity = date((2032, 10, 1));
        let mean_reversion = 0.03;
        let usd: Currency = "USD".parse()?;
    
        // Step 1: the callable. A six-year 4.75% semiannual bond, callable at par on
        // three anniversaries.
        let bond = FixedRateBond::bullet(usd, close, maturity, 0.0475, Frequency::SemiAnnual, DayCountFraction::Thirty360, 100.0)?;
        let calls: Vec<Callability> = (0..3).map(|k| Callability::call(date((2028 + k, 10, 1)), 100.0)).collect();
        let callable = CallableBond::new(bond, calls.clone());
    
        // Step 2: a curve, and one at-the-money normal vol per swaption the bond
        // implies: one per call date, each on a swap running to maturity.
        let mut sheet = QuoteSheet::new();
        for (tenor, quote) in [("1Y", 0.0441), ("2Y", 0.0452), ("5Y", 0.0468), ("10Y", 0.0481)] {
            sheet = sheet.with("USD-SOFR-OIS", Locator::parse(tenor)?, quote);
        }
        let mut market = MarketContext::as_of(close).with_recipe_quotes(&CollateralSpec::Currency(usd), &sheet)?;
        let strip = strip_from_callable(&callable, close);
        for (point, vol) in strip.iter().zip([0.0105, 0.0101, 0.0098]) {
            market = market.with_flat_swaption_vol(usd, point.expiry, point.end, vol);
        }
    
        // Step 3: fit σ with the mean reversion you chose, and get the market back with
        // both pinned.
        let (pinned, fit) = calibrate_and_pin(&market, &callable, mean_reversion, SigmaShape::Flat)?;
        for row in &fit.fits {
            println!(
                "{} into {}  price {:.4}  model {:.4}  vol misfit {:+.2} bp",
                row.point.expiry,
                row.point.end,
                row.market,
                row.model,
                row.vol_misfit * 1e4
            );
        }
        let sigma = fit.flat().expect("a flat fit");
        println!("sigma {sigma:.6} at mean reversion {:.2}", fit.mean_reversion);
    
        // Step 4: price the callable on the pinned market.
        let instrument = AnyInstrument::Callable(Box::new(callable));
        let value = instrument.npv(&pinned)?;
        println!("callable value  {value:.6}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    
    CLOSE = dt.date(2026, 10, 1)
    MATURITY = dt.date(2032, 10, 1)
    MEAN_REVERSION = 0.03
    
    # Step 1: the callable. A six-year 4.75% semiannual bond, callable at par on
    # three anniversaries.
    bond = hlx.products.fixed_rate_bond(
        "USD", CLOSE, MATURITY, coupon=0.0475, frequency="SemiAnnual", day_count="30/360"
    )
    calls = [(dt.date(2028 + k, 10, 1), 100.0, "call") for k in range(3)]
    callable_bond = hlx.products.callable_bond(bond, calls)
    
    # Step 2: a curve, and one at-the-money normal vol per swaption the bond
    # implies: one per call date, each on a swap running to maturity.
    sheet = {"USD-SOFR-OIS": {"1Y": 0.0441, "2Y": 0.0452, "5Y": 0.0468, "10Y": 0.0481}}
    market = hlx.MarketContext(CLOSE).with_recipe_quotes("USD", sheet)
    strip = hlx.models.strip_from_callable(callable_bond, CLOSE)
    for (expiry, end), vol in zip(strip, [0.0105, 0.0101, 0.0098]):
        market = market.with_flat_swaption_vol("USD", expiry, end, vol)
    
    # Step 3: fit σ with the mean reversion you chose, and get the market back with
    # both pinned.
    pinned, fit = hlx.calibration.hull_white_pinned(market, callable_bond, MEAN_REVERSION)
    rows = fit.fits().to_dict()
    for expiry, end, quoted, model, misfit in zip(rows["expiry"], rows["end"], rows["market"], rows["model"], rows["vol_misfit"]):
        print(f"{expiry} into {end}  price {quoted:.4f}  model {model:.4f}  vol misfit {misfit * 1e4:+.2f} bp")
    print(f"sigma {fit.sigma():.6f} at mean reversion {fit.mean_reversion():.2f}")
    
    # Step 4: price the callable on the pinned market.
    value = callable_bond.npv(pinned)
    print(f"callable value  {value:.6f}")
    2028-10-01 into 2032-10-01  price 0.0193  model 0.0188  vol misfit -2.81 bp
    2029-10-01 into 2032-10-01  price 0.0166  model 0.0168  vol misfit +1.14 bp
    2030-10-01 into 2032-10-01  price 0.0121  model 0.0126  vol misfit +4.17 bp
    sigma 0.010638 at mean reversion 0.03
    callable value  98.067145

    Check it worked

    • The fit has one row per call date, each on a swap running to the bond's maturity.
    • With a flat σ\sigma the misfits are small and of both signs. If one point dominates, try shape="piecewise", which matches every quote.
    • The bond prices on the pinned market. On the market before pinning it raises a PricingError, because no model volatility is filed there.
    • Hull-White calibration, the reference for the fit and its shapes.
    • Callable bonds, for the bond's option-adjusted spread and risk.

    From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.