# Migrating from 1.x to 2.x
regit-svi 2.0 is a deliberate breaking release. The mathematical families
remain Raw SVI, Jump-Wings, and SSVI, but the API now prevents invalid public
state and makes the strength of every arbitrage conclusion inspectable.
This guide uses 2.0.0 names. Read [MATH.md](MATH.md) when migrating code
that treats an arbitrage flag as a control or risk decision.
## Migration checklist
1. Update imports to the domain modules or use the retained top-level
re-exports.
2. Replace field reads with accessors and remove every unchecked constructor.
3. Replace boolean arbitrage logic with status and evidence matching.
4. Update scan code for the fixed bounded diagnostic API.
5. Update calibration result access and inspect termination/report metadata.
6. Choose Constrained or BestEffort explicitly when the policy matters.
7. Rename the modified-power smoothing constructor.
8. Preserve maturity order before constructing term structures or surfaces.
9. Revisit Raw/JW inverse error handling for newly explicit degeneracies.
10. Revalidate density mass windows and tolerances; no integration error is
inferred by the API.
## 1. Module map
| regit_svi::raw::RawSvi | regit_svi::smile::raw::RawSvi |
| regit_svi::jw::SviJw | regit_svi::smile::jump_wings::SviJw |
| regit_svi::convert::* | regit_svi::smile::conversion::* |
| regit_svi::ssvi::* | regit_svi::surface::ssvi::* |
| regit_svi::surface::Surface | regit_svi::surface::interpolation::Surface |
| regit_svi::arbitrage::* | regit_svi::no_arb::{butterfly,calendar,evidence}::* |
| regit_svi::types::* | regit_svi::market::{quote,units,conventions}::* |
| regit_svi::errors::* | regit_svi::error::* |
| regit_svi::calibration::ssvi::* | regit_svi::calibration::surface::* |
| regit_svi::math::* | private implementation; no public replacement |
Frequently used data, model, configuration, and report types remain available
at the crate root: RawSvi, SviJw, Phi, Ssvi, Surface, TermStructure, Quote,
surface-calibration inputs and results, market units, report/config types,
errors, and arbitrage evidence types. Pure coordinate and parametrisation
conversions are also re-exported; calibration, assessment, scan, density, and
other algorithmic entry points remain under their domain modules. Prefer
domain paths in reusable libraries when the source of an item is useful to
readers.
Before:
~~~rust,compile_fail
use regit_svi::{
arbitrage,
raw::RawSvi,
types::{Quote, log_moneyness},
};
~~~
After:
~~~rust
use regit_svi::{
Quote, RawSvi,
market::conventions::log_moneyness,
no_arb::butterfly,
};
~~~
There is no 2.x public access to Brent, Cholesky, Nelder–Mead, LM, or integer
conversion helpers. Those are implementation details, not stable numerical
utility APIs.
## 2. Validated, immutable model values
### 2.1 Raw SVI
RawSvi fields are private. Construction validates finite values, b>=0,
Before:
~~~rust,compile_fail
use regit_svi::raw::RawSvi;
let raw = RawSvi {
a: 0.04,
b: 0.30,
rho: -0.20,
m: 0.0,
sigma: 0.10,
};
let slope = raw.b;
~~~
After:
~~~rust
use regit_svi::smile::raw::RawSvi;
let raw = RawSvi::new(0.04, 0.30, -0.20, 0.0, 0.10)?;
let slope = raw.b();
let (a, b, rho, m, sigma) = raw.parameters();
# let _ = (slope, a, b, rho, m, sigma);
# Ok::<(), regit_svi::ParamError>(())
~~~
Named accessors are a, b, rho, m, sigma, parameters, total_variance,
w_prime, w_double_prime, implied_vol, k_min, w_min, atm_total_variance,
atm_skew, atm_curvature, left_wing_slope, and right_wing_slope.
RawSvi::new_unchecked is no longer public. If an input is trusted, validation
is still required at the API boundary; propagate ParamError or validate it
once when loading the data.
### 2.2 Jump-Wings
SviJw follows the same pattern. Use SviJw::new and the accessors
atm_variance, atm_skew, put_wing, call_wing, min_variance, maturity,
atm_total_variance, min_total_variance, and atm_vol. Its fields and unchecked
constructor are no longer public.
### 2.3 SSVI
Phi is now an opaque validated value rather than a publicly constructible enum.
Before:
~~~rust,compile_fail
use regit_svi::ssvi::{Phi, Ssvi};
let phi = Phi::PowerLaw { eta: 0.5, gamma: 0.5 };
let ssvi = Ssvi { rho: -0.3, phi };
~~~
After:
~~~rust
use regit_svi::surface::ssvi::{Phi, Ssvi};
let phi = Phi::modified_power_law(0.5, 0.5)?;
let ssvi = Ssvi::new(-0.3, phi)?;
# let _ = ssvi;
# Ok::<(), regit_svi::ParamError>(())
~~~
The family name changed from power_law to modified_power_law because the
implemented formula is the modified Gatheral–Jacquier family:
~~~text
eta/[theta^gamma(1+theta)^(1-gamma)]
~~~
not a bare eta/theta^gamma power law. Use heston(lambda) for the Heston-like
family. Family parameter access is through heston_lambda or
modified_power_law_parameters.
## 3. Validated market inputs
Quote::new still accepts raw f64 coordinates and remains the convenient
calibration input:
~~~rust
use regit_svi::Quote;
let quote = Quote::new(-0.10, 0.04, 1.0)?;
# let _ = quote;
# Ok::<(), regit_svi::ParamError>(())
~~~
The new unit types allow validation earlier in an ingestion pipeline:
~~~rust
use regit_svi::market::{LogMoneyness, Maturity, TotalVariance};
let k = LogMoneyness::new(-0.10)?;
let t = Maturity::new(1.0)?;
let w = TotalVariance::new(0.04)?;
# let _ = (k, t, w);
# Ok::<(), regit_svi::ParamError>(())
~~~
`log_moneyness` and `total_variance_from_vol` are intentionally infallible
raw-`f64` formula helpers; they do not validate. The former requires finite,
strictly positive strike and forward, while the latter requires finite
non-negative volatility and finite non-negative maturity. Violating those
preconditions follows ordinary IEEE-754 propagation. There are no `Strike` or `Forward`
newtypes in 2.x: validate the result with `LogMoneyness`, and use `Maturity`
and `TotalVariance` for the corresponding model inputs. `SliceQuotes`
validates a non-empty quote collection. `quotes_from_triples` remains
available.
Quote fields are private in 2.x. Use log_moneyness(), total_variance(), and
weight(). The implied_vol(t) helper still returns a typed error for invalid
maturity.
## 4. Arbitrage results: status plus evidence
### 4.1 Replace boolean control flow
The most important migration is conceptual. A boolean could not distinguish:
- an exact necessary-and-sufficient theorem;
- a sufficient condition whose failure proves nothing;
- a finite grid scan;
- a theorem-guided floating search.
Before:
~~~rust,compile_fail
let ok = regit_svi::arbitrage::is_butterfly_free(&raw, -1.0, 1.0);
if !ok {
reject_surface();
}
# fn reject_surface() {}
~~~
After:
~~~rust
use regit_svi::{ArbitrageEvidence, ArbitrageStatus};
use regit_svi::no_arb::butterfly::assess_raw;
# use regit_svi::RawSvi;
# let raw = RawSvi::new(0.04, 0.1, -0.2, 0.0, 0.3).expect("valid fixture");
let assessment = assess_raw(&raw, 1e-10);
match (assessment.status(), assessment.evidence()) {
(ArbitrageStatus::ViolationDetected, _) => {
// A failed necessary condition or a concrete witness.
}
(
ArbitrageStatus::NoViolationDetected,
ArbitrageEvidence::NumericalSearch(search),
) if search.terminated() => {
// Apply the application's policy to the recorded uncertainty.
}
_ => {
// Indeterminate or weaker evidence: escalate, widen, or reject.
}
}
~~~
Do not translate NoViolationDetected mechanically to “proved arbitrage-free.”
Always retain or inspect the evidence variant and its declared support.
### 4.2 Raw assessment
Use the no_arb::butterfly::assess_raw function with a validated Raw slice and
an explicit boundary tolerance.
The result includes status, evidence, signed margin, and optional witness.
Regular Raw success is NumericalSearch evidence. Analytic classifications are
used for the positive-flat case, the isolated-zero violation, and failed
necessary conditions.
The 2.x tail contract is more precise than the old symmetric-looking helper:
~~~text
b(1+rho) < 2 right call boundary, strict
b(1-rho) <= 2 left density-factor bound
~~~
Unit continuous positive-strike mass needs the separate strict left condition.
### 4.3 Bounded scans
butterfly_scan and calendar_scan now take only the model(s) and two requested
ordered f64 bounds. They reject k_lo>=k_hi and evaluate 401 points over the
requested interval expanded by one log-moneyness unit on each side.
~~~rust
use regit_svi::no_arb::butterfly::butterfly_scan;
# use regit_svi::RawSvi;
# let raw = RawSvi::new(0.04, 0.1, -0.2, 0.0, 0.3).expect("valid fixture");
let scan = butterfly_scan(&raw, -0.5, 0.5)?;
if scan.violation_observed() {
eprintln!(
"sampled g minimum {} at {}",
scan.min_g(),
scan.worst_k(),
);
}
let effective = scan.evidence().config();
# let _ = effective;
# Ok::<(), regit_svi::DiagnosticError>(())
~~~
The diagnostics expose minimum/worst location, a violation flag, an optional
nearby refined crossing/boundary, and `ScanEvidence`. Its accessors preserve
the caller's `requested_domain`, the pre-expansion `requested_points`, the
expanded effective `config`, `samples_evaluated`, whether
`refinement_attempted`, and optional `RootEvidence` with final bracket,
returned root, residual at that root, evaluation count, and `RootTermination`.
Aggregated surface evidence additionally reports `scan_count`,
`total_samples_evaluated`, and `selected_scan`, whose per-scan evidence is
retained. Selection prioritizes violation over unresolved boundary over clean,
then the smallest `min_difference/tolerance` within that class; the aggregate
margin and witness come from the same selected adjacent pair. The diagnostics
do not expose all violation intervals or prove anything outside the effective
domain.
`ScanEvidence::config().tolerance()` records a scale-aware evaluation band. A
violation flag requires the minimum to lie below the negative tolerance; a
minimum inside the band must not be treated as a clean mathematical pass.
Slice-backed `Surface::calendar_assessment` returns Indeterminate for such an
unresolved adjacent minimum.
Old ButterflyReport and CalendarReport names are replaced by
ButterflyDiagnostic and CalendarDiagnostic.
### 4.4 SSVI assessments
Replace is_butterfly_free_at and is_butterfly_free with:
- butterfly_assessment_at(theta);
- butterfly_assessment(&thetas);
- global_butterfly_assessment().
Replace is_calendar_free_at/is_calendar_free with calendar_assessment on the
declared theta support. The butterfly theorem is sufficient: failure returns
Indeterminate rather than a claimed violation. Calendar Theorem 4.1 evidence
is necessary-and-sufficient on valid declared support. Empty/invalid support is
Indeterminate. A negative adjacent theta difference smaller in magnitude than
the 10^-12 boundary tolerance is also Indeterminate; a decrease beyond that
band is ViolationDetected.
## 5. Calibration
### 5.1 Result access
CalibrationResult fields are private.
Before:
~~~rust,compile_fail
let fit = regit_svi::calibration::quasi_explicit::calibrate("es)?;
let raw = fit.slice;
let rmse = fit.rmse;
let accepted = fit.butterfly_free;
~~~
After:
~~~rust
use regit_svi::calibration::quasi_explicit;
# use regit_svi::Quote;
# let quotes = [
# Quote::new(-0.20, 0.0512, 1.0)?,
# Quote::new(-0.10, 0.0432, 1.0)?,
# Quote::new(0.00, 0.0400, 1.0)?,
# Quote::new(0.10, 0.0420, 1.0)?,
# Quote::new(0.20, 0.0480, 1.0)?,
# ];
let fit = quasi_explicit::calibrate("es)?;
let raw = fit.slice();
let rmse = fit.rmse();
let assessment = fit.arbitrage_assessment();
let report = fit.report();
# let _ = (raw, rmse, assessment, report);
# Ok::<(), regit_svi::CalibrationError>(())
~~~
CalibrationReport carries algorithm, starts, selected_start, tolerance,
condition_estimate, parameterization, recomputed Raw margins, iterations,
evaluations, termination, ResidualDiagnostics, and the independent Raw
assessment. ResidualDiagnostics includes weighted objective, RMSE, maximum
absolute residual, effective quote count, distinct strike count, omitted
quotes, and total weight. condition_estimate is populated for a selected
quasi-explicit normalized design and is None when that diagnostic does not
apply.
An optimizer iteration limit remains an iteration limit. A low RMSE does not
override feasibility evidence.
### 5.2 Default pipeline and explicit configuration
calibrate_slice remains the default quasi-explicit-plus-LM pipeline. Use
calibrate_slice_with_config when numerical and feasibility policy must be
stable and reviewable:
~~~rust
use regit_svi::{
ConstraintMode, InitializationPolicy, SliceCalibrationConfig,
calibration::calibrate_slice_with_config,
};
# use regit_svi::Quote;
# let quotes = [
# Quote::new(-0.20, 0.0512, 1.0)?,
# Quote::new(-0.10, 0.0432, 1.0)?,
# Quote::new(0.00, 0.0400, 1.0)?,
# Quote::new(0.10, 0.0420, 1.0)?,
# Quote::new(0.20, 0.0480, 1.0)?,
# ];
let config = SliceCalibrationConfig::new(
2_000, // outer iterations per start
500, // LM polish iterations
1e-12, // optimizer tolerance
1e-10, // arbitrage boundary tolerance
1e-12, // distinct-strike tolerance
ConstraintMode::Constrained,
InitializationPolicy::DeterministicMultiStart,
)?;
let fit = calibrate_slice_with_config("es, config)?;
# let _ = fit;
# Ok::<(), regit_svi::CalibrationError>(())
~~~
Constrained requires the configured optimizer/feasibility gates. BestEffort
can return a finite candidate with non-success termination or inconclusive
assessment, fully recorded in the report.
The built-in start grids are deterministic and data-scaled. selected_start in
the report identifies the zero-based attempted start that produced the
returned model; it is None for a fallback with no optimizer start.
### 5.3 Effective quote validation
2.x requires at least five positive-weight quotes at sufficiently distinct
strikes for a five-parameter Raw fit. An input vector can have five elements
and still fail with AllWeightsZero or InsufficientEffectiveQuotes. The latter
reports usable, distinct, and required counts. Zero-weight quotes are counted
as omitted in diagnostics. The older TooFewQuotes variant was removed; 2.x
calibrators use the more informative effective-count error.
### 5.4 Quasi-explicit recovery
The inner affine problem remains the de Marco–Martini/Zeliade compact
polytope, and the outer (m,log sigma) problem remains local multi-start
model. It discards a non-representable candidate and compares recomputed
objectives from successfully constructed Raw values. BestEffort has an
explicit valid flat fallback at the weighted mean quote variance. Do not
compare an old inner objective to the new report expecting identity near a
rejected boundary or fallback selection.
### 5.5 Direct refinement
least_squares::refine and refine_with_config accept a validated Raw seed.
least_squares::calibrate remains available, but the high-level default pipeline
is calibration::calibrate_slice. Direct LM now preserves singular,
stagnation, non-finite, step, objective, and iteration-limit termination.
## 6. SSVI surface calibration
The module moved from calibration::ssvi to calibration::surface.
SsviMaturity is constructed with a positive maturity, positive theta, and
quotes. SurfaceCalibrationConfig controls iteration limit, tolerance,
constraint mode, and initialization policy.
Heston fitting requires at least two positive-weight observations at distinct
strikes. Power-law fitting requires at least three and at least two distinct
theta levels; otherwise it returns InsufficientEffectiveQuotes or
InsufficientThetaLevels, respectively.
~~~rust,no_run
use regit_svi::{
ConstraintMode, InitializationPolicy, Quote, SurfaceCalibrationConfig,
};
use regit_svi::calibration::surface::{
PhiFamily, SsviMaturity, calibrate_with_config,
};
# let quotes_short: Vec<Quote> = Vec::new();
# let quotes_long: Vec<Quote> = Vec::new();
let maturities = vec![
SsviMaturity::new(0.5, 0.02, quotes_short)?,
SsviMaturity::new(1.0, 0.04, quotes_long)?,
];
let config = SurfaceCalibrationConfig::new(
3_000,
1e-12,
ConstraintMode::Constrained,
InitializationPolicy::DeterministicMultiStart,
)?;
let fit = calibrate_with_config(&maturities, PhiFamily::Heston, config)?;
let report = fit.report();
# let _ = report;
# Ok::<(), Box<dyn std::error::Error>>(())
~~~
In 1.x, the no-arbitrage inequalities were described as penalty-enforced.
2.x maps candidates into the supported family's global sufficient butterfly
envelope and independently reports butterfly and calendar assessments. This
is still a local least-squares optimization, not a globally optimal fit.
SurfaceCalibrationReport additionally exposes selected_start,
parameterization, and recomputed correlation, family-scale, family-shape,
butterfly, and calendar margins.
## 7. Surface construction and calendar semantics
Surface::from_slices no longer sorts input. Supply strictly increasing,
positive finite maturities. It also requires non-decreasing ATM total
variance.
~~~rust
use regit_svi::Surface;
# use regit_svi::RawSvi;
# let short_slice = RawSvi::new(0.02, 0.1, -0.2, 0.0, 0.3)?;
# let long_slice = RawSvi::new(0.04, 0.1, -0.2, 0.0, 0.3)?;
let surface = Surface::from_slices(vec![
(0.5, short_slice),
(1.0, long_slice),
])?;
# let _ = surface;
# Ok::<(), regit_svi::ParamError>(())
~~~
This constructor check is not a global Raw calendar proof: two slices can be
ordered at k=0 and cross elsewhere. Use calendar_assessment(k_lo,k_hi) and
inspect both status and evidence:
~~~rust
use regit_svi::{ArbitrageEvidence, ArbitrageStatus};
# use regit_svi::{RawSvi, Surface};
# let short = RawSvi::new(0.02, 0.1, -0.2, 0.0, 0.3)?;
# let long = RawSvi::new(0.04, 0.1, -0.2, 0.0, 0.3)?;
# let surface = Surface::from_slices(vec![(0.5, short), (1.0, long)])?;
let assessment = surface.calendar_assessment(-0.5, 0.5)?;
match (assessment.status(), assessment.evidence()) {
(ArbitrageStatus::ViolationDetected, _) => {
// At least one adjacent pair has a concrete bounded witness.
}
(
ArbitrageStatus::NoViolationDetected,
ArbitrageEvidence::NumericalScan(_),
) => {
// Raw-backed surface: clean only on the recorded aggregate domain.
}
(ArbitrageStatus::NoViolationDetected, _) => {
// SSVI-backed surface: inspect the analytic theorem evidence.
}
_ => {
// Indeterminate.
}
}
# Ok::<(), Box<dyn std::error::Error>>(())
~~~
For Raw backing, the aggregate assessment preserves bounded numerical-scan
evidence, effective domain, and sample count across adjacent pairs. For SSVI
backing it carries the analytic Theorem 4.1 evidence.
Surface::from_ssvi accepts a value convertible to TermStructure. That type
also preserves order and rejects duplicate/decreasing knots; it never silently
sorts or repairs.
Inside the knot range, slice-backed surfaces interpolate linearly in total
variance at fixed k. Outside the range, total variance is scaled to keep
implied volatility flat. SSVI-backed surfaces interpolate theta before
evaluating the SSVI formula. Surface::total_variance now returns
Result<f64,ParamError> and rejects non-finite k and non-finite/non-positive
maturity; propagate the result instead of assuming every f64 coordinate is
valid.
## 8. Raw and Jump-Wings conversion
Function paths moved but top-level re-exports remain:
~~~rust
use regit_svi::{jw_to_raw, raw_to_jw};
# use regit_svi::RawSvi;
# let raw = RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1)?;
let jw = raw_to_jw(&raw, 1.0)?;
let back = jw_to_raw(&jw)?;
# let _ = back;
# Ok::<(), regit_svi::ConvertError>(())
~~~
The forward map requires positive maturity and positive ATM total variance.
The inverse now rejects non-identifiable tuples rather than fabricating a Raw
scale:
- flat wings;
- zero put or call wing;
- minimum variance equal to ATM variance;
- |beta|>=1;
- a near-zero stable scale A.
Code that assumed every constructed SviJw value had a Raw preimage must handle
ConvertError::DegenerateJw and ConvertError::JwHasNoRawPreimage. beta=0 means
m=0; minimum-at-ATM is a different degeneracy.
## 9. Density
The public density module remains, but its mathematical naming is now explicit:
risk_neutral_density returns density with respect to log-strike k,
~~~text
q_k(k)=g(k) exp(-d_-^2/2)/sqrt(2 pi w(k)).
~~~
Strike density is q_k/K. The bounded integral integrates q_k over k.
~~~rust
use regit_svi::density::{density_report, integral};
# use regit_svi::RawSvi;
# let raw = RawSvi::new(0.04, 0.05, -0.1, 0.0, 0.4).expect("valid fixture");
let mass = integral(&raw, -8.0, 8.0, 4_000)?;
let report = density_report(&raw, -8.0, 8.0, 4_000)?;
if report.violation_observed() {
assert!(report.min_density() < 0.0);
}
let audit = (
report.domain(),
report.panels(),
report.integration_error(),
);
# let _ = (mass, audit);
# Ok::<(), regit_svi::DensityError>(())
~~~
For integral, the final integer n gives 2n Simpson panels and no embedded error
estimate. density_report computes nested 2n/4n-panel estimates, returns the
4n-panel value, samples its minimum on that fine grid, and reports
abs(I_4n-I_2n)/15 as integration_error. That is quadrature-only error on the
declared domain, not tail truncation error. If a downstream gate treated
abs(mass-1) as fully explained numerical error, add explicit window expansion
tests.
The density-report violation flag uses negativity of the sampled g factor
beyond its scale-aware evaluation band. A tiny negative `min_density` whose g
factor remains inside that band is unresolved and need not set the flag.
DensityError now distinguishes NonPositiveVariance for exact zero/negative
variance from IllConditionedVariance for a tiny positive value that is unsafe
to divide by reliably. Do not collapse the latter into a mathematical
zero-variance claim.
## 10. Error handling
Error modules moved from errors to error, while ParamError, ConvertError, and
CalibrationError remain top-level re-exports. New or more frequently observed
variants include:
- ParamError::NotStrictlyIncreasing and DecreasingAtmVariance;
- ConvertError::DegenerateJw and NonPositiveAtmVariance;
- CalibrationError::InvalidConfig, InsufficientEffectiveQuotes, and
InsufficientThetaLevels;
- DiagnosticError for invalid/non-finite bounded diagnostics;
- DensityError for density-specific point and integration domains.
Avoid exhaustive matching without a deliberate upgrade review. Preserve the
source chain for conversion/calibration errors that wrap ParamError.
## 11. Toolchain and dependency contract
The 2.x shipped library requires Rust 1.85 and edition 2024. It has zero normal
and build dependencies and remains safe std-only Rust.
All-target development checks require a current stable compiler because
Criterion 0.8 requires Rust 1.86 or newer. CI therefore checks:
- the library alone at 1.85;
- tests, examples, and benchmarks on stable;
- a release wasm32-unknown-unknown library build;
- the normal/build and development dependency graphs separately.
The Python oracle under tools/oracle is external verification tooling, not a
Cargo, build, runtime, test-harness, or benchmark dependency.
## 12. Recommended rollout
For a risk-sensitive application:
1. Compile after import/accessor changes without weakening error handling.
2. Snapshot 1.x market inputs and 2.x reports side by side.
3. Decide which evidence variants the application accepts for each workflow.
4. Fix bounded scan and density domains in configuration and regression data.
5. Record report termination, tolerances, and effective quote counts.
6. Exercise strict wing equality, isolated-zero, JW degeneracy, unordered
maturity, zero-weight, and duplicate-strike cases.
7. Compare selected outputs with the repository oracle or an independent
high-precision implementation.
8. Deploy 2.0.0 behind normal model-validation controls before broad
production adoption.