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.
Pillars
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.
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
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.
ChainStatesnapshots + 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
Architecture
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.
Inputs
PricingInputs, IvSolveInputs, and ChainState typed at the boundary. Time in years, rates and dividend yields continuously compounded — no implicit conversions.
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.
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.
Aggregate
Position / Strategy / Book / Portfolio containers feed aggregate Greek and bucket reports. ChainExposureState derives DEX / GEX / VEX with ferro-wave scenario attribution.
Use Cases
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.
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
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)
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!(), }
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()
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);
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
Benchmarks
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.
Black-Scholes-Merton — the per-tick latency of pricing a streaming quote.
First- and second-order, on one shared evaluation path — no per-Greek bumping.
Jaeckel's rational Let's-Be-Rational solver, to machine precision in a handful of iterations.
All ten Greeks for 100,000 contracts, parallelized across cores with deterministic Rayon.
Solved IV, explicit forward, and all ten Greeks per contract — for 100,000 contracts at once.
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
| Path | FerroRisk | QuantLib (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 exposed | FerroRisk 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.
Foundations
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.
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.
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.
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.
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.
Code Quality
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.
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.
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.
Conventions
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.
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.
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.
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.
API Surface
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
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.comTell 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