# Migration guide — v1.x → v2.0

This document lists every breaking change between the last `1.x` release and
`2.0.0`. The changes fall into five buckets:

1. [Workspace split](#1-workspace-split) (mostly transparent — paths preserved)
2. [Trait surface changes](#2-trait-surface-changes) (API-level breaks)
3. [Numerical correctness fixes](#3-numerical-correctness-fixes) (silent behaviour change — please re-test)
4. [New primitives](#4-new-primitives) (additive — no migration needed)
5. [Build & packaging](#5-build--packaging) (Cargo.toml / pyproject.toml)

For the full audit trail see `docs/API_AUDIT_2026-04.md` and
`docs/API_AUDIT_2026-04-28.md`. The detailed workspace split steps are in
`docs/WORKSPACE_MIGRATION.md`.

---

## 1. Workspace split

`stochastic-rs` is now a Cargo workspace with 9 sub-crates:

```
stochastic-rs/                        (umbrella crate — re-exports everything)
├── stochastic-rs-core/               — simd_rng, foundation
├── stochastic-rs-distributions/      — distributions + FloatExt
├── stochastic-rs-stochastic/         — 140+ processes + ProcessExt
├── stochastic-rs-copulas/            — copulas + BivariateExt
├── stochastic-rs-stats/              — estimators
├── stochastic-rs-quant/              — pricing/calibration/vol_surface
├── stochastic-rs-ai/                 — neural surrogates (feature = "ai")
├── stochastic-rs-viz/                — Plotly grid plotter
└── stochastic-rs-py/                 — Python (pyo3) cdylib
```

**Existing paths still work via the umbrella `pub use` re-exports.** No code
change is required if you depend on `stochastic-rs` and import via
`stochastic_rs::stochastic::*`, `stochastic_rs::quant::*`, etc.

### Optional: depend on a single sub-crate

If you only need samplers, you can now depend on a single sub-crate to skip
the quant/stats/ai compilation cost:

```toml
[dependencies]
stochastic-rs-distributions = "2.0"
```

```rust
use stochastic_rs_distributions::normal::SimdNormal;
```

---

## 2. Trait surface changes

### 2.1. New `prelude`

```rust
use stochastic_rs::prelude::*;
```

Brings 20 items:

- **Trait core** — `FloatExt`, `SimdFloatExt`, `ProcessExt`, `BivariateExt`,
  `DistributionExt`, `DistributionSampler`, `TimeExt`.
- **Pricing** — `PricerExt`, `ModelPricer`, `GreeksExt`.
- **Calibration** — `Calibrator`, `CalibrationResult`, `ToModel`.
- **Instrument / engine (QuantLib-style decoupling)** — `Instrument`,
  `InstrumentExt`, `PricingEngine`, `PricingResult`.
- **Option types** — `Moneyness`, `OptionStyle`, `OptionType`.

`MalliavinExt` and `Malliavin2DExt` are **not** in the prelude (they have no
in-tree impls yet). Import them from `stochastic_rs::traits` when needed.

`MultivariateExt` (openblas-only) and `CallableDist` (python-only) are also
reachable via `stochastic_rs::traits::*` but kept out of the prelude to
avoid a hard feature-gate dependency on the prelude itself.

### 2.2. `Calibrator` is now `Result`-based

**Before (v1):**

```rust
pub trait Calibrator {
  type InitialGuess;
  type Output: CalibrationResult;
  fn calculate(&self, initial: Option<Self::InitialGuess>) -> Self::Output;
}

let result = calibrator.calculate(None);
```

**After (v2):**

```rust
pub trait Calibrator {
  type InitialGuess;
  type Params: Clone;
  type Output: CalibrationResult<Params = Self::Params>;
  type Error;

  fn calibrate(&self, initial: Option<Self::InitialGuess>)
    -> Result<Self::Output, Self::Error>;
}

let result = calibrator.calibrate(None)?;
```

All 9 in-tree impls use `type Error = anyhow::Error;`. Inherent
`*Calibrator::calibrate` methods are preserved for back-compat — Rust's
inherent-method preference means `calibrator.calibrate(None)` keeps the legacy
signature; the trait variant is reached via UFCS
(`<C as Calibrator>::calibrate(&calibrator, None)?`) or behind a generic
bound.

New parameter wrapper structs (re-exported from `crate::calibration`):
`HscmParams`, `HullWhiteParams`, `LevyParams`, `SabrCapletParams`.

### 2.3. `CalibrationResult` extended

**Before:**

```rust
pub trait CalibrationResult {
  fn rmse(&self) -> f64;
  fn converged(&self) -> bool;
  fn loss_score(&self) -> Option<&CalibrationLossScore> { None }
}
```

**After:**

```rust
pub trait CalibrationResult {
  type Params: Clone;
  fn params(&self) -> Self::Params;
  fn rmse(&self) -> f64;
  fn converged(&self) -> bool;
  fn iterations(&self) -> Option<usize> { None }
  fn message(&self) -> Option<&str> { None }
  fn max_error(&self) -> f64 { f64::NAN }
  fn loss_score(&self) -> Option<&CalibrationLossScore> { None }
}
```

If you implement `CalibrationResult`, you must add `type Params` and
`fn params(&self)`. The other three accessors have defaults.

### 2.4. `PricerExt::implied_volatility` default → `f64::NAN`

The default was silently `0.0`. Now it is `f64::NAN`. If you relied on the
`0.0` sentinel, switch to `result.is_nan()`.

### 2.5. `PricerExt::derivatives()` deprecated → `GreeksExt::greeks()`

**Before:**

```rust
let derivs: Vec<f64> = pricer.derivatives();
let delta = derivs[0];  // type-erased
```

**After:**

```rust
use stochastic_rs::prelude::*;

let g: Greeks = pricer.greeks();
let delta = g.delta;
let gamma = g.gamma;
// also: vega, theta, rho, vanna, charm, volga, veta
```

The new `Greeks` struct has named fields. `derivatives()` is marked
`#[deprecated(since = "2.0.0-beta.3")]` but still works; the symbol will be
removed in a later 2.x release.

Default `Greeks` value is `Greeks { delta: NaN, gamma: NaN, vega: NaN, ... }`
— pricers that haven't wired specific Greeks will return NaN for those
fields.

### 2.6. `GreeksExt` extended with 2nd-order Greeks

`GreeksExt` now exposes (with `f64::NAN` defaults):

```rust
pub trait GreeksExt {
  fn delta(&self) -> f64;
  fn gamma(&self) -> f64 { f64::NAN }
  fn vega(&self) -> f64 { f64::NAN }
  fn theta(&self) -> f64 { f64::NAN }
  fn rho(&self) -> f64 { f64::NAN }
  fn vanna(&self) -> f64 { f64::NAN }
  fn charm(&self) -> f64 { f64::NAN }
  fn volga(&self) -> f64 { f64::NAN }
  fn veta(&self) -> f64 { f64::NAN }
  fn greeks(&self) -> Greeks { /* aggregator */ }
}
```

If you implement `GreeksExt` on a Monte-Carlo-based pricer, **override
`greeks()`** so it shares paths between Greeks instead of running independent
MC estimators. See `GbmMalliavinGreeks` and `HestonMalliavinGreeks` for
examples.

### 2.7. `DistributionExt` defaults: `0.0` → `unimplemented!()`

In v1 every method (`pdf`, `cdf`, `mean`, `variance`, …) defaulted to `0.0`.
Distributions that didn't override silently returned wrong values. In v2 the
default panics:

```rust
fn pdf(&self, _x: f64) -> f64 {
    unimplemented!("pdf not implemented for this distribution")
}
```

If you call `pdf`/`cdf`/etc. on a distribution that doesn't implement it, you
now get a panic instead of `0.0`. Three distributions ship with full impls:
`SimdNormal`, `SimdLogNormal`, `SimdGamma`. The remaining 16 will be filled
in across the 2.x line.

### 2.8. `MalliavinExt` / `Malliavin2DExt` removed from `prelude`

These traits remain at `stochastic_rs::traits::{MalliavinExt, Malliavin2DExt}`
but are no longer pulled in by `use prelude::*`, since they have 0 in-tree
impls yet. Explicit import:

```rust
use stochastic_rs::traits::MalliavinExt;
```

### 2.9. `ToModel` widely implemented; `ToShortRateModel` introduced

13 calibrators now implement `ToModel`:

```rust
let calibrated_pricer = sabr_calibrator
    .calibrate(initial)?
    .to_model(r, q);
```

The parallel trait `ToShortRateModel` covers lattice/short-rate
calibration (e.g. `HullWhiteSwaptionCalibrator`).

---

## 3. Numerical correctness fixes

### 3.1. `calculate_tau_in_days()` removed

In v1, several pricers (`BSMPricer`, `HestonPricer`, `SabrPricer`,
`HestonStochCorrPricer`, `AsianPricer`, Vasicek bond) called a
`calculate_tau_in_days()` helper on `TimeExt` whose body was `tau * 365`.
The result was passed to `implied_vol::ImpliedBlackVolatility::expiry()`,
which expects **years**, so implied volatilities were understated by
`sqrt(365)`.

In v2 the helper is gone. Pricers use `tau_or_from_dates()` (which returns
years) directly. **If you held v1 IVs in storage, recompute them.**

### 3.2. Date support is now uniform

In v1, `BSMPricer::tau_required()` panicked when only dates (not `tau`)
were set; `HestonPricer` silently defaulted to `tau = 1.0`. In v2 both call
`tau_or_from_dates()`, so the date path works. New tests:
`bsm_dates_match_tau_pricing`.

### 3.3. Variance swap fair-strike replication fix

The Carr-Madan static-replication formula in `pricing/variance_swap.rs` had
a sign error on the `rT` and `ln(K0/S0)` correction terms in v1. Fixed.
Verified against a dense BSM strip recovering `σ² = 0.0625` within 2 % rel-err.
New `fair_strike_heston()` (Brockhaus-Long 2000 closed-form) and
`fair_strike_heston_discrete()` (Bernard-Cui 2013 leading-order discrete
correction) added.

### 3.4. Total Return Swap forward semantics

In v1, the proto-TRS implementation used the price-only forward
`F(t) = S · exp((r-q) t)`, which prices price-return rather than total-return.
The release version uses the total-return forward `F_TR(t) = S · exp(r t)`
(under the risk-neutral measure, total wealth grows at `r` regardless of
dividend yield). The `dividend_yield` field on `TotalReturnSwap` was
**removed** — under proper TR semantics it is irrelevant.

### 3.5. `BSMPricer` / `HestonPricer` / `SabrPricer` /
`HestonStochCorrPricer::implied_volatility` spot/forward fix

In v1 these methods passed the spot price as the forward to
`implied_vol::ImpliedBlackVolatility`, with the option price not properly
discounted. Both errors are fixed; new test
`bsm_iv_round_trip_across_strikes_and_maturities` covers a strikes × maturities
grid.

### 3.6. fBM property test pinned seed

`process::fbm::tests::fbm_terminal_marginal_is_gaussian_with_correct_scale`
in v1 was a 5 %-tolerance Monte Carlo test without a pinned RNG seed, which
made it 5 %-flaky by construction. In v2 it uses
`Fbm::seeded(h, n, t, 0xF_B_C0FFEE_u64)`.

### 3.7. rc.1 → stable breaking changes

These are the v2.0.0-rc.1 → v2.0.0 stable changes (see `docs/V2_RC2_TODO.md`
for the full audit trail):

- **`HestonPricer` (and full Heston family) switched to "Little Heston Trap"
  form** (Albrecher-Mayer-Schoutens-Tistaert 2007). Original `(b + d)·t` /
  `exp(d·t)` numerics are replaced with the swap form `(b - d)·t` /
  `exp(-d·t)` to keep the principal-branch logarithm continuous for
  long-maturity high-|ρ| Heston (regression test on T=5y, ρ=-0.9). The
  observable price is unchanged for typical equity parameters; users who
  pinned numerics on the rc.1 mid-rho mid-tau ChF should re-baseline.
- **`simulate_rbergomi_terminal_samples`** signature gained a `q: f64`
  parameter (continuous dividend yield) — pass `0.0` to preserve rc.0 behavior.
- **`SsviSurface::is_calendar_spread_free`** signature gained a `ks: &[T]`
  parameter (full GJ 2014 Theorem 4.2 over a strike grid). Old callers
  that wanted only the necessary ATM-only check should call
  `is_atm_calendar_spread_free()` instead. New helper
  `SsviParams::project_with_theta_range(theta_max)` enforces both butterfly
  and calendar bounds during projection.
- **`bonds::HullWhite`** rewritten — old `theta: fn(f64)->f64` and
  flat-curve fields are gone; users must now provide a `DiscountCurve` via
  `HullWhite::from_curve(...)`.
- **`CarrMadanPricer::price_call`** now returns `f64::NAN` for out-of-grid
  strikes (was `0.0`). Detect via `is_nan()` and either widen the FFT grid
  via `CarrMadanPricer::cumulant_sized(model, t, s, l_factor)` or pre-check
  with `CarrMadanPricer::strike_in_grid(...)`.
- **`empirical_cvar`** (and `cvar_alpha` field) asserts `alpha < 0.5`.
  `alpha` is the **tail proportion** (0.05 → average worst 5% of returns),
  not a confidence level. If you previously passed `0.95`, pass `0.05`.
- **`SabrParams` `From<DVector>`** now requires a 4-vec `[α, β, ν, ρ]` (was
  3-vec `[α, ν, ρ]`).
- **`multi_period_kyle`** output values changed (canonical Cetin-Larsen
  recursion). Tests that asserted β·λ = 0.25 at the terminal node now
  produce 0.5.
- **`TimeExt::tau_or_from_dates` / `tau_with_dcc`** now return `NaN`
  instead of panicking on missing date data.
- **`OptimizerMethod::from_str` / `WeightScheme::from_str`** now return
  `Result<_, UnknownOptimizerMethod>` / `Result<_, UnknownWeightScheme>`
  instead of silently falling back to `Markowitz` / `Equal`.
- **`optimize_with_method`** signature gained a final `&OptimizerConfig`
  parameter (controls `periods_per_year` annualization and `lambda` target-
  return penalty). Pass `&OptimizerConfig::default()` to keep rc.0/rc.1
  behavior.
- **Per-method optimizers** (`optimize_markowitz`, `optimize_markowitz_long_short`,
  `optimize_black_litterman`) gained a `lambda: f64` parameter (penalty
  coefficient, default 10).
- **Mean-CVaR optimizers** (`optimize_mean_cvar`, `optimize_mean_cvar_long_short`)
  gained a `&OptimizerConfig` parameter.
- **`PortfolioEngineConfig`** gained an `optimizer_config: OptimizerConfig`
  field. Default-construct via `OptimizerConfig::default()`.
- **`align_return_series`** documented as right-aligned and date-blind.
  New `align_return_series_by_index(returns, start_indices, length)` for
  date-aware callers.
- **`SabrSmileCalibrator`** now exposes `with_strike_bounds(lo, hi)` and
  defaults to `(s * 0.5, s * 2.0)` (was hardcoded `[1, 10]`). `success` flag
  now requires `objective < success_tol` (default 1e-3); pure-finite no
  longer counts as success.
- **`scilib`** dependency dropped from `stochastic-rs-quant`. Internal-only
  change; users importing `scilib::math::basic::gamma` from this crate
  should switch to `stochastic_rs_distributions::special::gamma`.
- **`ReturnType::Absolute`** in `yahoo` documented as gross return
  (`p_t / p_{t-1}`); the variant was previously named "Absolute" but
  computed gross.
- **`migration::TransitionMatrix::power(0)`** now returns the identity
  matrix (was a panic).
- **`Almgren-Chriss` Buy direction** now flips inventory and trades (not
  just rates) so consumers see buy-frame numbers consistently.

---

## 4. New primitives (additive)

These are new — no migration needed, but they may simplify your code.

### 4.1. `Instrument` + `PricingEngine`

QuantLib-style decoupling between a payoff and its valuation engine:

```rust
use stochastic_rs::prelude::*;
use stochastic_rs::quant::instruments::equity::EuropeanOption;
use stochastic_rs::quant::pricing::engines::analytic_bs::AnalyticBSEngine;

let opt = EuropeanOption::call(strike, expiry);
let engine = AnalyticBSEngine::new(spot_handle, vol_handle, rate_handle);
let npv = engine.npv(&opt);
```

Engines provided: `AnalyticBSEngine`, `AnalyticHestonEngine`. Existing
`*Pricer` types are unchanged.

### 4.2. Reactive market cache

`market::cached::{Cached<T>, MarketObserver}` lazily recomputes on first read
after an upstream `Observable` change. See `examples/full_pipeline.rs` for a
full demo (spot 100 → 105 reactive update).

### 4.3. FX module

`stochastic_rs::quant::fx::*`:
- `FxDeltaConvention`: Spot, Forward, SpotPremiumAdjusted, ForwardPremiumAdjusted
- `AtmConvention`: Forward, DNS, DNS-PA
- `delta()`, `strike_from_delta()`, `atm_strike()`
- `FxMarketQuotes` (ATM + RR + BFLY) and `VannaVolgaSmile` (first-order
  Lagrange interpolation through 25Δ-put / ATM / 25Δ-call pivots)

### 4.4. Total Return Swap

`stochastic_rs::quant::instruments::swap::trs`: `TotalReturnSwap`,
`TrsPeriod`, `TrsDirection`, `TrsValuation` (equity / funding leg PVs,
fair_spread, spread_annuity, per-period cashflows).

### 4.5. Variance swap

`stochastic_rs::quant::pricing::variance_swap`: `fair_strike_replication()`
(Carr-Madan), `fair_strike_heston()` (Brockhaus-Long), `fair_strike_heston_discrete()`
(Bernard-Cui), `replication_weights()` (Demeterfi).

### 4.6. End-to-end pipeline example

`examples/full_pipeline.rs` runs market quotes → 2 engines → portfolio
(call + put + variance swap + TRS) → reactive bump → FX vol smile → aggregate
Greeks → parametric VaR. Run it via:

```sh
cargo run --no-default-features --example full_pipeline
```

### 4.7. Stats `*Result` structs

15+ new dedicated `*Result` types replace tuple/`Vec` returns:
`HestonMleResult`, `FukasawaResult`, `MleResult`, `JohansenResult`, …

### 4.8. Naming aliases

`Vasicek` ⇄ `OU` and `Kou` ⇄ `KOU` are interchangeable.

### 4.9. `numerics` namespace

Numerical utilities have moved under `stochastic_rs::numerics`:
`IsoNormal`, `ItoCalculator`, `Mlmc`, `HaltonSeq`, `SobolSeq`, `RlKernel`,
`MarkovLift`, `Lsm` (openblas-gated).

---

## 5. Build & packaging

### 5.1. Edition

The workspace and all sub-crates are now on `edition = "2024"`. Update your
`rust-toolchain.toml` (or shared profile) to a recent stable / nightly that
supports it.

### 5.2. Feature flags

Umbrella feature flags now correctly propagate to sub-crates:
`gpu`, `cuda-native`, `metal`, `accelerate`, `openblas`, `python`.

If you previously built with `--features gpu` on v1 expecting GPU samplers,
re-build on v2 — you'll now actually get them.

### 5.3. `docs.rs` config

`Cargo.toml` declares an explicit `features = ["openblas", "ai", "yahoo"]`
list for docs.rs (no longer `all-features = true`, which pulled native
GPU SDKs and broke the docs build).

### 5.4. Python module

The Python (`pyo3`) cdylib lives in **`stochastic-rs-py/`**. The umbrella
`stochastic-rs` is a regular `lib`. From the workspace root:

```sh
maturin develop                       # via [tool.maturin] manifest-path
# or:
cd stochastic-rs-py && maturin develop
```

`pyproject.toml` at the workspace root sets
`[tool.maturin] manifest-path = "stochastic-rs-py/Cargo.toml"`, so
`maturin build` from the root works correctly.

The Python module currently exposes distributions and stochastic processes.
Quant pricers, calibrators, copulas, and AI surrogates are deferred to a
follow-up release.

### 5.5. `statrs` is gone from production

Distribution math (`pdf`, `cdf`, `inv_cdf`, moments, characteristic
functions) is now implemented from closed-form in `stochastic-rs-distributions`.
`statrs` remains as a `[dev-dependencies]` entry only, used by the
`distribution_ext_vs_reference` cross-validation tests.

---

## Appendix — quick break-fix checklist

If you are upgrading from `1.x` and just want a checklist:

- [ ] Bump `stochastic-rs = "2.0"` in your `Cargo.toml`
- [ ] Add `?` after every `calibrator.calibrate(None)` (now `Result`)
- [ ] Replace `pricer.derivatives()[i]` reads with `pricer.greeks().delta` etc.
- [ ] Replace `iv == 0.0` checks with `iv.is_nan()`
- [ ] If you implement `CalibrationResult`: add `type Params` and
      `fn params(&self) -> Self::Params`
- [ ] If you implement `DistributionExt`: override every method you actually
      call (defaults now `unimplemented!()`)
- [ ] If you store implied vols computed by `BSMPricer` / `HestonPricer` /
      `SabrPricer`: recompute (the v1 IV was understated by `sqrt(365)`)
- [ ] If you build with `--features gpu`: verify the build still does what you
      expect (the flag now actually flips on GPU samplers in
      `stochastic-rs-stochastic`)
- [ ] If you build the Python module: run `maturin` from the workspace root or
      from `stochastic-rs-py/`, not via the umbrella crate

---

*Migration guide last updated: 2026-05-04 — `v2.0.0-beta.3` → `v2.0.0-rc.0`*
