use crate::errors::ParamError;
use crate::raw::RawSvi;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Phi {
Heston {
lambda: f64,
},
PowerLaw {
eta: f64,
gamma: f64,
},
}
impl Phi {
pub fn heston(lambda: f64) -> Result<Self, ParamError> {
if !lambda.is_finite() {
return Err(ParamError::NonFinite { name: "lambda" });
}
if lambda <= 0.0 {
return Err(ParamError::InvalidPhiParameter {
name: "lambda",
value: lambda,
});
}
Ok(Self::Heston { lambda })
}
pub fn power_law(eta: f64, gamma: f64) -> Result<Self, ParamError> {
if !eta.is_finite() {
return Err(ParamError::NonFinite { name: "eta" });
}
if !gamma.is_finite() {
return Err(ParamError::NonFinite { name: "gamma" });
}
if eta <= 0.0 {
return Err(ParamError::InvalidPhiParameter {
name: "eta",
value: eta,
});
}
if gamma <= 0.0 || gamma >= 1.0 {
return Err(ParamError::InvalidPhiParameter {
name: "gamma",
value: gamma,
});
}
Ok(Self::PowerLaw { eta, gamma })
}
#[must_use]
pub fn eval(&self, theta: f64) -> f64 {
match *self {
Self::Heston { lambda } => {
let x = lambda * theta;
(1.0 / x) * (1.0 - (1.0 - (-x).exp()) / x)
}
Self::PowerLaw { eta, gamma } => {
eta / (theta.powf(gamma) * (1.0 + theta).powf(1.0 - gamma))
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Ssvi {
pub rho: f64,
pub phi: Phi,
}
impl Ssvi {
pub fn new(rho: f64, phi: Phi) -> Result<Self, ParamError> {
if !rho.is_finite() {
return Err(ParamError::NonFinite { name: "rho" });
}
if rho.abs() >= 1.0 {
return Err(ParamError::CorrelationOutOfRange { rho });
}
Ok(Self { rho, phi })
}
#[must_use]
pub fn total_variance(&self, k: f64, theta: f64) -> f64 {
let phi = self.phi.eval(theta);
let pk = phi * k;
let inner = (pk + self.rho).mul_add(pk + self.rho, 1.0 - self.rho * self.rho);
(theta / 2.0) * (1.0 + self.rho * pk + inner.sqrt())
}
pub fn slice_at(&self, theta: f64) -> Result<RawSvi, ParamError> {
if theta <= 0.0 || !theta.is_finite() {
return Err(ParamError::NonPositiveTheta { theta });
}
let phi = self.phi.eval(theta);
if phi <= 0.0 || !phi.is_finite() {
return Err(ParamError::InvalidPhiParameter {
name: "phi(theta)",
value: phi,
});
}
let one_minus_rho2 = 1.0 - self.rho * self.rho;
let a = (theta / 2.0) * one_minus_rho2;
let b = theta * phi / 2.0;
let m = -self.rho / phi;
let sigma = one_minus_rho2.sqrt() / phi;
RawSvi::new(a, b, self.rho, m, sigma)
}
#[must_use]
pub fn is_butterfly_free_at(&self, theta: f64) -> bool {
if theta <= 0.0 || !theta.is_finite() {
return false;
}
let phi = self.phi.eval(theta);
if phi <= 0.0 || !phi.is_finite() {
return false;
}
let factor = 1.0 + self.rho.abs();
let tp = theta * phi;
(tp * factor < 4.0) && (tp * phi * factor <= 4.0)
}
#[must_use]
pub fn is_butterfly_free(&self, thetas: &[f64]) -> bool {
thetas.iter().all(|&theta| self.is_butterfly_free_at(theta))
}
#[must_use]
pub fn is_calendar_free_at(&self, theta: f64) -> bool {
if theta <= 0.0 || !theta.is_finite() {
return false;
}
let h = (theta * 1e-6).max(1e-9);
let theta_phi = |x: f64| x * self.phi.eval(x);
let deriv = (theta_phi(theta + h) - theta_phi(theta - h)) / (2.0 * h);
if deriv < -1e-12 {
return false;
}
let rho2 = self.rho * self.rho;
if rho2 < 1e-300 {
return true;
}
let phi = self.phi.eval(theta);
let upper = (1.0 / rho2) * (1.0 + (1.0 - rho2).sqrt()) * phi;
deriv <= upper + 1e-12
}
#[must_use]
pub fn is_calendar_free(&self, thetas: &[f64]) -> bool {
for pair in thetas.windows(2) {
if pair[1] < pair[0] - 1e-12 {
return false;
}
}
thetas.iter().all(|&theta| self.is_calendar_free_at(theta))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn phi_heston_validation() {
assert!(Phi::heston(1.0).is_ok());
assert!(Phi::heston(0.0).is_err());
assert!(Phi::heston(-1.0).is_err());
assert!(Phi::heston(f64::NAN).is_err());
}
#[test]
fn phi_power_law_validation() {
assert!(Phi::power_law(0.5, 0.5).is_ok());
assert!(Phi::power_law(0.0, 0.5).is_err());
assert!(Phi::power_law(0.5, 0.0).is_err());
assert!(Phi::power_law(0.5, 1.0).is_err());
assert!(Phi::power_law(f64::INFINITY, 0.5).is_err());
}
#[test]
fn phi_eval_positive_and_decreasing() {
for phi in [Phi::heston(1.0).unwrap(), Phi::power_law(0.5, 0.5).unwrap()] {
let a = phi.eval(0.01);
let b = phi.eval(0.04);
let c = phi.eval(0.16);
assert!(a > 0.0 && b > 0.0 && c > 0.0);
assert!(a > b && b > c, "phi should be decreasing in theta");
}
}
#[test]
fn phi_power_law_golden() {
let p = Phi::power_law(1.0, 0.5).unwrap();
assert!((p.eval(1.0) - 1.0 / 2.0_f64.sqrt()).abs() < 1e-12);
}
#[test]
fn ssvi_new_validation() {
let phi = Phi::heston(1.0).unwrap();
assert!(Ssvi::new(-0.4, phi).is_ok());
assert!(Ssvi::new(1.0, phi).is_err());
assert!(Ssvi::new(f64::NAN, phi).is_err());
}
#[test]
fn ssvi_total_variance_atm_zero_rho() {
let ssvi = Ssvi::new(0.0, Phi::power_law(0.5, 0.5).unwrap()).unwrap();
assert!((ssvi.total_variance(0.0, 0.04) - 0.04).abs() < 1e-15);
}
#[test]
fn slice_at_reproduces_total_variance() {
let ssvi = Ssvi::new(-0.3, Phi::power_law(0.5, 0.5).unwrap()).unwrap();
let raw = ssvi.slice_at(0.04).unwrap();
for &k in &[-0.5, -0.1, 0.0, 0.1, 0.5] {
let direct = ssvi.total_variance(k, 0.04);
assert!((raw.total_variance(k) - direct).abs() < 1e-12, "k = {k}");
}
}
#[test]
fn slice_at_rejects_non_positive_theta() {
let ssvi = Ssvi::new(-0.3, Phi::heston(1.0).unwrap()).unwrap();
assert!(matches!(
ssvi.slice_at(0.0),
Err(ParamError::NonPositiveTheta { .. })
));
}
#[test]
fn slice_at_heston_reproduces_total_variance() {
let ssvi = Ssvi::new(0.2, Phi::heston(2.0).unwrap()).unwrap();
let raw = ssvi.slice_at(0.09).unwrap();
for &k in &[-0.4, 0.0, 0.4] {
let direct = ssvi.total_variance(k, 0.09);
assert!((raw.total_variance(k) - direct).abs() < 1e-12, "k = {k}");
}
}
#[test]
fn butterfly_free_holds_for_small_eta() {
let ssvi = Ssvi::new(-0.3, Phi::power_law(0.5, 0.5).unwrap()).unwrap();
assert!(ssvi.is_butterfly_free(&[0.01, 0.04, 0.09, 0.25]));
}
#[test]
fn butterfly_violation_for_large_phi() {
let ssvi = Ssvi::new(0.5, Phi::power_law(20.0, 0.9).unwrap()).unwrap();
assert!(!ssvi.is_butterfly_free_at(1e-3));
}
#[test]
fn butterfly_free_rejects_bad_theta() {
let ssvi = Ssvi::new(0.0, Phi::heston(1.0).unwrap()).unwrap();
assert!(!ssvi.is_butterfly_free_at(0.0));
assert!(!ssvi.is_butterfly_free_at(-1.0));
}
#[test]
fn calendar_free_for_monotone_thetas() {
let ssvi = Ssvi::new(-0.3, Phi::power_law(0.5, 0.5).unwrap()).unwrap();
assert!(ssvi.is_calendar_free(&[0.01, 0.04, 0.09]));
}
#[test]
fn calendar_arbitrage_for_decreasing_thetas() {
let ssvi = Ssvi::new(-0.3, Phi::power_law(0.5, 0.5).unwrap()).unwrap();
assert!(!ssvi.is_calendar_free(&[0.09, 0.04, 0.01]));
}
#[test]
fn calendar_free_at_zero_rho() {
let ssvi = Ssvi::new(0.0, Phi::power_law(0.5, 0.5).unwrap()).unwrap();
assert!(ssvi.is_calendar_free_at(0.04));
}
}