use crate::derivatives::norm::{norm_cdf, norm_pdf};
use crate::derivatives::types::{
forward_moneyness, intrinsic, time_value, validate_bsm_params, BsmParams, OptionType,
ValidatedBsm,
};
use crate::util::error::FinanceResult;
use crate::{columns_with_strings, print_table_locale_opt};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BsmGreeks {
pub delta: f64,
pub gamma: f64,
pub vega: f64,
pub theta: f64,
pub rho: f64,
}
impl BsmGreeks {
#[inline]
pub fn vega_per_vol_point(self) -> f64 {
self.vega / 100.0
}
#[inline]
pub fn theta_per_calendar_day(self) -> f64 {
self.theta / 365.25
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BsmTerms {
pub d1: f64,
pub d2: f64,
pub discount: f64,
pub dividend_discount: f64,
pub sqrt_t: f64,
}
#[derive(Clone, Debug)]
pub struct BsmSolution {
pub option_type: OptionType,
pub params: BsmParams,
pub price: f64,
pub greeks: BsmGreeks,
pub terms: BsmTerms,
pub intrinsic: f64,
pub time_value: f64,
pub forward_moneyness: f64,
pub parity_residual: f64,
formula: String,
symbolic_formula: String,
}
impl BsmSolution {
pub fn formula(&self) -> &str {
&self.formula
}
pub fn symbolic_formula(&self) -> &str {
&self.symbolic_formula
}
pub fn print_table(&self) {
self.print_table_locale_opt(None, None);
}
pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
self.print_table_locale_opt(Some(locale), Some(precision));
}
fn print_table_locale_opt(
&self,
locale: Option<&num_format::Locale>,
precision: Option<usize>,
) {
let columns = columns_with_strings(&[
("type", "s", true),
("price", "f", true),
("delta", "f", true),
("gamma", "f", true),
("vega", "f", true),
("theta", "f", true),
("rho", "f", true),
]);
let data = vec![vec![
self.option_type.to_string(),
self.price.to_string(),
self.greeks.delta.to_string(),
self.greeks.gamma.to_string(),
self.greeks.vega.to_string(),
self.greeks.theta.to_string(),
self.greeks.rho.to_string(),
]];
print_table_locale_opt(&columns, data, locale, precision);
}
}
pub fn bsm_price(params: BsmParams, option_type: OptionType) -> FinanceResult<f64> {
validate_bsm_params(params)?;
Ok(price_unchecked(params, option_type))
}
pub fn bsm_greeks(params: BsmParams, option_type: OptionType) -> FinanceResult<BsmGreeks> {
validate_bsm_params(params)?;
Ok(greeks_unchecked(params, option_type))
}
pub fn bsm_terms(params: BsmParams) -> FinanceResult<BsmTerms> {
validate_bsm_params(params)?;
Ok(terms_unchecked(params))
}
pub fn put_call_parity_residual(params: BsmParams) -> FinanceResult<f64> {
let c = bsm_price(params, OptionType::Call)?;
let p = bsm_price(params, OptionType::Put)?;
let disc = (-params.rate * params.time_years).exp();
let div = (-params.dividend_yield * params.time_years).exp();
Ok(c - p - (params.spot * div - params.strike * disc))
}
pub fn bsm_solution(params: BsmParams, option_type: OptionType) -> FinanceResult<BsmSolution> {
let _ = ValidatedBsm::new(params)?;
let price = price_unchecked(params, option_type);
let greeks = greeks_unchecked(params, option_type);
let terms = terms_unchecked(params);
let intrinsic_v = intrinsic(params.spot, params.strike, option_type)?;
let tv = time_value(price, params.spot, params.strike, option_type)?;
let fm = forward_moneyness(params)?;
let parity = put_call_parity_residual(params)?;
let formula = format!(
"{option_type} BSM S={} K={} T={} r={} q={} σ={} → price={:.6}",
params.spot,
params.strike,
params.time_years,
params.rate,
params.dividend_yield,
params.vol,
price
);
let symbolic = match option_type {
OptionType::Call => {
"C = S e^{-qT} N(d1) - K e^{-rT} N(d2); d1 = [ln(S/K)+(r-q+σ²/2)T]/(σ√T); d2 = d1-σ√T"
.to_string()
}
OptionType::Put => "P = K e^{-rT} N(-d2) - S e^{-qT} N(-d1); d1,d2 as in call".to_string(),
};
Ok(BsmSolution {
option_type,
params,
price,
greeks,
terms,
intrinsic: intrinsic_v,
time_value: tv,
forward_moneyness: fm,
parity_residual: parity,
formula,
symbolic_formula: symbolic,
})
}
fn terms_unchecked(p: BsmParams) -> BsmTerms {
let sqrt_t = p.time_years.sqrt();
let discount = (-p.rate * p.time_years).exp();
let dividend_discount = (-p.dividend_yield * p.time_years).exp();
if p.time_years == 0.0 || p.vol == 0.0 {
let forward = p.spot * ((p.rate - p.dividend_yield) * p.time_years).exp();
let d1 = if forward > p.strike {
f64::INFINITY
} else if forward < p.strike {
f64::NEG_INFINITY
} else {
0.0
};
return BsmTerms {
d1,
d2: d1,
discount,
dividend_discount,
sqrt_t,
};
}
let sig_s = p.vol * sqrt_t;
let d1 = ((p.spot / p.strike).ln()
+ (p.rate - p.dividend_yield + 0.5 * p.vol * p.vol) * p.time_years)
/ sig_s;
let d2 = d1 - sig_s;
BsmTerms {
d1,
d2,
discount,
dividend_discount,
sqrt_t,
}
}
fn price_unchecked(p: BsmParams, option_type: OptionType) -> f64 {
if p.time_years == 0.0 {
return match option_type {
OptionType::Call => (p.spot - p.strike).max(0.0),
OptionType::Put => (p.strike - p.spot).max(0.0),
};
}
if p.vol == 0.0 {
let f = p.spot * ((p.rate - p.dividend_yield) * p.time_years).exp();
let disc = (-p.rate * p.time_years).exp();
return match option_type {
OptionType::Call => disc * (f - p.strike).max(0.0),
OptionType::Put => disc * (p.strike - f).max(0.0),
};
}
let t = terms_unchecked(p);
let df_q = t.dividend_discount;
let df_r = t.discount;
match option_type {
OptionType::Call => p.spot * df_q * norm_cdf(t.d1) - p.strike * df_r * norm_cdf(t.d2),
OptionType::Put => p.strike * df_r * norm_cdf(-t.d2) - p.spot * df_q * norm_cdf(-t.d1),
}
}
fn greeks_unchecked(p: BsmParams, option_type: OptionType) -> BsmGreeks {
if p.time_years == 0.0 {
let delta = match option_type {
OptionType::Call => {
if p.spot > p.strike {
1.0
} else if p.spot < p.strike {
0.0
} else {
0.5
}
}
OptionType::Put => {
if p.spot < p.strike {
-1.0
} else if p.spot > p.strike {
0.0
} else {
-0.5
}
}
};
return BsmGreeks {
delta,
gamma: 0.0,
vega: 0.0,
theta: 0.0,
rho: 0.0,
};
}
if p.vol == 0.0 {
let price_up = {
let mut q = p;
q.spot *= 1.0 + 1e-6;
price_unchecked(q, option_type)
};
let price_0 = price_unchecked(p, option_type);
let delta = (price_up - price_0) / (p.spot * 1e-6);
return BsmGreeks {
delta,
gamma: 0.0,
vega: 0.0,
theta: 0.0,
rho: 0.0,
};
}
let t = terms_unchecked(p);
let df_q = t.dividend_discount;
let df_r = t.discount;
let n_d1 = norm_pdf(t.d1);
let sqrt_t = t.sqrt_t;
let gamma = df_q * n_d1 / (p.spot * p.vol * sqrt_t);
let vega = p.spot * df_q * n_d1 * sqrt_t;
let (delta, theta, rho) = match option_type {
OptionType::Call => {
let delta = df_q * norm_cdf(t.d1);
let theta = -p.spot * df_q * n_d1 * p.vol / (2.0 * sqrt_t)
- p.rate * p.strike * df_r * norm_cdf(t.d2)
+ p.dividend_yield * p.spot * df_q * norm_cdf(t.d1);
let rho = p.strike * p.time_years * df_r * norm_cdf(t.d2);
(delta, theta, rho)
}
OptionType::Put => {
let delta = df_q * (norm_cdf(t.d1) - 1.0);
let theta = -p.spot * df_q * n_d1 * p.vol / (2.0 * sqrt_t)
+ p.rate * p.strike * df_r * norm_cdf(-t.d2)
- p.dividend_yield * p.spot * df_q * norm_cdf(-t.d1);
let rho = -p.strike * p.time_years * df_r * norm_cdf(-t.d2);
(delta, theta, rho)
}
};
BsmGreeks {
delta,
gamma,
vega,
theta,
rho,
}
}
pub(crate) fn price_raw(params: BsmParams, option_type: OptionType) -> FinanceResult<f64> {
bsm_price(params, option_type)
}
pub(crate) fn vega_raw(params: BsmParams, option_type: OptionType) -> FinanceResult<f64> {
Ok(bsm_greeks(params, option_type)?.vega)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn atm_call_textbook() {
let p = BsmParams::atm_one_year(100.0, 0.05, 0.20);
let c = bsm_price(p, OptionType::Call).unwrap();
assert!((c - 10.450_583_57).abs() < 1e-4);
}
#[test]
fn put_call_parity() {
let p = BsmParams {
spot: 100.0,
strike: 95.0,
time_years: 0.5,
rate: 0.03,
dividend_yield: 0.01,
vol: 0.25,
};
assert!(put_call_parity_residual(p).unwrap().abs() < 1e-10);
}
#[test]
fn expiry_intrinsic() {
let p = BsmParams {
spot: 110.0,
strike: 100.0,
time_years: 0.0,
rate: 0.05,
dividend_yield: 0.0,
vol: 0.2,
};
assert!((bsm_price(p, OptionType::Call).unwrap() - 10.0).abs() < 1e-12);
assert!((bsm_price(p, OptionType::Put).unwrap()).abs() < 1e-12);
}
#[test]
fn delta_bounds_call() {
let p = BsmParams::atm_one_year(100.0, 0.05, 0.2);
let d = bsm_greeks(p, OptionType::Call).unwrap().delta;
assert!(d > 0.0 && d < 1.0);
}
#[test]
fn vega_scale_helpers() {
let g = bsm_greeks(BsmParams::atm_one_year(100.0, 0.05, 0.2), OptionType::Call).unwrap();
assert!((g.vega_per_vol_point() * 100.0 - g.vega).abs() < 1e-12);
assert!((g.theta_per_calendar_day() * 365.25 - g.theta).abs() < 1e-12);
}
#[test]
fn rejects_bad_spot() {
let mut p = BsmParams::atm_one_year(100.0, 0.05, 0.2);
p.spot = 0.0;
assert!(bsm_price(p, OptionType::Call).is_err());
}
}