1pub mod tape;
31pub mod var;
32
33pub use tape::{Gradients, Tape};
34pub use var::Var;
35
36use crate::core::trade::PutOrCall;
37
38pub fn black_scholes<'a>(
41 s: Var<'a>,
42 k: Var<'a>,
43 r: Var<'a>,
44 q: Var<'a>,
45 sigma: Var<'a>,
46 t: Var<'a>,
47 put_or_call: PutOrCall,
48) -> Var<'a> {
49 let sqrt_t = t.sqrt();
50 let st = sigma * sqrt_t;
51 let d1 = ((s / k).ln() + (r - q + sigma * sigma * 0.5) * t) / st;
52 let d2 = d1 - st;
53 let df_q = (-(q * t)).exp();
54 let df_r = (-(r * t)).exp();
55 match put_or_call {
56 PutOrCall::Call => s * df_q * d1.norm_cdf() - k * df_r * d2.norm_cdf(),
57 PutOrCall::Put => k * df_r * (-d2).norm_cdf() - s * df_q * (-d1).norm_cdf(),
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use crate::equity::blackscholes::{bs_price, bs_vega};
65
66 const S: f64 = 100.0;
67 const K: f64 = 105.0;
68 const R: f64 = 0.05;
69 const Q: f64 = 0.02;
70 const SIG: f64 = 0.3;
71 const T: f64 = 1.0;
72
73 #[test]
74 fn one_sweep_reproduces_every_black_scholes_greek() {
75 let tape = Tape::new();
76 let (s, k, r, q, sigma, t) =
77 (tape.var(S), tape.var(K), tape.var(R), tape.var(Q), tape.var(SIG), tape.var(T));
78 let price = black_scholes(s, k, r, q, sigma, t, PutOrCall::Call);
79 assert!((price.value() - bs_price(S, K, R, Q, SIG, T, PutOrCall::Call)).abs() < 1e-12);
80
81 let g = price.grad();
83
84 assert!((g.wrt(sigma) - bs_vega(S, K, R, Q, SIG, T)).abs() < 1e-10, "vega");
86 let h = 1e-6;
88 let fd = |f: &dyn Fn(f64) -> f64| (f(h) - f(-h)) / (2.0 * h);
89 let cases: [(f64, Box<dyn Fn(f64) -> f64>); 5] = [
90 (g.wrt(s), Box::new(|e| bs_price(S + e, K, R, Q, SIG, T, PutOrCall::Call))),
91 (g.wrt(k), Box::new(|e| bs_price(S, K + e, R, Q, SIG, T, PutOrCall::Call))),
92 (g.wrt(r), Box::new(|e| bs_price(S, K, R + e, Q, SIG, T, PutOrCall::Call))),
93 (g.wrt(q), Box::new(|e| bs_price(S, K, R, Q + e, SIG, T, PutOrCall::Call))),
94 (g.wrt(t), Box::new(|e| bs_price(S, K, R, Q, SIG, T + e, PutOrCall::Call))),
95 ];
96 for (i, (aad, f)) in cases.iter().enumerate() {
97 let numeric = fd(f);
98 assert!((aad - numeric).abs() < 1e-7, "greek {i}: aad {aad} vs fd {numeric}");
99 }
100 let put = black_scholes(s, k, r, q, sigma, t, PutOrCall::Put);
102 let gp = put.grad();
103 let put_delta_fd =
104 (bs_price(S + h, K, R, Q, SIG, T, PutOrCall::Put)
105 - bs_price(S - h, K, R, Q, SIG, T, PutOrCall::Put))
106 / (2.0 * h);
107 assert!((gp.wrt(s) - put_delta_fd).abs() < 1e-7);
108 }
109
110 #[test]
111 fn pathwise_monte_carlo_greeks_from_one_sweep_per_path() {
112 use crate::core::montecarlo::path_rng;
115 use rand::Rng;
116
117 let n_paths = 40_000;
118 let mut acc = [[0.0f64; 2]; 4]; for i in 0..n_paths {
122 let mut rng = path_rng(2026, i);
123 let z: f64 = rng.sample(rand_distr::StandardNormal);
124 let tape = Tape::new();
125 let s0 = tape.var(S);
126 let sigma = tape.var(SIG);
127 let r = tape.var(R);
128 let drift = (r - Q - sigma * sigma * 0.5) * T;
129 let s_t = s0 * (drift + sigma * (T.sqrt() * z)).exp();
130 let payoff = (s_t - K).maxf(0.0) * (-(r * T)).exp();
131 let g = payoff.grad();
132 for (slot, x) in
133 [payoff.value(), g.wrt(s0), g.wrt(sigma), g.wrt(r)].into_iter().enumerate()
134 {
135 acc[slot][0] += x;
136 acc[slot][1] += x * x;
137 }
138 }
139 let n = n_paths as f64;
140 let stats = |slot: usize| -> (f64, f64) {
141 let mean = acc[slot][0] / n;
142 let var = (acc[slot][1] / n - mean * mean).max(0.0);
143 (mean, (var / n).sqrt())
144 };
145 let h = 1e-5;
146 let bs = |s: f64, sig: f64, r: f64| bs_price(s, K, r, Q, sig, T, PutOrCall::Call);
147 let truths = [
148 bs(S, SIG, R),
149 (bs(S + h, SIG, R) - bs(S - h, SIG, R)) / (2.0 * h),
150 bs_vega(S, K, R, Q, SIG, T),
151 (bs(S, SIG, R + h) - bs(S, SIG, R - h)) / (2.0 * h),
152 ];
153 for (slot, name) in ["price", "delta", "vega", "rho"].iter().enumerate() {
154 let (mean, se) = stats(slot);
155 assert!(
156 (mean - truths[slot]).abs() < 4.0 * se + 1e-10,
157 "{name}: {mean} vs {} (se {se})",
158 truths[slot]
159 );
160 }
161 }
162
163 #[test]
164 fn tape_cost_is_a_small_constant_multiple_of_pricing() {
165 let tape = Tape::new();
168 let (s, k, r, q, sigma, t) =
169 (tape.var(S), tape.var(K), tape.var(R), tape.var(Q), tape.var(SIG), tape.var(T));
170 let price = black_scholes(s, k, r, q, sigma, t, PutOrCall::Call);
171 let nodes = tape.len();
172 assert!(nodes < 60, "tape has {nodes} nodes");
173 let g = price.grad();
175 let six = [g.wrt(s), g.wrt(k), g.wrt(r), g.wrt(q), g.wrt(sigma), g.wrt(t)];
176 assert!(six.iter().all(|x| x.is_finite() && *x != 0.0));
177 }
178}