use crate::error::ConvertError;
use crate::smile::jump_wings::SviJw;
use crate::smile::raw::RawSvi;
use crate::surface::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.hypot(raw.sigma);
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();
if jw.p_t <= 0.0 || jw.c_t <= 0.0 || jw.v_tilde_t >= jw.v_t {
return Err(ConvertError::DegenerateJw);
}
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 = (jw.c_t - jw.p_t) / (jw.c_t + jw.p_t);
let beta = rho - 2.0 * jw.psi_t * sqrt_w / b;
if beta.abs() >= 1.0 || !beta.is_finite() {
return Err(ConvertError::JwHasNoRawPreimage { beta });
}
let r_rho = (1.0 - rho * rho).sqrt();
let r_beta = (1.0 - beta * beta).sqrt();
let a_scale = 0.5 * ((rho - beta).powi(2) + (r_rho - r_beta).powi(2));
let tolerance = 64.0 * f64::EPSILON;
if a_scale <= tolerance || !a_scale.is_finite() {
return Err(ConvertError::DegenerateJw);
}
let radial = (jw.v_t - jw.v_tilde_t) * t / (b * a_scale);
let m = beta * radial;
let sigma = r_beta * radial;
let a = jw.v_tilde_t * t - b * sigma * r_rho;
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)
}
#[cfg(test)]
#[allow(clippy::expect_used)] mod tests {
use super::*;
use crate::surface::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).expect("valid test or documentation fixture");
let jw = raw_to_jw(&raw, 1.0).expect("valid test or documentation fixture");
let back = jw_to_raw(&jw).expect("valid test or documentation fixture");
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).expect("valid test or documentation fixture");
let jw = raw_to_jw(&raw, 2.0).expect("valid test or documentation fixture");
let back = jw_to_raw(&jw).expect("valid test or documentation fixture");
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)
.expect("valid test or documentation fixture");
let jw = raw_to_jw(&raw, t).expect("valid test or documentation fixture");
let back = jw_to_raw(&jw).expect("valid test or documentation fixture");
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).expect("valid test or documentation fixture");
let jw = raw_to_jw(&raw, 1.0).expect("valid test or documentation fixture");
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).expect("valid test or documentation fixture");
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).expect("valid test or documentation fixture");
let jw = raw_to_jw(&raw, 1.0).expect("valid test or documentation fixture");
assert!(
jw.psi_t.abs() > 1e-9,
"m = 0 with rho != 0 has non-zero skew"
);
let back = jw_to_raw(&jw).expect("valid test or documentation fixture");
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).expect("valid test or documentation fixture");
let jw = raw_to_jw(&raw, 1.0).expect("valid test or documentation fixture");
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::modified_power_law(0.5, 0.5).expect("valid test or documentation fixture"),
)
.expect("valid test or documentation fixture");
let raw = ssvi_to_raw(&ssvi, 0.04).expect("valid test or documentation fixture");
let direct = ssvi
.slice_at(0.04)
.expect("valid test or documentation fixture");
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).expect("valid test or documentation fixture"),
)
.expect("valid test or documentation fixture");
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)
.expect("valid test or documentation fixture");
let back = jw_to_raw(&raw_to_jw(&raw, 1.5).expect("valid test or documentation fixture"))
.expect("valid test or documentation fixture");
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}"
);
}
}
}