regit-svi 2.0.0

Arbitrage-free SVI volatility surfaces in pure Rust. Raw, Jump-Wings and SSVI parametrisations, calibration, and static-arbitrage checks. Zero dependencies.
Documentation
// Copyright 2026 Regit.io — Nicolas Koenig
// SPDX-License-Identifier: Apache-2.0

//! Validated scalar market coordinates and units.

use crate::error::ParamError;

macro_rules! finite_unit {
    ($name:ident, $doc:literal, $field:literal) => {
        #[doc = $doc]
        #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
        pub struct $name(f64);

        impl $name {
            /// Creates a value after checking that it is finite.
            ///
            /// # Errors
            ///
            /// Returns [`ParamError::NonFinite`] for `NaN` or infinity.
            pub fn new(value: f64) -> Result<Self, ParamError> {
                if value.is_finite() {
                    Ok(Self(value))
                } else {
                    Err(ParamError::NonFinite { name: $field })
                }
            }

            /// Returns the underlying scalar.
            #[must_use]
            pub const fn get(self) -> f64 {
                self.0
            }
        }

        impl TryFrom<f64> for $name {
            type Error = ParamError;

            fn try_from(value: f64) -> Result<Self, Self::Error> {
                Self::new(value)
            }
        }

        impl From<$name> for f64 {
            fn from(value: $name) -> Self {
                value.get()
            }
        }
    };
}

finite_unit!(
    LogMoneyness,
    r"A finite forward log-moneyness `ln(strike / forward)`.

# Examples

```
# fn main() -> Result<(), Box<dyn std::error::Error>> {
use regit_svi::LogMoneyness;

let k = LogMoneyness::new(-0.25)?;
assert_eq!(k.get(), -0.25);
# Ok(())
# }
```",
    "log_moneyness"
);

/// A finite, strictly positive time to expiry in years.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use regit_svi::Maturity;
///
/// let maturity = Maturity::new(1.5)?;
/// assert_eq!(maturity.get(), 1.5);
/// assert!(Maturity::new(0.0).is_err());
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Maturity(f64);

impl Maturity {
    /// Creates a maturity after checking finiteness and strict positivity.
    ///
    /// # Errors
    ///
    /// Returns [`ParamError::NonFinite`] or [`ParamError::NonPositiveMaturity`].
    pub fn new(value: f64) -> Result<Self, ParamError> {
        if !value.is_finite() {
            return Err(ParamError::NonFinite { name: "maturity" });
        }
        if value <= 0.0 {
            return Err(ParamError::NonPositiveMaturity { t: value });
        }
        Ok(Self(value))
    }

    /// Returns the year fraction.
    #[must_use]
    pub const fn get(self) -> f64 {
        self.0
    }
}

impl TryFrom<f64> for Maturity {
    type Error = ParamError;

    fn try_from(value: f64) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl From<Maturity> for f64 {
    fn from(value: Maturity) -> Self {
        value.get()
    }
}

/// A finite, non-negative total implied variance.
///
/// # Examples
///
/// ```
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use regit_svi::TotalVariance;
///
/// let variance = TotalVariance::new(0.04)?;
/// assert_eq!(variance.get(), 0.04);
/// assert!(TotalVariance::new(-0.01).is_err());
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct TotalVariance(f64);

impl TotalVariance {
    /// Creates a total variance after checking its domain.
    ///
    /// # Errors
    ///
    /// Returns [`ParamError::NonFinite`] or [`ParamError::NegativeTotalVariance`].
    pub fn new(value: f64) -> Result<Self, ParamError> {
        if !value.is_finite() {
            return Err(ParamError::NonFinite {
                name: "total_variance",
            });
        }
        if value < 0.0 {
            return Err(ParamError::NegativeTotalVariance { w: value });
        }
        Ok(Self(value))
    }

    /// Returns the underlying total variance.
    #[must_use]
    pub const fn get(self) -> f64 {
        self.0
    }
}

impl TryFrom<f64> for TotalVariance {
    type Error = ParamError;

    fn try_from(value: f64) -> Result<Self, Self::Error> {
        Self::new(value)
    }
}

impl From<TotalVariance> for f64 {
    fn from(value: TotalVariance) -> Self {
        value.get()
    }
}

#[cfg(test)]
#[allow(clippy::expect_used)] // Validated fixtures use contextual expectations.
mod tests {
    use super::*;

    #[test]
    fn validated_units_enforce_boundaries() {
        assert!(LogMoneyness::new(0.0).is_ok());
        assert!(LogMoneyness::new(f64::NAN).is_err());
        assert!(Maturity::new(f64::MIN_POSITIVE).is_ok());
        assert!(Maturity::new(0.0).is_err());
        assert!(TotalVariance::new(0.0).is_ok());
        assert!(TotalVariance::new(-f64::MIN_POSITIVE).is_err());
    }
}