use crate::errors::ParamError;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SviJw {
pub v_t: f64,
pub psi_t: f64,
pub p_t: f64,
pub c_t: f64,
pub v_tilde_t: f64,
pub t: f64,
}
impl SviJw {
pub fn new(
v_t: f64,
psi_t: f64,
p_t: f64,
c_t: f64,
v_tilde_t: f64,
t: f64,
) -> Result<Self, ParamError> {
for (name, value) in [
("v_t", v_t),
("psi_t", psi_t),
("p_t", p_t),
("c_t", c_t),
("v_tilde_t", v_tilde_t),
("t", t),
] {
if !value.is_finite() {
return Err(ParamError::NonFinite { name });
}
}
if t <= 0.0 {
return Err(ParamError::NonPositiveMaturity { t });
}
if v_t <= 0.0 {
return Err(ParamError::NegativeTotalVariance { w: v_t });
}
if v_tilde_t < 0.0 {
return Err(ParamError::NegativeTotalVariance { w: v_tilde_t });
}
if p_t < 0.0 {
return Err(ParamError::NegativeSlope { b: p_t });
}
if c_t < 0.0 {
return Err(ParamError::NegativeSlope { b: c_t });
}
Ok(Self {
v_t,
psi_t,
p_t,
c_t,
v_tilde_t,
t,
})
}
#[must_use]
pub const fn new_unchecked(
v_t: f64,
psi_t: f64,
p_t: f64,
c_t: f64,
v_tilde_t: f64,
t: f64,
) -> Self {
Self {
v_t,
psi_t,
p_t,
c_t,
v_tilde_t,
t,
}
}
pub fn validate(&self) -> Result<(), ParamError> {
Self::new(
self.v_t,
self.psi_t,
self.p_t,
self.c_t,
self.v_tilde_t,
self.t,
)
.map(|_| ())
}
#[must_use]
#[inline]
pub fn atm_total_variance(&self) -> f64 {
self.v_t * self.t
}
#[must_use]
#[inline]
pub fn min_total_variance(&self) -> f64 {
self.v_tilde_t * self.t
}
#[must_use]
#[inline]
pub fn atm_vol(&self) -> f64 {
self.v_t.sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_accepts_valid_tuple() {
assert!(SviJw::new(0.04, -0.1, 0.3, 0.25, 0.035, 1.0).is_ok());
}
#[test]
fn new_rejects_non_positive_maturity() {
assert!(matches!(
SviJw::new(0.04, -0.1, 0.3, 0.25, 0.035, 0.0),
Err(ParamError::NonPositiveMaturity { .. })
));
}
#[test]
fn new_rejects_non_positive_v_t() {
assert!(matches!(
SviJw::new(0.0, -0.1, 0.3, 0.25, 0.0, 1.0),
Err(ParamError::NegativeTotalVariance { .. })
));
}
#[test]
fn new_rejects_negative_v_tilde() {
assert!(matches!(
SviJw::new(0.04, -0.1, 0.3, 0.25, -0.01, 1.0),
Err(ParamError::NegativeTotalVariance { .. })
));
}
#[test]
fn new_rejects_negative_wing_slopes() {
assert!(matches!(
SviJw::new(0.04, -0.1, -0.1, 0.25, 0.035, 1.0),
Err(ParamError::NegativeSlope { .. })
));
assert!(matches!(
SviJw::new(0.04, -0.1, 0.3, -0.1, 0.035, 1.0),
Err(ParamError::NegativeSlope { .. })
));
}
#[test]
fn new_rejects_non_finite() {
assert!(matches!(
SviJw::new(f64::NAN, -0.1, 0.3, 0.25, 0.035, 1.0),
Err(ParamError::NonFinite { .. })
));
}
#[test]
fn accessors() {
let jw = SviJw::new(0.04, -0.1, 0.3, 0.25, 0.035, 2.0).unwrap();
assert!((jw.atm_total_variance() - 0.08).abs() < 1e-15);
assert!((jw.min_total_variance() - 0.07).abs() < 1e-15);
assert!((jw.atm_vol() - 0.2).abs() < 1e-12);
}
#[test]
fn validate_round_trips() {
let jw = SviJw::new_unchecked(0.04, -0.1, 0.3, 0.25, 0.035, 1.0);
assert!(jw.validate().is_ok());
let bad = SviJw::new_unchecked(-1.0, 0.0, 0.3, 0.25, 0.0, 1.0);
assert!(bad.validate().is_err());
}
#[test]
fn is_copy() {
let jw = SviJw::new(0.04, -0.1, 0.3, 0.25, 0.035, 1.0).unwrap();
let copy = jw;
assert_eq!(jw, copy);
}
}