use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArbitrageReport {
pub is_free: bool,
pub butterfly_violations: Vec<ButterflyViolation>,
}
impl ArbitrageReport {
pub fn clean() -> Self {
Self {
is_free: true,
butterfly_violations: Vec::new(),
}
}
pub fn merge(&self, other: &ArbitrageReport) -> ArbitrageReport {
let mut violations = self.butterfly_violations.clone();
violations.extend(other.butterfly_violations.iter().cloned());
ArbitrageReport {
is_free: self.is_free && other.is_free,
butterfly_violations: violations,
}
}
pub fn worst_violation(&self) -> Option<&ButterflyViolation> {
self.butterfly_violations
.iter()
.max_by(|a, b| a.magnitude.total_cmp(&b.magnitude))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ButterflyViolation {
pub strike: f64,
pub density: f64,
pub magnitude: f64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::smile::SmileSection;
fn make_violation(strike: f64, density: f64) -> ButterflyViolation {
ButterflyViolation {
strike,
density,
magnitude: density.abs(),
}
}
fn violated_report() -> ArbitrageReport {
ArbitrageReport {
is_free: false,
butterfly_violations: vec![make_violation(80.0, -0.001), make_violation(85.0, -0.005)],
}
}
#[test]
fn merge_two_clean_reports() {
let a = ArbitrageReport::clean();
let b = ArbitrageReport::clean();
let merged = a.merge(&b);
assert!(merged.is_free);
assert!(merged.butterfly_violations.is_empty());
}
#[test]
fn merge_clean_and_violated() {
let clean = ArbitrageReport::clean();
let bad = violated_report();
let merged = clean.merge(&bad);
assert!(!merged.is_free);
assert_eq!(merged.butterfly_violations.len(), 2);
}
#[test]
fn merge_violated_and_clean() {
let bad = violated_report();
let clean = ArbitrageReport::clean();
let merged = bad.merge(&clean);
assert!(!merged.is_free);
assert_eq!(merged.butterfly_violations.len(), 2);
}
#[test]
fn merge_two_violated_combines_violations() {
let a = ArbitrageReport {
is_free: false,
butterfly_violations: vec![make_violation(80.0, -0.001)],
};
let b = ArbitrageReport {
is_free: false,
butterfly_violations: vec![make_violation(90.0, -0.003), make_violation(95.0, -0.002)],
};
let merged = a.merge(&b);
assert!(!merged.is_free);
assert_eq!(merged.butterfly_violations.len(), 3);
}
#[test]
fn merge_preserves_violation_data() {
let a = ArbitrageReport {
is_free: false,
butterfly_violations: vec![make_violation(80.0, -0.007)],
};
let b = ArbitrageReport::clean();
let merged = a.merge(&b);
assert_eq!(merged.butterfly_violations.len(), 1);
let v = &merged.butterfly_violations[0];
assert_eq!(v.strike, 80.0);
assert_eq!(v.density, -0.007);
assert_eq!(v.magnitude, 0.007);
}
#[test]
fn worst_violation_clean_report_returns_none() {
let clean = ArbitrageReport::clean();
assert!(clean.worst_violation().is_none());
}
#[test]
fn worst_violation_single_violation() {
let report = ArbitrageReport {
is_free: false,
butterfly_violations: vec![make_violation(80.0, -0.003)],
};
let worst = report.worst_violation().unwrap();
assert_eq!(worst.strike, 80.0);
assert_eq!(worst.magnitude, 0.003);
}
#[test]
fn worst_violation_picks_largest_magnitude() {
let report = ArbitrageReport {
is_free: false,
butterfly_violations: vec![
make_violation(80.0, -0.001),
make_violation(85.0, -0.010),
make_violation(90.0, -0.005),
],
};
let worst = report.worst_violation().unwrap();
assert_eq!(worst.strike, 85.0);
assert_eq!(worst.magnitude, 0.010);
}
#[test]
fn sabr_extreme_nu_detects_violations() {
use crate::smile::SabrSmile;
let sabr = SabrSmile::new(100.0, 1.0, 0.3, 0.5, -0.5, 2.0).unwrap();
let report = sabr.is_arbitrage_free().unwrap();
assert!(
!report.is_free,
"extreme nu should produce butterfly violations"
);
assert!(!report.butterfly_violations.is_empty());
let worst = report.worst_violation().unwrap();
assert!(worst.magnitude > 0.0);
}
#[test]
fn sabr_conservative_params_clean() {
use crate::smile::SabrSmile;
let sabr = SabrSmile::new(100.0, 1.0, 0.2, 0.5, -0.3, 0.3).unwrap();
let report = sabr.is_arbitrage_free().unwrap();
assert!(report.is_free, "conservative SABR should be arb-free");
assert!(report.worst_violation().is_none());
}
#[test]
fn ssvi_extreme_params_detects_violations() {
use crate::surface::SsviSlice;
let slice = SsviSlice::new(100.0, 1.0, -0.95, 3.0, 0.5, 0.16).unwrap();
let report = slice.is_arbitrage_free().unwrap();
assert!(
!report.is_free,
"extreme SSVI params should detect violations"
);
let worst = report.worst_violation().unwrap();
assert!(worst.magnitude > 0.0);
}
#[test]
fn ssvi_conservative_params_clean() {
use crate::surface::SsviSlice;
let slice = SsviSlice::new(100.0, 1.0, -0.3, 0.5, 0.5, 0.16).unwrap();
let report = slice.is_arbitrage_free().unwrap();
assert!(report.is_free, "conservative SSVI should be arb-free");
assert!(report.worst_violation().is_none());
}
}