use super::types::{IVParams, OptionType};
use std::f64::consts::PI;
const SQRT_2: f64 = std::f64::consts::SQRT_2;
pub struct BlackScholes;
impl BlackScholes {
#[must_use]
pub fn erf(x: f64) -> f64 {
const A1: f64 = 0.254829592;
const A2: f64 = -0.284496736;
const A3: f64 = 1.421413741;
const A4: f64 = -1.453152027;
const A5: f64 = 1.061405429;
const P: f64 = 0.3275911;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x = x.abs();
let t = 1.0 / (1.0 + P * x);
let y = 1.0 - (((((A5 * t + A4) * t) + A3) * t + A2) * t + A1) * t * (-x * x).exp();
sign * y
}
#[must_use]
pub fn norm_cdf(x: f64) -> f64 {
0.5 * (1.0 + Self::erf(x / SQRT_2))
}
#[must_use]
pub fn norm_pdf(x: f64) -> f64 {
(-0.5 * x * x).exp() / (2.0 * PI).sqrt()
}
#[must_use]
pub fn d1(spot: f64, strike: f64, rate: f64, time: f64, vol: f64) -> f64 {
let sqrt_time = time.sqrt();
((spot / strike).ln() + (rate + 0.5 * vol * vol) * time) / (vol * sqrt_time)
}
#[must_use]
pub fn d2(d1: f64, vol: f64, time: f64) -> f64 {
d1 - vol * time.sqrt()
}
#[must_use]
pub fn price(params: &IVParams, vol: f64) -> f64 {
if params.time_to_expiry <= 0.0 {
return params.intrinsic_value();
}
if vol <= 0.0 {
let discount = (-params.risk_free_rate * params.time_to_expiry).exp();
return match params.option_type {
OptionType::Call => (params.spot - params.strike * discount).max(0.0),
OptionType::Put => (params.strike * discount - params.spot).max(0.0),
};
}
let d1 = Self::d1(
params.spot,
params.strike,
params.risk_free_rate,
params.time_to_expiry,
vol,
);
let d2 = Self::d2(d1, vol, params.time_to_expiry);
let discount = (-params.risk_free_rate * params.time_to_expiry).exp();
match params.option_type {
OptionType::Call => {
params.spot * Self::norm_cdf(d1) - params.strike * discount * Self::norm_cdf(d2)
}
OptionType::Put => {
params.strike * discount * Self::norm_cdf(-d2) - params.spot * Self::norm_cdf(-d1)
}
}
}
#[must_use]
pub fn vega(params: &IVParams, vol: f64) -> f64 {
if params.time_to_expiry <= 0.0 || vol <= 0.0 {
return 0.0;
}
let d1 = Self::d1(
params.spot,
params.strike,
params.risk_free_rate,
params.time_to_expiry,
vol,
);
params.spot * Self::norm_pdf(d1) * params.time_to_expiry.sqrt()
}
#[must_use]
pub fn delta(params: &IVParams, vol: f64) -> f64 {
if params.time_to_expiry <= 0.0 {
return match params.option_type {
OptionType::Call => {
if params.spot > params.strike {
1.0
} else {
0.0
}
}
OptionType::Put => {
if params.spot < params.strike {
-1.0
} else {
0.0
}
}
};
}
let d1 = Self::d1(
params.spot,
params.strike,
params.risk_free_rate,
params.time_to_expiry,
vol,
);
match params.option_type {
OptionType::Call => Self::norm_cdf(d1),
OptionType::Put => Self::norm_cdf(d1) - 1.0,
}
}
#[must_use]
pub fn gamma(params: &IVParams, vol: f64) -> f64 {
if params.time_to_expiry <= 0.0 || vol <= 0.0 {
return 0.0;
}
let d1 = Self::d1(
params.spot,
params.strike,
params.risk_free_rate,
params.time_to_expiry,
vol,
);
Self::norm_pdf(d1) / (params.spot * vol * params.time_to_expiry.sqrt())
}
#[must_use]
pub fn theta(params: &IVParams, vol: f64) -> f64 {
if params.time_to_expiry <= 0.0 || vol <= 0.0 {
return 0.0;
}
let d1 = Self::d1(
params.spot,
params.strike,
params.risk_free_rate,
params.time_to_expiry,
vol,
);
let d2 = Self::d2(d1, vol, params.time_to_expiry);
let discount = (-params.risk_free_rate * params.time_to_expiry).exp();
let sqrt_time = params.time_to_expiry.sqrt();
let term1 = -params.spot * Self::norm_pdf(d1) * vol / (2.0 * sqrt_time);
let theta_annual = match params.option_type {
OptionType::Call => {
term1 - params.risk_free_rate * params.strike * discount * Self::norm_cdf(d2)
}
OptionType::Put => {
term1 + params.risk_free_rate * params.strike * discount * Self::norm_cdf(-d2)
}
};
theta_annual / 365.0
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOLERANCE: f64 = 1e-6;
#[test]
fn test_erf() {
assert!((BlackScholes::erf(0.0) - 0.0).abs() < TOLERANCE);
assert!((BlackScholes::erf(1.0) - 0.8427007929).abs() < 1e-5);
assert!((BlackScholes::erf(-1.0) + 0.8427007929).abs() < 1e-5);
}
#[test]
fn test_norm_cdf() {
assert!((BlackScholes::norm_cdf(0.0) - 0.5).abs() < TOLERANCE);
assert!(BlackScholes::norm_cdf(-10.0) < 1e-10);
assert!(BlackScholes::norm_cdf(10.0) > 1.0 - 1e-10);
}
#[test]
fn test_norm_pdf() {
assert!((BlackScholes::norm_pdf(0.0) - 0.3989422804).abs() < TOLERANCE);
assert!((BlackScholes::norm_pdf(1.0) - BlackScholes::norm_pdf(-1.0)).abs() < TOLERANCE);
}
#[test]
fn test_call_price_atm() {
let params = IVParams::call(100.0, 100.0, 1.0, 0.0);
let price = BlackScholes::price(¶ms, 0.25);
assert!(price > 9.0 && price < 11.0);
}
#[test]
fn test_put_price_atm() {
let params = IVParams::put(100.0, 100.0, 1.0, 0.0);
let price = BlackScholes::price(¶ms, 0.25);
let call_params = IVParams::call(100.0, 100.0, 1.0, 0.0);
let call_price = BlackScholes::price(&call_params, 0.25);
assert!((price - call_price).abs() < TOLERANCE);
}
#[test]
fn test_put_call_parity() {
let spot = 100.0;
let strike = 105.0;
let time = 0.5;
let rate = 0.05;
let vol = 0.3;
let call_params = IVParams::call(spot, strike, time, rate);
let put_params = IVParams::put(spot, strike, time, rate);
let call_price = BlackScholes::price(&call_params, vol);
let put_price = BlackScholes::price(&put_params, vol);
let expected_diff = spot - strike * (-rate * time).exp();
assert!((call_price - put_price - expected_diff).abs() < TOLERANCE);
}
#[test]
fn test_vega_positive() {
let params = IVParams::call(100.0, 100.0, 0.25, 0.05);
let vega = BlackScholes::vega(¶ms, 0.25);
assert!(vega > 0.0);
let put_params = IVParams::put(100.0, 100.0, 0.25, 0.05);
let put_vega = BlackScholes::vega(&put_params, 0.25);
assert!(put_vega > 0.0);
assert!((vega - put_vega).abs() < TOLERANCE);
}
#[test]
fn test_delta_bounds() {
let call_params = IVParams::call(100.0, 100.0, 0.25, 0.05);
let call_delta = BlackScholes::delta(&call_params, 0.25);
assert!(call_delta > 0.0 && call_delta < 1.0);
let put_params = IVParams::put(100.0, 100.0, 0.25, 0.05);
let put_delta = BlackScholes::delta(&put_params, 0.25);
assert!(put_delta > -1.0 && put_delta < 0.0);
assert!((call_delta - put_delta - 1.0).abs() < TOLERANCE);
}
#[test]
fn test_gamma_positive() {
let params = IVParams::call(100.0, 100.0, 0.25, 0.05);
let gamma = BlackScholes::gamma(¶ms, 0.25);
assert!(gamma > 0.0);
}
#[test]
fn test_theta_negative_for_long() {
let params = IVParams::call(100.0, 100.0, 0.25, 0.0);
let theta = BlackScholes::theta(¶ms, 0.25);
assert!(theta < 0.0);
}
#[test]
fn test_price_at_expiry() {
let itm_call = IVParams::call(110.0, 100.0, 0.0, 0.05);
let price = BlackScholes::price(&itm_call, 0.25);
assert!((price - 10.0).abs() < TOLERANCE);
let otm_call = IVParams::call(90.0, 100.0, 0.0, 0.05);
let price = BlackScholes::price(&otm_call, 0.25);
assert!(price.abs() < TOLERANCE);
}
#[test]
fn test_deep_itm_call() {
let params = IVParams::call(150.0, 100.0, 0.25, 0.0);
let price = BlackScholes::price(¶ms, 0.25);
assert!(price > 50.0);
}
#[test]
fn test_deep_otm_call() {
let params = IVParams::call(50.0, 100.0, 0.25, 0.0);
let price = BlackScholes::price(¶ms, 0.25);
assert!(price < 0.01);
}
}