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.
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, ¶ms)?;
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(())
}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 and its volatility follow
with a shift that admits negative forwards. Hagan's expansion gives an implied vol in closed form, lognormal or normal. Given an ATM vol, α is the root of . What each parameter does to the smile is in how calibration works.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the vol at one strike | sabr_vol with SabrParams |
vols.sabr_vol |
|
| α from | 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 ; 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:
HaganNormalreads normal vols,HaganLognormallognormal ones, andNormalBetaZeroreads a negative forward without a shift. - A dense ATM matrix:
vols.calibrate_sabr_with_atmtakes 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.