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