use crate::math::normal::{cdf, pdf};
pub fn bachelier_call(forward: f64, strike: f64, variance: f64) -> f64 {
if variance <= 0.0 {
return (forward - strike).max(0.0);
}
let sqrt_v = variance.sqrt();
let d = (forward - strike) / sqrt_v;
sqrt_v * pdf(d) + (forward - strike) * cdf(d)
}
pub fn bachelier_put(forward: f64, strike: f64, variance: f64) -> f64 {
if variance <= 0.0 {
return (strike - forward).max(0.0);
}
let sqrt_v = variance.sqrt();
let d = (forward - strike) / sqrt_v;
sqrt_v * pdf(d) + (strike - forward) * cdf(-d)
}
pub fn bachelier_vega_variance(forward: f64, strike: f64, variance: f64) -> f64 {
if variance <= 0.0 {
return 0.0;
}
let sqrt_v = variance.sqrt();
let d = (forward - strike) / sqrt_v;
pdf(d) / (2.0 * sqrt_v)
}
#[cfg(test)]
mod tests {
use super::{bachelier_call, bachelier_put, bachelier_vega_variance};
#[test]
fn atm_call_closed_form() {
let v = 0.01_f64.powi(2) * 1.0; let c = bachelier_call(0.03, 0.03, v);
let expected = v.sqrt() / (2.0 * std::f64::consts::PI).sqrt();
assert!(
(c - expected).abs() < 1e-12,
"ATM call {} vs expected {}",
c,
expected
);
}
#[test]
fn put_call_parity() {
let v = 0.0075_f64.powi(2) * 2.5;
let c = bachelier_call(0.035, 0.03, v);
let p = bachelier_put(0.035, 0.03, v);
let parity = 0.035 - 0.03;
assert!(
(c - p - parity).abs() < 1e-12,
"C-P={} vs {}",
c - p,
parity
);
}
#[test]
fn intrinsic_at_zero_variance() {
assert!((bachelier_call(0.04, 0.03, 0.0) - 0.01).abs() < 1e-15);
assert_eq!(bachelier_call(0.02, 0.03, 0.0), 0.0);
assert!((bachelier_put(0.02, 0.03, 0.0) - 0.01).abs() < 1e-15);
assert_eq!(bachelier_put(0.04, 0.03, 0.0), 0.0);
}
#[test]
fn vega_matches_analytic() {
let v = 0.01_f64.powi(2);
let analytic = bachelier_vega_variance(0.03, 0.03, v);
let expected = (2.0 * std::f64::consts::PI).sqrt().recip() / (2.0 * v.sqrt());
assert!((analytic - expected).abs() < 1e-12);
let eps = 1e-9_f64;
let fd = (bachelier_call(0.03, 0.03, v + eps) - bachelier_call(0.03, 0.03, v - eps))
/ (2.0 * eps);
assert!((analytic - fd).abs() < 1e-5);
}
}