Use case · Surfaces

Run the canonical smile-panel in one call

surface_smile_risk_metrics returns the standard morning skew panel from one calibrated smile — ATM IV at the effective forward, the 25Δ and 10Δ risk reversals, and the 25Δ / 10Δ butterfly flies — replacing the handful of hand-rolled delta-to-strike lookups a desk would otherwise wire up.

When to use it

  • You have one calibrated parametric smile (SVI / SSVI / SABR) at one expiry and want the full skew panel — ATM IV, RR25, RR10, fly25, fly10 — in one round trip.
  • You want both delta pairs computed against the same effective forward and market context, so the two ATM IVs agree by construction.
  • You need per-wing snap-to-listed-strike output plus bracket-quality diagnostics for readiness / degradation signals.

Example

use ferro_risk::{
    surface_smile_risk_metrics, PricingModel, SurfaceDeltaConvention,
    SurfaceSmileDeltaSelectionPolicy,
};

// `smile` is a calibrated SviSmile / SsviSlice / SabrSmile; `market` is the
// SurfaceMarketContext used to calibrate it. `listed_strikes` is the venue grid.
let metrics = surface_smile_risk_metrics(
    &smile,
    &market,
    SurfaceDeltaConvention::Forward,
    PricingModel::BlackScholesMerton,
    SurfaceSmileDeltaSelectionPolicy::default(),
    &listed_strikes,
)?;

let atm_iv = metrics.atm_iv();              // ATM IV at the effective forward
let rr_25  = metrics.rr_25().risk_reversal(); // call_iv - put_iv at 25Δ
let rr_10  = metrics.rr_10().risk_reversal(); // call_iv - put_iv at 10Δ
let fly_25 = metrics.fly_25();              // 25Δ butterfly fly
let fly_10 = metrics.fly_10();              // 10Δ butterfly fly

// Per-wing bracket diagnostics on the 25Δ put wing.
let put = metrics.rr_25().put_wing();
let snap_distance = put.bracket_delta_distance(); // delta-space snap-to-target gap
let lever_arm     = put.bracket_spread();         // Option<f64>: interpolation width
# Ok::<(), ferro_risk::FerroRiskError>(())

The readout

AccessorDescription
metrics.atm_iv()Smile IV at the effective forward.
metrics.rr_25() / rr_10()The 25Δ / 10Δ risk reversal (each with its own wings, butterfly, and ATM IV).
metrics.fly_25() / fly_10()25Δ / 10Δ butterfly fly.
.risk_reversal()On a risk reversal: call-wing IV minus put-wing IV, in decimal vol.
.put_wing() / .call_wing()The two wings of a risk reversal.
wing.theoretical_strike()Strike from the continuous delta inversion — where IV is read.
wing.selected_strike()Nearest eligible listed strike (for order routing).
wing.bracket_delta_distance()Snap-to-target distance: |Δ|-space gap between the requested target and the selected strike. Zero on an exact listed-strike hit.
wing.bracket_spread()Interpolation lever-arm: the |Δ|-space width of the bracket. None on the continuous parametric path.

Notes

  • SurfaceDeltaConvention has two variants: Forward (undiscounted forward delta — the typical desk choice) and Spot (PV-sensitivity delta, scaled by the discount factor).
  • IVs, RR, and flies are computed at the theoretical strikes, so the numbers track the requested delta rather than the snapped grid; selected_strike() is for routing.
  • The two bracket diagnostics are orthogonal — two wings can both read Interpolated yet differ in trustworthiness. Threshold on both bracket_delta_distance() and bracket_spread() instead of trusting the binary quality flag.
  • The 25Δ RR is computed first and short-circuits on failure — a chain whose 25Δ wings are uninvertible yields no result. Call surface_smile_risk_reversal at |Δ| = 0.10 directly if you need independent tail metrics.
Note

The bundle is a single source of truth: atm_iv(), fly_25(), and fly_10() are accessors over the two risk reversals, not separately stored fields.

σ-standardized wings, when fixed delta drifts

Fixed-delta anchors move with the vol level and with DTE, so a 25Δ wing read today and a 25Δ wing read six months ago are not sampling the same point on the smile — which makes them a poor axis for a z-score baseline. surface_select_standardized_moneyness_wing_strikes anchors instead on a signed multiple m of the log-moneyness scale σ_ATM·√T from the forward:

K = F · exp(m · σ_ATM · √T)
use ferro_risk::surface_select_standardized_moneyness_wing_strikes;

// One calibrated smile in; tenor and σ_ATM are read off the smile itself, so a
// live read and a historical backfill compute the identical anchor.
let wings = surface_select_standardized_moneyness_wing_strikes(
    &smile,
    &market,
    1.0,               // the multiple k; put wing resolves at −k, call at +k
    &listed_strikes,
)?;

let put = wings.put_wing();
// put.standardized_moneyness() → -1.0
// put.strike(), put.implied_volatility(), put.quality()
# Ok::<(), ferro_risk::FerroRiskError>(())
  • Negative m is the downside / put wing by convention.
  • Each wing carries a SurfaceStandardizedMoneynessWingQuality Interpolated or Extrapolated against the listed-strike support — so a z-score can refuse a silently strike-extrapolated wing rather than trade an unreliable read. The smile is read at the exact anchor, never snapped to it.
  • The selection echoes sigma_multiple(), sigma_atm(), forward(), and expiry() as provenance, and its validating serde shadow recomputes each strike from that provenance and rejects any payload that does not reproduce — the live/backfill parity guarantee.
  • surface_strike_from_standardized_moneyness returns the anchor alone when you have no listed-strike support to grade against.