pub mod arbitrage;
pub mod sabr;
pub mod spline;
pub mod svi;
pub use arbitrage::{ArbitrageReport, ButterflyViolation};
pub use sabr::SabrSmile;
pub use spline::SplineSmile;
pub use svi::SviSmile;
pub(crate) const BUTTERFLY_G_TOL: f64 = 1e-10;
pub(crate) const DENSITY_NEG_TOL: f64 = 1e-8;
use crate::error;
use crate::implied::black::black_price;
use crate::types::{OptionType, Strike, Variance, Vol};
use crate::validate::validate_positive;
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ArbitrageScanConfig {
pub n_points: usize,
pub k_min: f64,
pub k_max: f64,
}
impl ArbitrageScanConfig {
pub fn svi_default() -> Self {
Self {
n_points: 200,
k_min: -3.0,
k_max: 3.0,
}
}
pub fn sabr_default() -> Self {
Self {
n_points: 200,
k_min: -2.0,
k_max: 2.0,
}
}
pub(crate) fn validate(&self) -> error::Result<()> {
if self.n_points < 2 {
return Err(error::VolSurfError::InvalidInput {
message: format!("n_points must be >= 2, got {}", self.n_points),
});
}
if !self.k_min.is_finite() || !self.k_max.is_finite() {
return Err(error::VolSurfError::InvalidInput {
message: "k_min and k_max must be finite".to_string(),
});
}
if self.k_min >= self.k_max {
return Err(error::VolSurfError::InvalidInput {
message: format!("k_min ({}) must be < k_max ({})", self.k_min, self.k_max),
});
}
Ok(())
}
}
pub trait SmileSection: Send + Sync + std::fmt::Debug {
fn vol(&self, strike: Strike) -> error::Result<Vol>;
fn variance(&self, strike: Strike) -> error::Result<Variance> {
let v = self.vol(strike)?;
Ok(Variance(v.0 * v.0 * self.expiry()))
}
fn density(&self, strike: Strike) -> error::Result<f64> {
validate_positive(strike.0, "strike")?;
let h = strike.0 * 1e-4;
let k_lo = strike.0 - h;
let k_hi = strike.0 + h;
let v_lo = self.vol(Strike(k_lo))?;
let v_mid = self.vol(strike)?;
let v_hi = self.vol(Strike(k_hi))?;
let c_lo = black_price(
self.forward(),
k_lo,
v_lo.0,
self.expiry(),
OptionType::Call,
)?;
let c_mid = black_price(
self.forward(),
strike.0,
v_mid.0,
self.expiry(),
OptionType::Call,
)?;
let c_hi = black_price(
self.forward(),
k_hi,
v_hi.0,
self.expiry(),
OptionType::Call,
)?;
Ok((c_lo - 2.0 * c_mid + c_hi) / (h * h))
}
fn forward(&self) -> f64;
fn expiry(&self) -> f64;
fn model_name(&self) -> &'static str;
fn is_arbitrage_free(&self) -> error::Result<ArbitrageReport> {
self.is_arbitrage_free_with(&ArbitrageScanConfig::svi_default())
}
fn is_arbitrage_free_with(
&self,
config: &ArbitrageScanConfig,
) -> error::Result<ArbitrageReport> {
config.validate()?;
let fwd = self.forward();
let n = config.n_points;
let mut violations = Vec::new();
for i in 0..n {
let k = config.k_min + (config.k_max - config.k_min) * (i as f64) / ((n - 1) as f64);
let strike = fwd * k.exp();
let d = match self.density(Strike(strike)) {
Ok(d) => d,
Err(_) => continue,
};
if d < -DENSITY_NEG_TOL {
violations.push(ButterflyViolation {
strike,
density: d,
magnitude: d.abs(),
});
}
}
Ok(ArbitrageReport {
expiry: self.expiry(),
butterfly_violations: violations,
})
}
}