Skip to main content

finquant/math/
normal.rs

1//! Standard-normal primitives used by the Black-Scholes pricer and the FX
2//! volatility surface machinery.
3//!
4//! The underlying implementation comes from `statrs`; wrapping it here keeps
5//! the call sites terse and centralises precision choices in one place.
6
7use statrs::distribution::{Continuous, ContinuousCDF, Normal};
8
9fn standard() -> Normal {
10    // statrs::distribution::Normal::new(0, 1) cannot fail — the args are fixed.
11    Normal::new(0.0, 1.0).expect("N(0,1) is well-defined")
12}
13
14/// Standard-normal CDF, Φ(x).
15pub fn cdf(x: f64) -> f64 {
16    standard().cdf(x)
17}
18
19/// Standard-normal PDF, φ(x) = exp(-x²/2) / √(2π).
20pub fn pdf(x: f64) -> f64 {
21    standard().pdf(x)
22}
23
24/// Standard-normal inverse CDF, Φ⁻¹(p). Panics if `p` is not in (0, 1).
25pub fn inverse_cdf(p: f64) -> f64 {
26    assert!(
27        p > 0.0 && p < 1.0,
28        "inverse_cdf input must be in (0, 1), got {}",
29        p
30    );
31    standard().inverse_cdf(p)
32}
33
34#[cfg(test)]
35mod tests {
36    use super::{cdf, inverse_cdf};
37
38    #[test]
39    fn cdf_is_symmetric_around_zero() {
40        assert!((cdf(0.0) - 0.5).abs() < 1e-12);
41        assert!((cdf(-1.0) + cdf(1.0) - 1.0).abs() < 1e-12);
42    }
43
44    #[test]
45    fn inverse_cdf_round_trip() {
46        for p in [0.1, 0.25, 0.5, 0.75, 0.9] {
47            let x = inverse_cdf(p);
48            assert!((cdf(x) - p).abs() < 1e-9);
49        }
50    }
51
52    #[test]
53    fn quartile_matches_known_value() {
54        // 25-pct quantile of the standard normal is approximately -0.6745.
55        let q = inverse_cdf(0.25);
56        assert!((q - (-0.6744897501960818)).abs() < 1e-9);
57    }
58}