use statrs::distribution::{Continuous, ContinuousCDF, Normal};
#[inline]
#[must_use]
pub fn d1_d2(f: f64, k: f64, t: f64, sigma: f64) -> (f64, f64) {
debug_assert!(
f > 0.0 && k > 0.0 && f.is_finite() && k.is_finite(),
"d1_d2 requires finite F > 0 and K > 0 (got F={f}, K={k})"
);
let v = sigma * t.sqrt();
let d1 = 0.5_f64.mul_add(v, (f / k).ln() / v);
let d2 = d1 - v;
(d1, d2)
}
#[must_use]
pub fn call_price(f: f64, k: f64, t: f64, sigma: f64, r: f64) -> f64 {
if t <= 0.0 || sigma <= 0.0 {
return intrinsic_value(f, k, true);
}
let (d1, d2) = d1_d2(f, k, t, sigma);
let norm = Normal::standard();
let df = (-r * t).exp();
df * f.mul_add(norm.cdf(d1), -(k * norm.cdf(d2)))
}
#[must_use]
pub fn put_price(f: f64, k: f64, t: f64, sigma: f64, r: f64) -> f64 {
if t <= 0.0 || sigma <= 0.0 {
return intrinsic_value(f, k, false);
}
let (d1, d2) = d1_d2(f, k, t, sigma);
let norm = Normal::standard();
let df = (-r * t).exp();
df * k.mul_add(norm.cdf(-d2), -(f * norm.cdf(-d1)))
}
#[inline]
#[must_use]
pub fn price(f: f64, k: f64, t: f64, sigma: f64, r: f64, is_call: bool) -> f64 {
if is_call {
call_price(f, k, t, sigma, r)
} else {
put_price(f, k, t, sigma, r)
}
}
#[must_use]
pub fn vega(f: f64, k: f64, t: f64, sigma: f64, r: f64) -> f64 {
if t <= 0.0 || sigma <= 0.0 {
return 0.0;
}
let (d1, _) = d1_d2(f, k, t, sigma);
let norm = Normal::standard();
let df = (-r * t).exp();
df * f * norm.pdf(d1) * t.sqrt()
}
#[inline]
#[must_use]
pub fn intrinsic_value(f: f64, k: f64, is_call: bool) -> f64 {
if is_call {
(f - k).max(0.0)
} else {
(k - f).max(0.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn atm_call_price_known_value() {
let c = call_price(100.0, 100.0, 1.0, 0.20, 0.0);
assert!(
(c - 7.96556746).abs() < 1e-6,
"ATM call price should match Hull §17.6 to 1e-6, got {c}"
);
}
#[test]
fn put_call_parity() {
let f = 100.0_f64;
let t = 0.5_f64;
let sigma = 0.30_f64;
let r = 0.05_f64;
let df = (-r * t).exp();
for &k in &[80.0, 90.0, 100.0, 110.0, 120.0] {
let c = call_price(f, k, t, sigma, r);
let p = put_price(f, k, t, sigma, r);
let parity = df * (f - k);
let diff = (c - p - parity).abs();
assert!(
diff < 1e-10,
"Put-call parity violated at K={k}: C-P={}, df*(F-K)={parity}, diff={diff}",
c - p
);
}
}
#[test]
fn vega_positive_and_call_put_equal() {
let v = vega(100.0, 100.0, 1.0, 0.20, 0.0);
assert!(v > 0.0, "vega should be positive, got {v}");
}
#[test]
fn deep_otm_call_near_zero() {
let c = call_price(100.0, 200.0, 0.25, 0.20, 0.0);
assert!(c < 1e-6, "deep OTM call should be near zero, got {c}");
}
#[test]
fn near_zero_t_returns_intrinsic() {
assert!((call_price(110.0, 100.0, 0.0, 0.20, 0.0) - 10.0).abs() < f64::EPSILON);
assert!(call_price(90.0, 100.0, 0.0, 0.20, 0.0).abs() < f64::EPSILON);
assert!((put_price(90.0, 100.0, 0.0, 0.20, 0.0) - 10.0).abs() < f64::EPSILON);
assert!((call_price(110.0, 100.0, 1.0, 0.0, 0.0) - 10.0).abs() < f64::EPSILON);
}
#[test]
fn vega_finite_difference() {
let f = 100.0;
let k = 100.0;
let t = 1.0;
let sigma = 0.20;
let r = 0.0;
let h = 1e-5;
let v_analytic = vega(f, k, t, sigma, r);
let v_fd =
(call_price(f, k, t, sigma + h, r) - call_price(f, k, t, sigma - h, r)) / (2.0 * h);
assert!(
(v_analytic - v_fd).abs() < 1e-4,
"vega analytic ({v_analytic}) vs FD ({v_fd}) diff > 1e-4"
);
}
#[test]
fn price_dispatch() {
let c = price(100.0, 100.0, 1.0, 0.20, 0.0, true);
let p = price(100.0, 100.0, 1.0, 0.20, 0.0, false);
assert!((c - call_price(100.0, 100.0, 1.0, 0.20, 0.0)).abs() < f64::EPSILON);
assert!((p - put_price(100.0, 100.0, 1.0, 0.20, 0.0)).abs() < f64::EPSILON);
}
#[test]
fn vega_degenerate_inputs() {
assert!(vega(100.0, 100.0, 0.0, 0.20, 0.0).abs() < f64::EPSILON);
assert!(vega(100.0, 100.0, 1.0, 0.0, 0.0).abs() < f64::EPSILON);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "requires finite F > 0 and K > 0")]
fn d1_d2_debug_asserts_positive_strike() {
let _ = d1_d2(100.0, 0.0, 1.0, 0.20);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "requires finite F > 0 and K > 0")]
fn d1_d2_debug_asserts_positive_forward() {
let _ = d1_d2(0.0, 100.0, 1.0, 0.20);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "requires finite F > 0 and K > 0")]
fn d1_d2_debug_asserts_finite_forward() {
let _ = d1_d2(f64::NAN, 100.0, 1.0, 0.20);
}
#[cfg(not(debug_assertions))]
#[test]
fn release_non_finite_inputs_yield_non_finite_no_panic() {
for &f in &[-1.0_f64, f64::NAN, f64::INFINITY] {
assert!(
!call_price(f, 100.0, 1.0, 0.20, 0.0).is_finite(),
"call_price F={f}"
);
assert!(
!put_price(f, 100.0, 1.0, 0.20, 0.0).is_finite(),
"put_price F={f}"
);
assert!(!vega(f, 100.0, 1.0, 0.20, 0.0).is_finite(), "vega F={f}");
}
for &k in &[-1.0_f64, f64::NAN, f64::INFINITY] {
assert!(
!call_price(100.0, k, 1.0, 0.20, 0.0).is_finite(),
"call_price K={k}"
);
assert!(
!put_price(100.0, k, 1.0, 0.20, 0.0).is_finite(),
"put_price K={k}"
);
}
}
#[test]
fn intrinsic_value_correctness() {
assert!((intrinsic_value(110.0, 100.0, true) - 10.0).abs() < f64::EPSILON);
assert!(intrinsic_value(90.0, 100.0, true).abs() < f64::EPSILON);
assert!((intrinsic_value(90.0, 100.0, false) - 10.0).abs() < f64::EPSILON);
assert!(intrinsic_value(110.0, 100.0, false).abs() < f64::EPSILON);
}
}