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 pub itm_prob: T,
176}
177
178#[inline(always)]
180fn pricing_kernel_price_vega<T: BlackScholesReal>(
181 s: T,
182 k: T,
183 disc: T,
184 d1: T,
185 d2: T,
186 sqrt_t: T,
187 is_call: T::Mask,
188) -> (T, T) {
189 let (n_d1, pdf_d1) = d1.cdf_with_pdf();
190 let n_d2 = d2.cdf();
191
192 let c_price = s * n_d1 - k * disc * n_d2;
193 let p_price = c_price - s + k * disc;
194 let price = T::select(is_call, c_price, p_price);
195
196 let vega = s * sqrt_t * pdf_d1;
197
198 (price, vega)
199}
200
201#[allow(clippy::too_many_arguments)]
202#[inline(always)]
203fn pricing_kernel<T: BlackScholesReal>(
204 s: T,
205 k: T,
206 disc: T,
207 d1: T,
208 d2: T,
209 inv_vol_sqrt_t: T,
210 vol: T,
211 sqrt_t: T,
212 t: T,
213 r: T,
214 b: T,
215 s_orig: T,
216 is_call: T::Mask,
217) -> Greeks<T> {
218 let (n_d1, pdf_d1) = d1.cdf_with_pdf();
219 let n_d2 = d2.cdf();
220
221 let c_price = s * n_d1 - k * disc * n_d2;
222 let p_price = c_price - s + k * disc;
223
224 let vega = s * sqrt_t * pdf_d1;
225 let df = ((b - r) * t).exp();
226 let gamma = df * pdf_d1 * inv_vol_sqrt_t / s_orig;
227
228 let theta_base = -(vega * vol) * (T::splat(2.0) * t).recip_precise();
229 let phi_theta = T::select(is_call, T::splat(1.0), -T::splat(1.0));
230 let c_theta = theta_base - r * k * disc * n_d2 - phi_theta * (b - r) * s * n_d1;
231 let p_theta =
232 theta_base + r * k * disc * (T::splat(1.0) - n_d2) - phi_theta * (b - r) * s * n_d1;
233
234 let price = T::select(is_call, c_price, p_price);
235 let delta = T::select(is_call, n_d1, n_d1 - T::splat(1.0));
236 let theta = T::select(is_call, c_theta, p_theta);
237 let itm_prob = T::select(is_call, n_d2, T::splat(1.0) - n_d2);
238
239 Greeks {
240 price,
241 vol,
242 delta,
243 gamma,
244 vega,
245 theta,
246 itm_prob,
247 }
248}
249
250#[inline(always)]
252pub fn compute_greeks<T: BlackScholesReal>(
253 s: T,
254 k: T,
255 t: T,
256 r: T,
257 b: T,
258 vol: T,
259 is_call: T::Mask,
260) -> Greeks<T> {
261 let sqrt_t = t.sqrt();
262 let vol_sqrt_t = vol * sqrt_t;
263 let inv_vol_sqrt_t = vol_sqrt_t.recip_precise();
264 let disc = (-r * t).exp();
265 let d1 = ((s / k).ln() + (b + T::splat(0.5) * vol * vol) * t) * inv_vol_sqrt_t;
266 let s_forward = s * ((b - r) * t).exp();
267
268 pricing_kernel(
269 s_forward,
270 k,
271 disc,
272 d1,
273 d1 - vol_sqrt_t,
274 inv_vol_sqrt_t,
275 vol,
276 sqrt_t,
277 t,
278 r,
279 b,
280 s,
281 is_call,
282 )
283}
284
285#[allow(clippy::too_many_arguments)]
307#[inline(always)]
308pub fn compute_iv_and_greeks<T: BlackScholesReal>(
309 mkt_price: T,
310 s: T,
311 k: T,
312 t: T,
313 r: T,
314 b: T,
315 is_call: T::Mask,
316 initial_guess: T,
317) -> Greeks<T> {
318 let sqrt_t = t.sqrt();
320 let inv_sqrt_t = sqrt_t.recip_precise();
321 let ln_sk_bt = (s.ln() - k.ln()) + (b * t); let half_t = T::splat(0.5) * t; let disc = (-r * t).exp();
324 let mut vol = initial_guess;
325
326 let inv_vol = vol.recip_precise();
328 let inv_vst = inv_vol * inv_sqrt_t;
329 let d1 = (ln_sk_bt + half_t * vol * vol) * inv_vst;
330 let d2 = d1 - vol * sqrt_t;
331 let s_forward = s * ((b - r) * t).exp();
332 let (price, vega_raw) = pricing_kernel_price_vega(s_forward, k, disc, d1, d2, sqrt_t, is_call);
333
334 let diff = price - mkt_price;
335 let vega = vega_raw.abs().max(T::splat(1e-9));
336 let volga = (vega * d1 * d2) * inv_vol;
337 let num = T::splat(2.0) * diff * vega;
338 let den = T::splat(2.0) * vega * vega - diff * volga;
339 let den_safe = den.signum() * den.abs().max(T::splat(1e-9));
341 vol = vol - (num * den_safe.recip_precise());
342
343 vol = vol.max(T::splat(1e-6)).min(T::splat(10.0));
347
348 let inv_vol_f = vol.recip_precise();
350 let inv_vst_f = inv_vol_f * inv_sqrt_t;
351 let d1_f = (ln_sk_bt + half_t * vol * vol) * inv_vst_f;
352 let mut g_final = pricing_kernel(
353 s_forward,
354 k,
355 disc,
356 d1_f,
357 d1_f - vol * sqrt_t,
358 inv_vst_f,
359 vol,
360 sqrt_t,
361 t,
362 r,
363 b,
364 s,
365 is_call,
366 );
367 g_final.vol = vol;
368
369 g_final
370}
371
372#[cfg(test)]
374mod tests {
375 use rstest::*;
376
377 use super::*;
378 use crate::data::greeks::black_scholes_greeks_exact;
379
380 #[rstest]
381 fn test_accuracy_1e7() {
382 let s = 100.0;
383 let k = 100.0;
384 let t = 1.0;
385 let r = 0.05;
386 let vol = 0.2;
387 let g = compute_greeks::<f32>(s, k, t, r, r, vol, true); assert!((g.price - 10.45058).abs() < 1e-5);
389 let solved = compute_iv_and_greeks::<f32>(g.price, s, k, t, r, r, true, vol); assert!((solved.vol - vol).abs() < 1e-6);
391 }
392
393 #[rstest]
394 fn test_compute_greeks_accuracy_vs_exact() {
395 let s = 100.0f64;
396 let k = 100.0f64;
397 let t = 1.0f64;
398 let r = 0.05f64;
399 let b = 0.05f64; let vol = 0.2f64;
401 let multiplier = 1.0f64;
402
403 let g_fast = compute_greeks::<f32>(
405 s as f32, k as f32, t as f32, r as f32, b as f32, vol as f32, true,
406 );
407
408 let g_exact = black_scholes_greeks_exact(s, r, b, vol, true, k, t);
410
411 let price_tol = 1e-4;
413 let greeks_tol = 1e-3;
414
415 assert!(
416 (g_fast.price as f64 - g_exact.price).abs() < price_tol,
417 "Price mismatch: fast={}, exact={}",
418 g_fast.price,
419 g_exact.price
420 );
421 assert!(
422 (g_fast.delta as f64 - g_exact.delta).abs() < greeks_tol,
423 "Delta mismatch: fast={}, exact={}",
424 g_fast.delta,
425 g_exact.delta
426 );
427 assert!(
428 (g_fast.gamma as f64 - g_exact.gamma).abs() < greeks_tol,
429 "Gamma mismatch: fast={}, exact={}",
430 g_fast.gamma,
431 g_exact.gamma
432 );
433 let vega_exact_raw = g_exact.vega / (multiplier * 0.01);
435 assert!(
436 (g_fast.vega as f64 - vega_exact_raw).abs() < greeks_tol,
437 "Vega mismatch: fast={}, exact_raw={}, exact_scaled={}",
438 g_fast.vega,
439 vega_exact_raw,
440 g_exact.vega
441 );
442 let theta_daily_factor = 0.0027378507871321013;
444 let theta_exact_raw = g_exact.theta / (multiplier * theta_daily_factor);
445 assert!(
446 (g_fast.theta as f64 - theta_exact_raw).abs() < greeks_tol,
447 "Theta mismatch: fast={}, exact_raw={}, exact_scaled={}",
448 g_fast.theta,
449 theta_exact_raw,
450 g_exact.theta
451 );
452 }
453
454 #[rstest]
455 fn test_compute_iv_and_greeks_halley_accuracy() {
456 let s = 100.0f64;
457 let k = 100.0f64;
458 let t = 1.0f64;
459 let r = 0.05f64;
460 let b = 0.05f64; let vol_true = 0.2f64; let initial_guess = 0.25f64; let multiplier = 1.0f64;
464
465 let g_exact = black_scholes_greeks_exact(s, r, b, vol_true, true, k, t);
467 let mkt_price = g_exact.price;
468
469 let g_halley = compute_iv_and_greeks::<f32>(
471 mkt_price as f32,
472 s as f32,
473 k as f32,
474 t as f32,
475 r as f32,
476 b as f32,
477 true,
478 initial_guess as f32,
479 );
480
481 let vol_error = (g_halley.vol as f64 - vol_true).abs();
483
484 assert!(
486 vol_error < 0.01,
487 "Halley step accuracy: vol_error={}, initial_guess={}, vol_true={}, computed_vol={}",
488 vol_error,
489 initial_guess,
490 vol_true,
491 g_halley.vol
492 );
493
494 let price_tol = 5e-3; let greeks_tol = 5e-3; assert!(
499 (g_halley.price as f64 - g_exact.price).abs() < price_tol,
500 "Price mismatch after Halley: halley={}, exact={}, diff={}",
501 g_halley.price,
502 g_exact.price,
503 (g_halley.price as f64 - g_exact.price).abs()
504 );
505 assert!(
506 (g_halley.delta as f64 - g_exact.delta).abs() < greeks_tol,
507 "Delta mismatch after Halley: halley={}, exact={}",
508 g_halley.delta,
509 g_exact.delta
510 );
511 assert!(
512 (g_halley.gamma as f64 - g_exact.gamma).abs() < greeks_tol,
513 "Gamma mismatch after Halley: halley={}, exact={}",
514 g_halley.gamma,
515 g_exact.gamma
516 );
517 let vega_exact_raw = g_exact.vega / (multiplier * 0.01);
519 assert!(
520 (g_halley.vega as f64 - vega_exact_raw).abs() < greeks_tol,
521 "Vega mismatch after Halley: halley={}, exact_raw={}",
522 g_halley.vega,
523 vega_exact_raw
524 );
525 let theta_daily_factor = 0.0027378507871321013;
527 let theta_exact_raw = g_exact.theta / (multiplier * theta_daily_factor);
528 assert!(
529 (g_halley.theta as f64 - theta_exact_raw).abs() < greeks_tol,
530 "Theta mismatch after Halley: halley={}, exact_raw={}",
531 g_halley.theta,
532 theta_exact_raw
533 );
534 }
535
536 #[rstest]
537 fn test_print_halley_iv() {
538 let s = 100.0f64;
539 let k = 100.0f64;
540 let t = 1.0f64;
541 let r = 0.05f64;
542 let b = 0.05f64;
543 let vol_true = 0.2f64;
544
545 let g_exact = black_scholes_greeks_exact(s, r, b, vol_true, true, k, t);
546 let mkt_price = g_exact.price;
547
548 println!("\n=== Halley Step IV Test (Using True Vol as Initial Guess) ===");
549 println!("True volatility: {vol_true}");
550 println!("Market price: {mkt_price:.8}");
551 println!("Initial guess: {vol_true} (using true vol)");
552
553 let g_halley = compute_iv_and_greeks::<f32>(
554 mkt_price as f32,
555 s as f32,
556 k as f32,
557 t as f32,
558 r as f32,
559 b as f32,
560 true,
561 vol_true as f32, );
563
564 println!("\nAfter one Halley step:");
565 println!("Computed volatility: {:.8}", g_halley.vol);
566 println!("True volatility: {vol_true:.8}");
567 println!(
568 "Absolute error: {:.8}",
569 (g_halley.vol as f64 - vol_true).abs()
570 );
571 println!(
572 "Relative error: {:.4}%",
573 (g_halley.vol as f64 - vol_true).abs() / vol_true * 100.0
574 );
575 }
576
577 #[rstest]
578 fn test_compute_iv_and_greeks_deep_itm_otm() {
579 let t = 1.0f64;
580 let r = 0.05f64;
581 let b = 0.05f64;
582 let vol_true = 0.2f64;
583
584 let s_itm = 150.0f64;
586 let k_itm = 100.0f64;
587 let g_exact_itm = black_scholes_greeks_exact(s_itm, r, b, vol_true, true, k_itm, t);
588 let mkt_price_itm = g_exact_itm.price;
589
590 println!("\n=== Deep ITM Test ===");
591 println!("Spot: {s_itm}, Strike: {k_itm}, True vol: {vol_true}");
592 println!("Market price: {mkt_price_itm:.8}");
593
594 let g_recovered_itm = compute_iv_and_greeks::<f32>(
595 mkt_price_itm as f32,
596 s_itm as f32,
597 k_itm as f32,
598 t as f32,
599 r as f32,
600 b as f32,
601 true,
602 vol_true as f32, );
604
605 let vol_error_itm = (g_recovered_itm.vol as f64 - vol_true).abs();
606 let rel_error_itm = vol_error_itm / vol_true * 100.0;
607
608 println!("Recovered volatility: {:.8}", g_recovered_itm.vol);
609 println!("Absolute error: {vol_error_itm:.8}");
610 println!("Relative error: {rel_error_itm:.4}%");
611
612 let s_otm = 50.0f64;
614 let k_otm = 100.0f64;
615 let g_exact_otm = black_scholes_greeks_exact(s_otm, r, b, vol_true, true, k_otm, t);
616 let mkt_price_otm = g_exact_otm.price;
617
618 println!("\n=== Deep OTM Test ===");
619 println!("Spot: {s_otm}, Strike: {k_otm}, True vol: {vol_true}");
620 println!("Market price: {mkt_price_otm:.8}");
621
622 let g_recovered_otm = compute_iv_and_greeks::<f32>(
623 mkt_price_otm as f32,
624 s_otm as f32,
625 k_otm as f32,
626 t as f32,
627 r as f32,
628 b as f32,
629 false,
630 vol_true as f32, );
632
633 let vol_error_otm = (g_recovered_otm.vol as f64 - vol_true).abs();
634 let rel_error_otm = vol_error_otm / vol_true * 100.0;
635
636 println!("Recovered volatility: {:.8}", g_recovered_otm.vol);
637 println!("Absolute error: {vol_error_otm:.8}");
638 println!("Relative error: {rel_error_otm:.4}%");
639
640 let vol_tol_itm = 50.0; let vol_tol_otm = 150.0; assert!(
649 g_recovered_itm.vol.is_finite()
650 && g_recovered_itm.vol > 0.0
651 && g_recovered_itm.vol < 2.0,
652 "Deep ITM vol recovery: invalid result={}",
653 g_recovered_itm.vol
654 );
655
656 assert!(
657 g_recovered_otm.vol.is_finite()
658 && g_recovered_otm.vol > 0.0
659 && g_recovered_otm.vol < 2.0,
660 "Deep OTM vol recovery: invalid result={}",
661 g_recovered_otm.vol
662 );
663
664 assert!(
666 rel_error_itm < vol_tol_itm,
667 "Deep ITM vol recovery error too large: recovered={}, true={}, error={:.4}%",
668 g_recovered_itm.vol,
669 vol_true,
670 rel_error_itm
671 );
672
673 assert!(
674 rel_error_otm < vol_tol_otm,
675 "Deep OTM vol recovery error too large: recovered={}, true={}, error={:.4}%",
676 g_recovered_otm.vol,
677 vol_true,
678 rel_error_otm
679 );
680
681 println!("\n=== Summary ===");
682 println!("Deep ITM: One Halley iteration error = {rel_error_itm:.2}%");
683 println!(
684 "Deep OTM: One Halley iteration error = {rel_error_otm:.2}% (still challenging, deep OTM is difficult)"
685 );
686 }
687}