# Quantitative finance

URL: https://stochastic.rust-dd.com/docs/quant

> Pricing, calibration, vol surface, risk, credit, curves, bonds, instruments, portfolio, microstructure — the stochastic-rs-quant crate.

The `stochastic-rs-quant` crate is the largest sub-crate. It covers
pricing, calibration, vol surface construction, risk, credit, yield
curves, bonds, cashflows, instruments, portfolio, microstructure, and
factor / strategy primitives.

## Sub-areas

| Area               | What's inside                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Pricing**        | Closed-form (BSM, Bachelier with normal implied-vol inversion, Black76, Garman-Kohlhagen, Margrabe, Kirk, Geske compound, Stulz, Bjerksund-Stensland, digital / gap / supershare, geometric basket, Levy moment-matching, cliquet / forward-start chain). Fourier (Carr-Madan, Lewis, Gil-Pelaez) for Heston / Bates / Merton-jump / Kou / VG / CGMY / HKDE / double-Heston. Monte Carlo (basket, rainbow, cliquet, autocallable, spread). Finite difference (explicit / implicit / Crank-Nicolson, American); Heston PDE by ADI (Douglas / Craig-Sneyd / MCS / Hundsdorfer-Verwer on sinh meshes, Rannacher damping, down-and-out barrier). Bermudan LSM. Heston SLV (Guyon-Labordère). |
| **Calibration**    | Heston (Cui analytic Jacobian + NMLE / PMLE / NMLE-CEKF seeds), Heston SLV (leverage on a Dupire or supplied local volatility by the Guyon–Henry-Labordère particle method or the Wyns–Du Toit finite-volume forward Kolmogorov equation, at any mixing fraction), SABR per-expiry caplet smile, Lévy (CGMY, VG, NIG, Merton-jump, Kou, bilateral gamma), SVJ, rough Bergomi, double-Heston, BSM (multi-maturity), HKDE, Hull-White (Jamshidian), G2++ and Black-Karasinski (trinomial-tree repricing) swaption grids via Basin Nelder-Mead. Optional Tikhonov regularisation toward a parameter anchor on the Heston, SABR and swaption calibrators.                                    |
| **Vol surface**    | Implied-vol surface from market quotes, SVI / SSVI / eSSVI (anchored per-maturity slices with butterfly and calendar bounds), arbitrage-free interpolation, smile and skew analytics, SABR-smile fits with Basin L-BFGS-B.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| **Risk**           | VaR (Gaussian / historical / Monte Carlo), CVaR / ES, drawdown metrics, Sharpe / Sortino / IR / Calmar (no hard-coded annualisation), instrument-level Greeks, bucket DV01, scenario / shock / curve-shift stress framework, XVA (EPE / ENE / PFE exposure profiles, CVA / DVA / bilateral / FCA / FBA / FVA, Hull-White swap exposure engine).                                                                                                                                                                                                                                                                                                                                          |
| **Credit**         | Merton structural model (PD, equity / debt, distance-to-default, credit spread, implied recovery), reduced-form survival / hazard curves, CDS pricing (ISDA daily-grid, fair spread, risky PV01), hazard bootstrap from CDS par-spread term structure, CDS index with ISDA standard-model upfront, CDO tranches under the one-factor Gaussian copula (ASB recursion, Vasicek large-pool limit), JLT migration matrices with pure-Rust Padé-13 matrix exponential.                                                                                                                                                                                                                        |
| **Yield curves**   | Bootstrapping (deposit / FRA / future / swap / OIS helper), dual-curve tenor bootstrap against an OIS discount curve, Nelson-Siegel / Svensson, multi-curve (OIS discounting, tenor forecasting), discount-curve interpolation (linear / log-linear / cubic / monotone-convex).                                                                                                                                                                                                                                                                                                                                                                                                          |
| **Bonds**          | Fixed-rate / floating-rate / inflation-linked / amortizing bonds. YTM / Macaulay / modified duration / convexity / Z-spread / OAS. Vasicek / CIR / Hull-White short-rate bond pricing, callable / puttable coupon bonds on the Hull-White trinomial tree.                                                                                                                                                                                                                                                                                                                                                                                                                                |
| **Cashflows**      | Coupon, leg, engine, schedule builder. Day-count + business-day conventions.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| **Instruments**    | Option (vanilla / barrier / Asian / lookback / digital), bond, swap, equity. Plus the `Instrument` / `PricingEngine` pair as a cross-engine comparison harness.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| **Portfolio**      | Covariance estimation (sample, Ledoit-Wolf, OAS), momentum, and Basin Nelder-Mead optimizers (mean-variance, MVO with constraints).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| **Microstructure** | Almgren-Chriss optimal execution, Kyle (1985) strategic-trading equilibria, Bouchaud propagator (power-law / exponential / custom kernels), Roll / Corwin-Schultz spread estimators, full price-time priority order book.                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| **Factors**        | PCA, two-pass Fama-MacBeth, Ledoit-Wolf shrinkage.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| **Strategies**     | Cointegrated pairs trading (hedge ratio, spread, z-score, signal generator), forecast-momentum-volatility regime engine, delta hedge.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| **Inflation**      | Zero-coupon and YoY inflation curves, CPI / RPI / HICP indices with linear-interpolated reference ratio, ZC and YoY inflation swaps with par-rate solver.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| **FX**             | ISO 4217 currency definitions, FX quoting / cross-rate / triangulation, FX forward via covered interest parity (continuous and simple compounding).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| **Calendar**       | ACT/360, ACT/365, 30/360, ACT/ACT day-counts. Following / Modified Following / Preceding business-day adjustments. US, UK, TARGET, Tokyo, HKEX, ASX, SGX, B3 holiday calendars. Pluggable `CalendarExt`. `ScheduleBuilder` for coupon / payment dates.                                                                                                                                                                                                                                                                                                                                                                                                                                   |

## Choosing a pricer

| You have                                                                    | Reach for                                                                                                                                                          | Why                                                                  |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| A European vanilla, one flat volatility                                     | `BSMPricer` (`BachelierPricer` for rates / negative underlyings)                                                                                                   | closed form, all Greeks to third order                               |
| A stochastic-volatility model and a strike–maturity grid                    | `HestonPricer` (Fourier, Cui Jacobian) → `ModelSurface`                                                                                                            | one characteristic function prices the whole grid                    |
| A barrier or American payoff under Heston                                   | `HestonAdiPricer`                                                                                                                                                  | ADI finite differences with a down-and-out barrier; Rannacher-damped |
| An American vanilla under Black–Scholes                                     | `BjerksundStensland2002Pricer` (analytic approximation), `FiniteDifferencePricer` or `CrrModel` (exact to the grid), `LsmPricer` for path-dependent early exercise | speed vs. grid-exactness                                             |
| Path-dependent exotics (Asian, lookback, cliquet, autocallable, barrier MC) | the Monte Carlo pricers returning `McEstimate`                                                                                                                     | the estimate carries its standard error                              |
| Multi-asset                                                                 | `BasketPricer`, `RainbowPricer`, `KirkSpreadPricer` (spread), `QuantoPricer` (foreign underlying)                                                                  | closed forms where they exist, MC otherwise                          |
| Jumps or heavy tails                                                        | `Merton1976Pricer`, the Lévy / CGMY Fourier pricers (`CarrMadanPricer`, COS)                                                                                       | the characteristic function is the model                             |
| Rough volatility                                                            | `RBergomiPricer` (mSOE Monte Carlo)                                                                                                                                | no closed form exists                                                |
| A curve-driven instrument (swap, cap, swaption, bond, CDS)                  | the instrument's `.valuation(curve)` / the tree pricers under `lattice::short_rate`                                                                                | prices off a term structure, not a spot–strike query                 |

## Choosing a calibrator

| Market data                                                                               | Calibrator                                                                                                                                                                                                  | Fit                                                |
| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| Equity / FX vanilla surface                                                               | `HestonCalibrator` (Levenberg–Marquardt on the Cui Jacobian, NMLE / PMLE / CEKF seeds), `DoubleHestonCalibrator`, `SVJCalibrator`, `LevyCalibrator`, `CgmysvCalibrator`, `HKDECalibrator`, `HscmCalibrator` | least squares on prices or implied vols            |
| One smile at one expiry                                                                   | `SabrCalibrator` (equity / FX), `SabrCapletCalibrator` (shifted, rates), `SabrSmileCalibrator` (FX quotes: ATM, risk reversal, butterfly)                                                                   | Hagan expansion                                    |
| A whole surface, arbitrage-free                                                           | `calibrate_essvi` (eSSVI slices)                                                                                                                                                                            | butterfly and calendar bounds enforced             |
| Swaption grid                                                                             | `HullWhiteSwaptionCalibrator` (Jamshidian), `G2ppSwaptionCalibrator`, `BlackKarasinskiSwaptionCalibrator` (trinomial tree repricing)                                                                        | Basin Nelder–Mead on the tree                      |
| Rough volatility                                                                          | `RBergomiCalibrator` (Wasserstein loss on mSOE samples)                                                                                                                                                     | Monte Carlo                                        |
| A vanilla surface plus exotics that need the right mix of local and stochastic volatility | `HestonSlvCalibrator` (Heston fit → Dupire local vol → leverage by the Guyon–Henry-Labordère particle method or the finite-volume forward Kolmogorov equation)                                              | reprices the surface at any mixing fraction $\eta$ |
| A trained surrogate                                                                       | `HestonSurrogateCalibrator`, `RBergomiSurrogateCalibrator` (`ai` feature)                                                                                                                                   | LM on the network's exact Jacobian                 |

All of them implement `Calibrator` (`calibrate(initial) -> Result<…>`) and hand
their result to a pricer through `ToModel` / `ToShortRateModel`; `Regularization`
adds a Tikhonov pull toward an anchor on the Heston, SABR and swaption ones.

## Examples

### Black-Scholes-Merton — closed-form European call

```rust title="tests/doctest_quant_bsm.rs"
// docs: quant#black-scholes-merton-closed-form-european-call
//! Backs the BSM example on the quant catalog page.

use stochastic_rs::quant::pricing::bsm::BSMCoc;
use stochastic_rs::quant::pricing::bsm::BSMPricer;
use stochastic_rs::quant::types::OptionType;
use stochastic_rs::traits::ModelPricer;

#[test]
fn bsm_price_and_greeks() {
  // Model state only: volatility + cost-of-carry convention.
  let model = BSMPricer::new(/* v */ 0.2, BSMCoc::Bsm1973);
  // Query: (s, k, r, q, tau).
  let (s, k, r, q, tau) = (100.0, 100.0, 0.05, 0.0, 1.0);

  let call = model.price_call(s, k, r, q, tau);
  let put = model.price_put(s, k, r, q, tau);
  let delta = model.delta(s, k, r, q, tau, OptionType::Call);
  let vega = model.vega(s, k, r, q, tau);

  assert!(call > 0.0 && put > 0.0);
  assert!((0.0..=1.0).contains(&delta));
  assert!(vega > 0.0);
}
```

```python
import stochastic_rs as srs

# The Python wrapper still bundles the query into the constructor — the
# option type included — so the accessors below take no arguments.
pricer = srs.BSMPricer(s=100, v=0.2, k=100, r=0.05, tau=1.0, option_type="call", q=0.0)
call, put = pricer.call_put()
print("call =", call)
print("put  =", put)

print(f"delta={pricer.delta():.4f}, gamma={pricer.gamma():.4f}, "
      f"vega={pricer.vega():.4f}, theta={pricer.theta():.4f}, "
      f"rho={pricer.rho():.4f}")
```

### Quanto option — fixed exchange rate foreign equity

`QuantoPricer` prices an option on a foreign asset whose payoff is converted to the domestic currency at a fixed exchange rate `E_p` (Reiner 1992; Haug §2.13.4). Under the domestic risk-neutral measure the asset drifts at `r_f − q − ρ σ_S σ_E`, the quanto adjustment, so the price is `E_p` times a Black–Scholes–Merton price with that cost of carry, discounted at the domestic rate; `forward()` is `E_p · S · e^{bτ}` and the put overrides parity with the quanto carry. The model holds `(σ_S, σ_E, ρ, r_f, E_p)`; the query `(s, k, r, q, τ)` travels as arguments like every `ModelPricer`.

```rust title="tests/doctest_quant_quanto.rs"
// docs: quant#quanto-option-fixed-exchange-rate-foreign-equity
//! Backs the quanto example on the quant catalog page.

use stochastic_rs::quant::pricing::bsm::BSMCoc;
use stochastic_rs::quant::pricing::bsm::BSMPricer;
use stochastic_rs::quant::pricing::quanto::QuantoPricer;
use stochastic_rs::traits::ModelPricer;

#[test]
fn quanto_price_forward_and_merton_reduction() {
  // Model state: asset vol, FX vol, correlation, foreign rate, fixed rate.
  let model = QuantoPricer::new(0.2, 0.12, 0.3, 0.05, 1.5);
  // Query: (s, k, r = domestic rate, q, tau) — the Haug §2.13.4 inputs.
  let (s, k, r, q, tau) = (100.0, 105.0, 0.08, 0.04, 0.5);

  let (call, put) = model.call_put(s, k, r, q, tau);
  assert!((call - 5.2936847941).abs() < 1e-4);
  assert!((put - 12.2976985036).abs() < 1e-4);
  assert_eq!(model.price_call(s, k, r, q, tau), call);

  // The quanto forward carries the adjusted drift r_f − q − ρ σ_S σ_E.
  assert!((model.forward(s, q, tau) - 150.2101470686).abs() < 1e-9);

  // Without correlation and with equal rates it is E_p times Merton (1973).
  let plain = QuantoPricer::new(0.2, 0.12, 0.0, r, 1.5);
  let merton = BSMPricer::new(0.2, BSMCoc::Merton1973);
  assert!(
    (plain.price_call(s, k, r, q, tau) - 1.5 * merton.price_call(s, k, r, q, tau)).abs() < 1e-12
  );
}
```

```python
import stochastic_rs as srs

# Foreign equity option paid in domestic currency at the fixed rate 1.5;
# r is the domestic rate, r_f the foreign one, rho = corr(asset, FX).
pricer = srs.QuantoPricer(s=100, v=0.2, k=105, r=0.08, tau=0.5, r_f=0.05, v_fx=0.12, rho=0.3, fixed_rate=1.5, q=0.04)
call, put = pricer.call_put()
print(call, put)          # 5.2937, 12.2977 on the Reiner / Haug §2.13.4 inputs
print(pricer.forward())   # 150.21 = E_p · S · exp((r_f − q − ρ σ_S σ_E) τ)
```

### Dual-curve bootstrap — OIS discounting, tenor forecasting

Post-crisis curve building separates the two roles a single curve used to play. `OisRateHelper` turns par OIS quotes into the fixed-leg schedules that pin the **discount** curve (the compounded overnight leg telescopes, so only the fixed dates matter); `bootstrap_forecast` then builds a tenor's **forecast** curve against that exogenous OIS discounting from tenor deposits, FRAs and par swaps, solving each pillar's pseudo-discount factor by bisection because the floating leg no longer telescopes (Ametrano & Bianchetti 2013, §4–5). `MultiCurve` holds both and reads projected forwards, fair swap rates and the tenor basis off the pair.

```rust title="tests/doctest_quant_dual_curve.rs"
// docs: quant#dual-curve-bootstrap-ois-discounting-tenor-forecasting
//! Backs the dual-curve bootstrap example on the quant catalog page.

use ndarray::Array1;
use stochastic_rs::quant::curves::DiscountCurve;
use stochastic_rs::quant::curves::InterpolationMethod;
use stochastic_rs::quant::curves::MultiCurve;
use stochastic_rs::quant::curves::dual_curve::ForecastInstrument;
use stochastic_rs::quant::curves::dual_curve::bootstrap_forecast;

#[test]
fn dual_curve_bootstrap_recovers_the_tenor_basis() {
  // Exogenous OIS discount curve (flat 2 %) and a 3M tenor trading 50 bp above it.
  let ois = DiscountCurve::from_zero_rates(
    &Array1::from_vec(vec![0.25, 1.0, 5.0, 10.0]),
    &Array1::from_vec(vec![0.02; 4]),
    InterpolationMethod::LogLinearOnDiscountFactors,
  );
  let tenor_df = |t: f64| (-0.025_f64 * t).exp();

  // Market quotes implied by that tenor curve: 3M deposit, 3×6 FRA, par swaps
  // against 3M with annual fixed and quarterly floating payments.
  let mut quotes = vec![
    ForecastInstrument::Deposit {
      maturity: 0.25,
      rate: (1.0 / tenor_df(0.25) - 1.0) / 0.25,
    },
    ForecastInstrument::Fra {
      start: 0.25,
      end: 0.5,
      rate: (tenor_df(0.25) / tenor_df(0.5) - 1.0) / 0.25,
    },
  ];
  for years in [1_usize, 2, 3, 5] {
    let fixed_times: Vec<f64> = (1..=years).map(|i| i as f64).collect();
    let float_times: Vec<f64> = (1..=4 * years).map(|i| 0.25 * i as f64).collect();
    let float_pv: f64 = float_times
      .iter()
      .scan(0.0, |prev, &t| {
        let leg = ois.discount_factor(t) * (tenor_df(*prev) / tenor_df(t) - 1.0);
        *prev = t;
        Some(leg)
      })
      .sum();
    let annuity: f64 = fixed_times
      .iter()
      .scan(0.0, |prev, &t| {
        let leg = (t - *prev) * ois.discount_factor(t);
        *prev = t;
        Some(leg)
      })
      .sum();
    quotes.push(ForecastInstrument::Swap {
      rate: float_pv / annuity,
      fixed_times,
      float_times,
    });
  }

  // Forecast curve against OIS discounting: pseudo-discount factors come back exactly.
  let forecast = bootstrap_forecast(
    &quotes,
    &ois,
    InterpolationMethod::LogLinearOnDiscountFactors,
  );
  for t in [0.25, 0.5, 1.0, 2.0, 3.0, 5.0] {
    assert!((forecast.discount_factor(t) - tenor_df(t)).abs() < 1e-9);
  }

  // The multi-curve container reads the 50 bp tenor basis back off the two curves.
  let mut multi = MultiCurve::new(ois);
  multi.add_forecast("3M", forecast);
  let basis = multi.basis_spread("3M", 1.0, 1.25).unwrap();
  assert!((basis - 0.005).abs() < 2e-4, "basis {basis}");
}
```

```python
import numpy as np
import stochastic_rs as srs

# OIS discount curve from par OIS quotes (one fixed-leg schedule per quote) …
ois = srs.DiscountCurve.bootstrap_ois([[1.0], [1.0, 2.0], [1.0, 2.0, 3.0]], [0.0202, 0.0204, 0.0206], interp="log_df")
# … and a 3M forecast curve bootstrapped against it from tenor quotes.
forecast = srs.DiscountCurve.bootstrap_forecast(
    ois,
    deposits=[(0.25, 0.0251)],
    fras=[(0.25, 0.5, 0.0252)],
    swaps=[(0.0254, [1.0], [0.25, 0.5, 0.75, 1.0]), (0.0256, [1.0, 2.0], [0.25 * i for i in range(1, 9)])],
    interp="log_df",
)
multi = srs.MultiCurve(ois)
multi.add_forecast("3M", forecast)
print(multi.basis_spread("3M", 1.0, 1.25))               # ≈ 0.005: the tenor trades ~50 bp over OIS
print(multi.fair_swap_rate("3M", [0.0, 0.5, 1.0, 1.5, 2.0]))
```

### Callable and puttable bonds on the Hull–White tree

`price_callable_bond` runs the one-factor short-rate trinomial tree backwards with the bond's coupons as intermediate cash flows and the embedded options exercised at their decision nodes: the issuer calls when the ex-coupon continuation value exceeds the call price, the holder puts when it falls below the put price (Hull & White 1994; Hull, OFOD §31.5–31.6). `CallableBondSpec` carries face, coupon rate, coupon dates and the `(time, clean price)` call / put schedules; the result reports the straight price and the values of the two embedded options beside the bond price. Any `OneFactorShortRateModel` tree works, Hull–White and Black–Karasinski included.

```rust title="tests/doctest_quant_callable_bond.rs"
// docs: quant#callable-and-puttable-bonds-on-the-hullwhite-tree
//! Backs the callable bond example on the quant catalog page.

use stochastic_rs::quant::lattice::CallableBondSpec;
use stochastic_rs::quant::lattice::HullWhiteTree;
use stochastic_rs::quant::lattice::HullWhiteTreeModel;
use stochastic_rs::quant::lattice::price_callable_bond;

#[test]
fn callable_and_puttable_bonds_bracket_the_straight_bond() {
  // Hull–White tree: r₀ = 4 %, a = 0.3, θ = 4 %, σ = 1 %; monthly steps to the 3-year maturity.
  let tree = HullWhiteTree::new(HullWhiteTreeModel::new(0.04, 0.3, 0.04, 0.01), 3.0, 36);
  // 6 % annual coupon, face 100, callable and puttable at par after years 1 and 2.
  let bond = CallableBondSpec::new(100.0, 0.06, vec![1.0, 2.0, 3.0]);
  let callable = bond.clone().with_calls(vec![(1.0, 100.0), (2.0, 100.0)]);
  let puttable = bond.clone().with_puts(vec![(1.0, 100.0), (2.0, 100.0)]);

  let straight = price_callable_bond(&tree.tree, &tree.model, &bond);
  let called = price_callable_bond(&tree.tree, &tree.model, &callable);
  let put = price_callable_bond(&tree.tree, &tree.model, &puttable);

  // The issuer's call lowers the price, the holder's put raises it.
  assert!(called.price < straight.price && put.price > straight.price);
  assert!(called.call_value > 0.0 && put.put_value > 0.0);
  assert_eq!(straight.price, straight.straight_price);
}
```

```python
import stochastic_rs as srs

tree = srs.HullWhiteCallableBond(initial_rate=0.04, mean_reversion=0.3, theta=0.04, sigma=0.01, horizon=3.0, steps=36)
price, straight, call_value, put_value = tree.price(
    face=100.0, coupon_rate=0.06, coupon_times=[1.0, 2.0, 3.0],
    calls=[(1.0, 100.0), (2.0, 100.0)],
)
print(price, straight, call_value)   # callable < straight; call_value = straight − callable
```

### Bachelier normal model and implied normal volatility

`BachelierPricer` prices vanilla options under the arithmetic Brownian (normal) model, `c = e^{−rτ}[(F − K) Φ(d) + σ_N √τ φ(d)]` with `d = (F − K)/(σ_N √τ)` and `F = S e^{(r−q)τ}`, so negative strikes and forwards are well defined and the smile is flat in normal rather than lognormal volatility. `normal_implied_volatility` inverts an undiscounted forward price with a safeguarded Newton iteration (relative tolerance 1e-14, `NaN` below intrinsic value); the pricer's `implied_volatility` wraps it for spot quotes, and the forward-space entry point serves the caplet / floorlet and swaption code that already prices with normal vols.

```rust title="tests/doctest_quant_bachelier.rs"
// docs: quant#bachelier-normal-model-and-implied-normal-volatility
//! Backs the Bachelier example on the quant catalog page.

use stochastic_rs::quant::pricing::bachelier::BachelierPricer;
use stochastic_rs::quant::pricing::bachelier::normal_implied_volatility;
use stochastic_rs::quant::types::OptionType;
use stochastic_rs::traits::ModelPricer;

#[test]
fn bachelier_price_and_normal_implied_volatility() {
  // Model state: normal volatility of 20 price units per √year.
  let model = BachelierPricer::new(20.0);
  // Query: (s, k, r, q, tau) — a strike below the forward.
  let (s, k, r, q, tau) = (100.0, 95.0, 0.05, 0.02, 0.75);

  let call = model.price_call(s, k, r, q, tau);
  let put = model.price_put(s, k, r, q, tau);
  let forward = model.forward(s, r, q, tau);
  assert!((call - put - (-r * tau).exp() * (forward - k)).abs() < 1e-9);

  // The inversion recovers the normal volatility from the price.
  let implied = model.implied_volatility(call, s, k, r, q, tau, OptionType::Call);
  assert!((implied - 20.0).abs() < 1e-9);

  // Forward-space entry point for rate options quoted in normal vol (bp).
  let undiscounted = call * (r * tau).exp();
  assert!(
    (normal_implied_volatility(undiscounted, forward, k, tau, OptionType::Call) - 20.0).abs()
      < 1e-9
  );
}
```

```python
import stochastic_rs as srs

pricer = srs.BachelierPricer(s=100, v=20.0, k=95, r=0.05, tau=0.75, q=0.02)
call, put = pricer.call_put()
print(call, put, pricer.forward())
print(pricer.implied_volatility(call, "call"))   # 20.0: the normal vol round-trips
```

### Swaption calibration of the tree short-rate models

Black–Karasinski and G2++ have no closed-form European swaption price, so `BlackKarasinskiSwaptionCalibrator` and `G2ppSwaptionCalibrator` reprice the quote grid on the crate's trinomial trees: each quote becomes a one-exercise swaption snapped to the tree levels and priced on a tree rebuilt per parameter trial, against the same Black-76 at-the-money-forward market price the Hull–White calibrator uses, with Nelder–Mead on the squared price residuals. Both results implement `ToShortRateModel`, so a calibrated `(a, σ)` or `(a, b, σ, η, ρ)` drops straight into the lattice instruments (Brigo & Mercurio §3.5, §4.2, Ch. 13).

```rust title="tests/doctest_quant_tree_swaption.rs"
// docs: quant#swaption-calibration-of-the-tree-short-rate-models
//! Backs the tree swaption calibration example on the quant catalog page.

use ndarray::Array1;
use stochastic_rs::quant::calibration::hw_swaption::SwaptionQuote;
use stochastic_rs::quant::calibration::tree_swaption::BlackKarasinskiSwaptionCalibrator;
use stochastic_rs::quant::curves::DiscountCurve;
use stochastic_rs::quant::curves::InterpolationMethod;
use stochastic_rs::quant::instruments::option::types::SwaptionDirection;
use stochastic_rs::traits::Calibrator;
use stochastic_rs::traits::ToShortRateModel;

#[test]
fn black_karasinski_fits_a_small_swaption_grid() {
  // Flat 3 % curve and two ATM payer swaptions quoted in Black-76 vol.
  let curve = DiscountCurve::from_zero_rates(
    &Array1::from_vec(vec![0.5, 1.0, 5.0, 10.0]),
    &Array1::from_vec(vec![0.03; 4]),
    InterpolationMethod::LogLinearOnDiscountFactors,
  );
  let quote = |expiry: f64, tenor: f64, black_vol: f64| SwaptionQuote {
    expiry,
    tenor,
    black_vol,
    fixed_accrual: 0.5,
    direction: SwaptionDirection::Payer,
    weight: None,
  };
  let quotes = [quote(1.0, 2.0, 0.22), quote(2.0, 2.0, 0.20)];

  // (a, σ) of the log-rate, repriced on the Black–Karasinski tree at 8 levels per year.
  let calibrator =
    BlackKarasinskiSwaptionCalibrator::new(&quotes, &curve, 1.0, 0.03, 0.03, 8).with_max_iters(200);
  let result = calibrator.calibrate(Some((0.1, 0.2))).unwrap();
  let scale = result.market_prices.iter().sum::<f64>() / 2.0;
  assert!(
    result.rmse / scale < 0.05,
    "relative rmse {}",
    result.rmse / scale
  );

  // The result plugs straight into the lattice pipeline through `ToShortRateModel`.
  let model = ToShortRateModel::to_short_rate_model(&result, 0.03, 0.03);
  assert_eq!(model.sigma, result.sigma);
}
```

```python
import numpy as np
import stochastic_rs as srs

curve = srs.DiscountCurve.from_zero_rates(np.array([0.5, 1.0, 5.0, 10.0]), np.full(4, 0.03), interp="log_df")
quotes = [(1.0, 2.0, 0.22, 0.5, "payer"), (2.0, 2.0, 0.20, 0.5, "payer")]   # (expiry, tenor, black_vol, accrual, direction)

bk = srs.BlackKarasinskiSwaptionCalibrator(quotes, curve, initial_rate=0.03, long_run_rate=0.03, steps_per_year=8)
print(bk.calibrate(initial_guess=(0.1, 0.2)))          # (a, sigma, rmse, converged)
hw = srs.HullWhiteSwaptionCalibrator(quotes, curve)
print(hw.calibrate())                                    # (a, sigma, rmse, converged) via Jamshidian
g2 = srs.G2ppSwaptionCalibrator(quotes, curve, initial_rate=0.03, steps_per_year=4, max_iters=200)
print(g2.calibrate())                                    # (a, b, sigma, eta, rho, rmse, converged)
```

For these three swaption calibrators and `SabrCapletCalibrator`, `converged`
means the sample standard deviation of the simplex objective values fell below
`sd_tolerance`. Reaching `max_iters` or encountering nonfinite simplex costs
returns `false`, while retaining the best parameters found. Earlier versions
reported successful optimizer completion as convergence, so the same quote grid
can now return `false`. Check both `converged` and `rmse` before using a fit.
The SABR caplet Python tuple is `(alpha, beta, nu, rho, rmse, converged)`.

### eSSVI slices with arbitrage-free interpolation

`calibrate_essvi` fits one SSVI slice per maturity with its own `(θ, ρ, ψ)` — the extended SSVI surface of Hendriks and Martini — using the anchored scheme of Corbetta, Cohort, Laachir and Martini (2019): each slice is pinned to its quote closest to the money, the Gatheral–Jacquier butterfly bounds become an explicit cap on `ψ`, and the calendar-spread conditions against the previous slice become a floor, so the surface is arbitrage-free by construction rather than by a post-hoc projection. `EssviSurface` interpolates `(θ, ψ, ρψ)` linearly between slices, which the note shows keeps the surface calendar-spread-free, and reports both admissibility checks.

```rust title="tests/doctest_quant_essvi.rs"
// docs: quant#essvi-slices-with-arbitrage-free-interpolation
//! Backs the eSSVI example on the quant catalog page.

use stochastic_rs::quant::vol_surface::SsviParams;
use stochastic_rs::quant::vol_surface::essvi::calibrate_essvi;
use stochastic_rs::quant::vol_surface::ssvi::SsviSlice;

#[test]
fn essvi_slices_recover_a_global_ssvi_surface_and_interpolate_without_arbitrage() {
  // Market slices generated by a global SSVI surface (ρ = −0.4, η = 0.6, γ = 0.4) at four maturities.
  let params = SsviParams::new(-0.4, 0.6, 0.4);
  let maturities = [0.25, 0.5, 1.0, 2.0];
  let ks: Vec<f64> = (0..21).map(|i| -0.5 + 0.05 * i as f64).collect();
  let slices: Vec<SsviSlice<f64>> = maturities
    .iter()
    .map(|&t| SsviSlice {
      log_moneyness: ks.clone(),
      total_variance: ks
        .iter()
        .map(|&k| params.total_variance(k, 0.04 * t))
        .collect(),
      theta: 0.04 * t,
    })
    .collect();

  // Anchored (ρ, ψ) per slice, going forward in maturity within the butterfly and calendar bounds.
  let surface = calibrate_essvi(&slices, &maturities);
  assert!(surface.is_butterfly_free() && surface.is_calendar_spread_free());
  for (slice, &t) in surface.slices.iter().zip(&maturities) {
    assert!((slice.rho + 0.4).abs() < 2e-3);
    assert!((slice.total_variance(0.2) - params.total_variance(0.2, 0.04 * t)).abs() < 2e-6);
  }

  // Between the slices the parameters interpolate linearly, keeping w non-decreasing in t.
  let w_early = surface.total_variance(0.1, 0.75);
  let w_late = surface.total_variance(0.1, 1.5);
  assert!(w_early > surface.total_variance(0.1, 0.5) && w_late > w_early);
}
```

```python
import numpy as np
import stochastic_rs as srs

ks = np.linspace(-0.5, 0.5, 21)
maturities = [0.25, 0.5, 1.0, 2.0]
ssvi = srs.SsviParams(rho=-0.4, eta=0.6, gamma=0.4)
slices = [(ks, np.array([ssvi.total_variance(k, 0.04 * t) for k in ks]), 0.04 * t) for t in maturities]

surface = srs.EssviSurface.calibrate(maturities, slices)     # (ks, total_variances, theta) per slice
print(surface.slices())                                      # [(t, theta, rho, psi), ...]
print(surface.is_butterfly_free(), surface.is_calendar_spread_free())
print(surface.implied_vol(0.1, 0.75))                        # interpolated between the 0.5y and 1y slices
```

### Regularised calibration

Every least-squares and Nelder–Mead calibrator accepts an optional `Regularization`: a Tikhonov pull `Σ λ_j (θ_j − θ_j⁰)²` toward an anchor, entering the Levenberg–Marquardt problems (Heston, SABR) as extra residual rows `√λ_j (θ_j − θ_j⁰)` with matching Jacobian rows and the swaption calibrators (Hull–White, Black–Karasinski, G2++) as a cost penalty. Weights are in price² per parameter² and the default `None` leaves every calibrator bit-for-bit on its unregularised path, so existing results do not move; a weight that is large against the squared price residuals pins its parameter.

```rust title="tests/doctest_quant_regularization.rs"
// docs: quant#regularised-calibration
//! Backs the regularised calibration example on the quant catalog page.

use ndarray::Array1;
use stochastic_rs::quant::calibration::Regularization;
use stochastic_rs::quant::calibration::SabrCalibrator;
use stochastic_rs::quant::pricing::sabr::SabrPricer;
use stochastic_rs::quant::types::OptionType;
use stochastic_rs::traits::Calibrator;

#[test]
fn a_tikhonov_anchor_pulls_the_sabr_fit() {
  // Synthetic smile from SABR (α = 0.2, β = 1, ν = 0.6, ρ = −0.3), one year, seven strikes.
  let (s, r, tau) = (100.0, 0.01, 1.0);
  let strikes = vec![80.0, 90.0, 95.0, 100.0, 105.0, 110.0, 120.0];
  let pricer = SabrPricer::new(0.2, 1.0, 0.6, -0.3);
  let prices: Vec<f64> = strikes
    .iter()
    .map(|&k| pricer.call_put(s, k, r, 0.0, tau).0)
    .collect();
  let calibrator = |regularization: Option<Regularization>| {
    let mut c = SabrCalibrator::new(
      None,
      Array1::from_vec(prices.clone()),
      Array1::from_elem(strikes.len(), s),
      Array1::from_vec(strikes.clone()),
      r,
      None,
      tau,
      OptionType::Call,
      false,
    );
    c.regularization = regularization;
    c
  };

  // Plain least squares recovers ν; a heavy anchor at ν⁰ = 0.9 (weights in price² units,
  // natural order (α, ν, ρ)) pulls the fit there.
  let plain = calibrator(None).calibrate(None).unwrap();
  let pulled = calibrator(Some(Regularization::new(
    vec![0.2, 0.9, -0.3],
    vec![0.0, 1e4, 0.0],
  )))
  .calibrate(None)
  .unwrap();
  assert!((plain.nu - 0.6).abs() < 0.05 && (pulled.nu - 0.9).abs() < 0.05);
}
```

```python
import stochastic_rs as srs

strikes = [80.0, 90.0, 95.0, 100.0, 105.0, 110.0, 120.0]
prices = [srs.SabrPricer(s=100.0, alpha=0.2, beta=1.0, nu=0.6, rho=-0.3, k=k, r=0.01, tau=1.0).price() for k in strikes]

plain = srs.SabrCalibrator(strikes, prices, s=100.0, r=0.01, tau=1.0).calibrate()
pulled = srs.SabrCalibrator(strikes, prices, s=100.0, r=0.01, tau=1.0,
                            regularization=([0.2, 0.9, -0.3], [0.0, 1e4, 0.0])).calibrate()
print(plain[2], pulled[2])     # nu ≈ 0.6 without the anchor, ≈ 0.9 with it
```

### Heston PDE by ADI finite differences

`HestonAdiPricer` solves the two-dimensional Heston PDE with correlation on the sinh-stretched meshes of in 't Hout and Foulon (2010) — points clustered at the strike and at `v = 0`, second-order stencils with upwinding of the variance drift where it points outward — and steps it in time with their four Alternating Direction Implicit schemes (Douglas, Craig–Sneyd, Modified Craig–Sneyd, Hundsdorfer–Verwer) plus Rannacher start-up damping; the default is the paper's recommendation, MCS at `θ = ⅓`. Each step costs a tridiagonal sweep in `s` and a banded sweep in `v`, the mixed derivative staying explicit. A down-and-out barrier moves the lower spot boundary to the barrier. The pricer implements `ModelPricer` (`r = r_d`, `q = r_f`) and, without a barrier, `VanillaEuropeanCall`.

```rust title="tests/doctest_quant_heston_adi.rs"
// docs: quant#heston-pde-by-adi-finite-differences
//! Backs the Heston ADI example on the quant catalog page.

use stochastic_rs::quant::pricing::heston::HestonPricer;
use stochastic_rs::quant::pricing::heston_adi::AdiScheme;
use stochastic_rs::quant::pricing::heston_adi::HestonAdiPricer;
use stochastic_rs::traits::ModelPricer;

#[test]
fn adi_solver_reprices_the_semi_analytic_heston_call() {
  // Case 1 of in 't Hout & Foulon: κ = 1.5, η = 0.04, σ = 0.3, ρ = −0.9, r_d = 2.5 %, one year.
  let (v0, kappa, eta, sigma, rho) = (0.04, 1.5, 0.04, 0.3, -0.9);
  let (s, k, r_d, r_f, tau) = (100.0, 100.0, 0.025, 0.0, 1.0);

  // Model + numerics state; the query travels as arguments (r = r_d, q = r_f).
  let pde = HestonAdiPricer::new(v0, kappa, eta, sigma, rho)
    .with_grid(100, 50, 50)
    .with_scheme(AdiScheme::ModifiedCraigSneyd);
  let adi = pde.price_call(s, k, r_d, r_f, tau);

  // Semi-analytic reference through the crate's Heston pricer.
  let analytic = HestonPricer::new(v0, rho, kappa, eta, sigma, Some(0.0))
    .call_put(s, k, r_d, r_f, tau)
    .0;
  assert!(
    (adi - analytic).abs() / analytic < 1e-2,
    "adi {adi} vs analytic {analytic}"
  );

  // The same grid prices a down-and-out call by moving the lower boundary to the barrier.
  let knocked = pde.with_barrier(90.0).price_call(s, k, r_d, r_f, tau);
  assert!(knocked > 0.0 && knocked < adi);
}
```

```python
import stochastic_rs as srs

pde = srs.HestonAdiPricer(s=100.0, k=100.0, r=0.025, tau=1.0, v0=0.04, kappa=1.5, theta=0.04, sigma=0.3, rho=-0.9,
                          q=0.0, m1=100, m2=50, steps=50, scheme="mcs")
call, put = pde.call_put()
analytic = srs.HestonPricer(s=100.0, k=100.0, r=0.025, tau=1.0, v0=0.04, kappa=1.5, theta=0.04, sigma=0.3, rho=-0.9, q=0.0).price()
print(call, analytic)                     # agree to ~1 %
print(srs.HestonAdiPricer(s=100.0, k=100.0, r=0.025, tau=1.0, v0=0.04, kappa=1.5, theta=0.04, sigma=0.3, rho=-0.9,
                          barrier=90.0).price())   # down-and-out call
```

### XVA — exposure profiles and CVA / DVA / FVA

`ExposureProfile` reduces a matrix of simulated mark-to-market values (paths × dates) to expected positive and negative exposure and a PFE quantile; `cva`, `dva`, `bilateral_cva`, `bilateral_dva`, `fca`, `fba` and `fva` integrate those profiles against survival, discount and funding curves on the exposure grid (Gregory 2015, Ch. 7, 14, 15). The core is model-agnostic; `HullWhiteSwapExposure` shows it fed by the crate's own stack, simulating the short rate as the Ornstein–Uhlenbeck factor plus the curve-fitting shift and revaluing a payer swap on its payment dates from the Hull–White bond formula.

```rust title="tests/doctest_quant_xva.rs"
// docs: quant#xva-exposure-profiles-and-cva--dva--fva
//! Backs the XVA example on the quant catalog page.

use ndarray::Array1;
use stochastic_rs::quant::credit::survival_curve::HazardInterpolation;
use stochastic_rs::quant::credit::survival_curve::SurvivalCurve;
use stochastic_rs::quant::curves::DiscountCurve;
use stochastic_rs::quant::curves::InterpolationMethod;
use stochastic_rs::quant::risk::xva::cva;
use stochastic_rs::quant::risk::xva::fva;
use stochastic_rs::quant::risk::xva::irs::HullWhiteSwapExposure;
use stochastic_rs::simd_rng::Deterministic;

#[test]
fn cva_of_a_par_swap_under_hull_white() {
  // Flat 3 % curve, five-year annual payer swap of 1m notional at par.
  let curve = DiscountCurve::from_zero_rates(
    &Array1::from_vec(vec![0.5, 1.0, 5.0, 10.0]),
    &Array1::from_vec(vec![0.03; 4]),
    InterpolationMethod::LogLinearOnDiscountFactors,
  );
  let mut swap = HullWhiteSwapExposure::new(
    0.1,
    0.01,
    1_000_000.0,
    0.0,
    vec![1.0, 2.0, 3.0, 4.0, 5.0],
    1.0,
  );
  swap.fixed_rate = swap.par_rate(&curve);

  // Exposure profile on the payment dates from 4 000 Hull–White short-rate paths.
  let profile = swap.profile(&curve, 4_000, 0.95, Deterministic::new(7));
  assert!(profile.peak_epe() > 0.0 && profile.epe[4] == 0.0);

  // CVA against a 2 % flat hazard at 60 % LGD, and the symmetric FVA at a 50 bp funding spread.
  let counterparty = SurvivalCurve::from_hazard_rates(
    &Array1::from_vec(vec![1.0, 5.0, 10.0]),
    &Array1::from_vec(vec![0.02; 3]),
    HazardInterpolation::PiecewiseConstantHazard,
  );
  let cva_value = cva(&profile, &counterparty, &curve, 0.6);
  assert!(cva_value > 0.0 && cva_value < 0.01 * swap.notional);
  let _funding = fva(&profile, &curve, 0.005);
}
```

```python
import numpy as np
import stochastic_rs as srs

curve = srs.DiscountCurve.from_zero_rates(np.array([0.5, 1.0, 5.0, 10.0]), np.full(4, 0.03), interp="log_df")
swap = srs.HullWhiteSwapExposure(mean_reversion=0.1, sigma=0.01, notional=1e6, payment_times=[1.0, 2.0, 3.0, 4.0, 5.0])
profile = swap.profile(curve, paths=4000, quantile=0.95, seed=7)     # at par on the curve
print(profile.epe(), profile.pfe())
print(profile.cva(hazard_rate=0.02, discount=curve, lgd=0.6), profile.fva(curve, funding_spread=0.005))

# Any MtM matrix works: here a Brownian one, whose EPE is σ√t/√(2π).
mtm = 10.0 * np.cumsum(np.random.default_rng(1).standard_normal((20000, 4)) * np.sqrt(0.5), axis=1)
print(srs.ExposureProfile.from_mtm(mtm, [0.5, 1.0, 1.5, 2.0]).epe())
```

### CDS index and CDO tranches

`CdsIndex` values an untranched index name by name with the ISDA-style single-name engine and aggregates by notional weight, so the index fair spread is the annuity-weighted average of the names'; `isda_upfront` applies the standard-model convention (a flat hazard solved so a running CDS at the quoted spread is worth zero, then the coupon shortfall on that curve). `CdoTranche` prices a `[A, D]` tranche under the one-factor Gaussian copula: conditional on the market factor the defaults are independent, the loss distribution follows from the Andersen–Sidenius–Basu recursion on a loss grid, the factor is integrated by Gauss–Hermite quadrature, and the protection and premium legs follow O'Kane's mid-period convention; the Vasicek large-pool limit is exposed as a cross-check.

```rust title="tests/doctest_quant_credit_portfolio.rs"
// docs: quant#cds-index-and-cdo-tranches
//! Backs the CDS index and tranche example on the quant catalog page.

use chrono::NaiveDate;
use ndarray::Array1;
use stochastic_rs::quant::credit::index::CdsIndex;
use stochastic_rs::quant::credit::index::flat_survival;
use stochastic_rs::quant::credit::tranche::CdoTranche;
use stochastic_rs::quant::credit::tranche::PoolName;
use stochastic_rs::quant::curves::DiscountCurve;
use stochastic_rs::quant::curves::InterpolationMethod;

#[test]
fn index_upfront_and_tranche_spreads() {
  let discount = DiscountCurve::from_zero_rates(
    &Array1::from_vec(vec![0.5, 1.0, 5.0, 10.0]),
    &Array1::from_vec(vec![0.03; 4]),
    InterpolationMethod::LogLinearOnDiscountFactors,
  );
  let date = |y: i32, m: u32, d: u32| NaiveDate::from_ymd_opt(y, m, d).unwrap();

  // A 125-name index at a 100 bp coupon: fair spread and the ISDA upfront for a 120 bp quote.
  let index = CdsIndex::homogeneous(
    vec![flat_survival(0.02); 125],
    0.4,
    0.01,
    10_000_000.0,
    date(2026, 3, 20),
    date(2031, 6, 20),
  );
  let fair = index.fair_spread(date(2026, 3, 20), &discount);
  let upfront = index.isda_upfront(date(2026, 3, 20), &discount, 0.012, 0.4);
  assert!(fair > 0.0 && upfront > 0.0);

  // Equity and mezzanine tranches on the same pool under a 30 % Gaussian copula correlation.
  let pool: Vec<PoolName> = (0..125)
    .map(|_| PoolName {
      weight: 1.0 / 125.0,
      recovery: 0.4,
      survival: flat_survival(0.02),
    })
    .collect();
  let times: Vec<f64> = (1..=5).map(|i| i as f64).collect();
  let equity =
    CdoTranche::new(0.0, 0.03, 0.05, times.clone(), 1.0, 0.3).valuation(&pool, &discount);
  let mezz = CdoTranche::new(0.03, 0.07, 0.01, times, 1.0, 0.3).valuation(&pool, &discount);
  assert!(equity.fair_spread > mezz.fair_spread && mezz.fair_spread > 0.0);
}
```

```python
import numpy as np
import stochastic_rs as srs

curve = srs.DiscountCurve.from_zero_rates(np.array([0.5, 1.0, 5.0, 10.0]), np.full(4, 0.03), interp="log_df")
names = [(1 / 125, 0.4, 0.02)] * 125                     # (weight, recovery, hazard)
index = srs.CdsIndex(names, coupon=0.01, notional=1e7, effective_date="2026-03-20", maturity_date="2031-06-20")
print(index.fair_spread("2026-03-20", curve), index.isda_upfront("2026-03-20", curve, quoted_spread=0.012))

equity = srs.CdoTranche(names, attachment=0.0, detachment=0.03, spread=0.05, payment_times=[1, 2, 3, 4, 5], correlation=0.3)
print(equity.valuation(curve))                            # (protection, annuity, premium, fair_spread, upfront)
print(equity.expected_tranche_loss(5.0), equity.large_pool_expected_tranche_loss(p=1 - np.exp(-0.1), lgd=0.6))
```

### Heston — Fourier pricer with Cui analytic Jacobian

```rust title="tests/doctest_quant_heston_pricer.rs"
// docs: quant#heston-fourier-pricer-with-cui-analytic-jacobian
//! Backs the Heston pricer example on the quant catalog page. The model
//! holds only its six Heston parameters; spot, strike, rate, dividend
//! yield and maturity travel to the call, so one instance prices a whole
//! strike/maturity grid.

use stochastic_rs::quant::OptionType;
use stochastic_rs::quant::pricing::heston::HestonPricer;
use stochastic_rs::traits::ModelPricer;

#[test]
fn heston_pricer_price_and_greeks() {
  let model = HestonPricer::new(
    /* v0 */ 0.04, /* rho */ -0.5, /* kappa */ 2.0, /* theta */ 0.04,
    /* sigma */ 0.3, /* lambda */ None,
  );
  let (s, k, r, q, tau) = (100.0, 100.0, 0.03, 0.0, 1.0);

  let price = model.price_call(s, k, r, q, tau);
  let g = model.greeks(s, k, r, q, tau, OptionType::Call);

  assert!(price > 0.0);
  assert!((0.0..=1.0).contains(&g.delta));
  assert!(g.vega > 0.0);
  assert!(g.vanna.is_finite());
}
```

```python
import stochastic_rs as srs

pricer = srs.HestonPricer(
    s=100, v0=0.04, k=100, r=0.03, kappa=2.0, theta=0.04, sigma=0.3,
    rho=-0.5, tau=1.0, q=0.0,
)
call, put = pricer.call_put()
print(f"call={call:.4f}, put={put:.4f}")
```

### Heston calibration to a market vol surface

```rust title="tests/doctest_quant_heston_calibration.rs"
// docs: quant#heston-calibration-to-a-market-vol-surface
//! Backs the Heston calibration example on the quant catalog page.
//! `HestonCalibrator` fits a single maturity slice directly against market
//! call prices (spot/strike/price vectors) rather than an
//! `ImpliedVolSurface` object.

use stochastic_rs::quant::calibration::heston::HestonCalibrator;
use stochastic_rs::quant::calibration::heston::HestonParams;
use stochastic_rs::quant::types::OptionType;
use stochastic_rs::traits::CalibrationResult;
use stochastic_rs::traits::Calibrator;

#[test]
fn heston_calibrator_recovers_plausible_params() {
  let s = vec![100.0; 9];
  let k = vec![80.0, 85.0, 90.0, 95.0, 100.0, 105.0, 110.0, 115.0, 120.0];
  let c_market = vec![21.5, 17.9, 14.2, 11.0, 8.2, 6.0, 4.3, 3.1, 2.2];

  let calibrator = HestonCalibrator::new(
    Some(HestonParams {
      v0: 0.04,
      kappa: 1.5,
      theta: 0.04,
      sigma: 0.5,
      rho: -0.7,
    }),
    c_market.into(),
    s.into(),
    k.into(),
    /* r */ 0.01,
    /* q */ Some(0.0),
    /* tau */ 0.5,
    OptionType::Call,
    None,
    None,
    None,
    /* record_history */ true,
  );

  let result = calibrator.calibrate(None).unwrap();
  let p = result.params();

  assert!(p.kappa > 0.0);
  assert!(p.theta > 0.0);
  assert!(p.sigma > 0.0);
  assert!((-1.0..=1.0).contains(&p.rho));
  assert!(result.rmse() >= 0.0);
}
```

```python
import stochastic_rs as srs

# The calibrator takes one MarketSlice per expiry — strikes, observed
# prices, per-strike call/put flags, and tau — not a surface object.
slices = [
    srs.MarketSlice(strikes, prices, [True] * len(strikes), tau)
    for strikes, prices, tau in quotes
]
v0, kappa, theta, sigma, rho, converged, rmse = srs.HestonCalibrator(
    slices, s=100.0, r=0.03
).calibrate()
print(f"kappa={kappa:.3f}, theta={theta:.4f}, sigma={sigma:.3f}, "
      f"rho={rho:.3f}, v0={v0:.4f}")
print(f"RMSE = {rmse:.6f} (converged={converged})")
```

<a id="heston-slv-leverage-calibration-by-the-particle-method" />

### Heston SLV leverage calibration

A vanilla call surface in, a Heston stochastic-local volatility model out:
the Heston parameters are fitted to the quotes (or pinned), the Dupire local
volatility is read off the calls (or supplied on the same grid), and the
leverage $L(t, S)$ that makes the model reproduce the surface at the chosen
mixing fraction $\eta$ is calibrated either by the Guyon–Henry-Labordère
particle method — a Gaussian kernel regression of $\mathbb{E}[V_t \mid S_t]$
over an interacting particle cloud — or, with `with_fokker_planck`, by the
Wyns–Du Toit finite-volume solution of the forward Kolmogorov equation,
marched by the Hundsdorfer–Verwer ADI scheme with an inner iteration on the
non-linearity. Its first two steps are replaced by four fully implicit Euler
half-steps, including the mixed derivative, solved by sparse LU. The variance
mesh resolves both zero and the initial variance; the forward mixed flux at
zero is used only when the Feller condition is violated. The result reprices
the input grid from its own density and
hands a rate-anchored `HestonSlvPricer` back through `ToModel`; the same
surface drives the `HestonSlv` process for exotics, and `heston_slv_density`
prices the vanilla grid of a calibrated model without Monte Carlo noise.
Both routes were cross-checked against QuantLib's `HestonSLVFDMModel` on the
same local volatility: the leverage surfaces agree to a mean of 0.001–0.006,
closer than QuantLib's own FDM and MC models agree with each other. Supply a
smooth local volatility through `with_local_vol` (an SSVI fit, say) where one
exists — the finite-difference Dupire read of a call grid is the dominant
error otherwise.

```rust title="tests/doctest_quant_heston_slv.rs"
// docs: quant#heston-slv-leverage-calibration-by-the-particle-method
//! Backs the Heston SLV calibration example on the quant catalog page: a
//! vanilla call surface in, a leverage-calibrated model and its Monte Carlo
//! pricer out.

use ndarray::Array1;
use ndarray::Array2;
use stochastic_rs::quant::calibration::heston::HestonParams;
use stochastic_rs::quant::calibration::heston_slv::HestonSlvCalibrator;
use stochastic_rs::quant::pricing::fourier::HestonFourier;
use stochastic_rs::quant::pricing::slv::ParticleMethod;
use stochastic_rs::traits::CalibrationResult;
use stochastic_rs::traits::Calibrator;
use stochastic_rs::traits::ModelPricer;

#[test]
fn heston_slv_calibrates_a_vanilla_surface_and_prices_under_half_mixing() {
  let (s, r, q) = (100.0, 0.02, 0.0);
  let heston = HestonParams {
    v0: 0.04,
    kappa: 2.0,
    theta: 0.05,
    sigma: 0.4,
    rho: -0.6,
  };
  // The vanilla surface — generated from a Heston model here, market quotes
  // in practice: `calls[[j, i]]` is the call at `strikes[i]`, `maturities[j]`.
  let strikes = Array1::linspace(70.0, 140.0, 36).to_vec();
  let maturities = vec![0.25, 0.5, 0.75, 1.0];
  let model = HestonFourier {
    v0: heston.v0,
    kappa: heston.kappa,
    theta: heston.theta,
    sigma: heston.sigma,
    rho: heston.rho,
    r,
    q,
  };
  let calls = Array2::from_shape_fn((maturities.len(), strikes.len()), |(j, i)| {
    model.price_call(s, strikes[i], r, q, maturities[j])
  });

  // Half the vol-of-vol; the leverage makes up the difference so the surface
  // is reproduced. The Heston fit is pinned here — leave it out to have
  // `HestonCalibrator` fit the same quotes.
  let result = HestonSlvCalibrator::new(s, r, q, strikes, maturities, calls)
    .with_mixing(0.5)
    .with_heston_params(heston)
    .with_particle_method(
      ParticleMethod::default()
        .with_particles(5_000)
        .with_steps_per_year(50)
        .with_seed(7),
    )
    .calibrate(None)
    .unwrap();
  assert!(result.converged());
  assert!(
    result.rmse() < 1.0,
    "in-sample repricing rmse {}",
    result.rmse()
  );
  assert!(result.leverage().covers(100.0, 0.5));

  // The pricer is anchored to the calibration rates and reprices the market.
  let pricer = result
    .to_model(r, q)
    .with_paths(4_000)
    .with_steps_per_year(50);
  let call = pricer.price_call(s, 100.0, r, q, 0.5);
  let market = model.price_call(s, 100.0, r, q, 0.5);
  assert!((call - market).abs() < 1.0, "slv {call} vs market {market}");
}
```

```python
import stochastic_rs as srs

# calls[j, i] = C(strikes[i], maturities[j]); eta scales the vol-of-vol and
# the leverage makes up the difference, so the surface is reproduced either way.
calibrator = srs.HestonSlvCalibrator(
    s=100.0, r=0.02, q=0.0, strikes=list(strikes), maturities=list(maturities),
    calls=calls, eta=0.5, n_particles=50_000, steps_per_year=100,
)   # method="fokker_planck" swaps the cloud for the finite-volume PDE
result = calibrator.calibrate()
kappa, theta, sigma, rho, v0, eta = result.params()
print(f"rmse = {result.rmse:.4f} (converged={result.converged})")
surface = result.leverage()             # .spots, .times, .values, .interpolate(s, t)
pricer = result.to_model(n_paths=50_000)  # anchored to the calibration (r, q)
print(pricer.price_call(100.0, 100.0, 0.02, 0.0, 0.5))
```

## Trait overview

The relevant traits live in [Concepts](/docs/concepts/traits):

* [`ModelPricer`](/docs/concepts/model-pricer)
* [`Calibrator`, `CalibrationResult`, `ToModel`](/docs/concepts/traits#calibrator-and-calibrationresult)
* [Greeks aggregator](/docs/concepts/traits#greeksext-and-the-greeks-aggregator)

## Adding a pricer / calibrator

* [`calibration-pattern`](https://github.com/dancixx/stochastic-rs/blob/main/.claude/skills/calibration-pattern/SKILL.md)
  — Heston / SABR / SVI / Lévy calibrators
* [`greeks-pattern`](https://github.com/dancixx/stochastic-rs/blob/main/.claude/skills/greeks-pattern/SKILL.md)
  — exposing first- + second-order Greeks
* [`add-mc-variance-reduction`](https://github.com/dancixx/stochastic-rs/blob/main/.claude/skills/add-mc-variance-reduction/SKILL.md)
  — antithetic / control-variate / quasi-MC / MLMC

