Pricing, IV,
Greeks.

Seven pricing models, SVI / SSVI / SABR surface calibration with fit-quality diagnostics, Dupire local-volatility extraction, and live chain analytics — DEX, GEX, VEX, exposure walls, ferro-wave scenario attribution. Full first- and second-order Greeks throughout.

7 models
BSM · Black-76 · Bachelier · Heston · Jumps · Local-Vol · Bjerksund
3 surfaces
SVI · SSVI · SABR — calibration + diagnostics
10 Greeks
First + Second Order

Three pillars on one evaluation path

Pricing with full Greeks coverage, surface calibration, local-vol extraction, and live chain analytics — built so consumers get the same numerical answer whether they call a single contract, fit a surface, or stream a venue's option chain.

7M

Seven pricing models

European to American, lognormal to normal, deterministic to stochastic vol, with jumps. Each model named explicitly at the call site; intrinsic at expiry is model-independent.

  • BlackScholesMerton · Black-76 · Bachelier
  • DisplacedBlack — shifted-lognormal forwards
  • Heston — stochastic-vol via Fang-Oosterlee COS
  • JumpDiffusion — Merton (1976) lognormal jumps
  • Bjerksund-Stensland 2002 — American
  • Plus opt-in CRR reference + Dupire local-vol kernels
  • Full 10-Greek coverage on every model
σ²

Surfaces, calibrated and diagnosed

SVI, SSVI, and SABR surface calibration with fit-quality diagnostics, pluggable losses, prior regularization, and static-arbitrage repair workflows. Jaeckel IV for per-contract inversion.

  • SVI / SSVI / SABR — public calibration entry points
  • Joint cross-tenor SSVI — Gatheral-Jacquier globals shared across the surface
  • Four-tier fit quality — Healthy / Acceptable / Degenerate / Failed
  • Pluggable loss + prior — Huber, Trimmed-MSE, Bayesian smoothing
  • Quality-filtered prep + bid-ask feasibility filter
  • Vega · bid-ask precision weighting axes for liquidity-aware SVI prep
  • Model-free put-call-parity forward recovery + consistency diagnostic
  • Cross-tenor parity forward curve — market-implied borrow / financing rate
  • Provenance-aware firewall — Live anomaly gates skip Modeled / Theoretical rows
  • Static-arbitrage diagnostics + repair workflows
  • Smile-panel readouts — 25Δ / 10Δ RR + fly + ATM IV with bracket diagnostics
  • Non-parametric short-DTE fallback for degenerate parametric smiles
  • Dupire local-vol extraction with PDE / forward-PDE / MC
CH

Live chain analytics

Deterministic ChainState snapshots with incremental refresh, derived market metrics, exposure analytics, and typed ferro-wave scenario attribution across baseline and shocked flow.

  • ChainState snapshots + incremental updates
  • ATM term structure · implied move · RR · butterfly
  • DEX / GEX / VEX / vanna / charm exposures
  • Call / put OI concentration · strike + expiry walls
  • Spot ladders · flip levels
  • ferro-wave scenario attribution — spot · vol · rate · time · interaction
  • Smile-aware delta selection · risk reversal · butterfly

From inputs to portfolio risk

Four layers. Inputs typed at the boundary, pricing dispatched explicitly across seven models, surfaces calibrated with fit-quality contracts, and analytics composed over ordered containers and live chain state — never hidden mutable state.

1

Inputs

PricingInputs, IvSolveInputs, and ChainState typed at the boundary. Time in years, rates and dividend yields continuously compounded — no implicit conversions.

2

Price

Seven models dispatch through PricingModel — BSM, Black-76, Bachelier, DisplacedBlack, Heston, JumpDiffusion, Bjerksund-Stensland — plus opt-in CRR reference and Dupire local-vol kernels.

3

Calibrate

SVI / SSVI / SABR surface calibration — per-slice or a joint cross-tenor Gatheral-Jacquier SSVI fit — with pluggable loss, prior regularization, and fit-quality classification. Dupire extracts local-vol from any calibrated surface. Smile-aware delta selectors snap to listed strikes.

4

Aggregate

Position / Strategy / Book / Portfolio containers feed aggregate Greek and bucket reports. ChainExposureState derives DEX / GEX / VEX with ferro-wave scenario attribution.

Six workflows on one evaluation path

Entry points across the three pillars. Each one is a 10–20 line snippet that takes the workflow from typed input shape to typed output. The deeper guides live in the docs.

01 · Pricing

Price a contract, read all ten Greeks

The single-contract path. One typed PricingInputs, one named model, one call returns the full first- and second-order Greek surface on a shared internal evaluation path — no per-Greek bumping pattern, no inconsistency between sensitivities computed on different paths.

use ferro_risk::{ExerciseStyle, OptionType, PricingInputs,
             PricingModel, greeks_all};

let inputs = PricingInputs {
    option_type: OptionType::Call,
    exercise_style: ExerciseStyle::European,
    spot: 4_850.0,
    strike: 4_900.0,
    time_to_expiry: 0.25,
    rate: 0.045,
    dividend_yield: 0.0,
    volatility: 0.22,
};

let greeks = greeks_all(&inputs, PricingModel::Black76)?;
// greeks.delta, .gamma, .theta, .vega, .rho,
// .vanna, .volga, .charm, .veta, .color
Read the full guide →
Long ATM call · payoff + 10-Greek readout
02 · Pricing

Solve IV, forward, and Greeks in one call

When a workload needs solved IV, explicit forward, AND Greeks on the same contract, contract_analyticsfuses them on a shared internal evaluation path. Jaeckel's Let's-Be-Rational drives the IV solve to machine precision in a handful of iterations.

use ferro_risk::{ExerciseStyle, IvSolveInputs, OptionType,
             PricingModel, contract_analytics};

let inputs = IvSolveInputs {
    option_type: OptionType::Call,
    exercise_style: ExerciseStyle::European,
    spot: 100.0,
    strike: 100.0,
    time_to_expiry: 0.5,
    rate: 0.03,
    dividend_yield: 0.01,
};

let analytics = contract_analytics(
    &inputs,
    8.50,  // observed market price
    PricingModel::BlackScholesMerton,
)?;

// analytics.iv, .forward, .greeks (all 10)
Read the full guide →
Jaeckel solver · |residual| vs iteration
03 · Surfaces

Calibrate a vol surface with fit-quality contract

From normalized chain quotes to a typed SviSmile / SsviSlice / SabrSmile with a SurfaceCalibrationReport that grades the fit Healthy / Acceptable / Degenerate / Failed. The grade follows the residual, not just the optimizer: a slice that exhausts the iteration budget without proving convergence is promoted to Healthy when its weighted IV-RMSE is decisively small (≤ 0.005) and it carries enough quotes — so liquid mid-DTE SPY / QQQ chains read Healthy across every 5–22 DTE band. The Acceptabletier still catches the tolerable-but-unconfirmed middle. Put-call-parity forward recovery reconstructs the forward and discount factor model-free — no rate or dividend input — so a missed discrete dividend can't hand calibration two inconsistent smiles and inflate IV-RMSE 11–34× on short-DTE chains. When per-slice parity is itself too noisy on a thin near-dated chain, the cross-tenor parity forward curve fits a borrow / financing rate across the whole term structure so one tenor can borrow strength from the rest; and when a short-DTE slice has too few quotes to identify its own wing, the joint Gatheral-Jacquier SSVI calibrator shares (η, γ, ρ)globally and drives the front end from the neighbouring tenors' term-structure fit. Pluggable loss + prior smooth the optimization; the non-parametric short-DTE path takes over when the parametric smile degenerates. The QuoteSource marker keeps the live-anomaly firewall from rejecting resampled-model or sandbox quotes blended into a fit.

use ferro_risk::{calibrate_svi_surface, FitQuality,
             SviCalibrationPolicy, SurfaceCalibrationSearchPolicy};

let policy = SviCalibrationPolicy::default()
    .with_search(SurfaceCalibrationSearchPolicy::default());

let report = calibrate_svi_surface(&input, &policy)?;

match report.fit_quality() {
    FitQuality::Healthy => use_parametric(&report),
    FitQuality::Acceptable { reason } => // RR25 / ATM IV /
        readouts_only(&report),       // term-structure
    FitQuality::Degenerate { reason } => // shape-model breakdown
        surface_raw_interpolated_skew(/* … */)?,
    FitQuality::Failed { reason } => bail_or_retry(reason),
    _ => unreachable!(),
}
Read the full guide →
SVI fit · SPY 30D · 2026-05-07 · 11 OTM quotes
04 · Surfaces

Run the canonical smile-panel in one call

ATM IV at the smile's effective forward, 25Δ and 10Δ risk reversals, 25Δ and 10Δ butterfly flies — the canonical morning smile-panel readout — bundled into SurfaceSmileRiskMetrics for a single-call workflow that replaces 3-5 hand-rolled lookups. Each RR wing carries bracket diagnostics — bracket_delta_distance (snap to target) and bracket_spread (interpolation lever-arm) — so consumers can degrade loose-bracket wings instead of trusting the binary Interpolated flag.

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

let metrics = surface_smile_risk_metrics(
    &smile,
    &market,
    SurfaceDeltaConvention::Forward,
    PricingModel::BlackScholesMerton,
    SurfaceSmileDeltaSelectionPolicy::default(),
    &listed_strikes,
)?;

// metrics.atm_iv()
// metrics.rr_25().risk_reversal()
// metrics.fly_25(), metrics.fly_10()

// bracket diagnostics quantify wing quality
// metrics.rr_25().put_wing().bracket_delta_distance()
// metrics.rr_25().put_wing().bracket_spread()
Read the full guide →
Smile-panel · SPY 30D · 2026-05-07
05 · Chain Analytics

Derive DEX, GEX, and flow walls from a live chain

Live ChainState in, ChainExposureReport out — per-strike and per-expiry DEX / GEX / VEX / vanna / charm, call/put OI concentration, strike walls, spot ladders, and flip-level detection. ChainExposureStatecaches incremental updates so streaming workloads don't recompute the whole chain on each refresh.

use ferro_risk::{ChainExposureInputs, ChainExposureMetric,
             PricingModel, derive_chain_exposures, strike_walls};

// chain_state, market, policy set up upstream
let inputs = ChainExposureInputs::new(
    &chain_state, &market,
    PricingModel::BlackScholesMerton, policy)?;
let report = derive_chain_exposures(&inputs)?;

// per-strike GEX, via the metric projection
for b in &report.strike_buckets {
    let gex = ChainExposureMetric::Gex
        .value(b.net_exposure);
}

// OI-side walls — one per metric
let walls = strike_walls(&report);
let call_wall = walls.iter().find(|w|
    w.metric == ChainExposureMetric::OpenInterestCall);
Read the full guide →
GEX by strike · SPY 30D · 2026-05-07 · call wall at 740
06 · Scenarios

Explain a portfolio scenario with attribution

A ScenarioDefinition composed of typed ScenarioShock variants — spot-relative, parallel vol, skew, rate, time, custom — runs over a Portfolio and returns exact PnL attribution per position, bucket, and ordered strategy group. The FerroWaveScenarioAdapter maps upstream regime / vol / jump signals into the same shock list.

use ferro_risk::{ScenarioDefinition, ScenarioShock,
             explain_portfolio_scenario_pnl};

let scenario = ScenarioDefinition::new(
    "spot_up_vol_up",
    vec![
        ScenarioShock::SpotRelative { relative: 0.05 },
        ScenarioShock::ParallelVol  { shift: 0.02 },
        ScenarioShock::Skew { slope: -0.01 },
    ],
)?;

let report = explain_portfolio_scenario_pnl(
    &portfolio,
    &scenario,
)?;

// report.pnl · report.books[..] · report.expiry_buckets
// report.position_rows() — per-position PnL, reconciles to total
Read the full guide →
PnL attribution · by position → total

Performance as a first-class feature

Speed is designed in, not bolted on. These are wall-clock measurements on an Apple M1 Pro (10-core), release build — single-contract latency for streaming, and 100,000-contract batch throughput — with the hot paths held to a Criterion budget in CI so a regression fails the build before it ships.

~107 ns
9.4M / sec · 1 core
European price, per contract

Black-Scholes-Merton — the per-tick latency of pricing a streaming quote.

~347 ns
2.9M sets / sec · 1 core
All 10 Greeks, per contract

First- and second-order, on one shared evaluation path — no per-Greek bumping.

~459 ns
2.2M solves / sec · 1 core
Implied-vol solve, per contract

Jaeckel's rational Let's-Be-Rational solver, to machine precision in a handful of iterations.

5.8 ms
17M sets / sec · 10 cores
100K full Greek sets

All ten Greeks for 100,000 contracts, parallelized across cores with deterministic Rayon.

8.2 ms
12M / sec · 10 cores
100K fused analytics

Solved IV, explicit forward, and all ten Greeks per contract — for 100,000 contracts at once.

~4.9 µs
Bjerksund-Stensland 2002
American price, per contract

A two-stage exercise boundary — higher-fidelity than a single-boundary approximation (see below).

Head to head vs QuantLib

Same Apple M1 Pro · same models and inputs · single-threaded · QuantLib 1.41 called from Python

PathFerroRiskQuantLib (Python)Result
European · price~107 ns~1,990 ns~19× faster
European · implied vol~459 ns~14,020 ns~30× faster
European · Greeks~347 ns (10)~3,480 ns (5)~10× faster
American · price~4.9 µs~2.6 µs~0.5× · +3× accuracy
American · Greeks~26.7 µs (10)not exposedFerroRisk only

Medians of repeated runs, release build. QuantLib is driven from Python the way desks use it — option and engine built once, spot re-quoted per tick — so the figures reflect the realistic from-Python cost, single-threaded. Batched across 10 cores, FerroRisk prices 100K full Greek sets in 5.8 ms — roughly 60× a single-threaded QuantLib-from-Python loop over the same chain. American pricing is the one path where QuantLib is faster per call (~1.9×), because it solves the cheaper single-boundary approximation. FerroRisk runs the Bjerksund-Stensland 2002 two-stage boundary: against a 16,384-step CRR oracle on the same contract it prices to 0.016 error versus QuantLib's 0.048 — roughly 3× more accurate for about 2× the work — and it exposes full analytical Greeks on the American path, which QuantLib's analytic engine does not.

Built from first principles

FerroRisk is not a port of someone else's library. Every model starts with the source research, is implemented in-tree in pure Rust, and is validated against the canonical reference and an independent oracle before it ships.

01

Start from the research

Each model comes from its source paper, not a second-hand transcription — Bjerksund-Stensland (2002) for American, Merton (1976) jump-diffusion, Heston via Fang-Oosterlee COS, Gatheral-Jacquier (2014) SSVI, Hagan SABR, Dupire local-vol, and Jaeckel's Let's-Be-Rational IV solver.

02

Implement in-tree

The numerics live in the crate — closed-form wherever the math allows, with no hidden BLAS and no math DSL. The Cody normal CDF, the rational-branch IV solver, and the dual-number analytical Greeks are written and owned here.

03

Validate against canon and oracles

European IV round-trips match Jäckel's reference C++ to 4 ULPs — fixtures stored as exact bit patterns, never a paper's rounded tables. The fast American path is held against a 16,384-step CRR tree, second-order Greeks against a Richardson high-precision reference, and the SIMD transcendentals against MPFR grids.

04

Reproducible by construction

The same inputs always produce the same numbers. Parallel batch APIs use deterministic Rayon for byte-identical reports across thread counts, and every SIMD kernel is bit-tracked against the scalar twin it replaces.

Mathematical correctness as a gate

Pricing libraries are easy to write and hard to verify. FerroRisk treats numerical correctness, calibrated performance, and regression coverage as release gates — not aspirations.

Mathematical Correctness

Numerical equality round-trips, property tests, and known-answer suites against reference implementations — CRR for American, closed-form analytics for European. 800+ unit tests, 45+ property assertions, and 15 fuzz targets cover the pricing, surface, and chain-analytics paths. Every release passes the gate.

PT

Calibrated Performance Gates

scripts/check_perf_thresholds.sh rejects regressions against testing/perf_thresholds.json with RAYON_NUM_THREADS=4fixed; stale Criterion output is rejected before thresholds are checked, so a regression can't merge. The measured throughput figures are in the benchmarks above — hot kernels run through pulp's runtime-dispatched SIMD, bit-identical to scalar on the same FP unit.

Rs

Pure Rust. Lean by design.

Zero unsafe in the core — the SIMD intrinsics are encapsulated by pulp. Three runtime dependencies — rayon for parallel batch APIs, thiserror for typed errors, and pulp for portable runtime-dispatched SIMD (on by default; --no-default-features drops it for the always-present scalar path) — plus serde behind a feature flag. The numerics live in-tree: no hidden BLAS, no math DSL, no surprise transitive graph. High performance follows from the absence of surprises.

No surprises in the API contract

Pricing-library bugs hide in unit conventions. FerroRisk names them on the surface so a reader of the call site can reproduce the math without reading the source.

yr

Time in years

time_to_expiry is expressed in years. A 30-day expiry is 30.0 / 365.0. There is no implicit calendar conversion at any pricing entry point.

e

Continuously-compounded rates

Both rate and dividend_yield are continuously compounded. For Black-76, set dividend_yield = 0.0 — the model uses the risk-free rate as the effective carry term.

/d

Per-calendar-day decay

theta, charm, veta, and colorare reported per calendar day, not per year. This matches how risk desks read time-decay on a daily P&L.

∂σ

Vega and rho on raw scale

vega is reported as dV/dσ, not per one volatility point. rho is reported as dV/dr, not per basis point. Convert at the surface, not inside the engine.

All Greeks at once

A taste below — the guides walk each workflow end to end, and the complete API reference documents every public type, function, and trait.

Compute the full first- and second-order Greeks surface in one call, on a shared internal evaluation path

greeks_all takes a typed PricingInputs and a named PricingModel and returns the 10-Greek surface for the contract — no per-Greek bumping pattern, no inconsistency between sensitivities computed on different internal paths.

For larger workloads, greeks_batch and contract_analytics_batch parallelize via Rayon with per-contract error isolation. Use contract_analytics when a workload needs solved IV, explicit forward, and Greeks for the same contract in one fused step.

use ferro_risk::{ExerciseStyle, OptionType, PricingInputs,
             PricingModel, greeks_all};

let inputs = PricingInputs {
    option_type: OptionType::Call,
    exercise_style: ExerciseStyle::European,
    spot: 4_850.0,
    strike: 4_900.0,
    time_to_expiry: 0.25,
    rate: 0.045,
    dividend_yield: 0.0,
    volatility: 0.22,
};

let greeks = greeks_all(&inputs, PricingModel::Black76).unwrap();
// greeks.delta, .gamma, .theta, .vega, .rho,
// .vanna, .volga, .charm, .veta, .color
10 Greeks per callShared eval pathRayon batchPer-call error isolation

Talk to us

Reach out for design-partner support, integration guidance on the surface-calibration and chain-analytics surfaces, or term-structure / calibration optimizer planning.

hello@morphiqlabs.com

Tell us about your use case

  • Asset class and model — equity, futures, American / European, stochastic-vol, jumps
  • Workload — single contracts, surface calibration, streaming chain analytics
  • Surface needs — SVI / SSVI / SABR, fit-quality tolerances, Dupire local-vol
  • Integration timeline and existing infrastructure