Use case · Volatility
Calibrate the Merton jump triple from quotes, anchored in the physical measure
calibrate_jump_diffusion fits the risk-neutral jump triple (λ, μ_J, σ_J) against option quotes, identified by a physical-measure JumpAssumption prior — the wire payload of an external jump detector such as ferro-wave's parametric wavelet detector. 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 deterministic gate.
When to use it
- You want the jump leg of the VRP decomposition to come from quotes rather than a hand-supplied JumpDiffusionFit — so jump_share_of_premium answers "is this premium harvestable diffusive carry, or jump/tail compensation?" with a calibrated, not assumed, jump triple.
- You have a physical-measure jump estimate (intensity per year, log-jump-size mean and std, with real estimator uncertainty) from historical returns — e.g. ferro-wave’s jump_diffusion_prior_signal payload, which maps field-for-field into JumpAssumption — and want the market-implied risk-neutral counterpart plus the implied jump risk premium.
- You want the calibration to fail loud on non-identifiable surfaces (no wings, too few quotes, tilt pinned at a bound) instead of returning a confident-looking but arbitrary triple.
Example
use ferro_risk::{
calibrate_jump_diffusion, jump_prior_cross_check, realized_jump_split,
vrp_decomposition, JumpAssumption, JumpDiffusionCalibrationPolicy,
JumpEstimateUncertainty, JumpMeasure, JumpPriorCrossCheckPolicy,
};
// Physical-measure anchor. These five fields are exactly the wire shape
// ferro-wave's jump_diffusion_prior_signal emits (prior payload field) —
// deserializing it re-runs this same validation.
let uncertainty = JumpEstimateUncertainty::new(32, 0.68, 0.005, 0.004)?;
let prior = JumpAssumption::new(
JumpMeasure::Physical,
3.66, // λ — jumps per year
-0.082, // μ_J — mean log jump size
0.025, // σ_J — std of log jump size
uncertainty,
)?;
// Independent realized check: the prior's λ·(μ_J² + σ_J²) must sit within
// a relative band of the bipower QV − BV jump variance from the same bars.
let realized = realized_jump_split(&bars, 252.0)?;
let cross_check = jump_prior_cross_check(
&prior,
&realized,
JumpPriorCrossCheckPolicy::new(0.35)?,
)?;
let outcome = calibrate_jump_diffusion(&input, &policy, &prior, Some(cross_check))?;
if outcome.is_confident() {
// Tilt identifiable, cross-check agrees, per-expiry profiles converged.
let fit = outcome.per_expiry()[0].fit();
let split = vrp_decomposition(fit, vrp);
// split.jump_share_of_premium() — jump/tail share of the premium
let _tilted = outcome.tilted_assumption(); // Q triple + propagated SEs
} else {
// The fit is still returned, flagged — gate, don't guess.
}
# Ok::<(), ferro_risk::FerroRiskError>(())Measure gate. The prior must carry JumpMeasure::Physical — feeding a risk-neutral triple back in as its own anchor is the self-confirming loop the design rejects, and returns MeasureMismatch. The calibrator owns the P → Q direction via JumpAssumption::into_risk_neutral(θ)(Esscher tilt), and the prior's estimator uncertainty (jump count, per-field standard errors) propagates through the tilt — a λ estimated from 6 jumps stays visibly wider than one from 600.
Where the anchor comes from
FerroRisk deliberately does not detect jumps. The physical-measure prior is produced upstream from return history — the reference producer is ferro-wave's jump_diffusion_prior_signal (0.16.0+): a wavelet detector that emits the triple with estimator-derived uncertainty and a truncation-corrected intensity, serialized in exactly the JumpAssumption wire shape (measure, intensity, mean, std, uncertainty). Neither crate depends on the other; the contract is the serde payload, and deserialization re-runs FerroRisk's own validation.
Exercise scope.The calibrator prices with Merton's closed form and is European-only: positive-weight non-European quotes are rejected with UnsupportedModelExerciseCombination rather than silently mispriced. Gate American-exercise universes accordingly.
What you get
- JumpDiffusionCalibrationOutcome — per-expiry JumpDiffusionExpiryFit profiles, the winning jump risk premium θ with its identifiability band, and the tilted (risk-neutral) JumpAssumption with propagated standard errors.
- is_confident() — one boolean folding outer/profile convergence, tilt-at-bound and identifiability-band truncation, and cross-check agreement; RMSE and band width stay available for your own policy.
- JumpPriorCrossCheck — both variance legs, the symmetric relative gap in [0, 1], and agrees(); five of its laws are machine-checked in Lean (JumpCrossCheck.lean).