regit-svi 2.0.0

Arbitrage-free SVI volatility surfaces in pure Rust. Raw, Jump-Wings and SSVI parametrisations, calibration, and static-arbitrage checks. Zero dependencies.
Documentation
// Copyright 2026 Regit.io — Nicolas Koenig
// SPDX-License-Identifier: Apache-2.0

//! Quickstart example for regit-svi.
//!
//! Walks through the full slice and surface workflow: calibrate a raw SVI
//! slice to market quotes, convert between parametrisations, check static
//! arbitrage, inspect the risk-neutral density, fit a constrained SSVI
//! surface, and evaluate its reported evidence.

use regit_svi::calibration::surface::{PhiFamily, SsviMaturity, calibrate as calibrate_ssvi};
use regit_svi::calibration::{calibrate_slice, quasi_explicit};
use regit_svi::density::density_report;
use regit_svi::market::quote::Quote;
use regit_svi::no_arb::butterfly::butterfly_scan;
use regit_svi::smile::conversion::{jw_to_raw, raw_to_jw};
use regit_svi::surface::interpolation::Surface;
use regit_svi::surface::ssvi::{Phi, Ssvi};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ── 1. Calibrate a raw SVI slice ────────────────────────────────────
    // Market quotes for one maturity: (log-moneyness, total variance, weight).
    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)?,
    ];

    // Default pipeline: quasi-explicit fit, then a Levenberg-Marquardt polish.
    let fit = calibrate_slice(&quotes)?;
    let svi = fit.slice();
    println!("Calibrated raw SVI slice");
    println!(
        "  a = {:.6}  b = {:.6}  rho = {:.6}",
        svi.a(),
        svi.b(),
        svi.rho()
    );
    println!("  m = {:.6}  sigma = {:.6}", svi.m(), svi.sigma());
    println!("  RMSE = {:.3e}", fit.rmse());
    println!(
        "  butterfly assessment = {:?}",
        fit.arbitrage_assessment().status()
    );

    // The quasi-explicit calibrator is also usable directly.
    let qe = quasi_explicit::calibrate(&quotes)?;
    println!("  quasi-explicit RMSE = {:.3e}", qe.rmse());

    // ── 2. Evaluate the slice ───────────────────────────────────────────
    let w = svi.total_variance(0.05);
    let vol = svi.implied_vol(0.05, 1.0)?; // 1-year expiry
    println!("\nAt k = 0.05:  w = {w:.6},  implied vol = {vol:.4}");
    println!("ATM variance = {:.6}", svi.atm_total_variance());
    println!("ATM skew     = {:.6}", svi.atm_skew());

    // ── 3. Convert between parametrisations ─────────────────────────────
    let jw = raw_to_jw(&svi, 1.0)?;
    println!("\nJump-Wings view (t = 1y)");
    println!(
        "  v_t = {:.6}  psi_t = {:.6}",
        jw.atm_variance(),
        jw.atm_skew()
    );
    println!("  p_t = {:.6}  c_t = {:.6}", jw.put_wing(), jw.call_wing());
    let back = jw_to_raw(&jw)?;
    println!(
        "  round-trip max param error: {:.2e}",
        (back.a() - svi.a()).abs()
    );

    // ── 4. Static-arbitrage checks ──────────────────────────────────────
    let report = butterfly_scan(&svi, -0.5, 0.5)?;
    println!("\nButterfly scan");
    println!("  violation observed = {}", report.violation_observed());
    println!(
        "  min g(k) = {:.6} at k = {:.4}",
        report.min_g(),
        report.worst_k()
    );

    // ── 5. Risk-neutral density ─────────────────────────────────────────
    let density = density_report(&svi, -6.0, 6.0, 2000)?;
    println!("\nRisk-neutral density");
    println!("  integral over [-6, 6] = {:.6}", density.integral());
    println!("  violation observed = {}", density.violation_observed());

    // ── 6. SSVI surface with hard-feasible parameter maps ──────────────
    let truth = Ssvi::new(-0.3, Phi::modified_power_law(0.5, 0.5)?)?;
    let ks = [-0.3, -0.15, 0.0, 0.15, 0.3];
    let maturities: Vec<SsviMaturity> = [(0.5, 0.02), (1.0, 0.04), (2.0, 0.07)]
        .iter()
        .map(|&(t, theta)| {
            let quotes = ks
                .iter()
                .map(|&k| Quote::new(k, truth.total_variance(k, theta), 1.0))
                .collect::<Result<Vec<_>, _>>()?;
            SsviMaturity::new(t, theta, quotes)
        })
        .collect::<Result<Vec<_>, _>>()?;

    let ssvi_fit = calibrate_ssvi(&maturities, PhiFamily::PowerLaw)?;
    println!("\nSSVI surface calibration");
    println!("  rho = {:.6}", ssvi_fit.ssvi().rho());
    println!("  RMSE = {:.3e}", ssvi_fit.rmse());
    println!(
        "  butterfly evidence = {:?}",
        ssvi_fit.report().butterfly_assessment().status()
    );

    // ── 7. Assemble and evaluate a surface ──────────────────────────────
    let surface = Surface::from_ssvi(
        ssvi_fit.ssvi(),
        maturities
            .iter()
            .map(|m| (m.maturity().get(), m.theta().get()))
            .collect::<Vec<_>>(),
    )?;
    let w_interp = surface.total_variance(0.05, 1.5)?;
    let vol_interp = surface.implied_vol(0.05, 1.5)?;
    println!("\nSurface at (k = 0.05, T = 1.5y)");
    println!("  w = {w_interp:.6},  implied vol = {vol_interp:.4}");
    println!(
        "  calendar assessment = {:?}",
        surface.calendar_assessment(-0.4, 0.4)?.status()
    );
    Ok(())
}