sanos 0.2.1

SANOS: Smooth strictly Arbitrage-free Non-parametric Option Surfaces (Rust implementation)
Documentation
use crate::error::{SanosError, SanosResult};
use crate::market::{AtmMidPolicy, NearestOrLinearLogMoneyness};

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AtmMidPolicyConfig {
    NearestOrLinearLogMoneyness { tol_log: f64 },
}

impl Default for AtmMidPolicyConfig {
    fn default() -> Self {
        Self::NearestOrLinearLogMoneyness { tol_log: 1e-10 }
    }
}

impl AtmMidPolicyConfig {
    pub fn build(self) -> SanosResult<Box<dyn AtmMidPolicy>> {
        match self {
            AtmMidPolicyConfig::NearestOrLinearLogMoneyness { tol_log } => {
                if !tol_log.is_finite() {
                    return Err(SanosError::NonFinite {
                        field: "atm_policy.tol_log",
                        value: tol_log,
                    });
                }
                if tol_log < 0.0 {
                    return Err(SanosError::InvalidBound {
                        field: "atm_policy.tol_log",
                        value: tol_log,
                        min: 0.0,
                        max: f64::INFINITY,
                    });
                }
                Ok(Box::new(NearestOrLinearLogMoneyness { tol_log }))
            }
        }
    }
}

#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BsTimeChangedConfig {
    pub atm_policy: AtmMidPolicyConfig,
    pub var_floor: f64,
    pub enforce_non_decreasing: bool,
    pub eta: f64,
    /// Minimum effective variance used in the backbone kernel evaluation.
    ///
    /// Applied **after** eta scaling: `v_eff = max(eta * W(T), effective_var_floor)`.
    /// This ensures the kernel `E[(a Y_T - b)^+]` remains well-conditioned at
    /// very short maturities where `eta * W(T)` would otherwise be tiny.
    ///
    /// The resulting `v_eff(T)` is still non-decreasing (since `W(T)` is non-decreasing
    /// and we only clamp upward), so Y remains a valid martingale.
    #[cfg_attr(feature = "serde", serde(default))]
    pub effective_var_floor: f64,
}

impl Default for BsTimeChangedConfig {
    fn default() -> Self {
        Self {
            atm_policy: AtmMidPolicyConfig::default(),
            var_floor: 1e-12,
            enforce_non_decreasing: true,
            eta: 0.25,
            effective_var_floor: 0.0,
        }
    }
}

impl BsTimeChangedConfig {
    pub fn validate(&self) -> SanosResult<()> {
        if !self.var_floor.is_finite() {
            return Err(SanosError::NonFinite {
                field: "bs_time_changed.var_floor",
                value: self.var_floor,
            });
        }
        if self.var_floor < 0.0 {
            return Err(SanosError::InvalidBound {
                field: "bs_time_changed.var_floor",
                value: self.var_floor,
                min: 0.0,
                max: f64::INFINITY,
            });
        }
        if self.eta < 0.0 || self.eta >= 1.0 {
            return Err(SanosError::InvalidBound {
                field: "bs_time_changed.eta",
                value: self.eta,
                min: 0.0,
                max: 1.0 - f64::EPSILON,
            });
        }
        if !self.effective_var_floor.is_finite() {
            return Err(SanosError::NonFinite {
                field: "bs_time_changed.effective_var_floor",
                value: self.effective_var_floor,
            });
        }
        if self.effective_var_floor < 0.0 {
            return Err(SanosError::InvalidBound {
                field: "bs_time_changed.effective_var_floor",
                value: self.effective_var_floor,
                min: 0.0,
                max: f64::INFINITY,
            });
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn atm_policy_build_accepts_non_negative_tol() {
        let cfg = AtmMidPolicyConfig::NearestOrLinearLogMoneyness { tol_log: 0.0 };
        assert!(cfg.build().is_ok());
    }

    #[test]
    fn atm_policy_build_rejects_negative_tol() {
        let cfg = AtmMidPolicyConfig::NearestOrLinearLogMoneyness { tol_log: -1e-6 };
        match cfg.build() {
            Err(SanosError::InvalidBound { field, .. }) => assert_eq!(field, "atm_policy.tol_log"),
            Err(err) => panic!("unexpected error variant: {err:?}"),
            Ok(_) => panic!("expected error for negative tol_log"),
        }
    }

    #[test]
    fn atm_policy_build_rejects_non_finite_tol() {
        let cfg = AtmMidPolicyConfig::NearestOrLinearLogMoneyness { tol_log: f64::NAN };
        match cfg.build() {
            Err(SanosError::NonFinite { field, .. }) => assert_eq!(field, "atm_policy.tol_log"),
            Err(err) => panic!("unexpected error variant: {err:?}"),
            Ok(_) => panic!("expected error for non-finite tol_log"),
        }
    }

    #[test]
    fn bs_time_changed_validate_accepts_non_negative_floor() {
        let cfg = BsTimeChangedConfig {
            var_floor: 0.0,
            ..BsTimeChangedConfig::default()
        };
        assert!(cfg.validate().is_ok());
    }

    #[test]
    fn bs_time_changed_validate_rejects_negative_floor() {
        let cfg = BsTimeChangedConfig {
            var_floor: -1e-6,
            ..BsTimeChangedConfig::default()
        };
        let err = cfg.validate().unwrap_err();
        match err {
            SanosError::InvalidBound { field, .. } => {
                assert_eq!(field, "bs_time_changed.var_floor")
            }
            _ => panic!("unexpected error variant: {err:?}"),
        }
    }

    #[test]
    fn bs_time_changed_validate_rejects_non_finite_floor() {
        let cfg = BsTimeChangedConfig {
            var_floor: f64::NAN,
            ..BsTimeChangedConfig::default()
        };
        let err = cfg.validate().unwrap_err();
        match err {
            SanosError::NonFinite { field, .. } => assert_eq!(field, "bs_time_changed.var_floor"),
            _ => panic!("unexpected error variant: {err:?}"),
        }
    }

    #[test]
    fn bs_time_changed_validate_rejects_eta_equal_one() {
        let cfg = BsTimeChangedConfig {
            eta: 1.0,
            ..BsTimeChangedConfig::default()
        };
        let err = cfg.validate().unwrap_err();
        match err {
            SanosError::InvalidBound { field, .. } => assert_eq!(field, "bs_time_changed.eta"),
            _ => panic!("unexpected error variant: {err:?}"),
        }
    }
}