use statrs::distribution::{Continuous, ContinuousCDF, Normal};
use crate::pricing;
use crate::types::InstrumentGreeks;
const DAYS_PER_YEAR: f64 = 365.25;
#[must_use]
pub fn compute_greeks(
f: f64,
k: f64,
t: f64,
sigma: f64,
r: f64,
is_call: bool,
) -> InstrumentGreeks {
if t <= 0.0 || sigma <= 0.0 {
let delta = if is_call {
if f > k { 1.0 } else { 0.0 }
} else if k > f {
-1.0
} else {
0.0
};
return InstrumentGreeks {
delta,
gamma: 0.0,
vega: 0.0,
theta: 0.0,
rho: 0.0,
};
}
let (d1, d2) = pricing::d1_d2(f, k, t, sigma);
let norm = Normal::standard();
let df = (-r * t).exp();
let n_d1 = norm.pdf(d1);
let sqrt_t = t.sqrt();
let delta = if is_call {
df * norm.cdf(d1)
} else {
df * (norm.cdf(d1) - 1.0)
};
let gamma = df * n_d1 / (f * sigma * sqrt_t);
let vega = pricing::vega(f, k, t, sigma, r) / 100.0;
let price = if is_call {
df * f.mul_add(norm.cdf(d1), -(k * norm.cdf(d2)))
} else {
df * k.mul_add(norm.cdf(-d2), -(f * norm.cdf(-d1)))
};
let time_decay = -df * f * n_d1 * sigma / (2.0 * sqrt_t);
let carry_cost = r * price; let theta = (time_decay + carry_cost) / DAYS_PER_YEAR;
let rho = (-t * price) / 100.0;
InstrumentGreeks {
delta,
gamma,
vega,
theta,
rho,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pricing::{call_price, put_price};
#[test]
fn atm_call_delta_near_half() {
let g = compute_greeks(100.0, 100.0, 1.0, 0.20, 0.0, true);
assert!(
(g.delta - 0.5).abs() < 0.05,
"ATM call delta ~0.5, got {}",
g.delta
);
}
#[test]
fn deep_itm_call_delta_near_one() {
let g = compute_greeks(150.0, 100.0, 1.0, 0.20, 0.0, true);
assert!(g.delta > 0.95);
}
#[test]
fn vega_positive_and_call_put_equal() {
let gc = compute_greeks(100.0, 100.0, 1.0, 0.20, 0.0, true);
let gp = compute_greeks(100.0, 100.0, 1.0, 0.20, 0.0, false);
assert!(gc.vega > 0.0);
assert!(gp.vega > 0.0);
assert!((gc.vega - gp.vega).abs() < 1e-10);
}
#[test]
fn otm_put_delta_negative() {
let g = compute_greeks(100.0, 90.0, 1.0, 0.20, 0.0, false);
assert!(g.delta < 0.0);
}
#[test]
fn theta_negative() {
let g = compute_greeks(100.0, 100.0, 1.0, 0.20, 0.0, true);
assert!(g.theta < 0.0);
}
#[test]
fn expired_returns_intrinsic_delta() {
let g = compute_greeks(110.0, 100.0, 0.0, 0.20, 0.0, true);
assert!((g.delta - 1.0).abs() < f64::EPSILON);
assert!(g.vega.abs() < f64::EPSILON);
assert!(g.theta.abs() < f64::EPSILON);
let g = compute_greeks(90.0, 100.0, 0.0, 0.20, 0.0, false);
assert!((g.delta + 1.0).abs() < f64::EPSILON);
}
#[test]
fn theta_sign_correct_at_nonzero_rate() {
let f = 100.0;
let k = 100.0;
let t = 1.0;
let sigma = 0.20;
let r = 0.05;
let g = compute_greeks(f, k, t, sigma, r, true);
let h = 1e-4;
let c_up = call_price(f, k, t + h, sigma, r);
let c_dn = call_price(f, k, t - h, sigma, r);
let theta_fd_per_day = -(c_up - c_dn) / (2.0 * h) / DAYS_PER_YEAR;
let rel_err = (g.theta - theta_fd_per_day).abs() / theta_fd_per_day.abs().max(1e-6);
assert!(
rel_err < 1e-5,
"theta analytic {} vs FD {} rel err {}",
g.theta,
theta_fd_per_day,
rel_err
);
}
#[test]
fn theta_sign_correct_at_nonzero_rate_put() {
let f = 100.0;
let k = 100.0;
let t = 1.0;
let sigma = 0.20;
let r = 0.05;
let g = compute_greeks(f, k, t, sigma, r, false);
let h = 1e-4;
let p_up = put_price(f, k, t + h, sigma, r);
let p_dn = put_price(f, k, t - h, sigma, r);
let theta_fd_per_day = -(p_up - p_dn) / (2.0 * h) / DAYS_PER_YEAR;
let rel_err = (g.theta - theta_fd_per_day).abs() / theta_fd_per_day.abs().max(1e-6);
assert!(
rel_err < 1e-5,
"put theta analytic vs FD rel err {}",
rel_err
);
}
#[test]
fn nonzero_rate_affects_delta() {
let g0 = compute_greeks(100.0, 100.0, 1.0, 0.20, 0.0, true);
let g5 = compute_greeks(100.0, 100.0, 1.0, 0.20, 0.05, true);
assert!(g5.delta < g0.delta);
}
#[test]
fn gamma_positive_and_matches_fd() {
let (f, k, t, sigma, r) = (100.0, 100.0, 1.0, 0.20, 0.0);
let g = compute_greeks(f, k, t, sigma, r, true);
assert!(g.gamma > 0.0);
let h = 1e-2;
let p_up = call_price(f + h, k, t, sigma, r);
let p_dn = call_price(f - h, k, t, sigma, r);
let p_0 = call_price(f, k, t, sigma, r);
let fd_gamma = (p_up - 2.0 * p_0 + p_dn) / (h * h);
assert!(
(g.gamma - fd_gamma).abs() < 1e-4,
"gamma analytic {} vs FD {}",
g.gamma,
fd_gamma
);
let gp = compute_greeks(f, k, t, sigma, r, false);
assert!((g.gamma - gp.gamma).abs() < 1e-12);
}
#[test]
fn rho_matches_fd_and_sign() {
let (f, k, t, sigma, r) = (100.0, 100.0, 1.0, 0.20, 0.05);
let g = compute_greeks(f, k, t, sigma, r, true);
assert!(g.rho < 0.0, "Black-76 call rho = -T*C < 0");
let h = 1e-6;
let c_up = call_price(f, k, t, sigma, r + h);
let c_dn = call_price(f, k, t, sigma, r - h);
let fd_rho = ((c_up - c_dn) / (2.0 * h)) / 100.0;
assert!(
(g.rho - fd_rho).abs() < 1e-6,
"rho analytic {} vs FD {}",
g.rho,
fd_rho
);
let gp = compute_greeks(f, k, t, sigma, r, false);
let p = put_price(f, k, t, sigma, r);
assert!((gp.rho - (-t * p) / 100.0).abs() < 1e-12);
}
#[test]
fn greeks_finite_at_zero_sigma() {
for &is_call in &[true, false] {
let g = compute_greeks(100.0, 100.0, 1.0, 0.0, 0.05, is_call);
assert!(g.delta.is_finite(), "delta NaN at sigma=0");
assert!(g.gamma.is_finite(), "gamma NaN at sigma=0");
assert!(g.vega.is_finite(), "vega NaN at sigma=0");
assert!(g.theta.is_finite(), "theta NaN at sigma=0");
assert!(g.rho.is_finite(), "rho NaN at sigma=0");
}
let g = compute_greeks(120.0, 100.0, 1.0, 0.0, 0.0, true);
assert!(g.delta.is_finite() && g.theta.is_finite());
}
}