1pub trait BlackScholesReal:
18 Sized
19 + Copy
20 + Send
21 + Sync
22 + Default
23 + std::ops::Add<Output = Self>
24 + std::ops::Sub<Output = Self>
25 + std::ops::Mul<Output = Self>
26 + std::ops::Div<Output = Self>
27 + std::ops::Neg<Output = Self>
28{
29 type Mask: Copy;
30 fn splat(val: f64) -> Self;
31 #[must_use]
32 fn abs(self) -> Self;
33 #[must_use]
34 fn sqrt(self) -> Self;
35 #[must_use]
36 fn ln(self) -> Self;
37 #[must_use]
38 fn exp(self) -> Self;
39 #[must_use]
40 fn cdf(self) -> Self;
41 fn cdf_with_pdf(self) -> (Self, Self);
42 #[must_use]
43 fn mul_add(self, a: Self, b: Self) -> Self;
44 #[must_use]
45 fn recip_precise(self) -> Self;
46 fn select(mask: Self::Mask, t: Self, f: Self) -> Self;
47 fn cmp_gt(self, other: Self) -> Self::Mask;
48 #[must_use]
49 fn max(self, other: Self) -> Self;
50 #[must_use]
51 fn min(self, other: Self) -> Self;
52 #[must_use]
53 fn signum(self) -> Self;
54}
55
56impl BlackScholesReal for f32 {
58 type Mask = bool;
59 #[inline(always)]
60 fn splat(val: f64) -> Self {
61 val as Self
62 }
63 #[inline(always)]
64 fn abs(self) -> Self {
65 self.abs()
66 }
67 #[inline(always)]
68 fn sqrt(self) -> Self {
69 self.sqrt()
70 }
71 #[inline(always)]
72 fn select(mask: bool, t: Self, f: Self) -> Self {
73 if mask { t } else { f }
74 }
75 #[inline(always)]
76 fn cmp_gt(self, other: Self) -> bool {
77 self > other
78 }
79 #[inline(always)]
80 fn recip_precise(self) -> Self {
81 1.0 / self
82 }
83 #[inline(always)]
84 fn max(self, other: Self) -> Self {
85 self.max(other)
86 }
87 #[inline(always)]
88 fn min(self, other: Self) -> Self {
89 self.min(other)
90 }
91 #[inline(always)]
92 fn signum(self) -> Self {
93 self.signum()
94 }
95 #[inline(always)]
96 fn mul_add(self, a: Self, b: Self) -> Self {
97 self.mul_add(a, b)
98 }
99
100 #[inline(always)]
101 fn ln(self) -> Self {
102 let bits = self.to_bits();
108 let exponent = ((bits >> 23) as i32 - 127) as Self;
109 let mantissa = Self::from_bits((bits & 0x007FFFFF) | 0x3f800000);
110 let x = (mantissa - 1.0) / (mantissa + 1.0);
111 let x2 = x * x;
112 let mut res = 0.23928285_f32;
113 res = x2.mul_add(res, 0.28518211);
114 res = x2.mul_add(res, 0.40000583);
115 res = x2.mul_add(res, 0.666_666_7);
116 res = x2.mul_add(res, 2.0);
117 x.mul_add(res, exponent * std::f32::consts::LN_2)
118 }
119
120 #[inline(always)]
121 fn exp(self) -> Self {
122 let k = (self.mul_add(
128 std::f32::consts::LOG2_E,
129 if self > 0.0 { 0.5 } else { -0.5 },
130 )) as i32;
131 let r = self - (k as Self * 0.69314575) - (k as Self * 1.4286068e-6);
132 let mut res = 0.00138889_f32;
133 res = r.mul_add(res, 0.00833333);
134 res = r.mul_add(res, 0.04166667);
135 res = r.mul_add(res, 0.16666667);
136 res = r.mul_add(res, 0.5);
137 res = r.mul_add(res, 1.0);
138 r.mul_add(res, 1.0) * Self::from_bits(((k + 127) as u32) << 23)
139 }
140
141 #[inline(always)]
142 fn cdf(self) -> Self {
143 self.cdf_with_pdf().0
144 }
145
146 #[inline(always)]
147 fn cdf_with_pdf(self) -> (Self, Self) {
148 let abs_x = self.abs();
154 let t = 1.0 / (1.0 + 0.2316419 * abs_x);
155 let mut poly = 1.330_274_5_f32.mul_add(t, -1.821_255_9);
156 poly = t.mul_add(poly, 1.781_477_9);
157 poly = t.mul_add(poly, -0.356_563_78);
158 poly = t.mul_add(poly, 0.319_381_54);
159 let pdf = 0.398_942_3 * (-0.5 * self * self).exp();
160 let res = 1.0 - pdf * (poly * t);
161 (if self >= 0.0 { res } else { 1.0 - res }, pdf)
163 }
164}
165
166#[derive(Debug, Clone, Copy, Default, PartialEq)]
168pub struct Greeks<T> {
169 pub price: T,
170 pub vol: T,
171 pub delta: T,
172 pub gamma: T,
173 pub vega: T,
174 pub theta: T,
175}
176
177#[inline(always)]
179fn pricing_kernel_price_vega<T: BlackScholesReal>(
180 s: T,
181 k: T,
182 disc: T,
183 d1: T,
184 d2: T,
185 sqrt_t: T,
186 is_call: T::Mask,
187) -> (T, T) {
188 let (n_d1, pdf_d1) = d1.cdf_with_pdf();
189 let n_d2 = d2.cdf();
190
191 let c_price = s * n_d1 - k * disc * n_d2;
192 let p_price = c_price - s + k * disc;
193 let price = T::select(is_call, c_price, p_price);
194
195 let vega = s * sqrt_t * pdf_d1;
196
197 (price, vega)
198}
199
200#[allow(clippy::too_many_arguments)]
201#[inline(always)]
202fn pricing_kernel<T: BlackScholesReal>(
203 s: T,
204 k: T,
205 disc: T,
206 d1: T,
207 d2: T,
208 inv_vol_sqrt_t: T,
209 vol: T,
210 sqrt_t: T,
211 t: T,
212 r: T,
213 b: T,
214 s_orig: T,
215 is_call: T::Mask,
216) -> Greeks<T> {
217 let (n_d1, pdf_d1) = d1.cdf_with_pdf();
218 let n_d2 = d2.cdf();
219
220 let c_price = s * n_d1 - k * disc * n_d2;
221 let p_price = c_price - s + k * disc;
222
223 let vega = s * sqrt_t * pdf_d1;
224 let df = ((b - r) * t).exp();
225 let gamma = df * pdf_d1 * inv_vol_sqrt_t / s_orig;
226
227 let theta_base = -(vega * vol) * (T::splat(2.0) * t).recip_precise();
228 let phi_theta = T::select(is_call, T::splat(1.0), -T::splat(1.0));
229 let c_theta = theta_base - r * k * disc * n_d2 - phi_theta * (b - r) * s * n_d1;
230 let p_theta =
231 theta_base + r * k * disc * (T::splat(1.0) - n_d2) - phi_theta * (b - r) * s * n_d1;
232
233 Greeks {
234 price: T::select(is_call, c_price, p_price),
235 vol,
236 delta: T::select(is_call, n_d1, n_d1 - T::splat(1.0)),
237 gamma,
238 vega,
239 theta: T::select(is_call, c_theta, p_theta),
240 }
241}
242
243#[inline(always)]
245pub fn compute_greeks<T: BlackScholesReal>(
246 s: T,
247 k: T,
248 t: T,
249 r: T,
250 b: T,
251 vol: T,
252 is_call: T::Mask,
253) -> Greeks<T> {
254 let sqrt_t = t.sqrt();
255 let vol_sqrt_t = vol * sqrt_t;
256 let inv_vol_sqrt_t = vol_sqrt_t.recip_precise();
257 let disc = (-r * t).exp();
258 let d1 = ((s / k).ln() + (b + T::splat(0.5) * vol * vol) * t) * inv_vol_sqrt_t;
259 let s_forward = s * ((b - r) * t).exp();
260
261 pricing_kernel(
262 s_forward,
263 k,
264 disc,
265 d1,
266 d1 - vol_sqrt_t,
267 inv_vol_sqrt_t,
268 vol,
269 sqrt_t,
270 t,
271 r,
272 b,
273 s,
274 is_call,
275 )
276}
277
278#[allow(clippy::too_many_arguments)]
300#[inline(always)]
301pub fn compute_iv_and_greeks<T: BlackScholesReal>(
302 mkt_price: T,
303 s: T,
304 k: T,
305 t: T,
306 r: T,
307 b: T,
308 is_call: T::Mask,
309 initial_guess: T,
310) -> Greeks<T> {
311 let sqrt_t = t.sqrt();
313 let inv_sqrt_t = sqrt_t.recip_precise();
314 let ln_sk_bt = (s.ln() - k.ln()) + (b * t); let half_t = T::splat(0.5) * t; let disc = (-r * t).exp();
317 let mut vol = initial_guess;
318
319 let inv_vol = vol.recip_precise();
321 let inv_vst = inv_vol * inv_sqrt_t;
322 let d1 = (ln_sk_bt + half_t * vol * vol) * inv_vst;
323 let d2 = d1 - vol * sqrt_t;
324 let s_forward = s * ((b - r) * t).exp();
325 let (price, vega_raw) = pricing_kernel_price_vega(s_forward, k, disc, d1, d2, sqrt_t, is_call);
326
327 let diff = price - mkt_price;
328 let vega = vega_raw.abs().max(T::splat(1e-9));
329 let volga = (vega * d1 * d2) * inv_vol;
330 let num = T::splat(2.0) * diff * vega;
331 let den = T::splat(2.0) * vega * vega - diff * volga;
332 let den_safe = den.signum() * den.abs().max(T::splat(1e-9));
334 vol = vol - (num * den_safe.recip_precise());
335
336 vol = vol.max(T::splat(1e-6)).min(T::splat(10.0));
340
341 let inv_vol_f = vol.recip_precise();
343 let inv_vst_f = inv_vol_f * inv_sqrt_t;
344 let d1_f = (ln_sk_bt + half_t * vol * vol) * inv_vst_f;
345 let mut g_final = pricing_kernel(
346 s_forward,
347 k,
348 disc,
349 d1_f,
350 d1_f - vol * sqrt_t,
351 inv_vst_f,
352 vol,
353 sqrt_t,
354 t,
355 r,
356 b,
357 s,
358 is_call,
359 );
360 g_final.vol = vol;
361
362 g_final
363}
364
365#[cfg(test)]
367mod tests {
368 use rstest::*;
369
370 use super::*;
371 use crate::data::greeks::black_scholes_greeks_exact;
372
373 #[rstest]
374 fn test_accuracy_1e7() {
375 let s = 100.0;
376 let k = 100.0;
377 let t = 1.0;
378 let r = 0.05;
379 let vol = 0.2;
380 let g = compute_greeks::<f32>(s, k, t, r, r, vol, true); assert!((g.price - 10.45058).abs() < 1e-5);
382 let solved = compute_iv_and_greeks::<f32>(g.price, s, k, t, r, r, true, vol); assert!((solved.vol - vol).abs() < 1e-6);
384 }
385
386 #[rstest]
387 fn test_compute_greeks_accuracy_vs_exact() {
388 let s = 100.0f64;
389 let k = 100.0f64;
390 let t = 1.0f64;
391 let r = 0.05f64;
392 let b = 0.05f64; let vol = 0.2f64;
394 let multiplier = 1.0f64;
395
396 let g_fast = compute_greeks::<f32>(
398 s as f32, k as f32, t as f32, r as f32, b as f32, vol as f32, true,
399 );
400
401 let g_exact = black_scholes_greeks_exact(s, r, b, vol, true, k, t, multiplier);
403
404 let price_tol = 1e-4;
406 let greeks_tol = 1e-3;
407
408 assert!(
409 (g_fast.price as f64 - g_exact.price).abs() < price_tol,
410 "Price mismatch: fast={}, exact={}",
411 g_fast.price,
412 g_exact.price
413 );
414 assert!(
415 (g_fast.delta as f64 - g_exact.delta).abs() < greeks_tol,
416 "Delta mismatch: fast={}, exact={}",
417 g_fast.delta,
418 g_exact.delta
419 );
420 assert!(
421 (g_fast.gamma as f64 - g_exact.gamma).abs() < greeks_tol,
422 "Gamma mismatch: fast={}, exact={}",
423 g_fast.gamma,
424 g_exact.gamma
425 );
426 let vega_exact_raw = g_exact.vega / (multiplier * 0.01);
428 assert!(
429 (g_fast.vega as f64 - vega_exact_raw).abs() < greeks_tol,
430 "Vega mismatch: fast={}, exact_raw={}, exact_scaled={}",
431 g_fast.vega,
432 vega_exact_raw,
433 g_exact.vega
434 );
435 let theta_daily_factor = 0.0027378507871321013;
437 let theta_exact_raw = g_exact.theta / (multiplier * theta_daily_factor);
438 assert!(
439 (g_fast.theta as f64 - theta_exact_raw).abs() < greeks_tol,
440 "Theta mismatch: fast={}, exact_raw={}, exact_scaled={}",
441 g_fast.theta,
442 theta_exact_raw,
443 g_exact.theta
444 );
445 }
446
447 #[rstest]
448 fn test_compute_iv_and_greeks_halley_accuracy() {
449 let s = 100.0f64;
450 let k = 100.0f64;
451 let t = 1.0f64;
452 let r = 0.05f64;
453 let b = 0.05f64; let vol_true = 0.2f64; let initial_guess = 0.25f64; let multiplier = 1.0f64;
457
458 let g_exact = black_scholes_greeks_exact(s, r, b, vol_true, true, k, t, multiplier);
460 let mkt_price = g_exact.price;
461
462 let g_halley = compute_iv_and_greeks::<f32>(
464 mkt_price as f32,
465 s as f32,
466 k as f32,
467 t as f32,
468 r as f32,
469 b as f32,
470 true,
471 initial_guess as f32,
472 );
473
474 let vol_error = (g_halley.vol as f64 - vol_true).abs();
476
477 assert!(
479 vol_error < 0.01,
480 "Halley step accuracy: vol_error={}, initial_guess={}, vol_true={}, computed_vol={}",
481 vol_error,
482 initial_guess,
483 vol_true,
484 g_halley.vol
485 );
486
487 let price_tol = 5e-3; let greeks_tol = 5e-3; assert!(
492 (g_halley.price as f64 - g_exact.price).abs() < price_tol,
493 "Price mismatch after Halley: halley={}, exact={}, diff={}",
494 g_halley.price,
495 g_exact.price,
496 (g_halley.price as f64 - g_exact.price).abs()
497 );
498 assert!(
499 (g_halley.delta as f64 - g_exact.delta).abs() < greeks_tol,
500 "Delta mismatch after Halley: halley={}, exact={}",
501 g_halley.delta,
502 g_exact.delta
503 );
504 assert!(
505 (g_halley.gamma as f64 - g_exact.gamma).abs() < greeks_tol,
506 "Gamma mismatch after Halley: halley={}, exact={}",
507 g_halley.gamma,
508 g_exact.gamma
509 );
510 let vega_exact_raw = g_exact.vega / (multiplier * 0.01);
512 assert!(
513 (g_halley.vega as f64 - vega_exact_raw).abs() < greeks_tol,
514 "Vega mismatch after Halley: halley={}, exact_raw={}",
515 g_halley.vega,
516 vega_exact_raw
517 );
518 let theta_daily_factor = 0.0027378507871321013;
520 let theta_exact_raw = g_exact.theta / (multiplier * theta_daily_factor);
521 assert!(
522 (g_halley.theta as f64 - theta_exact_raw).abs() < greeks_tol,
523 "Theta mismatch after Halley: halley={}, exact_raw={}",
524 g_halley.theta,
525 theta_exact_raw
526 );
527 }
528
529 #[rstest]
530 fn test_print_halley_iv() {
531 let s = 100.0f64;
532 let k = 100.0f64;
533 let t = 1.0f64;
534 let r = 0.05f64;
535 let b = 0.05f64;
536 let vol_true = 0.2f64;
537 let multiplier = 1.0f64;
538
539 let g_exact = black_scholes_greeks_exact(s, r, b, vol_true, true, k, t, multiplier);
540 let mkt_price = g_exact.price;
541
542 println!("\n=== Halley Step IV Test (Using True Vol as Initial Guess) ===");
543 println!("True volatility: {vol_true}");
544 println!("Market price: {mkt_price:.8}");
545 println!("Initial guess: {vol_true} (using true vol)");
546
547 let g_halley = compute_iv_and_greeks::<f32>(
548 mkt_price as f32,
549 s as f32,
550 k as f32,
551 t as f32,
552 r as f32,
553 b as f32,
554 true,
555 vol_true as f32, );
557
558 println!("\nAfter one Halley step:");
559 println!("Computed volatility: {:.8}", g_halley.vol);
560 println!("True volatility: {vol_true:.8}");
561 println!(
562 "Absolute error: {:.8}",
563 (g_halley.vol as f64 - vol_true).abs()
564 );
565 println!(
566 "Relative error: {:.4}%",
567 (g_halley.vol as f64 - vol_true).abs() / vol_true * 100.0
568 );
569 }
570
571 #[rstest]
572 fn test_compute_iv_and_greeks_deep_itm_otm() {
573 let t = 1.0f64;
574 let r = 0.05f64;
575 let b = 0.05f64;
576 let vol_true = 0.2f64;
577 let multiplier = 1.0f64;
578
579 let s_itm = 150.0f64;
581 let k_itm = 100.0f64;
582 let g_exact_itm =
583 black_scholes_greeks_exact(s_itm, r, b, vol_true, true, k_itm, t, multiplier);
584 let mkt_price_itm = g_exact_itm.price;
585
586 println!("\n=== Deep ITM Test ===");
587 println!("Spot: {s_itm}, Strike: {k_itm}, True vol: {vol_true}");
588 println!("Market price: {mkt_price_itm:.8}");
589
590 let g_recovered_itm = compute_iv_and_greeks::<f32>(
591 mkt_price_itm as f32,
592 s_itm as f32,
593 k_itm as f32,
594 t as f32,
595 r as f32,
596 b as f32,
597 true,
598 vol_true as f32, );
600
601 let vol_error_itm = (g_recovered_itm.vol as f64 - vol_true).abs();
602 let rel_error_itm = vol_error_itm / vol_true * 100.0;
603
604 println!("Recovered volatility: {:.8}", g_recovered_itm.vol);
605 println!("Absolute error: {vol_error_itm:.8}");
606 println!("Relative error: {rel_error_itm:.4}%");
607
608 let s_otm = 50.0f64;
610 let k_otm = 100.0f64;
611 let g_exact_otm =
612 black_scholes_greeks_exact(s_otm, r, b, vol_true, true, k_otm, t, multiplier);
613 let mkt_price_otm = g_exact_otm.price;
614
615 println!("\n=== Deep OTM Test ===");
616 println!("Spot: {s_otm}, Strike: {k_otm}, True vol: {vol_true}");
617 println!("Market price: {mkt_price_otm:.8}");
618
619 let g_recovered_otm = compute_iv_and_greeks::<f32>(
620 mkt_price_otm as f32,
621 s_otm as f32,
622 k_otm as f32,
623 t as f32,
624 r as f32,
625 b as f32,
626 false,
627 vol_true as f32, );
629
630 let vol_error_otm = (g_recovered_otm.vol as f64 - vol_true).abs();
631 let rel_error_otm = vol_error_otm / vol_true * 100.0;
632
633 println!("Recovered volatility: {:.8}", g_recovered_otm.vol);
634 println!("Absolute error: {vol_error_otm:.8}");
635 println!("Relative error: {rel_error_otm:.4}%");
636
637 let vol_tol_itm = 50.0; let vol_tol_otm = 150.0; assert!(
646 g_recovered_itm.vol.is_finite()
647 && g_recovered_itm.vol > 0.0
648 && g_recovered_itm.vol < 2.0,
649 "Deep ITM vol recovery: invalid result={}",
650 g_recovered_itm.vol
651 );
652
653 assert!(
654 g_recovered_otm.vol.is_finite()
655 && g_recovered_otm.vol > 0.0
656 && g_recovered_otm.vol < 2.0,
657 "Deep OTM vol recovery: invalid result={}",
658 g_recovered_otm.vol
659 );
660
661 assert!(
663 rel_error_itm < vol_tol_itm,
664 "Deep ITM vol recovery error too large: recovered={}, true={}, error={:.4}%",
665 g_recovered_itm.vol,
666 vol_true,
667 rel_error_itm
668 );
669
670 assert!(
671 rel_error_otm < vol_tol_otm,
672 "Deep OTM vol recovery error too large: recovered={}, true={}, error={:.4}%",
673 g_recovered_otm.vol,
674 vol_true,
675 rel_error_otm
676 );
677
678 println!("\n=== Summary ===");
679 println!("Deep ITM: One Halley iteration error = {rel_error_itm:.2}%");
680 println!(
681 "Deep OTM: One Halley iteration error = {rel_error_otm:.2}% (still challenging, deep OTM is difficult)"
682 );
683 }
684}