use crate::errors::{ConvertError, ParamError};
use crate::jw::SviJw;
use crate::raw::RawSvi;
use crate::ssvi::Ssvi;
pub fn raw_to_jw(raw: &RawSvi, t: f64) -> Result<SviJw, ConvertError> {
if t <= 0.0 || !t.is_finite() {
return Err(ConvertError::NonPositiveAtmVariance { w: t });
}
let w0 = raw.atm_total_variance();
if w0 <= 0.0 || !w0.is_finite() {
return Err(ConvertError::NonPositiveAtmVariance { w: w0 });
}
let sqrt_w0 = w0.sqrt();
let root_m = (raw.m * raw.m + raw.sigma * raw.sigma).sqrt();
let v_t = w0 / t;
let psi_t = (raw.b / (2.0 * sqrt_w0)) * (raw.rho - raw.m / root_m);
let p_t = (raw.b / sqrt_w0) * (1.0 - raw.rho);
let c_t = (raw.b / sqrt_w0) * (1.0 + raw.rho);
let v_tilde_t = raw.w_min() / t;
SviJw::new(v_t, psi_t, p_t, c_t, v_tilde_t, t).map_err(ConvertError::Param)
}
#[allow(clippy::many_single_char_names)]
pub fn jw_to_raw(jw: &SviJw) -> Result<RawSvi, ConvertError> {
let t = jw.t;
let w = jw.v_t * t;
if w <= 0.0 || !w.is_finite() {
return Err(ConvertError::NonPositiveAtmVariance { w });
}
let sqrt_w = w.sqrt();
let b = (sqrt_w / 2.0) * (jw.c_t + jw.p_t);
if b <= 0.0 || !b.is_finite() {
return Err(ConvertError::DegenerateJw);
}
let rho = 1.0 - jw.p_t * sqrt_w / b;
let beta = rho - 2.0 * jw.psi_t * sqrt_w / b;
if beta.abs() > 1.0 {
return Err(ConvertError::JwHasNoRawPreimage { beta });
}
let one_minus_rho2 = 1.0 - rho * rho;
let sqrt_one_minus_rho2 = one_minus_rho2.sqrt();
let numerator = (jw.v_t - jw.v_tilde_t) * t;
let (m, sigma) = if beta.abs() < 1e-12 {
let denom = b * (1.0 - sqrt_one_minus_rho2);
if denom.abs() < 1e-300 {
return Err(ConvertError::DegenerateJw);
}
let sigma = numerator / denom;
(0.0, sigma)
} else {
let alpha = sign(beta) * (1.0 / (beta * beta) - 1.0).sqrt();
let bracket =
-rho + sign(alpha) * (1.0 + alpha * alpha).sqrt() - alpha * sqrt_one_minus_rho2;
let denom = b * bracket;
if denom.abs() < 1e-12 || !denom.is_finite() {
return Err(ConvertError::DegenerateJw);
}
let m = numerator / denom;
let sigma = alpha * m;
(m, sigma)
};
let a = jw.v_tilde_t * t - b * sigma * sqrt_one_minus_rho2;
if sigma <= 0.0 {
return Err(ConvertError::Param(ParamError::NonPositiveSigma { sigma }));
}
RawSvi::new(a, b, rho, m, sigma).map_err(ConvertError::Param)
}
pub fn ssvi_to_raw(ssvi: &Ssvi, theta: f64) -> Result<RawSvi, ConvertError> {
ssvi.slice_at(theta).map_err(ConvertError::Param)
}
#[inline]
fn sign(x: f64) -> f64 {
if x < 0.0 { -1.0 } else { 1.0 }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ssvi::Phi;
fn assert_raw_close(a: &RawSvi, b: &RawSvi, tol: f64) {
assert!((a.a - b.a).abs() < tol, "a: {} vs {}", a.a, b.a);
assert!((a.b - b.b).abs() < tol, "b: {} vs {}", a.b, b.b);
assert!((a.rho - b.rho).abs() < tol, "rho: {} vs {}", a.rho, b.rho);
assert!((a.m - b.m).abs() < tol, "m: {} vs {}", a.m, b.m);
assert!(
(a.sigma - b.sigma).abs() < tol,
"sigma: {} vs {}",
a.sigma,
b.sigma
);
}
#[test]
fn raw_jw_roundtrip_identity() {
let raw = RawSvi::new(0.04, 0.4, -0.3, 0.05, 0.12).unwrap();
let jw = raw_to_jw(&raw, 1.0).unwrap();
let back = jw_to_raw(&jw).unwrap();
assert_raw_close(&raw, &back, 1e-9);
}
#[test]
fn raw_jw_roundtrip_positive_rho() {
let raw = RawSvi::new(0.05, 0.3, 0.4, -0.1, 0.2).unwrap();
let jw = raw_to_jw(&raw, 2.0).unwrap();
let back = jw_to_raw(&jw).unwrap();
assert_raw_close(&raw, &back, 1e-8);
}
#[test]
fn raw_jw_roundtrip_various_maturities() {
for &t in &[0.25, 0.5, 1.0, 2.0, 5.0] {
let raw = RawSvi::new(0.03, 0.35, -0.2, 0.02, 0.15).unwrap();
let jw = raw_to_jw(&raw, t).unwrap();
let back = jw_to_raw(&jw).unwrap();
assert_raw_close(&raw, &back, 1e-8);
}
}
#[test]
fn raw_to_jw_v_t_is_atm_variance() {
let raw = RawSvi::new(0.04, 0.4, -0.3, 0.05, 0.12).unwrap();
let jw = raw_to_jw(&raw, 1.0).unwrap();
assert!((jw.v_t * jw.t - raw.atm_total_variance()).abs() < 1e-12);
assert!((jw.v_tilde_t * jw.t - raw.w_min()).abs() < 1e-12);
}
#[test]
fn raw_to_jw_rejects_bad_maturity() {
let raw = RawSvi::new(0.04, 0.4, -0.3, 0.05, 0.12).unwrap();
assert!(raw_to_jw(&raw, 0.0).is_err());
}
#[test]
fn jw_to_raw_handles_m_zero_branch() {
let raw = RawSvi::new(0.04, 0.4, -0.3, 0.0, 0.15).unwrap();
let jw = raw_to_jw(&raw, 1.0).unwrap();
assert!(
jw.psi_t.abs() > 1e-9,
"m = 0 with rho != 0 has non-zero skew"
);
let back = jw_to_raw(&jw).unwrap();
assert!(
back.m.abs() < 1e-9,
"recovered m should be 0, got {}",
back.m
);
for &k in &[-0.3, 0.0, 0.3] {
assert!(
(back.total_variance(k) - raw.total_variance(k)).abs() < 1e-8,
"k = {k}"
);
}
}
#[test]
fn jw_to_raw_rejects_ambiguous_vertex_at_atm() {
let rho = -0.3_f64;
let sigma = 0.15_f64;
let m = rho * sigma / (1.0 - rho * rho).sqrt();
let raw = RawSvi::new(0.04, 0.4, rho, m, sigma).unwrap();
let jw = raw_to_jw(&raw, 1.0).unwrap();
assert!((jw.v_t - jw.v_tilde_t).abs() < 1e-12);
assert!(matches!(jw_to_raw(&jw), Err(ConvertError::DegenerateJw)));
}
#[test]
fn jw_to_raw_rejects_no_preimage() {
let jw = SviJw::new_unchecked(0.04, 5.0, 0.3, 0.25, 0.035, 1.0);
assert!(matches!(
jw_to_raw(&jw),
Err(ConvertError::JwHasNoRawPreimage { .. })
));
}
#[test]
fn jw_to_raw_rejects_zero_b() {
let jw = SviJw::new_unchecked(0.04, 0.0, 0.0, 0.0, 0.04, 1.0);
assert!(matches!(jw_to_raw(&jw), Err(ConvertError::DegenerateJw)));
}
#[test]
fn ssvi_to_raw_matches_slice_at() {
let ssvi = Ssvi::new(-0.3, Phi::power_law(0.5, 0.5).unwrap()).unwrap();
let raw = ssvi_to_raw(&ssvi, 0.04).unwrap();
let direct = ssvi.slice_at(0.04).unwrap();
assert_raw_close(&raw, &direct, 1e-15);
}
#[test]
fn ssvi_to_raw_rejects_bad_theta() {
let ssvi = Ssvi::new(-0.3, Phi::heston(1.0).unwrap()).unwrap();
assert!(ssvi_to_raw(&ssvi, 0.0).is_err());
}
#[test]
fn jw_roundtrip_total_variance_preserved() {
let raw = RawSvi::new(0.045, 0.38, -0.25, 0.03, 0.14).unwrap();
let back = jw_to_raw(&raw_to_jw(&raw, 1.5).unwrap()).unwrap();
for &k in &[-1.0, -0.4, 0.0, 0.4, 1.0] {
assert!(
(back.total_variance(k) - raw.total_variance(k)).abs() < 1e-9,
"k = {k}"
);
}
}
}