pub const FRAC_1_SQRT_2PI: f64 = 0.398_942_280_401_432_7_f64;
pub const SQRT_2: f64 = std::f64::consts::SQRT_2;
const NPDF_OVERFLOW_THRESHOLD: f64 = 700.0_f64;
const ERF_TINY: f64 = 1.387_778_780_781_445_7_e-17_f64;
const ERF_PP0: f64 = 1.283_791_670_955_125_6_e-1_f64;
const ERF_PP1: f64 = -3.250_421_072_470_015_e-1_f64;
const ERF_PP2: f64 = -2.848_174_957_559_851_e-2_f64;
const ERF_PP3: f64 = -5.770_270_296_489_442_e-3_f64;
const ERF_PP4: f64 = -2.376_301_665_665_016_3_e-5_f64;
const ERF_QQ1: f64 = 3.979_172_239_591_553_5_e-1_f64;
const ERF_QQ2: f64 = 6.502_224_998_876_73e-2_f64;
const ERF_QQ3: f64 = 5.081_306_281_875_766_e-3_f64;
const ERF_QQ4: f64 = 1.324_947_380_043_216_4_e-4_f64;
const ERF_QQ5: f64 = -3.960_228_278_775_368_e-6_f64;
const ERFC_ONE: f64 = 1.572_992_070_502_851_3_e-1_f64;
const ERFC_PA0: f64 = -2.362_118_560_752_659_4_e-3_f64;
const ERFC_PA1: f64 = 4.148_561_186_837_483_3_e-1_f64;
const ERFC_PA2: f64 = -3.722_078_760_357_013e-1_f64;
const ERFC_PA3: f64 = 3.183_466_199_011_617_5_e-1_f64;
const ERFC_PA4: f64 = -1.108_946_942_823_966_8_e-1_f64;
const ERFC_PA5: f64 = 3.547_830_431_952_019_e-2_f64;
const ERFC_PA6: f64 = -2.166_375_599_832_541_e-3_f64;
const ERFC_QA1: f64 = 1.064_208_804_008_442_3_e-1_f64;
const ERFC_QA2: f64 = 5.403_979_177_021_71_e-1_f64;
const ERFC_QA3: f64 = 7.182_865_441_419_625_e-2_f64;
const ERFC_QA4: f64 = 1.261_712_198_087_616_4_e-1_f64;
const ERFC_QA5: f64 = 1.363_708_391_202_905e-2_f64;
const ERFC_QA6: f64 = 1.198_449_984_679_911_e-2_f64;
const ERFC_RA0: f64 = -9.864_944_034_847_148_e-3_f64;
const ERFC_RA1: f64 = -6.938_585_727_071_818_e-1_f64;
const ERFC_RA2: f64 = -1.055_862_622_532_329_1_e1_f64;
const ERFC_RA3: f64 = -6.237_533_245_032_601_e1_f64;
const ERFC_RA4: f64 = -1.623_966_694_625_731_e2_f64;
const ERFC_RA5: f64 = -1.846_050_929_067_11_e2_f64;
const ERFC_RA6: f64 = -8.128_743_550_630_66e1_f64;
const ERFC_RA7: f64 = -9.814_329_344_169_145_e0_f64;
const ERFC_SA1: f64 = 1.965_127_166_743_925_7_e1_f64;
const ERFC_SA2: f64 = 1.376_577_541_435_197_e2_f64;
const ERFC_SA3: f64 = 4.345_658_774_752_292_3_e2_f64;
const ERFC_SA4: f64 = 6.453_872_717_332_679_e2_f64;
const ERFC_SA5: f64 = 4.290_081_400_275_678_3_e2_f64;
const ERFC_SA6: f64 = 1.086_350_055_417_794_4_e2_f64;
const ERFC_SA7: f64 = 6.570_249_770_319_282_e0_f64;
const ERFC_SA8: f64 = -6.042_441_521_485_81_e-2_f64;
const ERFC_RB0: f64 = -9.864_942_924_700_1e-3_f64;
const ERFC_RB1: f64 = -7.992_832_376_805_23_e-1_f64;
const ERFC_RB2: f64 = -1.775_795_491_775_475_2_e1_f64;
const ERFC_RB3: f64 = -1.606_363_848_555_579_4_e2_f64;
const ERFC_RB4: f64 = -6.375_664_433_683_891_e2_f64;
const ERFC_RB5: f64 = -1.025_095_131_611_077_2_e3_f64;
const ERFC_RB6: f64 = -4.835_191_916_086_514_e2_f64;
const ERFC_SB1: f64 = 3.033_806_078_756_258_e1_f64;
const ERFC_SB2: f64 = 3.257_925_129_965_739e2_f64;
const ERFC_SB3: f64 = 1.536_729_586_084_437_e3_f64;
const ERFC_SB4: f64 = 3.199_858_219_508_596_e3_f64;
const ERFC_SB5: f64 = 2.553_050_406_433_164_4_e3_f64;
const ERFC_SB6: f64 = 4.745_285_412_069_554_e2_f64;
const ERFC_SB7: f64 = -2.244_095_244_658_582_e1_f64;
#[inline]
#[must_use]
pub fn npdf(x: f64) -> f64 {
let half_x_sq = 0.5_f64 * x * x;
if half_x_sq > NPDF_OVERFLOW_THRESHOLD {
return 0.0_f64;
}
FRAC_1_SQRT_2PI * (-half_x_sq).exp()
}
#[inline]
fn erfc_impl(a: f64) -> f64 {
let x = a.abs();
let result = if x < 0.843_75_f64 {
erfc_small(x)
} else if x < 1.25_f64 {
erfc_mid(x)
} else if x < 28.0_f64 {
erfc_large(x)
} else {
0.0_f64
};
if a < 0.0_f64 {
2.0_f64 - result
} else {
result
}
}
#[inline]
fn erfc_small(x: f64) -> f64 {
if x < ERF_TINY {
return 1.0_f64;
}
let z = x * x;
let num = ERF_PP4
.mul_add(z, ERF_PP3)
.mul_add(z, ERF_PP2)
.mul_add(z, ERF_PP1)
.mul_add(z, ERF_PP0);
let den = ERF_QQ5
.mul_add(z, ERF_QQ4)
.mul_add(z, ERF_QQ3)
.mul_add(z, ERF_QQ2)
.mul_add(z, ERF_QQ1)
.mul_add(z, 1.0_f64);
1.0_f64 - (x + x * num / den)
}
#[inline]
fn erfc_mid(x: f64) -> f64 {
let s = x - 1.0_f64;
let p = ERFC_PA6
.mul_add(s, ERFC_PA5)
.mul_add(s, ERFC_PA4)
.mul_add(s, ERFC_PA3)
.mul_add(s, ERFC_PA2)
.mul_add(s, ERFC_PA1)
.mul_add(s, ERFC_PA0);
let q = ERFC_QA6
.mul_add(s, ERFC_QA5)
.mul_add(s, ERFC_QA4)
.mul_add(s, ERFC_QA3)
.mul_add(s, ERFC_QA2)
.mul_add(s, ERFC_QA1)
.mul_add(s, 1.0_f64);
ERFC_ONE - p / q
}
#[inline]
fn erfc_large(x: f64) -> f64 {
let s = 1.0_f64 / (x * x);
let ratio = if x < 2.857_142_857_142_857_f64 {
let r = ERFC_RA7
.mul_add(s, ERFC_RA6)
.mul_add(s, ERFC_RA5)
.mul_add(s, ERFC_RA4)
.mul_add(s, ERFC_RA3)
.mul_add(s, ERFC_RA2)
.mul_add(s, ERFC_RA1)
.mul_add(s, ERFC_RA0);
let sr = ERFC_SA8
.mul_add(s, ERFC_SA7)
.mul_add(s, ERFC_SA6)
.mul_add(s, ERFC_SA5)
.mul_add(s, ERFC_SA4)
.mul_add(s, ERFC_SA3)
.mul_add(s, ERFC_SA2)
.mul_add(s, ERFC_SA1)
.mul_add(s, 1.0_f64);
r / sr
} else {
let r = ERFC_RB6
.mul_add(s, ERFC_RB5)
.mul_add(s, ERFC_RB4)
.mul_add(s, ERFC_RB3)
.mul_add(s, ERFC_RB2)
.mul_add(s, ERFC_RB1)
.mul_add(s, ERFC_RB0);
let sr = ERFC_SB7
.mul_add(s, ERFC_SB6)
.mul_add(s, ERFC_SB5)
.mul_add(s, ERFC_SB4)
.mul_add(s, ERFC_SB3)
.mul_add(s, ERFC_SB2)
.mul_add(s, ERFC_SB1)
.mul_add(s, 1.0_f64);
r / sr
};
let z = f64::from_bits(x.to_bits() & 0xFFFF_FFFF_F000_0000_u64);
(-z * z - 0.562_5_f64).exp() * ((z - x) * (z + x) + ratio).exp() / x
}
#[inline]
#[must_use]
pub fn ncdf(x: f64) -> f64 {
0.5_f64 * erfc_impl(-x * std::f64::consts::FRAC_1_SQRT_2)
}
#[inline]
#[must_use]
pub fn ncdf_complement(x: f64) -> f64 {
0.5_f64 * erfc_impl(x * std::f64::consts::FRAC_1_SQRT_2)
}
#[inline]
#[must_use]
pub fn d1(spot: f64, strike: f64, rate: f64, div_yield: f64, vol: f64, time: f64) -> f64 {
let vol_sqrt_t = vol * time.sqrt();
((spot / strike).ln() + (rate - div_yield + 0.5_f64 * vol * vol) * time) / vol_sqrt_t
}
#[inline]
#[must_use]
pub fn d2(d1_val: f64, vol: f64, time: f64) -> f64 {
d1_val - vol * time.sqrt()
}
#[cfg(test)]
#[allow(clippy::similar_names)]
mod tests {
use super::*;
const CDF_IDENTITY_TOL: f64 = 1e-15_f64;
const KNOWN_VALUE_TOL: f64 = 1e-10_f64;
const D1_D2_TOL: f64 = 1e-15_f64;
#[test]
fn test_ncdf_complement_identity() {
let mut x = -8.0_f64;
while x <= 8.0_f64 {
let sum = ncdf(x) + ncdf(-x);
assert!(
(sum - 1.0_f64).abs() < CDF_IDENTITY_TOL,
"N({x}) + N({neg_x}) = {sum}, expected 1.0 (error = {err})",
x = x,
neg_x = -x,
sum = sum,
err = (sum - 1.0_f64).abs(),
);
x += 0.1_f64;
}
}
#[test]
fn test_ncdf_complement_via_dedicated_fn() {
let mut x = -8.0_f64;
while x <= 8.0_f64 {
let sum = ncdf(x) + ncdf_complement(x);
assert!(
(sum - 1.0_f64).abs() < CDF_IDENTITY_TOL,
"ncdf({x}) + ncdf_complement({x}) = {sum}, error = {err}",
x = x,
sum = sum,
err = (sum - 1.0_f64).abs(),
);
x += 0.1_f64;
}
}
#[test]
fn test_ncdf_known_values() {
assert!(
(ncdf(0.0_f64) - 0.5_f64).abs() < CDF_IDENTITY_TOL,
"N(0) should be 0.5, got {}",
ncdf(0.0_f64),
);
let n1_ref = 0.841_344_746_068_542_9_f64;
assert!(
(ncdf(1.0_f64) - n1_ref).abs() < KNOWN_VALUE_TOL,
"N(1) should be {n1_ref}, got {} (error = {})",
ncdf(1.0_f64),
(ncdf(1.0_f64) - n1_ref).abs(),
);
let nm1_ref = 0.158_655_253_931_457_07_f64;
assert!(
(ncdf(-1.0_f64) - nm1_ref).abs() < KNOWN_VALUE_TOL,
"N(-1) should be {nm1_ref}, got {} (error = {})",
ncdf(-1.0_f64),
(ncdf(-1.0_f64) - nm1_ref).abs(),
);
let n2_ref = 0.977_249_868_051_820_8_f64;
assert!(
(ncdf(2.0_f64) - n2_ref).abs() < KNOWN_VALUE_TOL,
"N(2) should be {n2_ref}, got {} (error = {})",
ncdf(2.0_f64),
(ncdf(2.0_f64) - n2_ref).abs(),
);
let nm3_ref = 0.001_349_898_031_630_094_6_f64;
assert!(
(ncdf(-3.0_f64) - nm3_ref).abs() < KNOWN_VALUE_TOL,
"N(-3) should be {nm3_ref}, got {} (error = {})",
ncdf(-3.0_f64),
(ncdf(-3.0_f64) - nm3_ref).abs(),
);
let n05_ref = 0.691_462_461_274_013_1_f64;
assert!(
(ncdf(0.5_f64) - n05_ref).abs() < KNOWN_VALUE_TOL,
"N(0.5) should be {n05_ref}, got {} (error = {})",
ncdf(0.5_f64),
(ncdf(0.5_f64) - n05_ref).abs(),
);
}
#[test]
fn test_ncdf_tail_values() {
let n6 = ncdf(6.0_f64);
assert!(
n6 > 0.999_999_999_f64,
"N(6) should be very close to 1.0, got {n6}",
);
let nm6 = ncdf(-6.0_f64);
assert!(
nm6 < 1e-8_f64 && nm6 > 0.0_f64,
"N(-6) should be tiny positive, got {nm6}",
);
assert!(
(ncdf(-6.0_f64) - ncdf_complement(6.0_f64)).abs() < CDF_IDENTITY_TOL,
"N(-6) should equal Q(6)",
);
assert!(ncdf(38.0_f64) <= 1.0_f64, "N(38) should be <= 1.0");
assert!(ncdf(-38.0_f64) >= 0.0_f64, "N(-38) should be >= 0.0");
}
#[test]
fn test_npdf_known_values() {
assert!(
(npdf(0.0_f64) - FRAC_1_SQRT_2PI).abs() < CDF_IDENTITY_TOL,
"φ(0) should be 1/√(2π) = {}, got {}",
FRAC_1_SQRT_2PI,
npdf(0.0_f64),
);
let phi1_ref = (-0.5_f64).exp() * FRAC_1_SQRT_2PI;
assert!(
(npdf(1.0_f64) - phi1_ref).abs() < CDF_IDENTITY_TOL,
"φ(1) should be {phi1_ref}, got {}",
npdf(1.0_f64),
);
assert!(
(npdf(-1.0_f64) - npdf(1.0_f64)).abs() < CDF_IDENTITY_TOL,
"φ(-1) should equal φ(1)",
);
}
#[test]
fn test_npdf_overflow_guard() {
let val = npdf(40.0_f64);
assert!(val == 0.0_f64, "npdf(40) should be exactly 0.0, got {val}");
assert!(!val.is_nan(), "npdf(40) must not be NaN");
assert!(!val.is_infinite(), "npdf(40) must not be Inf");
let val_neg = npdf(-40.0_f64);
assert!(
val_neg == 0.0_f64,
"npdf(-40) should be exactly 0.0, got {val_neg}",
);
}
#[test]
fn test_d1_d2_relationship() {
let test_cases: &[(f64, f64, f64, f64, f64, f64)] = &[
(100.0_f64, 100.0_f64, 0.05_f64, 0.02_f64, 0.20_f64, 1.0_f64),
(100.0_f64, 110.0_f64, 0.05_f64, 0.02_f64, 0.20_f64, 1.0_f64),
(50.0_f64, 100.0_f64, 0.05_f64, 0.00_f64, 0.40_f64, 2.0_f64),
(
200.0_f64, 150.0_f64, -0.01_f64, 0.03_f64, 0.10_f64, 0.25_f64,
),
];
for &(spot, strike, rate, div, vol, time) in test_cases {
let d1_val = d1(spot, strike, rate, div, vol, time);
let d2_val = d2(d1_val, vol, time);
let expected_d2 = d1_val - vol * time.sqrt();
assert!(
(d2_val - expected_d2).abs() < D1_D2_TOL,
"d2 = d1 - σ√T failed for S={spot}, K={strike}, r={rate}, q={div}, σ={vol}, T={time}: \
d2={d2_val}, expected={expected_d2}",
);
}
}
#[test]
fn test_d1_atm_value() {
let d1_val = d1(100.0_f64, 100.0_f64, 0.05_f64, 0.02_f64, 0.20_f64, 1.0_f64);
let expected = (0.05_f64 - 0.02_f64 + 0.02_f64) / 0.20_f64;
assert!(
(d1_val - expected).abs() < KNOWN_VALUE_TOL,
"d1 ATM should be {expected}, got {d1_val}",
);
}
}