use crate::arbitrage::g;
use crate::math::index_to_f64;
use crate::raw::RawSvi;
const TWO_PI: f64 = std::f64::consts::TAU;
#[must_use]
#[inline]
pub fn d_minus(svi: &RawSvi, k: f64) -> f64 {
let w = svi.total_variance(k);
let sqrt_w = w.sqrt();
-k / sqrt_w - sqrt_w / 2.0
}
#[must_use]
#[inline]
pub fn d_plus(svi: &RawSvi, k: f64) -> f64 {
let w = svi.total_variance(k);
let sqrt_w = w.sqrt();
-k / sqrt_w + sqrt_w / 2.0
}
#[must_use]
pub fn risk_neutral_density(svi: &RawSvi, k: f64) -> f64 {
let w = svi.total_variance(k);
if w <= 0.0 || !w.is_finite() {
return 0.0;
}
let dm = d_minus(svi, k);
g(svi, k) / (TWO_PI * w).sqrt() * (-0.5 * dm * dm).exp()
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DensityReport {
pub integral: f64,
pub min_density: f64,
pub is_non_negative: bool,
}
#[must_use]
pub fn integral(svi: &RawSvi, k_lo: f64, k_hi: f64, n: usize) -> f64 {
let n = n.max(1);
let panels = 2 * n;
let h = (k_hi - k_lo) / index_to_f64(panels);
let mut sum = risk_neutral_density(svi, k_lo) + risk_neutral_density(svi, k_hi);
for i in 1..panels {
let k = h.mul_add(index_to_f64(i), k_lo);
let weight = if i % 2 == 1 { 4.0 } else { 2.0 };
sum += weight * risk_neutral_density(svi, k);
}
sum * h / 3.0
}
#[must_use]
pub fn density_report(svi: &RawSvi, k_lo: f64, k_hi: f64, n: usize) -> DensityReport {
let n = n.max(1);
let panels = 2 * n;
let h = (k_hi - k_lo) / index_to_f64(panels);
let mut min_density = f64::INFINITY;
for i in 0..=panels {
let k = h.mul_add(index_to_f64(i), k_lo);
let p = risk_neutral_density(svi, k);
if p < min_density {
min_density = p;
}
}
DensityReport {
integral: integral(svi, k_lo, k_hi, n),
min_density,
is_non_negative: min_density >= 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn d_plus_d_minus_differ_by_sqrt_w() {
let svi = RawSvi::new(0.04, 0.2, -0.3, 0.05, 0.12).unwrap();
for &k in &[-0.5, 0.0, 0.3] {
let w = svi.total_variance(k);
assert!((d_plus(&svi, k) - d_minus(&svi, k) - w.sqrt()).abs() < 1e-12);
}
}
#[test]
fn density_positive_for_benign_slice() {
let svi = RawSvi::new(0.04, 0.1, -0.2, 0.0, 0.3).unwrap();
for &k in &[-1.0, -0.3, 0.0, 0.3, 1.0] {
assert!(risk_neutral_density(&svi, k) > 0.0, "p({k})");
}
}
#[test]
fn density_integrates_to_one() {
let svi = RawSvi::new(0.04, 0.05, -0.1, 0.0, 0.4).unwrap();
let mass = integral(&svi, -8.0, 8.0, 4000);
assert!((mass - 1.0).abs() < 1e-3, "mass = {mass}");
}
#[test]
fn density_integrates_to_one_low_vol() {
let svi = RawSvi::new(0.02, 0.04, -0.15, 0.0, 0.3).unwrap();
let mass = integral(&svi, -6.0, 6.0, 4000);
assert!((mass - 1.0).abs() < 1e-3, "mass = {mass}");
}
#[test]
fn density_report_benign_slice() {
let svi = RawSvi::new(0.04, 0.05, -0.1, 0.0, 0.4).unwrap();
let report = density_report(&svi, -8.0, 8.0, 4000);
assert!(report.is_non_negative);
assert!((report.integral - 1.0).abs() < 1e-3);
assert!(report.min_density >= 0.0);
}
#[test]
fn density_report_flags_vogt_slice() {
let vogt = RawSvi::new(-0.0410, 0.1331, 0.3060, 0.3586, 0.4153).unwrap();
let report = density_report(&vogt, -2.0, 2.0, 2000);
assert!(!report.is_non_negative);
assert!(report.min_density < 0.0);
}
#[test]
fn density_handles_zero_variance_gracefully() {
let svi = RawSvi::new(0.0, 0.1, 0.0, 0.0, 0.2).unwrap();
let p = risk_neutral_density(&svi, svi.k_min());
assert!(p.is_finite());
}
}