sanos 0.2.1

SANOS: Smooth strictly Arbitrage-free Non-parametric Option Surfaces (Rust implementation)
Documentation
use sanos::backbone::bs_call_forward_norm;
use sanos::backbone::{BackboneConfig, BsTimeChangedConfig};
use sanos::calibration::{calibrate, CalibrationConfig, ConvexOrderValidationMode};
use sanos::density::DensityTolerances;
use sanos::fit::{FitConfig, OmegaConfig, WarmStartMode};
use sanos::grid::StrikeGridPolicyConfig;
use sanos::interp::TimeInterpConfig;
use sanos::market::{CallQuote, OptionBook, OptionChain};

use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct IvSurfaceSnapshot {
    maturities: Vec<MaturityNode>,
}

#[derive(Debug, Deserialize)]
struct MaturityNode {
    t: f64,
    quotes: Vec<IvQuote>,
}

#[derive(Debug, Deserialize)]
struct IvQuote {
    k: f64,
    bid_iv: f64,
    ask_iv: f64,
}

fn load_book_from_snapshot() -> OptionBook {
    let json = include_str!("fixtures/tv_equity_like_001.snapshot.json");
    let snap: IvSurfaceSnapshot = serde_json::from_str(json).expect("snapshot must parse");

    let mut chains = Vec::with_capacity(snap.maturities.len());
    for m in snap.maturities {
        let mut quotes = Vec::with_capacity(m.quotes.len());

        for q in m.quotes {
            let bid_var = q.bid_iv * q.bid_iv * m.t;
            let ask_var = q.ask_iv * q.ask_iv * m.t;

            let bid = bs_call_forward_norm(q.k, bid_var).expect("bid price must be computable");
            let ask = bs_call_forward_norm(q.k, ask_var).expect("ask price must be computable");

            quotes.push(CallQuote::new(q.k, bid, ask, 1.0).expect("quote must validate"));
        }

        chains.push(OptionChain::new(m.t, quotes).expect("chain must validate"));
    }

    OptionBook::new(chains).expect("book must validate")
}

fn default_calibration_config_for_snapshot() -> CalibrationConfig {
    let backbone = BackboneConfig::BsTimeChanged(BsTimeChangedConfig {
        eta: 0.25,
        ..BsTimeChangedConfig::default()
    });

    let fit = FitConfig::default();

    CalibrationConfig {
        backbone,
        grid: StrikeGridPolicyConfig::default(),
        fit,
        time_interp: TimeInterpConfig::AtmVarianceTime,
        convex_order_validation: ConvexOrderValidationMode::Error,
    }
}

/// Mirrors `data/configs/default.json` (current repo default).
fn repo_default_like_calibration_config() -> CalibrationConfig {
    let backbone = BackboneConfig::BsTimeChanged(BsTimeChangedConfig {
        eta: 0.25,
        ..BsTimeChangedConfig::default()
    });

    let mut fit = FitConfig::default();
    fit.kernel.omega = OmegaConfig::Zero;
    fit.initialization.mode = WarmStartMode::BackboneSynthetic;
    fit.initialization.feasibility_tol = 1e-8;

    CalibrationConfig {
        backbone,
        grid: StrikeGridPolicyConfig::default(),
        fit,
        time_interp: TimeInterpConfig::AtmVarianceTime,
        convex_order_validation: ConvexOrderValidationMode::Error,
    }
}

#[test]
fn calibrate_snapshot_produces_valid_martingale_density() {
    let book = load_book_from_snapshot();
    let cfg = default_calibration_config_for_snapshot();

    let surface = calibrate(&book, &cfg).expect("calibration must succeed");

    let tol = DensityTolerances::from_tol(1e-6).unwrap();
    let q = surface.martingale_density();
    q.validate_marginals(tol).expect("marginals must be valid");
    q.validate_convex_order(tol)
        .expect("convex order must hold");
}

#[test]
fn calibrated_surface_produces_finite_values() {
    let book = load_book_from_snapshot();
    let cfg = default_calibration_config_for_snapshot();

    let surface = calibrate(&book, &cfg).expect("calibration must succeed");

    for chain in book.chains() {
        let t = chain.maturity();
        for q in chain.quotes() {
            let c = surface
                .call(t, q.k)
                .expect("surface call must be computable");
            assert!(c.is_finite(), "T={t}, k={}, c={c} must be finite", q.k);
            assert!(c >= -1e-8, "T={t}, k={}, c={c} must be non-negative", q.k);
        }
    }
}

#[test]
fn repo_default_like_config_calibrates_with_adaptive_completion() {
    let book = load_book_from_snapshot();
    let cfg = repo_default_like_calibration_config();

    let surface = calibrate(&book, &cfg).expect("calibration must succeed");
    let tol = DensityTolerances::from_tol(1e-6).unwrap();
    surface
        .martingale_density()
        .validate_marginals(tol)
        .expect("marginals must be valid");
    surface
        .martingale_density()
        .validate_convex_order(tol)
        .expect("convex order must hold");
}