use crate::arbitrage::calendar_scan;
use crate::errors::ParamError;
use crate::raw::RawSvi;
use crate::ssvi::Ssvi;
#[derive(Debug, Clone, PartialEq)]
enum Backing {
Slices(Vec<(f64, RawSvi)>),
Ssvi {
ssvi: Ssvi,
term: Vec<(f64, f64)>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct Surface {
backing: Backing,
}
impl Surface {
pub fn from_slices(mut slices: Vec<(f64, RawSvi)>) -> Result<Self, ParamError> {
if slices.is_empty() {
return Err(ParamError::NonPositiveMaturity { t: 0.0 });
}
for &(t, _) in &slices {
if !t.is_finite() {
return Err(ParamError::NonFinite { name: "maturity" });
}
if t <= 0.0 {
return Err(ParamError::NonPositiveMaturity { t });
}
}
slices.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
for pair in slices.windows(2) {
if (pair[1].0 - pair[0].0).abs() < 1e-12 {
return Err(ParamError::NonPositiveMaturity { t: pair[1].0 });
}
}
Ok(Self {
backing: Backing::Slices(slices),
})
}
pub fn from_ssvi(ssvi: Ssvi, mut term: Vec<(f64, f64)>) -> Result<Self, ParamError> {
if term.is_empty() {
return Err(ParamError::NonPositiveMaturity { t: 0.0 });
}
for &(t, theta) in &term {
if !t.is_finite() || !theta.is_finite() {
return Err(ParamError::NonFinite {
name: "term structure",
});
}
if t <= 0.0 {
return Err(ParamError::NonPositiveMaturity { t });
}
if theta <= 0.0 {
return Err(ParamError::NonPositiveTheta { theta });
}
}
term.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(core::cmp::Ordering::Equal));
for pair in term.windows(2) {
if (pair[1].0 - pair[0].0).abs() < 1e-12 {
return Err(ParamError::NonPositiveMaturity { t: pair[1].0 });
}
}
Ok(Self {
backing: Backing::Ssvi { ssvi, term },
})
}
#[must_use]
pub fn len(&self) -> usize {
match &self.backing {
Backing::Slices(s) => s.len(),
Backing::Ssvi { term, .. } => term.len(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn total_variance(&self, k: f64, t: f64) -> f64 {
match &self.backing {
Backing::Slices(slices) => Self::total_variance_slices(slices, k, t),
Backing::Ssvi { ssvi, term } => {
let theta = interpolate_theta(term, t);
ssvi.total_variance(k, theta)
}
}
}
pub fn implied_vol(&self, k: f64, t: f64) -> Result<f64, ParamError> {
if t <= 0.0 || !t.is_finite() {
return Err(ParamError::NonPositiveMaturity { t });
}
Ok((self.total_variance(k, t) / t).sqrt())
}
#[must_use]
pub fn is_calendar_free(&self, k_lo: f64, k_hi: f64) -> bool {
match &self.backing {
Backing::Slices(slices) => slices
.windows(2)
.all(|p| calendar_scan(&p[0].1, &p[1].1, k_lo, k_hi).is_free),
Backing::Ssvi { ssvi, term } => {
let thetas: Vec<f64> = term.iter().map(|&(_, theta)| theta).collect();
ssvi.is_calendar_free(&thetas)
}
}
}
fn total_variance_slices(slices: &[(f64, RawSvi)], k: f64, t: f64) -> f64 {
let n = slices.len();
let (t0, first) = slices[0];
let (tn, last) = slices[n - 1];
if t <= t0 {
return (first.total_variance(k) / t0) * t.max(0.0);
}
if t >= tn {
return (last.total_variance(k) / tn) * t;
}
for pair in slices.windows(2) {
let (tj, sj) = pair[0];
let (tj1, sj1) = pair[1];
if t >= tj && t <= tj1 {
let wj = sj.total_variance(k);
let wj1 = sj1.total_variance(k);
let frac = (t - tj) / (tj1 - tj);
return wj + frac * (wj1 - wj);
}
}
last.total_variance(k)
}
}
fn interpolate_theta(term: &[(f64, f64)], t: f64) -> f64 {
let n = term.len();
let (t_first, theta_first) = term[0];
let (t_last, theta_last) = term[n - 1];
if t <= t_first {
return (theta_first / t_first) * t.max(0.0);
}
if t >= t_last {
return (theta_last / t_last) * t;
}
for pair in term.windows(2) {
let (t_lo, theta_lo) = pair[0];
let (t_hi, theta_hi) = pair[1];
if t >= t_lo && t <= t_hi {
let frac = (t - t_lo) / (t_hi - t_lo);
return theta_lo + frac * (theta_hi - theta_lo);
}
}
theta_last
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ssvi::Phi;
#[test]
fn from_slices_sorts_and_validates() {
let s = RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1).unwrap();
let surface = Surface::from_slices(vec![(2.0, s), (0.5, s), (1.0, s)]).unwrap();
assert_eq!(surface.len(), 3);
}
#[test]
fn from_slices_rejects_bad_maturity() {
let s = RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1).unwrap();
assert!(Surface::from_slices(vec![(0.0, s)]).is_err());
assert!(Surface::from_slices(vec![(1.0, s), (1.0, s)]).is_err());
assert!(Surface::from_slices(vec![]).is_err());
}
#[test]
fn interpolation_is_linear_in_w() {
let s1 = RawSvi::new(0.02, 0.3, -0.2, 0.0, 0.1).unwrap();
let s2 = RawSvi::new(0.06, 0.3, -0.2, 0.0, 0.1).unwrap();
let surface = Surface::from_slices(vec![(1.0, s1), (3.0, s2)]).unwrap();
for &k in &[-0.3, 0.0, 0.3] {
let mid = surface.total_variance(k, 2.0);
let expect = 0.5 * (s1.total_variance(k) + s2.total_variance(k));
assert!((mid - expect).abs() < 1e-12, "k = {k}");
}
}
#[test]
fn interpolation_recovers_knot_values() {
let s1 = RawSvi::new(0.02, 0.3, -0.2, 0.0, 0.1).unwrap();
let s2 = RawSvi::new(0.06, 0.3, -0.2, 0.0, 0.1).unwrap();
let surface = Surface::from_slices(vec![(1.0, s1), (3.0, s2)]).unwrap();
assert!((surface.total_variance(0.1, 1.0) - s1.total_variance(0.1)).abs() < 1e-12);
assert!((surface.total_variance(0.1, 3.0) - s2.total_variance(0.1)).abs() < 1e-12);
}
#[test]
fn extrapolation_is_flat_in_vol() {
let s = RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1).unwrap();
let surface = Surface::from_slices(vec![(1.0, s)]).unwrap();
let w_half = surface.total_variance(0.0, 0.5);
assert!((w_half - s.total_variance(0.0) * 0.5).abs() < 1e-12);
let w_double = surface.total_variance(0.0, 2.0);
assert!((w_double - s.total_variance(0.0) * 2.0).abs() < 1e-12);
let v05 = surface.implied_vol(0.0, 0.5).unwrap();
let v20 = surface.implied_vol(0.0, 2.0).unwrap();
assert!((v05 - v20).abs() < 1e-12);
}
#[test]
fn implied_vol_rejects_bad_maturity() {
let s = RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1).unwrap();
let surface = Surface::from_slices(vec![(1.0, s)]).unwrap();
assert!(surface.implied_vol(0.0, 0.0).is_err());
}
#[test]
fn slice_surface_calendar_check() {
let early = RawSvi::new(0.02, 0.3, -0.2, 0.0, 0.1).unwrap();
let late = RawSvi::new(0.06, 0.3, -0.2, 0.0, 0.1).unwrap();
let ok = Surface::from_slices(vec![(0.5, early), (1.5, late)]).unwrap();
assert!(ok.is_calendar_free(-0.5, 0.5));
let bad = Surface::from_slices(vec![(0.5, late), (1.5, early)]).unwrap();
assert!(!bad.is_calendar_free(-0.5, 0.5));
}
#[test]
fn ssvi_surface_evaluates_and_interpolates() {
let ssvi = Ssvi::new(-0.3, Phi::power_law(0.5, 0.5).unwrap()).unwrap();
let surface =
Surface::from_ssvi(ssvi, vec![(0.5, 0.02), (1.0, 0.04), (2.0, 0.08)]).unwrap();
let w_knot = surface.total_variance(0.1, 1.0);
assert!((w_knot - ssvi.total_variance(0.1, 0.04)).abs() < 1e-12);
let w_mid = surface.total_variance(0.0, 1.5);
assert!(w_mid > ssvi.total_variance(0.0, 0.04));
assert!(w_mid < ssvi.total_variance(0.0, 0.08));
}
#[test]
fn ssvi_surface_calendar_check() {
let ssvi = Ssvi::new(-0.3, Phi::power_law(0.5, 0.5).unwrap()).unwrap();
let surface =
Surface::from_ssvi(ssvi, vec![(0.5, 0.02), (1.0, 0.04), (2.0, 0.08)]).unwrap();
assert!(surface.is_calendar_free(-0.5, 0.5));
}
#[test]
fn ssvi_surface_rejects_bad_term() {
let ssvi = Ssvi::new(-0.3, Phi::heston(1.0).unwrap()).unwrap();
assert!(Surface::from_ssvi(ssvi, vec![]).is_err());
assert!(Surface::from_ssvi(ssvi, vec![(1.0, 0.0)]).is_err());
assert!(Surface::from_ssvi(ssvi, vec![(0.0, 0.04)]).is_err());
}
#[test]
fn is_empty_is_false_for_built_surface() {
let s = RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1).unwrap();
let surface = Surface::from_slices(vec![(1.0, s)]).unwrap();
assert!(!surface.is_empty());
}
}