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
109 if !(0x0080_0000..0x7f80_0000).contains(&bits) {
113 return ln_f32_outside_normal_range(self);
114 }
115
116 let exponent = ((bits >> 23) as i32 - 127) as Self;
117 let mantissa = Self::from_bits((bits & 0x007F_FFFF) | 0x3f80_0000);
118 let x = (mantissa - 1.0) / (mantissa + 1.0);
119 let x2 = x * x;
120 let mut res = 0.239_282_85_f32;
121 res = x2.mul_add(res, 0.285_182_11);
122 res = x2.mul_add(res, 0.400_005_83);
123 res = x2.mul_add(res, 0.666_666_7);
124 res = x2.mul_add(res, 2.0);
125 x.mul_add(res, exponent * std::f32::consts::LN_2)
126 }
127
128 #[inline(always)]
129 fn exp(self) -> Self {
130 let k = (self.mul_add(
136 std::f32::consts::LOG2_E,
137 if self > 0.0 { 0.5 } else { -0.5 },
138 )) as i32;
139
140 if k <= -151 {
141 return 0.0;
142 }
143
144 if k >= 129 {
145 return Self::INFINITY;
146 }
147
148 let r = self - (k as Self * 0.693_145_75) - (k as Self * 1.428_606_8e-6);
149 let mut res = 0.001_388_89_f32;
150 res = r.mul_add(res, 0.008_333_33);
151 res = r.mul_add(res, 0.041_666_67);
152 res = r.mul_add(res, 0.166_666_67);
153 res = r.mul_add(res, 0.5);
154 res = r.mul_add(res, 1.0);
155 if (-126..=127).contains(&k) {
156 r.mul_add(res, 1.0) * Self::from_bits(((k + 127) as u32) << 23)
157 } else {
158 let ka = k >> 1;
163 let kb = k - ka;
164 r.mul_add(res, 1.0)
165 * Self::from_bits(((ka + 127) as u32) << 23)
166 * Self::from_bits(((kb + 127) as u32) << 23)
167 }
168 }
169
170 #[inline(always)]
171 fn cdf(self) -> Self {
172 self.cdf_with_pdf().0
173 }
174
175 #[inline(always)]
176 fn cdf_with_pdf(self) -> (Self, Self) {
177 let abs_x = self.abs();
183 let t = 1.0 / (1.0 + 0.231_641_9 * abs_x);
184 let mut poly = 1.330_274_5_f32.mul_add(t, -1.821_255_9);
185 poly = t.mul_add(poly, 1.781_477_9);
186 poly = t.mul_add(poly, -0.356_563_78);
187 poly = t.mul_add(poly, 0.319_381_54);
188 let pdf = 0.398_942_3 * (-0.5 * self * self).exp();
189 let res = 1.0 - pdf * (poly * t);
190 (if self >= 0.0 { res } else { 1.0 - res }, pdf)
192 }
193}
194
195#[derive(Debug, Clone, Copy, Default, PartialEq)]
197pub struct Greeks<T> {
198 pub price: T,
199 pub vol: T,
200 pub delta: T,
201 pub gamma: T,
202 pub vega: T,
203 pub theta: T,
204 pub itm_prob: T,
205}
206
207#[inline(always)]
210fn pricing_kernel_price_vega<T: BlackScholesReal>(
211 s_forward: T,
212 k: T,
213 df_r: T,
214 d1: T,
215 d2: T,
216 sqrt_t: T,
217 phi: T,
218) -> (T, T) {
219 let (cdf_phi_d1, pdf_d1) = (phi * d1).cdf_with_pdf();
220 let cdf_phi_d2 = (phi * d2).cdf();
221
222 let price = phi * (s_forward * cdf_phi_d1 - k * df_r * cdf_phi_d2);
223 let vega = s_forward * sqrt_t * pdf_d1;
224
225 (price, vega)
226}
227
228#[expect(clippy::too_many_arguments)]
229#[inline(always)]
230fn pricing_kernel<T: BlackScholesReal>(
231 s_forward: T,
232 k: T,
233 df_r: T,
234 d1: T,
235 d2: T,
236 inv_scaled_vol: T,
237 vol: T,
238 sqrt_t: T,
239 t: T,
240 r: T,
241 b: T,
242 s: T,
243 phi: T,
244) -> Greeks<T> {
245 let (cdf_phi_d1, pdf_d1) = (phi * d1).cdf_with_pdf();
246 let cdf_phi_d2 = (phi * d2).cdf();
247
248 let df_b = ((b - r) * t).exp();
249 let price = phi * (s_forward * cdf_phi_d1 - k * df_r * cdf_phi_d2);
250 let delta = phi * df_b * cdf_phi_d1;
251 let vega = s_forward * sqrt_t * pdf_d1;
252 let gamma = df_b * pdf_d1 * inv_scaled_vol / s;
253
254 let theta_v = -(s_forward * pdf_d1 * vol) * (T::splat(2.0) * sqrt_t).recip_precise();
255 let theta_b = -phi * (b - r) * s_forward * cdf_phi_d1;
256 let theta_r = -phi * r * k * df_r * cdf_phi_d2;
257 let theta = theta_v + theta_b + theta_r;
258
259 Greeks {
260 price,
261 vol,
262 delta,
263 gamma,
264 vega,
265 theta,
266 itm_prob: cdf_phi_d2,
267 }
268}
269
270#[inline(always)]
272pub fn compute_greeks<T: BlackScholesReal>(
273 s: T,
274 k: T,
275 t: T,
276 r: T,
277 b: T,
278 vol: T,
279 is_call: T::Mask,
280) -> Greeks<T> {
281 let sqrt_t = t.sqrt();
282 let scaled_vol = vol * sqrt_t;
283 let inv_scaled_vol = scaled_vol.recip_precise();
284 let df_r = (-r * t).exp();
285 let df_b = ((b - r) * t).exp();
286 let d1 = ((s / k).ln() + (b + T::splat(0.5) * vol * vol) * t) * inv_scaled_vol;
287 let d2 = d1 - scaled_vol;
288 let s_forward = s * df_b;
289 let phi = T::select(is_call, T::splat(1.0), T::splat(-1.0));
290
291 pricing_kernel(
292 s_forward,
293 k,
294 df_r,
295 d1,
296 d2,
297 inv_scaled_vol,
298 vol,
299 sqrt_t,
300 t,
301 r,
302 b,
303 s,
304 phi,
305 )
306}
307
308#[expect(clippy::too_many_arguments)]
330#[inline(always)]
331pub fn compute_iv_and_greeks<T: BlackScholesReal>(
332 mkt_price: T,
333 s: T,
334 k: T,
335 t: T,
336 r: T,
337 b: T,
338 is_call: T::Mask,
339 initial_guess: T,
340) -> Greeks<T> {
341 let sqrt_t = t.sqrt();
343 let inv_sqrt_t = sqrt_t.recip_precise();
344 let ln_sk_bt = (s.ln() - k.ln()) + (b * t); let half_t = T::splat(0.5) * t; let df_r = (-r * t).exp();
347 let mut vol = initial_guess;
348
349 let inv_vol = vol.recip_precise();
351 let inv_scaled_vol = inv_vol * inv_sqrt_t;
352 let d1 = (ln_sk_bt + half_t * vol * vol) * inv_scaled_vol;
353 let d2 = d1 - vol * sqrt_t;
354 let s_forward = s * ((b - r) * t).exp();
355 let phi = T::select(is_call, T::splat(1.0), T::splat(-1.0));
356 let (price, vega_raw) = pricing_kernel_price_vega(s_forward, k, df_r, d1, d2, sqrt_t, phi);
357
358 let diff = price - mkt_price;
359 let vega = vega_raw.abs().max(T::splat(1e-9));
360 let volga = (vega * d1 * d2) * inv_vol;
361 let num = T::splat(2.0) * diff * vega;
362 let den = T::splat(2.0) * vega * vega - diff * volga;
363 let den_safe = den.signum() * den.abs().max(T::splat(1e-9));
365 vol = vol - (num * den_safe.recip_precise());
366
367 vol = vol.max(T::splat(1e-6)).min(T::splat(10.0));
371
372 let inv_vol_f = vol.recip_precise();
374 let inv_scaled_vol_f = inv_vol_f * inv_sqrt_t;
375 let scaled_vol_f = vol * sqrt_t;
376 let d1_f = (ln_sk_bt + half_t * vol * vol) * inv_scaled_vol_f;
377 let d2_f = d1_f - scaled_vol_f;
378 let mut g_final = pricing_kernel(
379 s_forward,
380 k,
381 df_r,
382 d1_f,
383 d2_f,
384 inv_scaled_vol_f,
385 vol,
386 sqrt_t,
387 t,
388 r,
389 b,
390 s,
391 phi,
392 );
393 g_final.vol = vol;
394
395 g_final
396}
397
398#[cold]
404#[inline(never)]
405fn ln_f32_outside_normal_range(value: f32) -> f32 {
406 let bits = value.to_bits();
407 let magnitude = bits & 0x7fff_ffff;
408 if magnitude == 0 {
409 return f32::NEG_INFINITY;
410 }
411
412 let exponent_bits = magnitude & 0x7f80_0000;
413 let fraction_bits = magnitude & 0x007f_ffff;
414 if exponent_bits == 0x7f80_0000 && fraction_bits != 0 {
415 return f32::NAN;
416 }
417
418 if bits & 0x8000_0000 != 0 {
419 return f32::NAN;
420 }
421
422 if exponent_bits == 0x7f80_0000 {
423 return f32::INFINITY;
424 }
425
426 <f32 as BlackScholesReal>::ln(value * 8_388_608.0) - 23.0 * std::f32::consts::LN_2
429}
430
431#[cfg(test)]
433mod tests {
434 use rstest::*;
435
436 use super::*;
437 use crate::data::greeks::black_scholes_greeks_exact;
438
439 fn old_ln_formula(input: f32) -> f32 {
443 let bits = input.to_bits();
444 let exponent = ((bits >> 23) as i32 - 127) as f32;
445 let mantissa = f32::from_bits((bits & 0x007F_FFFF) | 0x3f80_0000);
446 let x = (mantissa - 1.0) / (mantissa + 1.0);
447 let x2 = x * x;
448 let mut res = 0.239_282_85_f32;
449 res = x2.mul_add(res, 0.285_182_11);
450 res = x2.mul_add(res, 0.400_005_83);
451 res = x2.mul_add(res, 0.666_666_7);
452 res = x2.mul_add(res, 2.0);
453 x.mul_add(res, exponent * std::f32::consts::LN_2)
454 }
455
456 fn assert_ln_close(actual: f32, expected: f32, max_ulps: u32) {
457 assert_eq!(actual.is_nan(), expected.is_nan());
458 assert_eq!(actual.is_infinite(), expected.is_infinite());
459 assert_eq!(actual.is_sign_negative(), expected.is_sign_negative());
460 if actual.is_finite() {
461 assert!(
462 actual.to_bits().abs_diff(expected.to_bits()) <= max_ulps,
463 "ln mismatch: actual={actual:e} ({:#010x}), expected={expected:e} ({:#010x})",
464 actual.to_bits(),
465 expected.to_bits(),
466 );
467 }
468 }
469
470 fn assert_exp_close(actual: f32, expected: f32, max_ulps: u32) {
471 assert_eq!(actual.is_nan(), expected.is_nan());
472 assert_eq!(actual.is_infinite(), expected.is_infinite());
473 assert_eq!(actual.is_sign_negative(), expected.is_sign_negative());
474 if actual.is_finite() {
475 assert!(
476 actual.to_bits().abs_diff(expected.to_bits()) <= max_ulps,
477 "exp mismatch: actual={actual:e} ({:#010x}), expected={expected:e} ({:#010x})",
478 actual.to_bits(),
479 expected.to_bits(),
480 );
481 }
482 }
483
484 #[rstest]
485 fn test_ln_special_values() {
486 for input in [0.0_f32, -0.0] {
487 let actual = <f32 as BlackScholesReal>::ln(input);
488 assert_eq!(actual, f32::NEG_INFINITY, "input={input:?}");
489 }
490
491 for input in [-1.0_f32, -2.0, -f32::MIN_POSITIVE, f32::NEG_INFINITY] {
492 assert!(
493 <f32 as BlackScholesReal>::ln(input).is_nan(),
494 "input={input:?}"
495 );
496 }
497
498 assert_eq!(<f32 as BlackScholesReal>::ln(f32::INFINITY), f32::INFINITY);
499
500 for input in [f32::from_bits(0x7fc0_1234), f32::from_bits(0xffc0_5678)] {
501 assert!(
502 <f32 as BlackScholesReal>::ln(input).is_nan(),
503 "input_bits={:#010x}",
504 input.to_bits()
505 );
506 }
507 }
508
509 #[rstest]
510 fn test_ln_positive_subnormals() {
511 for input in [
512 f32::from_bits(1),
513 f32::from_bits(0x0040_0000),
514 f32::from_bits(0x007f_ffff),
515 ] {
516 assert_ln_close(<f32 as BlackScholesReal>::ln(input), input.ln(), 3);
517 }
518 }
519
520 #[rstest]
521 fn test_ln_positive_normal_path_is_unchanged() {
522 let fractions = [
527 0x0000_0000,
528 0x0000_0001,
529 0x001f_ffff,
530 0x003f_ffff,
531 0x0040_0000,
532 0x0055_5555,
533 0x007f_fffe,
534 0x007f_ffff,
535 ];
536
537 for exponent in 1_u32..=254 {
538 for fraction in fractions {
539 let input = f32::from_bits((exponent << 23) | fraction);
540 assert_eq!(
541 <f32 as BlackScholesReal>::ln(input).to_bits(),
542 old_ln_formula(input).to_bits(),
543 "input={input:e} ({:#010x})",
544 input.to_bits()
545 );
546 }
547 }
548 }
549
550 #[rstest]
551 fn test_compute_greeks_negative_strike_returns_nan_price() {
552 let greeks = compute_greeks::<f32>(100.0, -100.0, 1.0, 0.05, 0.05, 0.2, true);
553
554 assert!(greeks.price.is_nan());
555 }
556
557 #[rstest]
558 fn test_compute_iv_and_greeks_negative_strike_returns_nan_price() {
559 let greeks = compute_iv_and_greeks::<f32>(10.0, 100.0, -100.0, 1.0, 0.05, 0.05, true, 0.2);
560
561 assert!(greeks.price.is_nan());
562 }
563
564 #[rstest]
565 fn test_exp_exponent_boundaries() {
566 let ln_2 = std::f32::consts::LN_2;
567 let inputs = [
568 -126.5 * ln_2 - 0.000_1,
569 -126.5 * ln_2 + 0.000_1,
570 -127.5 * ln_2 - 0.000_1,
571 -127.5 * ln_2 + 0.000_1,
572 -150.0 * ln_2 + 0.1,
573 88.7,
574 88.8,
575 129.0 * ln_2,
576 ];
577
578 for input in inputs {
579 assert_exp_close(<f32 as BlackScholesReal>::exp(input), input.exp(), 3);
580 }
581 }
582
583 #[rstest]
584 fn test_exp_tail_sweep() {
585 let mut previous = 0.0;
586 let mut input = -105.0_f32;
587 while input <= 89.0 {
588 let actual = <f32 as BlackScholesReal>::exp(input);
589 assert!(!actual.is_nan(), "exp({input}) returned NaN");
590 assert!(actual >= 0.0, "exp({input}) returned {actual}");
591 assert!(
592 actual >= previous,
593 "exp is not monotonic at {input}: {actual} < {previous}"
594 );
595 assert_exp_close(actual, input.exp(), 3);
596 previous = actual;
597 input += 0.031_25;
598 }
599 }
600
601 #[rstest]
602 fn test_exp_extreme_inputs() {
603 for input in [-1e10_f32, -1e30, f32::NEG_INFINITY] {
604 assert_eq!(<f32 as BlackScholesReal>::exp(input), 0.0, "input={input}");
605 }
606
607 for input in [1e10_f32, 1e30, f32::INFINITY] {
608 assert_eq!(
609 <f32 as BlackScholesReal>::exp(input),
610 f32::INFINITY,
611 "input={input}"
612 );
613 }
614 assert!(<f32 as BlackScholesReal>::exp(f32::NAN).is_nan());
615 }
616
617 #[rstest]
618 fn test_accuracy_1e7() {
619 let s = 100.0;
620 let k = 100.0;
621 let t = 1.0;
622 let r = 0.05;
623 let vol = 0.2;
624 let g = compute_greeks::<f32>(s, k, t, r, r, vol, true); assert!((g.price - 10.45058).abs() < 1e-5);
626 let solved = compute_iv_and_greeks::<f32>(g.price, s, k, t, r, r, true, vol); assert!((solved.vol - vol).abs() < 1e-6);
628 }
629
630 #[rstest]
631 fn test_compute_greeks_accuracy_vs_exact() {
632 let s = 100.0f64;
633 let k = 100.0f64;
634 let t = 1.0f64;
635 let r = 0.05f64;
636 let b = 0.05f64; let vol = 0.2f64;
638 let multiplier = 1.0f64;
639
640 let g_fast = compute_greeks::<f32>(
642 s as f32, k as f32, t as f32, r as f32, b as f32, vol as f32, true,
643 );
644
645 let g_exact = black_scholes_greeks_exact(s, r, b, vol, true, k, t);
647
648 let price_tol = 1e-4;
650 let greeks_tol = 1e-3;
651
652 assert!(
653 (f64::from(g_fast.price) - g_exact.price).abs() < price_tol,
654 "Price mismatch: fast={}, exact={}",
655 g_fast.price,
656 g_exact.price
657 );
658 assert!(
659 (f64::from(g_fast.delta) - g_exact.delta).abs() < greeks_tol,
660 "Delta mismatch: fast={}, exact={}",
661 g_fast.delta,
662 g_exact.delta
663 );
664 assert!(
665 (f64::from(g_fast.gamma) - g_exact.gamma).abs() < greeks_tol,
666 "Gamma mismatch: fast={}, exact={}",
667 g_fast.gamma,
668 g_exact.gamma
669 );
670 let vega_exact_raw = g_exact.vega / (multiplier * 0.01);
672 assert!(
673 (f64::from(g_fast.vega) - vega_exact_raw).abs() < greeks_tol,
674 "Vega mismatch: fast={}, exact_raw={}, exact_scaled={}",
675 g_fast.vega,
676 vega_exact_raw,
677 g_exact.vega
678 );
679 let theta_daily_factor = 0.002_737_850_787_132_101_3;
681 let theta_exact_raw = g_exact.theta / (multiplier * theta_daily_factor);
682 assert!(
683 (f64::from(g_fast.theta) - theta_exact_raw).abs() < greeks_tol,
684 "Theta mismatch: fast={}, exact_raw={}, exact_scaled={}",
685 g_fast.theta,
686 theta_exact_raw,
687 g_exact.theta
688 );
689 }
690
691 #[rstest]
692 fn test_put_theta_with_cost_of_carry_not_equal_to_rate() {
693 let s = 100.0f64;
694 let k = 100.0f64;
695 let t = 1.0f64;
696 let r = 0.05f64;
697 let b = 0.0f64; let vol = 0.2f64;
699 let multiplier = 1.0f64;
700
701 let g_fast = compute_greeks::<f32>(
702 s as f32, k as f32, t as f32, r as f32, b as f32, vol as f32, false,
703 );
704
705 let g_exact = black_scholes_greeks_exact(s, r, b, vol, false, k, t);
706
707 let theta_daily_factor = 0.002_737_850_787_132_101_3;
708 let theta_exact_raw = g_exact.theta / (multiplier * theta_daily_factor);
709 assert!(
710 (f64::from(g_fast.theta) - theta_exact_raw).abs() < 1e-3,
711 "Put theta mismatch with b!=r: fast={}, exact_raw={}",
712 g_fast.theta,
713 theta_exact_raw
714 );
715 }
716
717 #[rstest]
718 fn test_compute_iv_and_greeks_halley_accuracy() {
719 let s = 100.0f64;
720 let k = 100.0f64;
721 let t = 1.0f64;
722 let r = 0.05f64;
723 let b = 0.05f64; let vol_true = 0.2f64; let initial_guess = 0.25f64; let multiplier = 1.0f64;
727
728 let g_exact = black_scholes_greeks_exact(s, r, b, vol_true, true, k, t);
730 let mkt_price = g_exact.price;
731
732 let g_halley = compute_iv_and_greeks::<f32>(
734 mkt_price as f32,
735 s as f32,
736 k as f32,
737 t as f32,
738 r as f32,
739 b as f32,
740 true,
741 initial_guess as f32,
742 );
743
744 let vol_error = (f64::from(g_halley.vol) - vol_true).abs();
746
747 assert!(
749 vol_error < 0.01,
750 "Halley step accuracy: vol_error={}, initial_guess={}, vol_true={}, computed_vol={}",
751 vol_error,
752 initial_guess,
753 vol_true,
754 g_halley.vol
755 );
756
757 let price_tol = 5e-3; let greeks_tol = 5e-3; assert!(
762 (f64::from(g_halley.price) - g_exact.price).abs() < price_tol,
763 "Price mismatch after Halley: halley={}, exact={}, diff={}",
764 g_halley.price,
765 g_exact.price,
766 (f64::from(g_halley.price) - g_exact.price).abs()
767 );
768 assert!(
769 (f64::from(g_halley.delta) - g_exact.delta).abs() < greeks_tol,
770 "Delta mismatch after Halley: halley={}, exact={}",
771 g_halley.delta,
772 g_exact.delta
773 );
774 assert!(
775 (f64::from(g_halley.gamma) - g_exact.gamma).abs() < greeks_tol,
776 "Gamma mismatch after Halley: halley={}, exact={}",
777 g_halley.gamma,
778 g_exact.gamma
779 );
780 let vega_exact_raw = g_exact.vega / (multiplier * 0.01);
782 assert!(
783 (f64::from(g_halley.vega) - vega_exact_raw).abs() < greeks_tol,
784 "Vega mismatch after Halley: halley={}, exact_raw={}",
785 g_halley.vega,
786 vega_exact_raw
787 );
788 let theta_daily_factor = 0.002_737_850_787_132_101_3;
790 let theta_exact_raw = g_exact.theta / (multiplier * theta_daily_factor);
791 assert!(
792 (f64::from(g_halley.theta) - theta_exact_raw).abs() < greeks_tol,
793 "Theta mismatch after Halley: halley={}, exact_raw={}",
794 g_halley.theta,
795 theta_exact_raw
796 );
797 }
798
799 #[rstest]
800 fn test_print_halley_iv() {
801 let s = 100.0f64;
802 let k = 100.0f64;
803 let t = 1.0f64;
804 let r = 0.05f64;
805 let b = 0.05f64;
806 let vol_true = 0.2f64;
807
808 let g_exact = black_scholes_greeks_exact(s, r, b, vol_true, true, k, t);
809 let mkt_price = g_exact.price;
810
811 println!("\n=== Halley Step IV Test (Using True Vol as Initial Guess) ===");
812 println!("True volatility: {vol_true}");
813 println!("Market price: {mkt_price:.8}");
814 println!("Initial guess: {vol_true} (using true vol)");
815
816 let g_halley = compute_iv_and_greeks::<f32>(
817 mkt_price as f32,
818 s as f32,
819 k as f32,
820 t as f32,
821 r as f32,
822 b as f32,
823 true,
824 vol_true as f32, );
826
827 println!("\nAfter one Halley step:");
828 println!("Computed volatility: {:.8}", g_halley.vol);
829 println!("True volatility: {vol_true:.8}");
830 println!(
831 "Absolute error: {:.8}",
832 (f64::from(g_halley.vol) - vol_true).abs()
833 );
834 println!(
835 "Relative error: {:.4}%",
836 (f64::from(g_halley.vol) - vol_true).abs() / vol_true * 100.0
837 );
838 }
839
840 #[rstest]
841 fn test_compute_iv_and_greeks_deep_itm_otm() {
842 let t = 1.0f64;
843 let r = 0.05f64;
844 let b = 0.05f64;
845 let vol_true = 0.2f64;
846
847 let s_itm = 150.0f64;
849 let k_itm = 100.0f64;
850 let g_exact_itm = black_scholes_greeks_exact(s_itm, r, b, vol_true, true, k_itm, t);
851 let mkt_price_itm = g_exact_itm.price;
852
853 println!("\n=== Deep ITM Test ===");
854 println!("Spot: {s_itm}, Strike: {k_itm}, True vol: {vol_true}");
855 println!("Market price: {mkt_price_itm:.8}");
856
857 let g_recovered_itm = compute_iv_and_greeks::<f32>(
858 mkt_price_itm as f32,
859 s_itm as f32,
860 k_itm as f32,
861 t as f32,
862 r as f32,
863 b as f32,
864 true,
865 vol_true as f32, );
867
868 let vol_error_itm = (f64::from(g_recovered_itm.vol) - vol_true).abs();
869 let rel_error_itm = vol_error_itm / vol_true * 100.0;
870
871 println!("Recovered volatility: {:.8}", g_recovered_itm.vol);
872 println!("Absolute error: {vol_error_itm:.8}");
873 println!("Relative error: {rel_error_itm:.4}%");
874
875 let s_otm = 50.0f64;
877 let k_otm = 100.0f64;
878 let g_exact_otm = black_scholes_greeks_exact(s_otm, r, b, vol_true, true, k_otm, t);
879 let mkt_price_otm = g_exact_otm.price;
880
881 println!("\n=== Deep OTM Test ===");
882 println!("Spot: {s_otm}, Strike: {k_otm}, True vol: {vol_true}");
883 println!("Market price: {mkt_price_otm:.8}");
884
885 let g_recovered_otm = compute_iv_and_greeks::<f32>(
886 mkt_price_otm as f32,
887 s_otm as f32,
888 k_otm as f32,
889 t as f32,
890 r as f32,
891 b as f32,
892 false,
893 vol_true as f32, );
895
896 let vol_error_otm = (f64::from(g_recovered_otm.vol) - vol_true).abs();
897 let rel_error_otm = vol_error_otm / vol_true * 100.0;
898
899 println!("Recovered volatility: {:.8}", g_recovered_otm.vol);
900 println!("Absolute error: {vol_error_otm:.8}");
901 println!("Relative error: {rel_error_otm:.4}%");
902
903 let vol_tol_itm = 50.0; let vol_tol_otm = 150.0; assert!(
912 g_recovered_itm.vol.is_finite()
913 && g_recovered_itm.vol > 0.0
914 && g_recovered_itm.vol < 2.0,
915 "Deep ITM vol recovery: invalid result={}",
916 g_recovered_itm.vol
917 );
918
919 assert!(
920 g_recovered_otm.vol.is_finite()
921 && g_recovered_otm.vol > 0.0
922 && g_recovered_otm.vol < 2.0,
923 "Deep OTM vol recovery: invalid result={}",
924 g_recovered_otm.vol
925 );
926
927 assert!(
929 rel_error_itm < vol_tol_itm,
930 "Deep ITM vol recovery error too large: recovered={}, true={}, error={:.4}%",
931 g_recovered_itm.vol,
932 vol_true,
933 rel_error_itm
934 );
935
936 assert!(
937 rel_error_otm < vol_tol_otm,
938 "Deep OTM vol recovery error too large: recovered={}, true={}, error={:.4}%",
939 g_recovered_otm.vol,
940 vol_true,
941 rel_error_otm
942 );
943
944 println!("\n=== Summary ===");
945 println!("Deep ITM: One Halley iteration error = {rel_error_itm:.2}%");
946 println!(
947 "Deep OTM: One Halley iteration error = {rel_error_otm:.2}% (still challenging, deep OTM is difficult)"
948 );
949 }
950}