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>(())
Note

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.

Note

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).