use crate::errors::PricingError;
use crate::math::ncdf;
use crate::types::OptionType;
#[derive(Debug, Clone, Copy)]
pub struct Black76Params {
pub option_type: OptionType,
pub forward: f64,
pub strike: f64,
pub rate: f64,
pub vol: f64,
pub time: f64,
}
#[inline]
#[allow(clippy::many_single_char_names)]
#[allow(clippy::similar_names)]
pub fn price(params: &Black76Params) -> Result<f64, PricingError> {
if params.forward < 0.0_f64 {
return Err(PricingError::NegativeSpot);
}
if params.strike < 0.0_f64 {
return Err(PricingError::NegativeStrike);
}
if params.time < 0.0_f64 {
return Err(PricingError::NegativeTime);
}
if params.vol < 0.0_f64 {
return Err(PricingError::NegativeVolatility);
}
let f = params.forward;
let k = params.strike;
let r = params.rate;
let sigma = params.vol;
let t = params.time;
if t == 0.0_f64 {
let intrinsic = match params.option_type {
OptionType::Call => {
if f > k {
f - k
} else {
0.0_f64
}
}
OptionType::Put => {
if k > f {
k - f
} else {
0.0_f64
}
}
};
return Err(PricingError::IntrinsicOnly { intrinsic });
}
let sqrt_t = t.sqrt();
let sigma_sqrt_t = sigma * sqrt_t;
let discount = (-r * t).exp();
if sigma_sqrt_t == 0.0_f64 {
let intrinsic = match params.option_type {
OptionType::Call => {
if f > k {
f - k
} else {
0.0_f64
}
}
OptionType::Put => {
if k > f {
k - f
} else {
0.0_f64
}
}
};
return Ok(discount * intrinsic);
}
let d1 = ((f / k).ln() + 0.5_f64 * sigma * sigma * t) / sigma_sqrt_t;
let d2 = d1 - sigma_sqrt_t;
let nd1 = ncdf(d1);
let nd2 = ncdf(d2);
let nnd1 = 1.0_f64 - nd1; let nnd2 = 1.0_f64 - nd2;
let price_val = match params.option_type {
OptionType::Call => discount * f.mul_add(nd1, -k * nd2),
OptionType::Put => discount * k.mul_add(nnd2, -f * nnd1),
};
Ok(price_val)
}
#[cfg(test)]
mod tests {
use super::*;
const LOOSE: f64 = 1e-4_f64;
#[test]
fn test_call_atm_matches_golden_value() {
let params = Black76Params {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let p = price(¶ms).unwrap();
assert!((p - 7.5771_f64).abs() < LOOSE, "call ATM: got {p}");
}
#[test]
fn test_put_atm_matches_golden_value() {
let params = Black76Params {
option_type: OptionType::Put,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let p = price(¶ms).unwrap();
assert!((p - 7.5771_f64).abs() < LOOSE, "put ATM: got {p}");
}
#[test]
fn test_call_otm_matches_golden_value() {
let params = Black76Params {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 105.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let p = price(¶ms).unwrap();
assert!((p - 5.6176_f64).abs() < LOOSE, "call OTM: got {p}");
}
#[test]
fn test_put_otm_matches_golden_value() {
let params = Black76Params {
option_type: OptionType::Put,
forward: 100.0_f64,
strike: 105.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let p = price(¶ms).unwrap();
assert!((p - 10.3737_f64).abs() < LOOSE, "put OTM: got {p}");
}
#[test]
fn test_call_itm_matches_golden_value() {
let params = Black76Params {
option_type: OptionType::Call,
forward: 120.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let p = price(¶ms).unwrap();
assert!((p - 21.0672_f64).abs() < LOOSE, "call ITM: got {p}");
}
#[test]
fn test_put_itm_matches_golden_value() {
let params = Black76Params {
option_type: OptionType::Put,
forward: 120.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let p = price(¶ms).unwrap();
assert!((p - 2.0426_f64).abs() < LOOSE, "put ITM: got {p}");
}
#[test]
fn test_put_call_parity_atm() {
let call_params = Black76Params {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let put_params = Black76Params {
option_type: OptionType::Put,
..call_params
};
let c = price(&call_params).unwrap();
let p = price(&put_params).unwrap();
assert!((c - p).abs() < 1e-10_f64, "parity ATM: C-P={}", c - p);
}
#[test]
fn test_put_call_parity_otm() {
let call_params = Black76Params {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 105.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let put_params = Black76Params {
option_type: OptionType::Put,
..call_params
};
let c = price(&call_params).unwrap();
let p = price(&put_params).unwrap();
let parity = (-0.05_f64 * 1.0_f64).exp() * (100.0_f64 - 105.0_f64);
assert!(
(c - p - parity).abs() < 1e-10_f64,
"parity: C-P={}, expected={parity}",
c - p
);
}
#[test]
fn test_put_call_parity_itm() {
let call_params = Black76Params {
option_type: OptionType::Call,
forward: 120.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let put_params = Black76Params {
option_type: OptionType::Put,
..call_params
};
let c = price(&call_params).unwrap();
let p = price(&put_params).unwrap();
let parity = (-0.05_f64 * 1.0_f64).exp() * (120.0_f64 - 100.0_f64);
assert!(
(c - p - parity).abs() < 1e-10_f64,
"parity: C-P={}, expected={parity}",
c - p
);
}
#[test]
fn test_negative_forward_returns_error() {
let params = Black76Params {
option_type: OptionType::Call,
forward: -1.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
assert_eq!(price(¶ms), Err(PricingError::NegativeSpot));
}
#[test]
fn test_negative_strike_returns_error() {
let params = Black76Params {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: -1.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
assert_eq!(price(¶ms), Err(PricingError::NegativeStrike));
}
#[test]
fn test_negative_time_returns_error() {
let params = Black76Params {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: -1.0_f64,
};
assert_eq!(price(¶ms), Err(PricingError::NegativeTime));
}
#[test]
fn test_negative_vol_returns_error() {
let params = Black76Params {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: -0.20_f64,
time: 1.0_f64,
};
assert_eq!(price(¶ms), Err(PricingError::NegativeVolatility));
}
#[test]
fn test_t_zero_returns_intrinsic_call_itm() {
let params = Black76Params {
option_type: OptionType::Call,
forward: 110.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 0.0_f64,
};
match price(¶ms) {
Err(PricingError::IntrinsicOnly { intrinsic }) => {
assert!((intrinsic - 10.0_f64).abs() < 1e-10_f64);
}
other => panic!("expected IntrinsicOnly, got {other:?}"),
}
}
#[test]
fn test_t_zero_returns_intrinsic_put_otm() {
let params = Black76Params {
option_type: OptionType::Put,
forward: 110.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 0.0_f64,
};
match price(¶ms) {
Err(PricingError::IntrinsicOnly { intrinsic }) => {
assert!((intrinsic - 0.0_f64).abs() < 1e-10_f64);
}
other => panic!("expected IntrinsicOnly, got {other:?}"),
}
}
#[test]
fn test_zero_vol_returns_discounted_intrinsic() {
let params = Black76Params {
option_type: OptionType::Call,
forward: 110.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.0_f64,
time: 1.0_f64,
};
let p = price(¶ms).unwrap();
let expected = (-0.05_f64).exp() * 10.0_f64;
assert!(
(p - expected).abs() < 1e-10_f64,
"zero vol: got {p}, expected {expected}"
);
}
#[test]
fn test_atm_call_equals_put() {
let call_params = Black76Params {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let put_params = Black76Params {
option_type: OptionType::Put,
..call_params
};
let c = price(&call_params).unwrap();
let p = price(&put_params).unwrap();
assert!((c - p).abs() < 1e-10_f64, "ATM symmetry: C={c}, P={p}");
}
#[test]
fn test_negative_rate_no_panic() {
let params = Black76Params {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: -0.02_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let p = price(¶ms).unwrap();
assert!(p.is_finite(), "negative rate produced non-finite: {p}");
assert!(p > 0.0_f64);
}
}