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,
}
}
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");
}