use crate::math::{brent_root, index_to_f64};
use crate::raw::RawSvi;
const SCAN_POINTS: usize = 401;
const SCAN_MARGIN: f64 = 1.0;
const REFINE_TOL: f64 = 1e-10;
const REFINE_MAX_ITER: usize = 200;
#[must_use]
pub fn g(svi: &RawSvi, k: f64) -> f64 {
let w = svi.total_variance(k);
let wp = svi.w_prime(k);
let wpp = svi.w_double_prime(k);
let t1 = {
let inner = 1.0 - k * wp / (2.0 * w);
inner * inner
};
let t2 = {
let half_wp = wp / 2.0;
half_wp * half_wp * (1.0 / w + 0.25)
};
let t3 = wpp / 2.0;
t1 - t2 + t3
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ButterflyReport {
pub is_free: bool,
pub min_g: f64,
pub worst_k: f64,
pub wing_bound_ok: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CalendarReport {
pub is_free: bool,
pub min_difference: f64,
pub worst_k: f64,
}
#[must_use]
#[inline]
pub fn wing_bound_ok(svi: &RawSvi) -> bool {
svi.b * (1.0 + svi.rho.abs()) <= 2.0
}
#[must_use]
pub fn butterfly_scan(svi: &RawSvi, k_lo: f64, k_hi: f64) -> ButterflyReport {
let lo = k_lo.min(k_hi) - SCAN_MARGIN;
let hi = k_lo.max(k_hi) + SCAN_MARGIN;
let step = (hi - lo) / index_to_f64(SCAN_POINTS - 1);
let mut min_g = f64::INFINITY;
let mut worst_k = lo;
let mut worst_idx = 0_usize;
for i in 0..SCAN_POINTS {
let k = step.mul_add(index_to_f64(i), lo);
let gi = g(svi, k);
if gi < min_g {
min_g = gi;
worst_k = k;
worst_idx = i;
}
}
let wing_ok = wing_bound_ok(svi);
let is_free = min_g >= 0.0 && wing_ok;
if !is_free && min_g < 0.0 {
let lo_k = step.mul_add(index_to_f64(worst_idx.saturating_sub(1)), lo);
let hi_k = step.mul_add(index_to_f64((worst_idx + 1).min(SCAN_POINTS - 1)), lo);
if let Some(root) = brent_root(|x| g(svi, x), lo_k, hi_k, REFINE_TOL, REFINE_MAX_ITER) {
worst_k = root;
}
}
ButterflyReport {
is_free,
min_g,
worst_k,
wing_bound_ok: wing_ok,
}
}
#[must_use]
pub fn is_butterfly_free(svi: &RawSvi) -> bool {
butterfly_scan(svi, -1.0, 1.0).is_free
}
#[must_use]
pub fn calendar_scan(early: &RawSvi, late: &RawSvi, k_lo: f64, k_hi: f64) -> CalendarReport {
let lo = k_lo.min(k_hi) - SCAN_MARGIN;
let hi = k_lo.max(k_hi) + SCAN_MARGIN;
let step = (hi - lo) / index_to_f64(SCAN_POINTS - 1);
let diff = |k: f64| late.total_variance(k) - early.total_variance(k);
let mut min_diff = f64::INFINITY;
let mut worst_k = lo;
let mut worst_idx = 0_usize;
for i in 0..SCAN_POINTS {
let k = step.mul_add(index_to_f64(i), lo);
let d = diff(k);
if d < min_diff {
min_diff = d;
worst_k = k;
worst_idx = i;
}
}
let is_free = min_diff >= 0.0;
if !is_free {
let lo_k = step.mul_add(index_to_f64(worst_idx.saturating_sub(1)), lo);
let hi_k = step.mul_add(index_to_f64((worst_idx + 1).min(SCAN_POINTS - 1)), lo);
if let Some(root) = brent_root(diff, lo_k, hi_k, REFINE_TOL, REFINE_MAX_ITER) {
worst_k = root;
}
}
CalendarReport {
is_free,
min_difference: min_diff,
worst_k,
}
}
#[must_use]
pub fn is_calendar_free(early: &RawSvi, late: &RawSvi) -> bool {
calendar_scan(early, late, -1.0, 1.0).is_free
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ssvi::{Phi, Ssvi};
#[test]
fn g_is_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!(g(&svi, k) > 0.0, "g({k}) should be positive");
}
}
#[test]
fn g_equals_density_factor_at_atm() {
let flat = RawSvi::new(0.04, 0.0, 0.0, 0.0, 0.1).unwrap();
assert!((g(&flat, 0.0) - 1.0).abs() < 1e-12);
}
#[test]
fn wing_bound_accepts_gentle_rejects_steep() {
assert!(wing_bound_ok(
&RawSvi::new(0.04, 0.5, -0.3, 0.0, 0.1).unwrap()
));
assert!(!wing_bound_ok(
&RawSvi::new(0.04, 3.0, -0.3, 0.0, 0.1).unwrap()
));
}
#[test]
fn butterfly_scan_passes_benign_slice() {
let svi = RawSvi::new(0.04, 0.1, -0.2, 0.0, 0.3).unwrap();
let report = butterfly_scan(&svi, -0.5, 0.5);
assert!(report.is_free);
assert!(report.min_g > 0.0);
assert!(report.wing_bound_ok);
}
#[test]
fn butterfly_scan_flags_vogt_slice() {
let vogt = RawSvi::new(-0.0410, 0.1331, 0.3060, 0.3586, 0.4153).unwrap();
let report = butterfly_scan(&vogt, -1.5, 1.5);
assert!(
!report.is_free,
"Vogt slice must be flagged as arbitrageable"
);
assert!(report.min_g < 0.0, "min_g = {}", report.min_g);
}
#[test]
fn is_butterfly_free_convenience() {
let svi = RawSvi::new(0.04, 0.1, -0.2, 0.0, 0.3).unwrap();
assert!(is_butterfly_free(&svi));
let vogt = RawSvi::new(-0.0410, 0.1331, 0.3060, 0.3586, 0.4153).unwrap();
assert!(!is_butterfly_free(&vogt));
}
#[test]
fn calendar_scan_passes_ordered_slices() {
let early = RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1).unwrap();
let late = RawSvi::new(0.08, 0.3, -0.2, 0.0, 0.1).unwrap();
let report = calendar_scan(&early, &late, -0.5, 0.5);
assert!(report.is_free);
assert!(report.min_difference > 0.0);
}
#[test]
fn calendar_scan_flags_crossing_slices() {
let early = RawSvi::new(0.08, 0.3, -0.2, 0.0, 0.1).unwrap();
let late = RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1).unwrap();
let report = calendar_scan(&early, &late, -0.5, 0.5);
assert!(!report.is_free);
assert!(report.min_difference < 0.0);
}
#[test]
fn is_calendar_free_convenience() {
let early = RawSvi::new(0.04, 0.3, -0.2, 0.0, 0.1).unwrap();
let late = RawSvi::new(0.08, 0.3, -0.2, 0.0, 0.1).unwrap();
assert!(is_calendar_free(&early, &late));
}
#[test]
fn ssvi_slice_passing_theorem_42_is_butterfly_free() {
let ssvi = Ssvi::new(-0.3, Phi::power_law(0.5, 0.5).unwrap()).unwrap();
assert!(ssvi.is_butterfly_free_at(0.04));
let raw = ssvi.slice_at(0.04).unwrap();
let report = butterfly_scan(&raw, -1.0, 1.0);
assert!(report.is_free, "min_g = {}", report.min_g);
}
}