use serde::{Deserialize, Serialize};
fn norm_pdf(x: f64) -> f64 {
use std::f64::consts::PI;
(-0.5 * x * x).exp() / (2.0 * PI).sqrt()
}
fn erf(x: f64) -> f64 {
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x = x.abs();
let t = 1.0 / (1.0 + 0.327_591_1 * x);
let y = 1.0
- (((((1.061_405_429 * t - 1.453_152_027) * t) + 1.421_413_741) * t - 0.284_496_736) * t
+ 0.254_829_592)
* t
* (-x * x).exp();
sign * y
}
fn norm_cdf(x: f64) -> f64 {
use std::f64::consts::SQRT_2;
0.5 * (1.0 + erf(x / SQRT_2))
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Greeks {
pub delta: f64,
pub gamma: f64,
pub theta: f64,
pub vega: f64,
pub rho: f64,
}
fn d1_d2(spot: f64, strike: f64, t: f64, r: f64, vol: f64) -> (f64, f64) {
let sqrt_t = t.sqrt();
let d1 = ((spot / strike).ln() + (r + 0.5 * vol * vol) * t) / (vol * sqrt_t);
(d1, d1 - vol * sqrt_t)
}
pub fn bs_price(spot: f64, strike: f64, t: f64, r: f64, vol: f64, is_call: bool) -> f64 {
if t <= 0.0 || vol <= 0.0 {
let intrinsic = if is_call {
spot - strike
} else {
strike - spot
};
return intrinsic.max(0.0);
}
let (d1, d2) = d1_d2(spot, strike, t, r, vol);
let disc = (-r * t).exp();
if is_call {
spot * norm_cdf(d1) - strike * disc * norm_cdf(d2)
} else {
strike * disc * norm_cdf(-d2) - spot * norm_cdf(-d1)
}
}
pub fn bs_greeks(spot: f64, strike: f64, t: f64, r: f64, vol: f64, is_call: bool) -> Greeks {
if t <= 0.0 || vol <= 0.0 {
let delta = if is_call {
f64::from(spot > strike)
} else {
-f64::from(spot < strike)
};
return Greeks {
delta,
..Greeks::default()
};
}
let (d1, d2) = d1_d2(spot, strike, t, r, vol);
let sqrt_t = t.sqrt();
let disc = (-r * t).exp();
let pdf_d1 = norm_pdf(d1);
let delta = if is_call {
norm_cdf(d1)
} else {
norm_cdf(d1) - 1.0
};
let gamma = pdf_d1 / (spot * vol * sqrt_t);
let vega = spot * pdf_d1 * sqrt_t;
let theta = if is_call {
-(spot * pdf_d1 * vol) / (2.0 * sqrt_t) - r * strike * disc * norm_cdf(d2)
} else {
-(spot * pdf_d1 * vol) / (2.0 * sqrt_t) + r * strike * disc * norm_cdf(-d2)
};
let rho = if is_call {
strike * t * disc * norm_cdf(d2)
} else {
-strike * t * disc * norm_cdf(-d2)
};
Greeks {
delta,
gamma,
theta,
vega,
rho,
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Leg {
pub strike: f64,
pub t_years: f64,
pub is_call: bool,
pub qty: f64,
}
pub fn portfolio_greeks(legs: &[Leg], spot: f64, r: f64, vol: f64) -> Greeks {
let mut g = Greeks::default();
for leg in legs {
let lg = bs_greeks(spot, leg.strike, leg.t_years, r, vol, leg.is_call);
g.delta += leg.qty * lg.delta;
g.gamma += leg.qty * lg.gamma;
g.theta += leg.qty * lg.theta;
g.vega += leg.qty * lg.vega;
g.rho += leg.qty * lg.rho;
}
g
}
pub fn portfolio_payoff_at_expiry(legs: &[Leg], spot: f64) -> f64 {
legs.iter()
.map(|leg| {
let intrinsic = if leg.is_call {
(spot - leg.strike).max(0.0)
} else {
(leg.strike - spot).max(0.0)
};
leg.qty * intrinsic
})
.sum()
}
pub fn payoff_breakevens(legs: &[Leg], net_premium: f64, spots: &[f64]) -> Vec<f64> {
let mut out = Vec::new();
let f = |s: f64| portfolio_payoff_at_expiry(legs, s) - net_premium;
for pair in spots.windows(2) {
let (a, b) = (pair[0], pair[1]);
let (fa, fb) = (f(a), f(b));
if fa == 0.0 {
out.push(a);
} else if fa * fb < 0.0 {
out.push(0.5 * (a + b));
}
}
if let Some(&last) = spots.last() {
if f(last) == 0.0 {
out.push(last);
}
}
out
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct GreeksPolicy {
pub gamma_floor: f64,
pub vega_floor: f64,
}
impl Default for GreeksPolicy {
fn default() -> Self {
GreeksPolicy {
gamma_floor: 0.0,
vega_floor: 0.0,
}
}
}
#[derive(Clone, Copy, Debug, Serialize, PartialEq)]
pub struct GreeksRisk {
pub naked_short_gamma: bool,
pub unbounded_tail: bool,
pub short_vega: bool,
pub net_gamma: f64,
pub net_vega: f64,
}
pub fn classify_greeks_risk(greeks: &Greeks, policy: &GreeksPolicy) -> GreeksRisk {
let naked_short_gamma = greeks.gamma < policy.gamma_floor;
GreeksRisk {
naked_short_gamma,
unbounded_tail: naked_short_gamma,
short_vega: greeks.vega < policy.vega_floor,
net_gamma: greeks.gamma,
net_vega: greeks.vega,
}
}
#[cfg(test)]
mod tests {
use super::*;
const S: f64 = 100.0;
const K: f64 = 100.0;
const T: f64 = 1.0;
const R: f64 = 0.05;
const VOL: f64 = 0.2;
#[test]
fn atm_call_matches_textbook_value() {
let c = bs_price(S, K, T, R, VOL, true);
assert!((c - 10.4506).abs() < 1e-2, "call={c}");
}
#[test]
fn put_call_parity_holds() {
let c = bs_price(S, K, T, R, VOL, true);
let p = bs_price(S, K, T, R, VOL, false);
let rhs = S - K * (-R * T).exp();
assert!((c - p - rhs).abs() < 1e-6, "parity off: {}", c - p - rhs);
}
#[test]
fn deep_itm_call_delta_approaches_one() {
let g = bs_greeks(200.0, K, T, R, VOL, true);
assert!(g.delta > 0.99, "delta={}", g.delta);
}
#[test]
fn gamma_is_non_negative_and_vega_positive_for_a_long_option() {
let g = bs_greeks(S, K, T, R, VOL, true);
assert!(g.gamma >= 0.0);
assert!(g.vega > 0.0);
assert!(g.theta < 0.0);
}
#[test]
fn short_call_is_naked_short_gamma_and_unbounded_tail() {
let legs = [Leg {
strike: K,
t_years: T,
is_call: true,
qty: -1.0,
}];
let g = portfolio_greeks(&legs, S, R, VOL);
assert!(g.gamma < 0.0, "short call must be net-short gamma");
let risk = classify_greeks_risk(&g, &GreeksPolicy::default());
assert!(risk.naked_short_gamma);
assert!(risk.unbounded_tail);
assert!(risk.short_vega);
}
#[test]
fn a_long_option_is_not_flagged() {
let legs = [Leg {
strike: K,
t_years: T,
is_call: true,
qty: 1.0,
}];
let g = portfolio_greeks(&legs, S, R, VOL);
let risk = classify_greeks_risk(&g, &GreeksPolicy::default());
assert!(!risk.naked_short_gamma);
assert!(!risk.unbounded_tail);
assert!(!risk.short_vega);
}
#[test]
fn long_call_payoff_and_breakeven() {
let legs = [Leg {
strike: 100.0,
t_years: 0.0,
is_call: true,
qty: 1.0,
}];
assert!((portfolio_payoff_at_expiry(&legs, 120.0) - 20.0).abs() < 1e-12);
let grid: Vec<f64> = (90..=120).map(f64::from).collect();
let bes = payoff_breakevens(&legs, 5.0, &grid);
assert!(bes.iter().any(|b| (b - 105.0).abs() <= 1.0), "bes={bes:?}");
}
#[test]
fn degenerate_inputs_collapse_to_intrinsic() {
assert_eq!(bs_price(120.0, 100.0, 0.0, R, VOL, true), 20.0);
assert_eq!(bs_price(80.0, 100.0, 0.0, R, VOL, true), 0.0);
assert_eq!(bs_price(80.0, 100.0, 1.0, R, 0.0, false), 20.0);
}
}