use crate::errors::ParamError;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RawSvi {
pub a: f64,
pub b: f64,
pub rho: f64,
pub m: f64,
pub sigma: f64,
}
impl RawSvi {
pub fn new(a: f64, b: f64, rho: f64, m: f64, sigma: f64) -> Result<Self, ParamError> {
for (name, value) in [("a", a), ("b", b), ("rho", rho), ("m", m), ("sigma", sigma)] {
if !value.is_finite() {
return Err(ParamError::NonFinite { name });
}
}
if b < 0.0 {
return Err(ParamError::NegativeSlope { b });
}
if rho.abs() >= 1.0 {
return Err(ParamError::CorrelationOutOfRange { rho });
}
if sigma <= 0.0 {
return Err(ParamError::NonPositiveSigma { sigma });
}
let candidate = Self {
a,
b,
rho,
m,
sigma,
};
let w_min = candidate.w_min();
if w_min < 0.0 {
return Err(ParamError::NegativeMinVariance { w_min });
}
Ok(candidate)
}
#[must_use]
pub const fn new_unchecked(a: f64, b: f64, rho: f64, m: f64, sigma: f64) -> Self {
Self {
a,
b,
rho,
m,
sigma,
}
}
pub fn validate(&self) -> Result<(), ParamError> {
Self::new(self.a, self.b, self.rho, self.m, self.sigma).map(|_| ())
}
#[must_use]
#[inline]
pub fn total_variance(&self, k: f64) -> f64 {
let u = k - self.m;
let r = u.mul_add(u, self.sigma * self.sigma).sqrt();
self.b.mul_add(self.rho.mul_add(u, r), self.a)
}
#[must_use]
#[inline]
pub fn w_prime(&self, k: f64) -> f64 {
let u = k - self.m;
let r = u.mul_add(u, self.sigma * self.sigma).sqrt();
self.b * (self.rho + u / r)
}
#[must_use]
#[inline]
pub fn w_double_prime(&self, k: f64) -> f64 {
let u = k - self.m;
let s2 = self.sigma * self.sigma;
let r2 = u.mul_add(u, s2);
let r = r2.sqrt();
self.b * s2 / (r2 * r)
}
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).sqrt())
}
#[must_use]
#[inline]
pub fn k_min(&self) -> f64 {
let denom = (1.0 - self.rho * self.rho).sqrt();
self.m - self.rho * self.sigma / denom
}
#[must_use]
#[inline]
pub fn w_min(&self) -> f64 {
self.b
.mul_add(self.sigma * (1.0 - self.rho * self.rho).sqrt(), self.a)
}
#[must_use]
#[inline]
pub fn atm_total_variance(&self) -> f64 {
self.total_variance(0.0)
}
#[must_use]
#[inline]
pub fn atm_skew(&self) -> f64 {
self.w_prime(0.0)
}
#[must_use]
#[inline]
pub fn atm_curvature(&self) -> f64 {
self.w_double_prime(0.0)
}
#[must_use]
#[inline]
pub fn left_wing_slope(&self) -> f64 {
self.b * (self.rho - 1.0)
}
#[must_use]
#[inline]
pub fn right_wing_slope(&self) -> f64 {
self.b * (self.rho + 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn slice() -> RawSvi {
RawSvi::new(0.04, 0.4, -0.3, 0.05, 0.12).unwrap()
}
#[test]
fn new_accepts_valid_slice() {
assert!(RawSvi::new(0.04, 0.4, -0.3, 0.0, 0.1).is_ok());
}
#[test]
fn new_rejects_negative_b() {
assert!(matches!(
RawSvi::new(0.04, -0.1, 0.0, 0.0, 0.1),
Err(ParamError::NegativeSlope { .. })
));
}
#[test]
fn new_rejects_rho_out_of_range() {
assert!(matches!(
RawSvi::new(0.04, 0.4, 1.0, 0.0, 0.1),
Err(ParamError::CorrelationOutOfRange { .. })
));
assert!(matches!(
RawSvi::new(0.04, 0.4, -1.2, 0.0, 0.1),
Err(ParamError::CorrelationOutOfRange { .. })
));
}
#[test]
fn new_rejects_non_positive_sigma() {
assert!(matches!(
RawSvi::new(0.04, 0.4, 0.0, 0.0, 0.0),
Err(ParamError::NonPositiveSigma { .. })
));
}
#[test]
fn new_rejects_negative_min_variance() {
assert!(matches!(
RawSvi::new(-1.0, 0.4, 0.0, 0.0, 0.1),
Err(ParamError::NegativeMinVariance { .. })
));
}
#[test]
fn new_rejects_non_finite() {
assert!(matches!(
RawSvi::new(f64::NAN, 0.4, 0.0, 0.0, 0.1),
Err(ParamError::NonFinite { .. })
));
}
#[test]
fn total_variance_golden_value() {
let svi = RawSvi::new(0.04, 0.4, 0.0, 0.0, 0.1).unwrap();
assert!((svi.total_variance(0.0) - 0.08).abs() < 1e-15);
assert!((svi.total_variance(0.5) - svi.total_variance(-0.5)).abs() < 1e-15);
}
#[test]
fn total_variance_hand_computed() {
let svi = slice();
let u = 0.2 - 0.05;
let r = (u * u + 0.12 * 0.12_f64).sqrt();
let expected = 0.04 + 0.4 * ((-0.3) * u + r);
assert!((svi.total_variance(0.2) - expected).abs() < 1e-15);
}
#[test]
fn derivative_matches_finite_difference() {
let svi = slice();
let h = 1e-6;
for &k in &[-0.3, -0.1, 0.0, 0.15, 0.4] {
let fd = (svi.total_variance(k + h) - svi.total_variance(k - h)) / (2.0 * h);
assert!((svi.w_prime(k) - fd).abs() < 1e-6, "k = {k}");
}
}
#[test]
fn second_derivative_matches_finite_difference() {
let svi = slice();
let h = 1e-4;
for &k in &[-0.3, -0.1, 0.0, 0.15, 0.4] {
let fd = (svi.total_variance(k + h) - 2.0 * svi.total_variance(k)
+ svi.total_variance(k - h))
/ (h * h);
assert!((svi.w_double_prime(k) - fd).abs() < 1e-4, "k = {k}");
}
}
#[test]
fn second_derivative_strictly_positive() {
let svi = slice();
for &k in &[-2.0, -0.5, 0.0, 0.5, 2.0] {
assert!(svi.w_double_prime(k) > 0.0, "k = {k}");
}
}
#[test]
fn vertex_is_the_minimum() {
let svi = slice();
let km = svi.k_min();
assert!(svi.w_prime(km).abs() < 1e-12);
let wmin = svi.w_min();
assert!((wmin - svi.total_variance(km)).abs() < 1e-12);
assert!(svi.total_variance(km + 0.1) > wmin);
assert!(svi.total_variance(km - 0.1) > wmin);
}
#[test]
fn implied_vol_roundtrip() {
let svi = RawSvi::new(0.04, 0.0, 0.0, 0.0, 0.1).unwrap();
let vol = svi.implied_vol(0.0, 1.0).unwrap();
assert!((vol - 0.2).abs() < 1e-12);
}
#[test]
fn implied_vol_rejects_bad_maturity() {
let svi = slice();
assert!(matches!(
svi.implied_vol(0.0, 0.0),
Err(ParamError::NonPositiveMaturity { .. })
));
}
#[test]
fn atm_accessors_agree_with_evaluation() {
let svi = slice();
assert!((svi.atm_total_variance() - svi.total_variance(0.0)).abs() < 1e-15);
assert!((svi.atm_skew() - svi.w_prime(0.0)).abs() < 1e-15);
assert!((svi.atm_curvature() - svi.w_double_prime(0.0)).abs() < 1e-15);
}
#[test]
fn wing_slopes_have_correct_sign() {
let svi = slice();
assert!(svi.left_wing_slope() <= 0.0);
assert!(svi.right_wing_slope() >= 0.0);
let far = (svi.total_variance(1000.0) - svi.total_variance(999.0)) / 1.0;
assert!((far - svi.right_wing_slope()).abs() < 1e-3);
}
#[test]
fn validate_round_trips() {
let svi = RawSvi::new_unchecked(0.04, 0.4, -0.3, 0.0, 0.1);
assert!(svi.validate().is_ok());
let bad = RawSvi::new_unchecked(0.04, -1.0, 0.0, 0.0, 0.1);
assert!(bad.validate().is_err());
}
}