Esc

    The API is under testing and may change before release.

    SABR

    This page lists what you need to read a rate smile from SABR's four parameters, and to fit those parameters to a quoted smile: the formulas, the ATM root for α, the SabrSpec fields and the fit's report.

    Quick example

    This takes α from an at-the-money normal vol, reads the smile across seven strikes, then puts that smile into a quoted cube and fits it back. The fit returns the ρ and ν that made the smile. The example states its own numbers and runs as shown.

    Rust
    use helixrs::{
        RateVolSurface, SabrFormula, SabrParams, SabrSpec, VolConvention, alpha_from_atm,
        calibrate_sabr, sabr_vol,
    };
    
    type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
    
    const FORWARD: f64 = 0.045;
    const ATM: f64 = 0.0100;
    const EXPIRY: f64 = 1.0;
    const BETA: f64 = 0.5;
    const RHO: f64 = -0.2;
    const NU: f64 = 0.35;
    const SPREADS: [f64; 7] = [-0.02, -0.01, -0.005, 0.0, 0.005, 0.01, 0.02];
    
    fn main() -> Outcome<()> {
        let formula = SabrFormula::HaganNormal;
    
        // α from the ATM vol, then the normal-vol smile across strikes around the
        // forward.
        let alpha = alpha_from_atm(formula, FORWARD, ATM, EXPIRY, BETA, RHO, NU, 0.0)?;
        let params = SabrParams { alpha, beta: BETA, rho: RHO, nu: NU, shift: 0.0 };
        let mut smile = Vec::new();
        for spread in SPREADS {
            let vol = sabr_vol(formula, FORWARD, FORWARD + spread, EXPIRY, &params)?;
            println!("strike {:.4}  normal vol {vol:.6}", FORWARD + spread);
            smile.push(vol);
        }
    
        // The smile as a quoted cube — one expiry, one tenor, strikes as spreads to the
        // forward — fitted under the same formula and β. ρ and ν come back.
        let cube = RateVolSurface::cube(vec![EXPIRY], vec![5.0], SPREADS.to_vec(), &[vec![smile.clone()]], VolConvention::Normal)?
            .over_the_forward();
        let spec = SabrSpec::new(formula, BETA, 0.0)?.with_accept(2e-3).with_max_restarts(20);
        println!(
            "beta {:.2}  shift {:.4}  accept {:.3}  restarts {}",
            spec.beta, spec.shift, spec.accept, spec.max_restarts
        );
        let fit = calibrate_sabr(&cube, &[vec![FORWARD]], &spec)?;
        let slice = &fit.slices[0];
        println!("rho {:.10}  nu {:.10}", slice.rho, slice.nu);
    
        Ok(())
    }
    Python
    import helixio as hlx
    
    vols = hlx.vols
    FORWARD, ATM, EXPIRY, BETA, RHO, NU = 0.045, 0.0100, 1.0, 0.5, -0.2, 0.35
    SPREADS = [-0.02, -0.01, -0.005, 0.0, 0.005, 0.01, 0.02]
    
    # α from the ATM vol, then the normal-vol smile across strikes around the
    # forward.
    alpha = vols.alpha_from_atm("HaganNormal", FORWARD, ATM, EXPIRY, BETA, RHO, NU)
    smile = [vols.sabr_vol("HaganNormal", FORWARD, FORWARD + s, EXPIRY, alpha, BETA, RHO, NU) for s in SPREADS]
    for spread, vol in zip(SPREADS, smile):
        print(f"strike {FORWARD + spread:.4f}  normal vol {vol:.6f}")
    
    # The smile as a quoted cube — one expiry, one tenor, strikes as spreads to the
    # forward — fitted under the same formula and β. ρ and ν come back.
    cube = vols.RateVolSurface.cube([EXPIRY], [5.0], SPREADS, [[smile]], vol_convention="normal", strike_axis="spread_to_atm")
    spec = vols.SabrSpec("HaganNormal", BETA, 0.0)
    print(f"beta {spec.beta:.2f}  shift {spec.shift:.4f}  accept {spec.accept:.3f}  restarts {spec.max_restarts}")
    fit = vols.calibrate_sabr(cube, [[FORWARD]], spec)
    slice_ = {name: column[0] for name, column in fit.diagnostics().to_dict().items()}
    print(f"rho {slice_['rho']:.10f}  nu {slice_['nu']:.10f}")
    strike 0.0250  normal vol 0.010073
    strike 0.0350  normal vol 0.009940
    strike 0.0400  normal vol 0.009936
    strike 0.0450  normal vol 0.010000
    strike 0.0500  normal vol 0.010145
    strike 0.0550  normal vol 0.010373
    strike 0.0650  normal vol 0.011048
    beta 0.50  shift 0.0000  accept 0.002  restarts 20
    rho -0.2000000000  nu 0.3500000000

    The vol at the forward, 4.50%, is the ATM quote exactly, because α was solved to give it back.

    How it's priced

    The forward FF and its volatility α\alpha follow

    dFt=αt (Ft+s)β dWt,dαt=ν αt dZt,dWt dZt=ρ dt,dF_t = \alpha_t\, (F_t + s)^{\beta}\, dW_t, \qquad d\alpha_t = \nu\, \alpha_t\, dZ_t, \qquad dW_t\, dZ_t = \rho\, dt ,

    with ss a shift that admits negative forwards. Hagan's expansion gives an implied vol σ(K;F,T,α,β,ρ,ν)\sigma(K; F, T, \alpha, \beta, \rho, \nu) in closed form, lognormal or normal. Given an ATM vol, α is the root of σ(F;F,T,α,β,ρ,ν)=σATM\sigma(F; F, T, \alpha, \beta, \rho, \nu) = \sigma_{\text{ATM}}. What each parameter does to the smile is in how calibration works.

    API

    Symbol Meaning Rust Python
    σ(K;… )\sigma(K; \dots) the vol at one strike sabr_vol with SabrParams vols.sabr_vol
    α from σATM\sigma_{\text{ATM}} the ATM root alpha_from_atm vols.alpha_from_atm
    formula lognormal, normal or normal β = 0 SabrFormula vols.SabrFormula
    quoted smile vols by expiry, tenor and strike RateVolSurface::cube, over_the_forward vols.RateVolSurface.cube with strike_axis="spread_to_atm"
    fit spec formula, β, shift and acceptance SabrSpec vols.SabrSpec
    fit ρ and ν per node calibrate_sabr → SabrFit vols.calibrate_sabr → vols.SabrFit

    vols.sabr_vol(formula, forward, strike, expiry_years, alpha, beta, rho, nu, shift=0.0) reads one point; vols.alpha_from_atm(formula, forward, atm_vol, expiry_years, beta, rho, nu, shift=0.0) returns the α that gives atm_vol back. vols.SabrSpec takes:

    Argument Type Default Meaning
    formula SabrFormula or str required "HaganLognormal", "HaganNormal" or "NormalBetaZero"
    beta float required the CEV exponent, stated by the desk rather than fitted
    shift float required the displacement ss; 0.0 for none
    accept float 2e-3 the acceptance: the largest vega-weighted RMS vol error, as a fraction of the slice's mean quote
    max_restarts int 20 how many restarts from new starting points a slice may spend reaching it

    In Rust, SabrSpec::new(formula, beta, shift) takes the first three, and with_accept and with_max_restarts set the others. vols.calibrate_sabr(grid, forwards, spec) fits every node of the cube, each at forwards[expiry][tenor]. Its SabrFit has surface, the fitted surface, and diagnostics(), one row per node with the forward, the parameters, the errors, the restarts used, and whether the fit was accepted.

    Defaults and limits

    • What's fitted: β and the shift are stated; α comes from the ATM vol; ρ and ν are fitted to the wings.
    • Formulas: HaganNormal reads normal vols, HaganLognormal lognormal ones, and NormalBetaZero reads a negative forward without a shift.
    • A dense ATM matrix: vols.calibrate_sabr_with_atm takes the ATM vol from a full matrix and ρ and ν from a sparser smile cube; every cube node must be a matrix node.

    See also

    • Source: crates/helixrs/src/models/sabr.rs, crates/helixrs/src/calibration/sabr.rs
    • Related pages:
      • Hull-White calibration, for fitting a short-rate model instead.
      • Coupon options, for the caplets a cap smile prices.

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