use std::f64::consts::PI;
use std::fmt;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "snake_case")
)]
pub enum OptionType {
Call,
Put,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(rename_all = "snake_case")
)]
pub enum OptionStyle {
#[default]
European,
American,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BlackScholesInputs {
pub spot: f64,
pub strike: f64,
pub time_to_expiry_years: f64,
pub risk_free_rate: f64,
pub dividend_yield: f64,
pub volatility: f64,
}
impl BlackScholesInputs {
pub fn validate(&self) -> Result<(), OptionError> {
if !self.spot.is_finite() || self.spot <= 0.0 {
return Err(OptionError::InvalidInput(
"spot price must be positive and finite",
));
}
if !self.strike.is_finite() || self.strike <= 0.0 {
return Err(OptionError::InvalidInput(
"strike price must be positive and finite",
));
}
if !self.time_to_expiry_years.is_finite() || self.time_to_expiry_years < 0.0 {
return Err(OptionError::InvalidInput(
"time to expiry must be non-negative and finite",
));
}
if !self.risk_free_rate.is_finite() {
return Err(OptionError::InvalidInput("risk-free rate must be finite"));
}
if !self.dividend_yield.is_finite() {
return Err(OptionError::InvalidInput("dividend yield must be finite"));
}
if !self.volatility.is_finite() || self.volatility < 0.0 {
return Err(OptionError::InvalidInput(
"volatility must be non-negative and finite",
));
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OptionGreeks {
pub delta: f64,
pub gamma: f64,
pub vega: f64,
pub theta_annual: f64,
pub theta_daily: f64,
pub rho: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OptionPricingResult {
pub price: f64,
pub intrinsic_value: f64,
pub time_value: f64,
pub greeks: OptionGreeks,
}
#[derive(Debug, Clone, PartialEq)]
pub enum OptionError {
InvalidInput(&'static str),
PriceBelowIntrinsic,
PriceAboveBoundary,
SolverMaxIterationsExceeded,
SolverFailedToConverge,
}
impl fmt::Display for OptionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidInput(msg) => write!(f, "invalid option input: {msg}"),
Self::PriceBelowIntrinsic => write!(
f,
"option price violates lower arbitrage bound (below intrinsic value)"
),
Self::PriceAboveBoundary => write!(f, "option price violates upper arbitrage bound"),
Self::SolverMaxIterationsExceeded => {
write!(f, "implied volatility solver exceeded maximum iterations")
}
Self::SolverFailedToConverge => {
write!(f, "implied volatility solver failed to converge")
}
}
}
}
impl std::error::Error for OptionError {}
fn horner(x: f64, lead: f64, rest: &[f64]) -> f64 {
rest.iter()
.fold(lead, |acc, coefficient| acc * x + coefficient)
}
pub fn normal_cdf(x: f64) -> f64 {
let abs_x = x.abs();
if abs_x > 37.0 {
return if x > 0.0 { 1.0 } else { 0.0 };
}
let exponential = (-0.5 * abs_x * abs_x).exp();
let upper_tail = if abs_x < 7.071_067_811_865_475 {
let numerator = horner(
abs_x,
3.526_249_659_989_109e-2,
&[
0.700_383_064_443_688,
6.373_962_203_531_65,
33.912_866_078_383,
112.079_291_497_871,
221.213_596_169_931,
220.206_867_912_376,
],
);
let denominator = horner(
abs_x,
8.838_834_764_831_844e-2,
&[
1.755_667_163_182_64,
16.064_177_579_207,
86.780_732_202_946_1,
296.564_248_779_674,
637.333_633_378_831,
793.826_512_519_948,
440.413_735_824_752,
],
);
exponential * numerator / denominator
} else {
let mut fraction = abs_x + 0.65;
for term in [4.0, 3.0, 2.0, 1.0] {
fraction = abs_x + term / fraction;
}
exponential / (fraction * 2.506_628_274_631_000_5)
};
if x > 0.0 {
1.0 - upper_tail
} else {
upper_tail
}
}
pub fn normal_pdf(x: f64) -> f64 {
(1.0 / (2.0 * PI).sqrt()) * (-0.5 * x * x).exp()
}
pub fn black_scholes_merton(
option_type: OptionType,
inputs: &BlackScholesInputs,
) -> Result<OptionPricingResult, OptionError> {
inputs.validate()?;
let s = inputs.spot;
let k = inputs.strike;
let t = inputs.time_to_expiry_years;
let r = inputs.risk_free_rate;
let q = inputs.dividend_yield;
let sigma = inputs.volatility;
let intrinsic = match option_type {
OptionType::Call => (s - k).max(0.0),
OptionType::Put => (k - s).max(0.0),
};
if t <= 1e-12 || sigma <= 1e-12 {
let delta = match option_type {
OptionType::Call => {
if s > k {
1.0
} else if (s - k).abs() < 1e-12 {
0.5
} else {
0.0
}
}
OptionType::Put => {
if s < k {
-1.0
} else if (s - k).abs() < 1e-12 {
-0.5
} else {
0.0
}
}
};
return Ok(OptionPricingResult {
price: intrinsic,
intrinsic_value: intrinsic,
time_value: 0.0,
greeks: OptionGreeks {
delta,
gamma: 0.0,
vega: 0.0,
theta_annual: 0.0,
theta_daily: 0.0,
rho: 0.0,
},
});
}
let sqrt_t = t.sqrt();
let d1 = ((s / k).ln() + (r - q + 0.5 * sigma * sigma) * t) / (sigma * sqrt_t);
let d2 = d1 - sigma * sqrt_t;
let df_q = (-q * t).exp();
let df_r = (-r * t).exp();
let pdf_d1 = normal_pdf(d1);
let price = match option_type {
OptionType::Call => s * df_q * normal_cdf(d1) - k * df_r * normal_cdf(d2),
OptionType::Put => k * df_r * normal_cdf(-d2) - s * df_q * normal_cdf(-d1),
};
let delta = match option_type {
OptionType::Call => df_q * normal_cdf(d1),
OptionType::Put => df_q * (normal_cdf(d1) - 1.0),
};
let gamma = (df_q * pdf_d1) / (s * sigma * sqrt_t);
let vega = s * df_q * sqrt_t * pdf_d1;
let theta_common = -(s * df_q * pdf_d1 * sigma) / (2.0 * sqrt_t);
let theta_annual = match option_type {
OptionType::Call => {
theta_common - r * k * df_r * normal_cdf(d2) + q * s * df_q * normal_cdf(d1)
}
OptionType::Put => {
theta_common + r * k * df_r * normal_cdf(-d2) - q * s * df_q * normal_cdf(-d1)
}
};
let theta_daily = theta_annual / 365.0;
let rho = match option_type {
OptionType::Call => k * t * df_r * normal_cdf(d2),
OptionType::Put => -k * t * df_r * normal_cdf(-d2),
};
let time_value = (price - intrinsic).max(0.0);
Ok(OptionPricingResult {
price,
intrinsic_value: intrinsic,
time_value,
greeks: OptionGreeks {
delta,
gamma,
vega,
theta_annual,
theta_daily,
rho,
},
})
}
pub fn black_76(
option_type: OptionType,
forward: f64,
strike: f64,
time_to_expiry_years: f64,
risk_free_rate: f64,
volatility: f64,
) -> Result<OptionPricingResult, OptionError> {
if forward <= 0.0 {
return Err(OptionError::InvalidInput(
"forward must be positive and finite",
));
}
let inputs = BlackScholesInputs {
spot: forward,
strike,
time_to_expiry_years,
risk_free_rate,
dividend_yield: risk_free_rate,
volatility,
};
black_scholes_merton(option_type, &inputs)
}
pub fn implied_volatility(
option_type: OptionType,
market_price: f64,
spot: f64,
strike: f64,
time_to_expiry_years: f64,
risk_free_rate: f64,
dividend_yield: f64,
) -> Result<f64, OptionError> {
if !market_price.is_finite() || market_price <= 0.0 {
return Err(OptionError::InvalidInput(
"market price must be positive and finite",
));
}
if time_to_expiry_years <= 1e-12 {
return Err(OptionError::InvalidInput(
"cannot solve IV for expired option (T=0)",
));
}
let df_q = (-dividend_yield * time_to_expiry_years).exp();
let df_r = (-risk_free_rate * time_to_expiry_years).exp();
let (lower_bound, upper_bound) = match option_type {
OptionType::Call => ((spot * df_q - strike * df_r).max(0.0), spot * df_q),
OptionType::Put => ((strike * df_r - spot * df_q).max(0.0), strike * df_r),
};
if market_price < lower_bound - 1e-7 {
return Err(OptionError::PriceBelowIntrinsic);
}
if market_price > upper_bound + 1e-7 {
return Err(OptionError::PriceAboveBoundary);
}
let mut inputs = BlackScholesInputs {
spot,
strike,
time_to_expiry_years,
risk_free_rate,
dividend_yield,
volatility: 0.20,
};
let mut vol_low = 1e-4f64;
let mut vol_high = 5.0f64;
inputs.volatility = vol_low;
let price_low = black_scholes_merton(option_type, &inputs)?.price;
if (price_low - market_price).abs() < 1e-7 {
return Ok(vol_low);
}
inputs.volatility = vol_high;
let mut price_high = black_scholes_merton(option_type, &inputs)?.price;
while price_high < market_price && vol_high < 20.0 {
vol_high *= 2.0;
inputs.volatility = vol_high;
price_high = black_scholes_merton(option_type, &inputs)?.price;
}
if price_high < market_price {
return Err(OptionError::PriceAboveBoundary);
}
let mut current_vol = 0.5 * (vol_low + vol_high);
let max_iter = 100;
let tol = 1e-8;
for _ in 0..max_iter {
inputs.volatility = current_vol;
let res = black_scholes_merton(option_type, &inputs)?;
let diff = res.price - market_price;
if diff.abs() < tol {
return Ok(current_vol);
}
if diff > 0.0 {
vol_high = current_vol;
} else {
vol_low = current_vol;
}
let vega = res.greeks.vega;
let mut step_accepted = false;
if vega > 1e-12 {
let next_newton = current_vol - diff / vega;
if next_newton > vol_low && next_newton < vol_high {
current_vol = next_newton;
step_accepted = true;
}
}
if !step_accepted {
current_vol = 0.5 * (vol_low + vol_high);
}
if (vol_high - vol_low) < 1e-10 {
return Ok(current_vol);
}
}
Ok(current_vol)
}
pub fn verify_put_call_parity(
call_price: f64,
put_price: f64,
spot: f64,
strike: f64,
time_to_expiry_years: f64,
risk_free_rate: f64,
dividend_yield: f64,
) -> f64 {
let forward_term = spot * (-dividend_yield * time_to_expiry_years).exp();
let discount_strike = strike * (-risk_free_rate * time_to_expiry_years).exp();
(call_price - put_price) - (forward_term - discount_strike)
}