use super::ssvi::SsviParams;
use super::svi::SviRawParams;
use crate::traits::FloatExt;
#[derive(Clone, Debug)]
pub struct SmileAnalytics<T: FloatExt> {
pub atm_vol: T,
pub atm_skew: T,
pub atm_convexity: T,
pub atm_total_variance: T,
pub tau: T,
}
pub fn svi_analytics<T: FloatExt>(params: &SviRawParams<T>, tau: T) -> SmileAnalytics<T> {
let zero = T::zero();
let two = T::from_f64_fast(2.0);
let four = T::from_f64_fast(4.0);
let w0 = params.total_variance(zero);
let atm_vol = if w0 > zero && tau > zero {
(w0 / tau).sqrt()
} else {
T::nan()
};
let wp = params.w_prime(zero);
let wpp = params.w_double_prime(zero);
let (atm_skew, atm_convexity) = if atm_vol > zero && tau > zero {
let skew = wp / (two * atm_vol * tau);
let conv =
wpp / (two * atm_vol * tau) - wp * wp / (four * atm_vol * atm_vol * atm_vol * tau * tau);
(skew, conv)
} else {
(T::nan(), T::nan())
};
SmileAnalytics {
atm_vol,
atm_skew,
atm_convexity,
atm_total_variance: w0,
tau,
}
}
pub fn ssvi_analytics<T: FloatExt>(params: &SsviParams<T>, theta: T, tau: T) -> SmileAnalytics<T> {
let zero = T::zero();
let two = T::from_f64_fast(2.0);
let four = T::from_f64_fast(4.0);
let w0 = params.total_variance(zero, theta);
let atm_vol = if w0 > zero && tau > zero {
(w0 / tau).sqrt()
} else {
T::nan()
};
let wp = params.w_prime_k(zero, theta);
let wpp = params.w_double_prime_k(zero, theta);
let (atm_skew, atm_convexity) = if atm_vol > zero && tau > zero {
let skew = wp / (two * atm_vol * tau);
let conv =
wpp / (two * atm_vol * tau) - wp * wp / (four * atm_vol * atm_vol * atm_vol * tau * tau);
(skew, conv)
} else {
(T::nan(), T::nan())
};
SmileAnalytics {
atm_vol,
atm_skew,
atm_convexity,
atm_total_variance: w0,
tau,
}
}
pub fn atm_term_structure<T: FloatExt>(
params: &SsviParams<T>,
thetas: &[T],
maturities: &[T],
) -> (Vec<T>, Vec<T>) {
let zero = T::zero();
let vols: Vec<T> = thetas
.iter()
.zip(maturities.iter())
.map(|(&theta, &t)| {
let w = params.total_variance(zero, theta);
if w > zero && t > zero {
(w / t).sqrt()
} else {
T::nan()
}
})
.collect();
(maturities.to_vec(), vols)
}
pub fn skew_term_structure<T: FloatExt>(
params: &SsviParams<T>,
thetas: &[T],
maturities: &[T],
) -> (Vec<T>, Vec<T>) {
let skews: Vec<T> = thetas
.iter()
.zip(maturities.iter())
.map(|(&theta, &t)| {
let a = ssvi_analytics(params, theta, t);
a.atm_skew
})
.collect();
(maturities.to_vec(), skews)
}
pub fn risk_reversal_25d<T: FloatExt>(params: &SviRawParams<T>, tau: T) -> T {
let zero = T::zero();
let z = T::from_f64_fast(0.6745);
let atm_vol = params.implied_vol(zero, tau);
if !atm_vol.is_finite() || atm_vol <= zero {
return T::nan();
}
let k25 = atm_vol * tau.sqrt() * z;
let sigma_call = params.implied_vol(k25, tau);
let sigma_put = params.implied_vol(-k25, tau);
if sigma_call.is_finite() && sigma_put.is_finite() {
sigma_call - sigma_put
} else {
T::nan()
}
}
pub fn butterfly_25d<T: FloatExt>(params: &SviRawParams<T>, tau: T) -> T {
let zero = T::zero();
let half = T::from_f64_fast(0.5);
let z = T::from_f64_fast(0.6745);
let atm_vol = params.implied_vol(zero, tau);
if !atm_vol.is_finite() || atm_vol <= zero {
return T::nan();
}
let k25 = atm_vol * tau.sqrt() * z;
let sigma_call = params.implied_vol(k25, tau);
let sigma_put = params.implied_vol(-k25, tau);
if sigma_call.is_finite() && sigma_put.is_finite() {
half * (sigma_call + sigma_put) - atm_vol
} else {
T::nan()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn svi_analytics_basic() {
let p = SviRawParams::<f64>::new(0.04, 0.4, -0.4, 0.0, 0.2);
let a = svi_analytics(&p, 1.0);
assert!(a.atm_vol > 0.0);
assert!(
a.atm_skew < 0.0,
"negative rho should give negative ATM skew"
);
assert!(a.atm_convexity.is_finite());
assert!((a.atm_total_variance - p.total_variance(0.0)).abs() < 1e-12);
}
#[test]
fn ssvi_analytics_basic() {
let p = SsviParams::<f64>::new(-0.3, 0.5, 0.5);
let theta = 0.04;
let a = ssvi_analytics(&p, theta, 1.0);
assert!(a.atm_vol > 0.0);
assert!(
a.atm_skew < 0.0,
"negative rho should give negative ATM skew"
);
assert!((a.atm_total_variance - theta).abs() < 1e-10);
}
#[test]
fn risk_reversal_and_butterfly() {
let p = SviRawParams::<f64>::new(0.04, 0.4, -0.4, 0.0, 0.2);
let rr = risk_reversal_25d(&p, 1.0);
let bf = butterfly_25d(&p, 1.0);
assert!(rr.is_finite());
assert!(rr < 0.0, "negative rho should give negative RR25: rr={rr}");
assert!(bf.is_finite());
assert!(
bf > 0.0,
"butterfly should be positive (smile convexity): bf={bf}"
);
}
#[test]
fn atm_term_structure_increasing() {
let p = SsviParams::<f64>::new(-0.3, 0.5, 0.5);
let thetas = vec![0.01, 0.04, 0.09, 0.16];
let mats = vec![0.25, 0.50, 1.0, 2.0];
let (_ts, vols) = atm_term_structure(&p, &thetas, &mats);
assert!(vols.iter().all(|v| v.is_finite() && *v > 0.0));
}
#[test]
fn skew_term_structure_negative() {
let p = SsviParams::<f64>::new(-0.3, 0.5, 0.5);
let thetas = vec![0.01, 0.04, 0.09];
let mats = vec![0.25, 0.50, 1.0];
let (_ts, skews) = skew_term_structure(&p, &thetas, &mats);
assert!(skews.iter().all(|s| s.is_finite() && *s < 0.0));
}
}