use volsurf::surface::{EssviSurface, SsviSurface, VolSurface};
use volsurf::{Strike, Tenor};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let tenors = vec![0.25, 0.50, 1.0, 2.0];
let forwards = vec![100.0; 4];
let thetas = vec![0.01, 0.02, 0.04, 0.08];
let surface = EssviSurface::new(
-0.7, -0.3, 0.5, 0.5, 0.5, tenors.clone(),
forwards.clone(),
thetas.clone(),
)?;
println!(
"eSSVI surface: rho_0={:.1}, rho_m={:.1}, a={:.1}, eta={:.1}, gamma={:.1}",
surface.rho_0(),
surface.rho_m(),
surface.a(),
surface.eta(),
surface.gamma()
);
println!("\n--- rho(theta) across tenors ---\n");
println!("{:>8} {:>8} {:>8}", "T", "theta", "rho");
println!("{}", "-".repeat(28));
for (&t, &theta) in tenors.iter().zip(thetas.iter()) {
println!("{t:>8.2} {theta:>8.4} {:>8.4}", surface.rho(theta));
}
println!("\n--- Vol grid (expiry x strike) ---\n");
let query_tenors = [0.25, 0.50, 0.75, 1.00, 1.50, 2.00];
let query_strikes = [80.0, 90.0, 95.0, 100.0, 105.0, 110.0, 120.0];
print!("{:>8}", "T\\K");
for &k in &query_strikes {
print!("{k:>8.0}");
}
println!();
println!("{}", "-".repeat(64));
for &t in &query_tenors {
print!("{t:>8.2}");
for &k in &query_strikes {
let vol = surface.black_vol(Tenor(t), Strike(k))?;
print!("{:>8.2}%", vol.0 * 100.0);
}
println!();
}
println!("\n--- Two-stage calibration ---\n");
let cal_strikes: Vec<f64> = (70..=130).step_by(5).map(|k| k as f64).collect();
let market_data: Vec<Vec<(f64, f64)>> = tenors
.iter()
.map(|&t| {
cal_strikes
.iter()
.map(|&k| Ok((k, surface.black_vol(Tenor(t), Strike(k))?.0)))
.collect::<Result<Vec<_>, volsurf::VolSurfError>>()
})
.collect::<Result<_, _>>()?;
let fits = EssviSurface::fit_per_tenor(&market_data, &tenors, &forwards)?;
println!("Per-tenor SVI fits:");
for fit in &fits {
println!(
" T={:.2}: theta={:.6}, rms={:.2e}",
fit.tenor, fit.theta, fit.rms_error
);
}
let calibrated = EssviSurface::from_per_tenor(&fits)?;
println!(
"\nOriginal: rho_0={:.4}, rho_m={:.4}, a={:.4}, eta={:.4}, gamma={:.4}",
surface.rho_0(),
surface.rho_m(),
surface.a(),
surface.eta(),
surface.gamma()
);
println!(
"Calibrated: rho_0={:.4}, rho_m={:.4}, a={:.4}, eta={:.4}, gamma={:.4}",
calibrated.rho_0(),
calibrated.rho_m(),
calibrated.a(),
calibrated.eta(),
calibrated.gamma()
);
let mut max_err: f64 = 0.0;
for &t in &tenors {
for &k in &cal_strikes {
let orig = surface.black_vol(Tenor(t), Strike(k))?.0;
let cal = calibrated.black_vol(Tenor(t), Strike(k))?.0;
max_err = max_err.max((orig - cal).abs());
}
}
println!("Max vol error: {max_err:.2e}");
println!("\n--- One-shot calibrate ---\n");
let oneshot = EssviSurface::calibrate(&market_data, &tenors, &forwards)?;
println!(
"One-shot: rho_0={:.4}, rho_m={:.4}, a={:.4}, eta={:.4}, gamma={:.4}",
oneshot.rho_0(),
oneshot.rho_m(),
oneshot.a(),
oneshot.eta(),
oneshot.gamma()
);
println!("\n--- Calendar arbitrage (structural) ---\n");
let violations = surface.calendar_check_structural();
println!("Structural violations: {}", violations.len());
let diag = surface.diagnostics()?;
println!(
"Numerical calendar violations: {}",
diag.calendar_violations.len()
);
println!("Surface arb-free: {}", diag.is_free());
println!(
"\n--- eSSVI vs SSVI (rho fixed at rho_m={:.1}) ---\n",
surface.rho_m()
);
let ssvi = SsviSurface::new(
surface.rho_m(), surface.eta(),
surface.gamma(),
tenors.clone(),
forwards.clone(),
thetas.clone(),
)?;
println!(
"{:>6} {:>6} {:>10} {:>10} {:>10}",
"T", "K", "eSSVI", "SSVI", "Diff(bp)"
);
println!("{}", "-".repeat(46));
for &t in &[0.25, 0.50, 1.0, 2.0] {
for &k in &[90.0, 100.0, 110.0] {
let ev = surface.black_vol(Tenor(t), Strike(k))?.0;
let sv = ssvi.black_vol(Tenor(t), Strike(k))?.0;
let diff_bp = (ev - sv) * 10_000.0;
println!(
"{t:>6.2} {k:>6.0} {:>9.4}% {:>9.4}% {:>9.1}",
ev * 100.0,
sv * 100.0,
diff_bp
);
}
}
println!("\neSSVI diverges from SSVI most at short tenors where rho(theta)");
println!(
"is steeper ({:.2} vs {:.2}). They converge at theta_max where rho -> rho_m.",
surface.rho(*thetas.first().unwrap()),
surface.rho_m()
);
Ok(())
}