Pricing, IV, Greeks,
Volatility.
Institutional-grade options pricing and volatility analytics, in pure Rust — one shared evaluation path from a single contract to a live option chain.
Pillars
Four pillars on one evaluation path
Pricing with full Greeks coverage, surface calibration, local-vol extraction, live chain analytics, and volatility & microstructure analytics — built so consumers get the same numerical answer whether they call a single contract, fit a surface, stream a venue's option chain, or measure the premium between implied and realized vol.
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
- Discrete cash dividends — escrowed-spot American with full-map Greeks
- De-Americanization — European-equivalent price + IV with early-exercise-premium telemetry
- 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
- σ-standardized log-moneyness wing anchors — stationary across vol and DTE
- 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
Volatility & microstructure analytics
Realized volatility from OHLC bars, the variance risk premium split into diffusive and jump legs, option-implied correlation, and per-quote and multi-leg execution cost — typed reads on the same fail-loud numerical path.
- Realized vol — close-to-close · Parkinson · Garman-Klass · Yang-Zhang · bipower
- Realized jump split — jump-robust diffusive vs jump variance
- Directional semivariance — downside / upside legs summing exactly to RV
- Variance risk premium —
VrpReadin vol-point + variance units - VRP decomposition — diffusive vs jump/tail legs via a Merton fit
- Merton jump calibration — quote-anchored Q-side fit from a physical-measure prior (Esscher tilt)
- VRP z-score — normalize the premium against a rolling baseline
- Implied correlation + dispersion — index vs weighted constituents
- Effective spread — quoted / half / round-trip cost, typed outcomes
- Multi-leg round-trip cost — quantity-aware spread aggregation
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
Ten workflows on one evaluation path
Entry points across the four 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 and a SurfaceCalibrationReport that grades the fit Healthy / Acceptable / Degenerate / Failed — on the residual, not just the optimizer. Model-free put-call-parity forward recovery, a joint Gatheral-Jacquier SSVI fit, and a non-parametric short-DTE fallback keep thin near-dated chains honest.
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
Split the variance risk premium into diffusive and jump legs
variance_risk_premium takes implied and realized vol on a shared SurfaceTenor and returns a typed VrpRead in both vol-point and variance units. A JumpDiffusionFit — hand-supplied, or calibrated from quotes with a physical-measure anchor (09 below) — then drives vrp_decomposition, which splits the premium into its diffusive carry and its jump/tail compensation — so a raw implied-minus-realized number becomes two legs you can act on.
use ferro_risk::{variance_risk_premium, vrp_decomposition, JumpDiffusionFit, JumpDiffusionParameters, RvMethod, SurfaceTenor}; let tenor = SurfaceTenor::new(30.0 / 365.0)?; let vrp = variance_risk_premium( 0.22, 0.18, // implied, realized vol tenor, RvMethod::YangZhang, )?; let params = JumpDiffusionParameters::new(0.9, -0.02, 0.08); let fit = JumpDiffusionFit::new(0.18, params)?; let split = vrp_decomposition(fit, vrp); // split.diffusive_vrp(), .jump_vrp(), // split.jump_variance_share()
Read option-implied correlation from an index and its constituents
implied_correlationtakes the index's implied vol and a slice of typed BasketConstituent weights and IVs on a shared tenor, and returns a DispersionRead: the average pairwise correlation the market is pricing, plus the dispersion premium between the weighted-average constituent vol and the index vol — the read a dispersion book is built on. rho_implied is reported raw, so an out-of-range implied correlation is surfaced, not hidden.
use ferro_risk::{implied_correlation, BasketConstituent, SurfaceTenor}; let tenor = SurfaceTenor::new(30.0 / 365.0)?; let constituents = vec![ BasketConstituent::new(0.28, 0.35)?, BasketConstituent::new(0.24, 0.40)?, BasketConstituent::new(0.31, 0.25)?, ]; // index_iv on the same tenor let read = implied_correlation(0.19, &constituents, tenor)?; // read.rho_implied(), .dispersion_vol_pts(), // read.weighted_avg_vol()
Calibrate the jump triple from quotes, anchored in the physical measure
calibrate_jump_diffusion fits the risk-neutral Merton triple against option quotes, identified by a JumpAssumptioncarrying the physical measure — the wire payload of ferro-wave's parametric wavelet jump detector maps into it field-for-field. The calibrator owns the P → Q Esscher tilt (the jump risk premium θ), jump_prior_cross_check holds the prior against the bipower realized jump split, and is_confident() folds tilt identifiability and the cross-check into one gate — a fit that cannot be identified says so instead of converging anyway.
use ferro_risk::{calibrate_jump_diffusion, jump_prior_cross_check, realized_jump_split, JumpAssumption, JumpEstimateUncertainty, JumpMeasure, JumpPriorCrossCheckPolicy}; // physical-measure anchor — e.g. ferro-wave's jump detector let unc = JumpEstimateUncertainty::new(32, 0.68, 0.005, 0.004)?; let prior = JumpAssumption::new( JumpMeasure::Physical, 3.66, -0.082, 0.025, unc)?; let realized = realized_jump_split(&bars, 252.0)?; let check = jump_prior_cross_check(&prior, &realized, JumpPriorCrossCheckPolicy::new(0.35)?)?; let outcome = calibrate_jump_diffusion( &input, &policy, &prior, Some(check))?; // outcome.is_confident() · outcome.tilted_assumption() // outcome.per_expiry()[i].fit() → vrp_decomposition(..)
De-Americanize an equity quote across its dividend schedule
Listed equity options are American and their underlyings pay discrete cash dividends — so the quotes a surface fitter wants are the wrong shape twice over. A DividendSchedule on IvSolveInputs prices the escrowed spot S − Σ Dᵢ·e^(−r·tᵢ) through the unchanged Bjerksund-Stensland kernel, and de_americanize_quote strips the early-exercise premium to a European-equivalent price and IV that SSVI can actually fit. The premium is reported, never thresholded — the bias is yours to gate on.
use ferro_risk::{de_americanize_quote, DividendEvent, DividendSchedule, ExerciseStyle, IvSolveInputs, OptionType}; // one $0.96 ex-div 38 days out, 110-day expiry let schedule = DividendSchedule::new(vec![ DividendEvent::new(38.0 / 365.0, 0.96)?, ])?; let inputs = IvSolveInputs { option_type: OptionType::Put, exercise_style: ExerciseStyle::American, spot: 190.0, strike: 195.0, time_to_expiry: 110.0 / 365.0, rate: 0.043, dividend_yield: 0.0, dividends: Some(schedule), }; let quote = de_americanize_quote(&inputs, 12.9220)?; // quote.european_equivalent_iv() → 0.26000 // quote.european_equivalent_price() → 12.6626 // quote.eep_fraction() → 0.02008
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, is validated against the canonical reference and an independent oracle, and has its core pricing laws machine-checked in Lean 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.
Proven, not just tested
The pricing mathematics is machine-checked in Lean — 262 theorems with no sorry, kernel-verified against a minimal axiom base: put-call parity, every first-order Greek and the higher-order surface, the Black-Scholes PDE, and implied-vol existence & uniqueness. The standard normal CDF is the genuine Gaussian, not an assumption — and these laws can't drift from the code.
Verified down to the floating point
Beyond the reals: the Cody normal-CDF and normalised-Black kernels carry rigorous rounding bounds (Gappa) and an approximation bound against the true erf (Sollya), and the proved identities are checked holding in the shipping f64 code to ~1e-15. The Let's-Be-Rational IV solver converges to machine precision in ≤2 iterations across the moneyness wings.
Code Quality
Mathematical correctness as a gate
Pricing libraries are easy to write and hard to verify. FerroRisk treats numerical correctness, calibrated performance, regression coverage, and formal proof obligations over stable pricing and risk laws 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. 1,800+ tests, 600+ property assertions, and 17 fuzz targets cover the pricing, surface, and chain-analytics paths. Every release passes the gate.
Formal Proof Gates
262 machine-checked Lean theorems over the pricing mathematics — kernel-verified with no sorry on the three standard axioms — plus Gappa/Sollya floating-point rounding and approximation bounds on the numeric kernels, and an f64 conformance harness that holds the proved identities to ~1e-15. The claim stays scoped to those laws, not every market model or trading outcome.
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
One typed API, every pillar
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. The pricing core is formally verified — the math is proven, not just tested.
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