1use std::{
19 fmt::Display,
20 ops::{Add, Mul},
21};
22
23use implied_vol::{DefaultSpecialFn, ImpliedBlackVolatility, SpecialFn};
24use nautilus_core::{UnixNanos, datetime::unix_nanos_to_iso8601, math::quadratic_interpolation};
25
26use crate::{
27 data::{
28 HasTsInit,
29 black_scholes::{compute_greeks, compute_iv_and_greeks},
30 },
31 identifiers::InstrumentId,
32};
33
34const FRAC_SQRT_2_PI: f64 = f64::from_bits(0x3fd9884533d43651);
35
36#[inline(always)]
37fn norm_pdf(x: f64) -> f64 {
38 FRAC_SQRT_2_PI * (-0.5 * x * x).exp()
39}
40
41#[repr(C)]
44#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
45#[cfg_attr(
46 feature = "python",
47 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
48)]
49pub struct BlackScholesGreeksResult {
50 pub price: f64,
51 pub vol: f64,
52 pub delta: f64,
53 pub gamma: f64,
54 pub vega: f64,
55 pub theta: f64,
56}
57
58#[allow(clippy::too_many_arguments)]
61pub fn black_scholes_greeks_exact(
62 s: f64,
63 r: f64,
64 b: f64,
65 vol: f64,
66 is_call: bool,
67 k: f64,
68 t: f64,
69 multiplier: f64,
70) -> BlackScholesGreeksResult {
71 let phi = if is_call { 1.0 } else { -1.0 };
72 let scaled_vol = vol * t.sqrt();
73 let d1 = ((s / k).ln() + (b + 0.5 * vol.powi(2)) * t) / scaled_vol;
74 let d2 = d1 - scaled_vol;
75 let cdf_phi_d1 = DefaultSpecialFn::norm_cdf(phi * d1);
76 let cdf_phi_d2 = DefaultSpecialFn::norm_cdf(phi * d2);
77 let dist_d1 = norm_pdf(d1);
78 let df = ((b - r) * t).exp();
79 let s_t = s * df;
80 let k_t = k * (-r * t).exp();
81
82 let price = multiplier * phi * (s_t * cdf_phi_d1 - k_t * cdf_phi_d2);
83 let delta = multiplier * phi * df * cdf_phi_d1;
84 let gamma = multiplier * df * dist_d1 / (s * scaled_vol);
85 let vega = multiplier * s_t * t.sqrt() * dist_d1 * 0.01; let theta = multiplier
87 * (s_t * (-dist_d1 * vol / (2.0 * t.sqrt()) - phi * (b - r) * cdf_phi_d1)
88 - phi * r * k_t * cdf_phi_d2)
89 * 0.0027378507871321013; BlackScholesGreeksResult {
92 price,
93 vol,
94 delta,
95 gamma,
96 vega,
97 theta,
98 }
99}
100
101pub fn imply_vol(s: f64, r: f64, b: f64, is_call: bool, k: f64, t: f64, price: f64) -> f64 {
102 let forward = s * (b * t).exp();
103 let forward_price = price * (r * t).exp();
104
105 ImpliedBlackVolatility::builder()
106 .option_price(forward_price)
107 .forward(forward)
108 .strike(k)
109 .expiry(t)
110 .is_call(is_call)
111 .build_unchecked()
112 .calculate::<DefaultSpecialFn>()
113 .unwrap_or(0.0)
114}
115
116#[allow(clippy::too_many_arguments)]
119pub fn black_scholes_greeks(
120 s: f64,
121 r: f64,
122 b: f64,
123 vol: f64,
124 is_call: bool,
125 k: f64,
126 t: f64,
127 multiplier: f64,
128) -> BlackScholesGreeksResult {
129 let greeks = compute_greeks::<f32>(
132 s as f32, k as f32, t as f32, r as f32, b as f32, vol as f32, is_call,
133 );
134
135 let daily_factor = 0.0027378507871321013; BlackScholesGreeksResult {
141 price: (greeks.price as f64) * multiplier,
142 vol,
143 delta: (greeks.delta as f64) * multiplier,
144 gamma: (greeks.gamma as f64) * multiplier,
145 vega: (greeks.vega as f64) * multiplier * 0.01, theta: (greeks.theta as f64) * multiplier * daily_factor, }
148}
149
150#[allow(clippy::too_many_arguments)]
153pub fn imply_vol_and_greeks(
154 s: f64,
155 r: f64,
156 b: f64,
157 is_call: bool,
158 k: f64,
159 t: f64,
160 price: f64,
161 multiplier: f64,
162) -> BlackScholesGreeksResult {
163 let vol = imply_vol(s, r, b, is_call, k, t, price);
164 let safe_vol = if vol < 1e-8 { 1e-8 } else { vol };
168 black_scholes_greeks(s, r, b, safe_vol, is_call, k, t, multiplier)
169}
170
171#[allow(clippy::too_many_arguments)]
175pub fn refine_vol_and_greeks(
176 s: f64,
177 r: f64,
178 b: f64,
179 is_call: bool,
180 k: f64,
181 t: f64,
182 target_price: f64,
183 initial_vol: f64,
184 multiplier: f64,
185) -> BlackScholesGreeksResult {
186 let greeks = compute_iv_and_greeks::<f32>(
189 target_price as f32,
190 s as f32,
191 k as f32,
192 t as f32,
193 r as f32,
194 b as f32,
195 is_call,
196 initial_vol as f32,
197 );
198
199 let daily_factor = 0.0027378507871321013; BlackScholesGreeksResult {
204 price: (greeks.price as f64) * multiplier,
205 vol: greeks.vol as f64,
206 delta: (greeks.delta as f64) * multiplier,
207 gamma: (greeks.gamma as f64) * multiplier,
208 vega: (greeks.vega as f64) * multiplier * 0.01, theta: (greeks.theta as f64) * multiplier * daily_factor, }
211}
212
213#[derive(Debug, Clone)]
214pub struct GreeksData {
215 pub ts_init: UnixNanos,
216 pub ts_event: UnixNanos,
217 pub instrument_id: InstrumentId,
218 pub is_call: bool,
219 pub strike: f64,
220 pub expiry: i32,
221 pub expiry_in_days: i32,
222 pub expiry_in_years: f64,
223 pub multiplier: f64,
224 pub quantity: f64,
225 pub underlying_price: f64,
226 pub interest_rate: f64,
227 pub cost_of_carry: f64,
228 pub vol: f64,
229 pub pnl: f64,
230 pub price: f64,
231 pub delta: f64,
232 pub gamma: f64,
233 pub vega: f64,
234 pub theta: f64,
235 pub itm_prob: f64,
237}
238
239impl GreeksData {
240 #[allow(clippy::too_many_arguments)]
241 pub fn new(
242 ts_init: UnixNanos,
243 ts_event: UnixNanos,
244 instrument_id: InstrumentId,
245 is_call: bool,
246 strike: f64,
247 expiry: i32,
248 expiry_in_days: i32,
249 expiry_in_years: f64,
250 multiplier: f64,
251 quantity: f64,
252 underlying_price: f64,
253 interest_rate: f64,
254 cost_of_carry: f64,
255 vol: f64,
256 pnl: f64,
257 price: f64,
258 delta: f64,
259 gamma: f64,
260 vega: f64,
261 theta: f64,
262 itm_prob: f64,
263 ) -> Self {
264 Self {
265 ts_init,
266 ts_event,
267 instrument_id,
268 is_call,
269 strike,
270 expiry,
271 expiry_in_days,
272 expiry_in_years,
273 multiplier,
274 quantity,
275 underlying_price,
276 interest_rate,
277 cost_of_carry,
278 vol,
279 pnl,
280 price,
281 delta,
282 gamma,
283 vega,
284 theta,
285 itm_prob,
286 }
287 }
288
289 pub fn from_delta(
290 instrument_id: InstrumentId,
291 delta: f64,
292 multiplier: f64,
293 ts_event: UnixNanos,
294 ) -> Self {
295 Self {
296 ts_init: ts_event,
297 ts_event,
298 instrument_id,
299 is_call: true,
300 strike: 0.0,
301 expiry: 0,
302 expiry_in_days: 0,
303 expiry_in_years: 0.0,
304 multiplier,
305 quantity: 1.0,
306 underlying_price: 0.0,
307 interest_rate: 0.0,
308 cost_of_carry: 0.0,
309 vol: 0.0,
310 pnl: 0.0,
311 price: 0.0,
312 delta,
313 gamma: 0.0,
314 vega: 0.0,
315 theta: 0.0,
316 itm_prob: 0.0,
317 }
318 }
319}
320
321impl Default for GreeksData {
322 fn default() -> Self {
323 Self {
324 ts_init: UnixNanos::default(),
325 ts_event: UnixNanos::default(),
326 instrument_id: InstrumentId::from("ES.GLBX"),
327 is_call: true,
328 strike: 0.0,
329 expiry: 0,
330 expiry_in_days: 0,
331 expiry_in_years: 0.0,
332 multiplier: 0.0,
333 quantity: 0.0,
334 underlying_price: 0.0,
335 interest_rate: 0.0,
336 cost_of_carry: 0.0,
337 vol: 0.0,
338 pnl: 0.0,
339 price: 0.0,
340 delta: 0.0,
341 gamma: 0.0,
342 vega: 0.0,
343 theta: 0.0,
344 itm_prob: 0.0,
345 }
346 }
347}
348
349impl Display for GreeksData {
350 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351 write!(
352 f,
353 "GreeksData(instrument_id={}, expiry={}, itm_prob={:.2}%, vol={:.2}%, pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, quantity={}, ts_init={})",
354 self.instrument_id,
355 self.expiry,
356 self.itm_prob * 100.0,
357 self.vol * 100.0,
358 self.pnl,
359 self.price,
360 self.delta,
361 self.gamma,
362 self.vega,
363 self.theta,
364 self.quantity,
365 unix_nanos_to_iso8601(self.ts_init)
366 )
367 }
368}
369
370impl Mul<&GreeksData> for f64 {
372 type Output = GreeksData;
373
374 fn mul(self, greeks: &GreeksData) -> GreeksData {
375 GreeksData {
376 ts_init: greeks.ts_init,
377 ts_event: greeks.ts_event,
378 instrument_id: greeks.instrument_id,
379 is_call: greeks.is_call,
380 strike: greeks.strike,
381 expiry: greeks.expiry,
382 expiry_in_days: greeks.expiry_in_days,
383 expiry_in_years: greeks.expiry_in_years,
384 multiplier: greeks.multiplier,
385 quantity: greeks.quantity,
386 underlying_price: greeks.underlying_price,
387 interest_rate: greeks.interest_rate,
388 cost_of_carry: greeks.cost_of_carry,
389 vol: greeks.vol,
390 pnl: self * greeks.pnl,
391 price: self * greeks.price,
392 delta: self * greeks.delta,
393 gamma: self * greeks.gamma,
394 vega: self * greeks.vega,
395 theta: self * greeks.theta,
396 itm_prob: greeks.itm_prob,
397 }
398 }
399}
400
401impl HasTsInit for GreeksData {
402 fn ts_init(&self) -> UnixNanos {
403 self.ts_init
404 }
405}
406
407#[derive(Debug, Clone)]
408pub struct PortfolioGreeks {
409 pub ts_init: UnixNanos,
410 pub ts_event: UnixNanos,
411 pub pnl: f64,
412 pub price: f64,
413 pub delta: f64,
414 pub gamma: f64,
415 pub vega: f64,
416 pub theta: f64,
417}
418
419impl PortfolioGreeks {
420 #[allow(clippy::too_many_arguments)]
421 pub fn new(
422 ts_init: UnixNanos,
423 ts_event: UnixNanos,
424 pnl: f64,
425 price: f64,
426 delta: f64,
427 gamma: f64,
428 vega: f64,
429 theta: f64,
430 ) -> Self {
431 Self {
432 ts_init,
433 ts_event,
434 pnl,
435 price,
436 delta,
437 gamma,
438 vega,
439 theta,
440 }
441 }
442}
443
444impl Default for PortfolioGreeks {
445 fn default() -> Self {
446 Self {
447 ts_init: UnixNanos::default(),
448 ts_event: UnixNanos::default(),
449 pnl: 0.0,
450 price: 0.0,
451 delta: 0.0,
452 gamma: 0.0,
453 vega: 0.0,
454 theta: 0.0,
455 }
456 }
457}
458
459impl Display for PortfolioGreeks {
460 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461 write!(
462 f,
463 "PortfolioGreeks(pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, ts_event={}, ts_init={})",
464 self.pnl,
465 self.price,
466 self.delta,
467 self.gamma,
468 self.vega,
469 self.theta,
470 unix_nanos_to_iso8601(self.ts_event),
471 unix_nanos_to_iso8601(self.ts_init)
472 )
473 }
474}
475
476impl Add for PortfolioGreeks {
477 type Output = Self;
478
479 fn add(self, other: Self) -> Self {
480 Self {
481 ts_init: self.ts_init,
482 ts_event: self.ts_event,
483 pnl: self.pnl + other.pnl,
484 price: self.price + other.price,
485 delta: self.delta + other.delta,
486 gamma: self.gamma + other.gamma,
487 vega: self.vega + other.vega,
488 theta: self.theta + other.theta,
489 }
490 }
491}
492
493impl From<GreeksData> for PortfolioGreeks {
494 fn from(greeks: GreeksData) -> Self {
495 Self {
496 ts_init: greeks.ts_init,
497 ts_event: greeks.ts_event,
498 pnl: greeks.pnl,
499 price: greeks.price,
500 delta: greeks.delta,
501 gamma: greeks.gamma,
502 vega: greeks.vega,
503 theta: greeks.theta,
504 }
505 }
506}
507
508impl HasTsInit for PortfolioGreeks {
509 fn ts_init(&self) -> UnixNanos {
510 self.ts_init
511 }
512}
513
514#[derive(Debug, Clone)]
515pub struct YieldCurveData {
516 pub ts_init: UnixNanos,
517 pub ts_event: UnixNanos,
518 pub curve_name: String,
519 pub tenors: Vec<f64>,
520 pub interest_rates: Vec<f64>,
521}
522
523impl YieldCurveData {
524 pub fn new(
525 ts_init: UnixNanos,
526 ts_event: UnixNanos,
527 curve_name: String,
528 tenors: Vec<f64>,
529 interest_rates: Vec<f64>,
530 ) -> Self {
531 Self {
532 ts_init,
533 ts_event,
534 curve_name,
535 tenors,
536 interest_rates,
537 }
538 }
539
540 pub fn get_rate(&self, expiry_in_years: f64) -> f64 {
542 if self.interest_rates.len() == 1 {
543 return self.interest_rates[0];
544 }
545
546 quadratic_interpolation(expiry_in_years, &self.tenors, &self.interest_rates)
547 }
548}
549
550impl Display for YieldCurveData {
551 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552 write!(
553 f,
554 "InterestRateCurve(curve_name={}, ts_event={}, ts_init={})",
555 self.curve_name,
556 unix_nanos_to_iso8601(self.ts_event),
557 unix_nanos_to_iso8601(self.ts_init)
558 )
559 }
560}
561
562impl HasTsInit for YieldCurveData {
563 fn ts_init(&self) -> UnixNanos {
564 self.ts_init
565 }
566}
567
568impl Default for YieldCurveData {
569 fn default() -> Self {
570 Self {
571 ts_init: UnixNanos::default(),
572 ts_event: UnixNanos::default(),
573 curve_name: "USD".to_string(),
574 tenors: vec![0.5, 1.0, 1.5, 2.0, 2.5],
575 interest_rates: vec![0.04, 0.04, 0.04, 0.04, 0.04],
576 }
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use rstest::rstest;
583
584 use super::*;
585 use crate::identifiers::InstrumentId;
586
587 fn create_test_greeks_data() -> GreeksData {
588 GreeksData::new(
589 UnixNanos::from(1_000_000_000),
590 UnixNanos::from(1_500_000_000),
591 InstrumentId::from("SPY240315C00500000.OPRA"),
592 true,
593 500.0,
594 20240315,
595 91, 0.25,
597 100.0,
598 1.0,
599 520.0,
600 0.05,
601 0.05,
602 0.2,
603 250.0,
604 25.5,
605 0.65,
606 0.003,
607 15.2,
608 -0.08,
609 0.75,
610 )
611 }
612
613 fn create_test_portfolio_greeks() -> PortfolioGreeks {
614 PortfolioGreeks::new(
615 UnixNanos::from(1_000_000_000),
616 UnixNanos::from(1_500_000_000),
617 1500.0,
618 125.5,
619 2.15,
620 0.008,
621 42.7,
622 -2.3,
623 )
624 }
625
626 fn create_test_yield_curve() -> YieldCurveData {
627 YieldCurveData::new(
628 UnixNanos::from(1_000_000_000),
629 UnixNanos::from(1_500_000_000),
630 "USD".to_string(),
631 vec![0.25, 0.5, 1.0, 2.0, 5.0],
632 vec![0.025, 0.03, 0.035, 0.04, 0.045],
633 )
634 }
635
636 #[rstest]
637 fn test_black_scholes_greeks_result_creation() {
638 let result = BlackScholesGreeksResult {
639 price: 25.5,
640 vol: 0.2,
641 delta: 0.65,
642 gamma: 0.003,
643 vega: 15.2,
644 theta: -0.08,
645 };
646
647 assert_eq!(result.price, 25.5);
648 assert_eq!(result.delta, 0.65);
649 assert_eq!(result.gamma, 0.003);
650 assert_eq!(result.vega, 15.2);
651 assert_eq!(result.theta, -0.08);
652 }
653
654 #[rstest]
655 fn test_black_scholes_greeks_result_clone_and_copy() {
656 let result1 = BlackScholesGreeksResult {
657 price: 25.5,
658 vol: 0.2,
659 delta: 0.65,
660 gamma: 0.003,
661 vega: 15.2,
662 theta: -0.08,
663 };
664 let result2 = result1;
665 let result3 = result1;
666
667 assert_eq!(result1, result2);
668 assert_eq!(result1, result3);
669 }
670
671 #[rstest]
672 fn test_black_scholes_greeks_result_debug() {
673 let result = BlackScholesGreeksResult {
674 price: 25.5,
675 vol: 0.2,
676 delta: 0.65,
677 gamma: 0.003,
678 vega: 15.2,
679 theta: -0.08,
680 };
681 let debug_str = format!("{result:?}");
682
683 assert!(debug_str.contains("BlackScholesGreeksResult"));
684 assert!(debug_str.contains("25.5"));
685 assert!(debug_str.contains("0.65"));
686 }
687
688 #[rstest]
689 fn test_imply_vol_and_greeks_result_creation() {
690 let result = BlackScholesGreeksResult {
691 price: 25.5,
692 vol: 0.2,
693 delta: 0.65,
694 gamma: 0.003,
695 vega: 15.2,
696 theta: -0.08,
697 };
698
699 assert_eq!(result.vol, 0.2);
700 assert_eq!(result.price, 25.5);
701 assert_eq!(result.delta, 0.65);
702 assert_eq!(result.gamma, 0.003);
703 assert_eq!(result.vega, 15.2);
704 assert_eq!(result.theta, -0.08);
705 }
706
707 #[rstest]
708 fn test_black_scholes_greeks_basic_call() {
709 let s = 100.0;
710 let r = 0.05;
711 let b = 0.05;
712 let vol = 0.2;
713 let is_call = true;
714 let k = 100.0;
715 let t = 1.0;
716 let multiplier = 1.0;
717
718 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, multiplier);
719
720 assert!(greeks.price > 0.0);
721 assert!(greeks.delta > 0.0 && greeks.delta < 1.0);
722 assert!(greeks.gamma > 0.0);
723 assert!(greeks.vega > 0.0);
724 assert!(greeks.theta < 0.0); }
726
727 #[rstest]
728 fn test_black_scholes_greeks_basic_put() {
729 let s = 100.0;
730 let r = 0.05;
731 let b = 0.05;
732 let vol = 0.2;
733 let is_call = false;
734 let k = 100.0;
735 let t = 1.0;
736 let multiplier = 1.0;
737
738 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, multiplier);
739
740 assert!(
741 greeks.price > 0.0,
742 "Put option price should be positive, got: {}",
743 greeks.price
744 );
745 assert!(greeks.delta < 0.0 && greeks.delta > -1.0);
746 assert!(greeks.gamma > 0.0);
747 assert!(greeks.vega > 0.0);
748 assert!(greeks.theta < 0.0); }
750
751 #[rstest]
752 fn test_black_scholes_greeks_with_multiplier() {
753 let s = 100.0;
754 let r = 0.05;
755 let b = 0.05;
756 let vol = 0.2;
757 let is_call = true;
758 let k = 100.0;
759 let t = 1.0;
760 let multiplier = 100.0;
761
762 let greeks_1x = black_scholes_greeks(s, r, b, vol, is_call, k, t, 1.0);
763 let greeks_100x = black_scholes_greeks(s, r, b, vol, is_call, k, t, multiplier);
764
765 let tolerance = 1e-10;
766 assert!((greeks_100x.price - greeks_1x.price * 100.0).abs() < tolerance);
767 assert!((greeks_100x.delta - greeks_1x.delta * 100.0).abs() < tolerance);
768 assert!((greeks_100x.gamma - greeks_1x.gamma * 100.0).abs() < tolerance);
769 assert!((greeks_100x.vega - greeks_1x.vega * 100.0).abs() < tolerance);
770 assert!((greeks_100x.theta - greeks_1x.theta * 100.0).abs() < tolerance);
771 }
772
773 #[rstest]
774 fn test_black_scholes_greeks_deep_itm_call() {
775 let s = 150.0;
776 let r = 0.05;
777 let b = 0.05;
778 let vol = 0.2;
779 let is_call = true;
780 let k = 100.0;
781 let t = 1.0;
782 let multiplier = 1.0;
783
784 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, multiplier);
785
786 assert!(greeks.delta > 0.9); assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); }
789
790 #[rstest]
791 fn test_black_scholes_greeks_deep_otm_call() {
792 let s = 50.0;
793 let r = 0.05;
794 let b = 0.05;
795 let vol = 0.2;
796 let is_call = true;
797 let k = 100.0;
798 let t = 1.0;
799 let multiplier = 1.0;
800
801 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, multiplier);
802
803 assert!(greeks.delta < 0.1); assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); }
806
807 #[rstest]
808 fn test_black_scholes_greeks_zero_time() {
809 let s = 100.0;
810 let r = 0.05;
811 let b = 0.05;
812 let vol = 0.2;
813 let is_call = true;
814 let k = 100.0;
815 let t = 0.0001; let multiplier = 1.0;
817
818 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, multiplier);
819
820 assert!(greeks.price >= 0.0);
821 assert!(greeks.theta.is_finite());
822 }
823
824 #[rstest]
825 fn test_imply_vol_basic() {
826 let s = 100.0;
827 let r = 0.05;
828 let b = 0.05;
829 let vol = 0.2;
830 let is_call = true;
831 let k = 100.0;
832 let t = 1.0;
833
834 let theoretical_price = black_scholes_greeks(s, r, b, vol, is_call, k, t, 1.0).price;
835 let implied_vol = imply_vol(s, r, b, is_call, k, t, theoretical_price);
836
837 let tolerance = 1e-4;
839 assert!(
840 (implied_vol - vol).abs() < tolerance,
841 "Implied vol difference exceeds tolerance: {implied_vol} vs {vol}"
842 );
843 }
844
845 #[rstest]
852 fn test_greeks_data_new() {
853 let greeks = create_test_greeks_data();
854
855 assert_eq!(greeks.ts_init, UnixNanos::from(1_000_000_000));
856 assert_eq!(greeks.ts_event, UnixNanos::from(1_500_000_000));
857 assert_eq!(
858 greeks.instrument_id,
859 InstrumentId::from("SPY240315C00500000.OPRA")
860 );
861 assert!(greeks.is_call);
862 assert_eq!(greeks.strike, 500.0);
863 assert_eq!(greeks.expiry, 20240315);
864 assert_eq!(greeks.expiry_in_years, 0.25);
865 assert_eq!(greeks.multiplier, 100.0);
866 assert_eq!(greeks.quantity, 1.0);
867 assert_eq!(greeks.underlying_price, 520.0);
868 assert_eq!(greeks.interest_rate, 0.05);
869 assert_eq!(greeks.cost_of_carry, 0.05);
870 assert_eq!(greeks.vol, 0.2);
871 assert_eq!(greeks.pnl, 250.0);
872 assert_eq!(greeks.price, 25.5);
873 assert_eq!(greeks.delta, 0.65);
874 assert_eq!(greeks.gamma, 0.003);
875 assert_eq!(greeks.vega, 15.2);
876 assert_eq!(greeks.theta, -0.08);
877 assert_eq!(greeks.itm_prob, 0.75);
878 }
879
880 #[rstest]
881 fn test_greeks_data_from_delta() {
882 let delta = 0.5;
883 let multiplier = 100.0;
884 let ts_event = UnixNanos::from(2_000_000_000);
885 let instrument_id = InstrumentId::from("AAPL240315C00180000.OPRA");
886
887 let greeks = GreeksData::from_delta(instrument_id, delta, multiplier, ts_event);
888
889 assert_eq!(greeks.ts_init, ts_event);
890 assert_eq!(greeks.ts_event, ts_event);
891 assert_eq!(greeks.instrument_id, instrument_id);
892 assert!(greeks.is_call);
893 assert_eq!(greeks.delta, delta);
894 assert_eq!(greeks.multiplier, multiplier);
895 assert_eq!(greeks.quantity, 1.0);
896
897 assert_eq!(greeks.strike, 0.0);
899 assert_eq!(greeks.expiry, 0);
900 assert_eq!(greeks.price, 0.0);
901 assert_eq!(greeks.gamma, 0.0);
902 assert_eq!(greeks.vega, 0.0);
903 assert_eq!(greeks.theta, 0.0);
904 }
905
906 #[rstest]
907 fn test_greeks_data_default() {
908 let greeks = GreeksData::default();
909
910 assert_eq!(greeks.ts_init, UnixNanos::default());
911 assert_eq!(greeks.ts_event, UnixNanos::default());
912 assert_eq!(greeks.instrument_id, InstrumentId::from("ES.GLBX"));
913 assert!(greeks.is_call);
914 assert_eq!(greeks.strike, 0.0);
915 assert_eq!(greeks.expiry, 0);
916 assert_eq!(greeks.multiplier, 0.0);
917 assert_eq!(greeks.quantity, 0.0);
918 assert_eq!(greeks.delta, 0.0);
919 assert_eq!(greeks.gamma, 0.0);
920 assert_eq!(greeks.vega, 0.0);
921 assert_eq!(greeks.theta, 0.0);
922 }
923
924 #[rstest]
925 fn test_greeks_data_display() {
926 let greeks = create_test_greeks_data();
927 let display_str = format!("{greeks}");
928
929 assert!(display_str.contains("GreeksData"));
930 assert!(display_str.contains("SPY240315C00500000.OPRA"));
931 assert!(display_str.contains("20240315"));
932 assert!(display_str.contains("75.00%")); assert!(display_str.contains("20.00%")); assert!(display_str.contains("250.00")); assert!(display_str.contains("25.50")); assert!(display_str.contains("0.65")); }
938
939 #[rstest]
940 fn test_greeks_data_multiplication() {
941 let greeks = create_test_greeks_data();
942 let quantity = 5.0;
943 let scaled_greeks = quantity * &greeks;
944
945 assert_eq!(scaled_greeks.ts_init, greeks.ts_init);
946 assert_eq!(scaled_greeks.ts_event, greeks.ts_event);
947 assert_eq!(scaled_greeks.instrument_id, greeks.instrument_id);
948 assert_eq!(scaled_greeks.is_call, greeks.is_call);
949 assert_eq!(scaled_greeks.strike, greeks.strike);
950 assert_eq!(scaled_greeks.expiry, greeks.expiry);
951 assert_eq!(scaled_greeks.multiplier, greeks.multiplier);
952 assert_eq!(scaled_greeks.quantity, greeks.quantity);
953 assert_eq!(scaled_greeks.vol, greeks.vol);
954 assert_eq!(scaled_greeks.itm_prob, greeks.itm_prob);
955
956 assert_eq!(scaled_greeks.pnl, quantity * greeks.pnl);
958 assert_eq!(scaled_greeks.price, quantity * greeks.price);
959 assert_eq!(scaled_greeks.delta, quantity * greeks.delta);
960 assert_eq!(scaled_greeks.gamma, quantity * greeks.gamma);
961 assert_eq!(scaled_greeks.vega, quantity * greeks.vega);
962 assert_eq!(scaled_greeks.theta, quantity * greeks.theta);
963 }
964
965 #[rstest]
966 fn test_greeks_data_has_ts_init() {
967 let greeks = create_test_greeks_data();
968 assert_eq!(greeks.ts_init(), UnixNanos::from(1_000_000_000));
969 }
970
971 #[rstest]
972 fn test_greeks_data_clone() {
973 let greeks1 = create_test_greeks_data();
974 let greeks2 = greeks1.clone();
975
976 assert_eq!(greeks1.ts_init, greeks2.ts_init);
977 assert_eq!(greeks1.instrument_id, greeks2.instrument_id);
978 assert_eq!(greeks1.delta, greeks2.delta);
979 assert_eq!(greeks1.gamma, greeks2.gamma);
980 }
981
982 #[rstest]
983 fn test_portfolio_greeks_new() {
984 let portfolio_greeks = create_test_portfolio_greeks();
985
986 assert_eq!(portfolio_greeks.ts_init, UnixNanos::from(1_000_000_000));
987 assert_eq!(portfolio_greeks.ts_event, UnixNanos::from(1_500_000_000));
988 assert_eq!(portfolio_greeks.pnl, 1500.0);
989 assert_eq!(portfolio_greeks.price, 125.5);
990 assert_eq!(portfolio_greeks.delta, 2.15);
991 assert_eq!(portfolio_greeks.gamma, 0.008);
992 assert_eq!(portfolio_greeks.vega, 42.7);
993 assert_eq!(portfolio_greeks.theta, -2.3);
994 }
995
996 #[rstest]
997 fn test_portfolio_greeks_default() {
998 let portfolio_greeks = PortfolioGreeks::default();
999
1000 assert_eq!(portfolio_greeks.ts_init, UnixNanos::default());
1001 assert_eq!(portfolio_greeks.ts_event, UnixNanos::default());
1002 assert_eq!(portfolio_greeks.pnl, 0.0);
1003 assert_eq!(portfolio_greeks.price, 0.0);
1004 assert_eq!(portfolio_greeks.delta, 0.0);
1005 assert_eq!(portfolio_greeks.gamma, 0.0);
1006 assert_eq!(portfolio_greeks.vega, 0.0);
1007 assert_eq!(portfolio_greeks.theta, 0.0);
1008 }
1009
1010 #[rstest]
1011 fn test_portfolio_greeks_display() {
1012 let portfolio_greeks = create_test_portfolio_greeks();
1013 let display_str = format!("{portfolio_greeks}");
1014
1015 assert!(display_str.contains("PortfolioGreeks"));
1016 assert!(display_str.contains("1500.00")); assert!(display_str.contains("125.50")); assert!(display_str.contains("2.15")); assert!(display_str.contains("0.01")); assert!(display_str.contains("42.70")); assert!(display_str.contains("-2.30")); }
1023
1024 #[rstest]
1025 fn test_portfolio_greeks_addition() {
1026 let greeks1 = PortfolioGreeks::new(
1027 UnixNanos::from(1_000_000_000),
1028 UnixNanos::from(1_500_000_000),
1029 100.0,
1030 50.0,
1031 1.0,
1032 0.005,
1033 20.0,
1034 -1.0,
1035 );
1036 let greeks2 = PortfolioGreeks::new(
1037 UnixNanos::from(2_000_000_000),
1038 UnixNanos::from(2_500_000_000),
1039 200.0,
1040 75.0,
1041 1.5,
1042 0.003,
1043 25.0,
1044 -1.5,
1045 );
1046
1047 let result = greeks1 + greeks2;
1048
1049 assert_eq!(result.ts_init, UnixNanos::from(1_000_000_000)); assert_eq!(result.ts_event, UnixNanos::from(1_500_000_000)); assert_eq!(result.pnl, 300.0);
1052 assert_eq!(result.price, 125.0);
1053 assert_eq!(result.delta, 2.5);
1054 assert_eq!(result.gamma, 0.008);
1055 assert_eq!(result.vega, 45.0);
1056 assert_eq!(result.theta, -2.5);
1057 }
1058
1059 #[rstest]
1060 fn test_portfolio_greeks_from_greeks_data() {
1061 let greeks_data = create_test_greeks_data();
1062 let portfolio_greeks: PortfolioGreeks = greeks_data.clone().into();
1063
1064 assert_eq!(portfolio_greeks.ts_init, greeks_data.ts_init);
1065 assert_eq!(portfolio_greeks.ts_event, greeks_data.ts_event);
1066 assert_eq!(portfolio_greeks.pnl, greeks_data.pnl);
1067 assert_eq!(portfolio_greeks.price, greeks_data.price);
1068 assert_eq!(portfolio_greeks.delta, greeks_data.delta);
1069 assert_eq!(portfolio_greeks.gamma, greeks_data.gamma);
1070 assert_eq!(portfolio_greeks.vega, greeks_data.vega);
1071 assert_eq!(portfolio_greeks.theta, greeks_data.theta);
1072 }
1073
1074 #[rstest]
1075 fn test_portfolio_greeks_has_ts_init() {
1076 let portfolio_greeks = create_test_portfolio_greeks();
1077 assert_eq!(portfolio_greeks.ts_init(), UnixNanos::from(1_000_000_000));
1078 }
1079
1080 #[rstest]
1081 fn test_yield_curve_data_new() {
1082 let curve = create_test_yield_curve();
1083
1084 assert_eq!(curve.ts_init, UnixNanos::from(1_000_000_000));
1085 assert_eq!(curve.ts_event, UnixNanos::from(1_500_000_000));
1086 assert_eq!(curve.curve_name, "USD");
1087 assert_eq!(curve.tenors, vec![0.25, 0.5, 1.0, 2.0, 5.0]);
1088 assert_eq!(curve.interest_rates, vec![0.025, 0.03, 0.035, 0.04, 0.045]);
1089 }
1090
1091 #[rstest]
1092 fn test_yield_curve_data_default() {
1093 let curve = YieldCurveData::default();
1094
1095 assert_eq!(curve.ts_init, UnixNanos::default());
1096 assert_eq!(curve.ts_event, UnixNanos::default());
1097 assert_eq!(curve.curve_name, "USD");
1098 assert_eq!(curve.tenors, vec![0.5, 1.0, 1.5, 2.0, 2.5]);
1099 assert_eq!(curve.interest_rates, vec![0.04, 0.04, 0.04, 0.04, 0.04]);
1100 }
1101
1102 #[rstest]
1103 fn test_yield_curve_data_get_rate_single_point() {
1104 let curve = YieldCurveData::new(
1105 UnixNanos::default(),
1106 UnixNanos::default(),
1107 "USD".to_string(),
1108 vec![1.0],
1109 vec![0.05],
1110 );
1111
1112 assert_eq!(curve.get_rate(0.5), 0.05);
1113 assert_eq!(curve.get_rate(1.0), 0.05);
1114 assert_eq!(curve.get_rate(2.0), 0.05);
1115 }
1116
1117 #[rstest]
1118 fn test_yield_curve_data_get_rate_interpolation() {
1119 let curve = create_test_yield_curve();
1120
1121 assert_eq!(curve.get_rate(0.25), 0.025);
1123 assert_eq!(curve.get_rate(1.0), 0.035);
1124 assert_eq!(curve.get_rate(5.0), 0.045);
1125
1126 let rate_0_75 = curve.get_rate(0.75);
1128 assert!(rate_0_75 > 0.025 && rate_0_75 < 0.045);
1129 }
1130
1131 #[rstest]
1132 fn test_yield_curve_data_display() {
1133 let curve = create_test_yield_curve();
1134 let display_str = format!("{curve}");
1135
1136 assert!(display_str.contains("InterestRateCurve"));
1137 assert!(display_str.contains("USD"));
1138 }
1139
1140 #[rstest]
1141 fn test_yield_curve_data_has_ts_init() {
1142 let curve = create_test_yield_curve();
1143 assert_eq!(curve.ts_init(), UnixNanos::from(1_000_000_000));
1144 }
1145
1146 #[rstest]
1147 fn test_yield_curve_data_clone() {
1148 let curve1 = create_test_yield_curve();
1149 let curve2 = curve1.clone();
1150
1151 assert_eq!(curve1.curve_name, curve2.curve_name);
1152 assert_eq!(curve1.tenors, curve2.tenors);
1153 assert_eq!(curve1.interest_rates, curve2.interest_rates);
1154 }
1155
1156 #[rstest]
1157 fn test_black_scholes_greeks_extreme_values() {
1158 let s = 1000.0;
1159 let r = 0.1;
1160 let b = 0.1;
1161 let vol = 0.5;
1162 let is_call = true;
1163 let k = 10.0; let t = 0.1;
1165 let multiplier = 1.0;
1166
1167 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, multiplier);
1168
1169 assert!(greeks.price.is_finite());
1170 assert!(greeks.delta.is_finite());
1171 assert!(greeks.gamma.is_finite());
1172 assert!(greeks.vega.is_finite());
1173 assert!(greeks.theta.is_finite());
1174 assert!(greeks.price > 0.0);
1175 assert!(greeks.delta > 0.99); }
1177
1178 #[rstest]
1179 fn test_black_scholes_greeks_high_volatility() {
1180 let s = 100.0;
1181 let r = 0.05;
1182 let b = 0.05;
1183 let vol = 2.0; let is_call = true;
1185 let k = 100.0;
1186 let t = 1.0;
1187 let multiplier = 1.0;
1188
1189 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, multiplier);
1190
1191 assert!(greeks.price.is_finite());
1192 assert!(greeks.delta.is_finite());
1193 assert!(greeks.gamma.is_finite());
1194 assert!(greeks.vega.is_finite());
1195 assert!(greeks.theta.is_finite());
1196 assert!(greeks.price > 0.0);
1197 }
1198
1199 #[rstest]
1200 fn test_greeks_data_put_option() {
1201 let greeks = GreeksData::new(
1202 UnixNanos::from(1_000_000_000),
1203 UnixNanos::from(1_500_000_000),
1204 InstrumentId::from("SPY240315P00480000.OPRA"),
1205 false, 480.0,
1207 20240315,
1208 91, 0.25,
1210 100.0,
1211 1.0,
1212 500.0,
1213 0.05,
1214 0.05,
1215 0.25,
1216 -150.0, 8.5,
1218 -0.35, 0.002,
1220 12.8,
1221 -0.06,
1222 0.25,
1223 );
1224
1225 assert!(!greeks.is_call);
1226 assert!(greeks.delta < 0.0);
1227 assert_eq!(greeks.pnl, -150.0);
1228 }
1229
1230 #[rstest]
1232 fn test_greeks_accuracy_call() {
1233 let s = 100.0;
1234 let k = 100.1;
1235 let t = 1.0;
1236 let r = 0.01;
1237 let b = 0.005;
1238 let vol = 0.2;
1239 let is_call = true;
1240 let eps = 1e-3;
1241
1242 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, 1.0);
1243
1244 let price0 = |s: f64| black_scholes_greeks_exact(s, r, b, vol, is_call, k, t, 1.0).price;
1246
1247 let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
1248 let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
1249 let vega_bnr = (black_scholes_greeks_exact(s, r, b, vol + eps, is_call, k, t, 1.0).price
1250 - black_scholes_greeks_exact(s, r, b, vol - eps, is_call, k, t, 1.0).price)
1251 / (2.0 * eps)
1252 / 100.0;
1253 let theta_bnr = (black_scholes_greeks_exact(s, r, b, vol, is_call, k, t - eps, 1.0).price
1254 - black_scholes_greeks_exact(s, r, b, vol, is_call, k, t + eps, 1.0).price)
1255 / (2.0 * eps)
1256 / 365.25;
1257
1258 let tolerance = 5e-3;
1261 assert!(
1262 (greeks.delta - delta_bnr).abs() < tolerance,
1263 "Delta difference exceeds tolerance: {} vs {}",
1264 greeks.delta,
1265 delta_bnr
1266 );
1267 let gamma_tolerance = 0.1;
1269 assert!(
1270 (greeks.gamma - gamma_bnr).abs() < gamma_tolerance,
1271 "Gamma difference exceeds tolerance: {} vs {}",
1272 greeks.gamma,
1273 gamma_bnr
1274 );
1275 assert!(
1276 (greeks.vega - vega_bnr).abs() < tolerance,
1277 "Vega difference exceeds tolerance: {} vs {}",
1278 greeks.vega,
1279 vega_bnr
1280 );
1281 assert!(
1282 (greeks.theta - theta_bnr).abs() < tolerance,
1283 "Theta difference exceeds tolerance: {} vs {}",
1284 greeks.theta,
1285 theta_bnr
1286 );
1287 }
1288
1289 #[rstest]
1290 fn test_greeks_accuracy_put() {
1291 let s = 100.0;
1292 let k = 100.1;
1293 let t = 1.0;
1294 let r = 0.01;
1295 let b = 0.005;
1296 let vol = 0.2;
1297 let is_call = false;
1298 let eps = 1e-3;
1299
1300 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, 1.0);
1301
1302 let price0 = |s: f64| black_scholes_greeks_exact(s, r, b, vol, is_call, k, t, 1.0).price;
1304
1305 let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
1306 let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
1307 let vega_bnr = (black_scholes_greeks_exact(s, r, b, vol + eps, is_call, k, t, 1.0).price
1308 - black_scholes_greeks_exact(s, r, b, vol - eps, is_call, k, t, 1.0).price)
1309 / (2.0 * eps)
1310 / 100.0;
1311 let theta_bnr = (black_scholes_greeks_exact(s, r, b, vol, is_call, k, t - eps, 1.0).price
1312 - black_scholes_greeks_exact(s, r, b, vol, is_call, k, t + eps, 1.0).price)
1313 / (2.0 * eps)
1314 / 365.25;
1315
1316 let tolerance = 5e-3;
1319 assert!(
1320 (greeks.delta - delta_bnr).abs() < tolerance,
1321 "Delta difference exceeds tolerance: {} vs {}",
1322 greeks.delta,
1323 delta_bnr
1324 );
1325 let gamma_tolerance = 0.1;
1327 assert!(
1328 (greeks.gamma - gamma_bnr).abs() < gamma_tolerance,
1329 "Gamma difference exceeds tolerance: {} vs {}",
1330 greeks.gamma,
1331 gamma_bnr
1332 );
1333 assert!(
1334 (greeks.vega - vega_bnr).abs() < tolerance,
1335 "Vega difference exceeds tolerance: {} vs {}",
1336 greeks.vega,
1337 vega_bnr
1338 );
1339 assert!(
1340 (greeks.theta - theta_bnr).abs() < tolerance,
1341 "Theta difference exceeds tolerance: {} vs {}",
1342 greeks.theta,
1343 theta_bnr
1344 );
1345 }
1346
1347 #[rstest]
1348 fn test_imply_vol_and_greeks_accuracy_call() {
1349 let s = 100.0;
1350 let k = 100.1;
1351 let t = 1.0;
1352 let r = 0.01;
1353 let b = 0.005;
1354 let vol = 0.2;
1355 let is_call = true;
1356
1357 let base_greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, 1.0);
1358 let price = base_greeks.price;
1359
1360 let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price, 1.0);
1361
1362 let tolerance = 2e-4;
1364 assert!(
1365 (implied_result.vol - vol).abs() < tolerance,
1366 "Vol difference exceeds tolerance: {} vs {}",
1367 implied_result.vol,
1368 vol
1369 );
1370 assert!(
1371 (implied_result.price - base_greeks.price).abs() < tolerance,
1372 "Price difference exceeds tolerance: {} vs {}",
1373 implied_result.price,
1374 base_greeks.price
1375 );
1376 assert!(
1377 (implied_result.delta - base_greeks.delta).abs() < tolerance,
1378 "Delta difference exceeds tolerance: {} vs {}",
1379 implied_result.delta,
1380 base_greeks.delta
1381 );
1382 assert!(
1383 (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
1384 "Gamma difference exceeds tolerance: {} vs {}",
1385 implied_result.gamma,
1386 base_greeks.gamma
1387 );
1388 assert!(
1389 (implied_result.vega - base_greeks.vega).abs() < tolerance,
1390 "Vega difference exceeds tolerance: {} vs {}",
1391 implied_result.vega,
1392 base_greeks.vega
1393 );
1394 assert!(
1395 (implied_result.theta - base_greeks.theta).abs() < tolerance,
1396 "Theta difference exceeds tolerance: {} vs {}",
1397 implied_result.theta,
1398 base_greeks.theta
1399 );
1400 }
1401
1402 #[rstest]
1403 fn test_black_scholes_greeks_target_price_refinement() {
1404 let s = 100.0;
1405 let r = 0.05;
1406 let b = 0.05;
1407 let initial_vol = 0.2;
1408 let is_call = true;
1409 let k = 100.0;
1410 let t = 1.0;
1411 let multiplier = 1.0;
1412
1413 let initial_greeks = black_scholes_greeks(s, r, b, initial_vol, is_call, k, t, multiplier);
1415 let target_price = initial_greeks.price;
1416
1417 let refined_vol = initial_vol * 1.1; let refined_greeks = refine_vol_and_greeks(
1420 s,
1421 r,
1422 b,
1423 is_call,
1424 k,
1425 t,
1426 target_price,
1427 refined_vol,
1428 multiplier,
1429 );
1430
1431 let price_tolerance = (s * 5e-5 * multiplier).max(1e-4) * 2.0;
1434 assert!(
1435 (refined_greeks.price - target_price).abs() < price_tolerance,
1436 "Refined price should match target: {} vs {}",
1437 refined_greeks.price,
1438 target_price
1439 );
1440
1441 assert!(
1443 refined_vol > refined_greeks.vol && refined_greeks.vol > initial_vol * 0.9,
1444 "Refined vol should converge towards initial: {} (initial: {}, refined: {})",
1445 refined_greeks.vol,
1446 initial_vol,
1447 refined_vol
1448 );
1449 }
1450
1451 #[rstest]
1452 fn test_black_scholes_greeks_target_price_refinement_put() {
1453 let s = 100.0;
1454 let r = 0.05;
1455 let b = 0.05;
1456 let initial_vol = 0.25;
1457 let is_call = false;
1458 let k = 105.0;
1459 let t = 0.5;
1460 let multiplier = 1.0;
1461
1462 let initial_greeks = black_scholes_greeks(s, r, b, initial_vol, is_call, k, t, multiplier);
1464 let target_price = initial_greeks.price;
1465
1466 let refined_vol = initial_vol * 0.8; let refined_greeks = refine_vol_and_greeks(
1469 s,
1470 r,
1471 b,
1472 is_call,
1473 k,
1474 t,
1475 target_price,
1476 refined_vol,
1477 multiplier,
1478 );
1479
1480 let price_tolerance = (s * 5e-5 * multiplier).max(1e-4) * 2.0;
1483 assert!(
1484 (refined_greeks.price - target_price).abs() < price_tolerance,
1485 "Refined price should match target: {} vs {}",
1486 refined_greeks.price,
1487 target_price
1488 );
1489
1490 assert!(
1492 refined_vol < refined_greeks.vol && refined_greeks.vol < initial_vol * 1.1,
1493 "Refined vol should converge towards initial: {} (initial: {}, refined: {})",
1494 refined_greeks.vol,
1495 initial_vol,
1496 refined_vol
1497 );
1498 }
1499
1500 #[rstest]
1501 fn test_imply_vol_and_greeks_accuracy_put() {
1502 let s = 100.0;
1503 let k = 100.1;
1504 let t = 1.0;
1505 let r = 0.01;
1506 let b = 0.005;
1507 let vol = 0.2;
1508 let is_call = false;
1509
1510 let base_greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, 1.0);
1511 let price = base_greeks.price;
1512
1513 let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price, 1.0);
1514
1515 let tolerance = 2e-4;
1517 assert!(
1518 (implied_result.vol - vol).abs() < tolerance,
1519 "Vol difference exceeds tolerance: {} vs {}",
1520 implied_result.vol,
1521 vol
1522 );
1523 assert!(
1524 (implied_result.price - base_greeks.price).abs() < tolerance,
1525 "Price difference exceeds tolerance: {} vs {}",
1526 implied_result.price,
1527 base_greeks.price
1528 );
1529 assert!(
1530 (implied_result.delta - base_greeks.delta).abs() < tolerance,
1531 "Delta difference exceeds tolerance: {} vs {}",
1532 implied_result.delta,
1533 base_greeks.delta
1534 );
1535 assert!(
1536 (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
1537 "Gamma difference exceeds tolerance: {} vs {}",
1538 implied_result.gamma,
1539 base_greeks.gamma
1540 );
1541 assert!(
1542 (implied_result.vega - base_greeks.vega).abs() < tolerance,
1543 "Vega difference exceeds tolerance: {} vs {}",
1544 implied_result.vega,
1545 base_greeks.vega
1546 );
1547 assert!(
1548 (implied_result.theta - base_greeks.theta).abs() < tolerance,
1549 "Theta difference exceeds tolerance: {} vs {}",
1550 implied_result.theta,
1551 base_greeks.theta
1552 );
1553 }
1554
1555 #[rstest]
1558 fn test_black_scholes_greeks_vs_exact(
1559 #[values(90.0, 100.0, 110.0)] spot: f64,
1560 #[values(true, false)] is_call: bool,
1561 #[values(0.15, 0.25, 0.5)] vol: f64,
1562 #[values(0.01, 0.25, 2.0)] t: f64,
1563 ) {
1564 let r = 0.05;
1565 let b = 0.05;
1566 let k = 100.0;
1567 let multiplier = 1.0;
1568
1569 let greeks_fast = black_scholes_greeks(spot, r, b, vol, is_call, k, t, multiplier);
1570 let greeks_exact = black_scholes_greeks_exact(spot, r, b, vol, is_call, k, t, multiplier);
1571
1572 let rel_tolerance = if t < 0.1 {
1577 1e-4 } else {
1579 8e-6 };
1581 let abs_tolerance = 1e-10; let check_7_sig_figs = |fast: f64, exact: f64, name: &str| {
1585 let abs_diff = (fast - exact).abs();
1586 let small_value_threshold = 1e-4;
1590 let max_allowed = if exact.abs() < small_value_threshold {
1591 if t < 0.1 {
1593 1e-5 } else {
1595 1e-6 }
1597 } else {
1598 exact.abs().max(abs_tolerance) * rel_tolerance
1600 };
1601 let rel_diff = if exact.abs() > abs_tolerance {
1602 abs_diff / exact.abs()
1603 } else {
1604 0.0 };
1606
1607 assert!(
1608 abs_diff < max_allowed,
1609 "{name} mismatch for spot={spot}, is_call={is_call}, vol={vol}, t={t}: fast={fast:.10}, exact={exact:.10}, abs_diff={abs_diff:.2e}, rel_diff={rel_diff:.2e}, max_allowed={max_allowed:.2e}"
1610 );
1611 };
1612
1613 check_7_sig_figs(greeks_fast.price, greeks_exact.price, "Price");
1614 check_7_sig_figs(greeks_fast.delta, greeks_exact.delta, "Delta");
1615 check_7_sig_figs(greeks_fast.gamma, greeks_exact.gamma, "Gamma");
1616 check_7_sig_figs(greeks_fast.vega, greeks_exact.vega, "Vega");
1617 check_7_sig_figs(greeks_fast.theta, greeks_exact.theta, "Theta");
1618 }
1619
1620 #[rstest]
1623 fn test_refine_vol_and_greeks_vs_imply_vol_and_greeks(
1624 #[values(90.0, 100.0, 110.0)] spot: f64,
1625 #[values(true, false)] is_call: bool,
1626 #[values(0.15, 0.25, 0.5)] target_vol: f64,
1627 #[values(0.01, 0.25, 2.0)] t: f64,
1628 ) {
1629 let r = 0.05;
1630 let b = 0.05;
1631 let k = 100.0;
1632 let multiplier = 1.0;
1633
1634 let base_greeks = black_scholes_greeks(spot, r, b, target_vol, is_call, k, t, multiplier);
1636 let target_price = base_greeks.price;
1637
1638 let initial_guess = target_vol - 0.01;
1640
1641 let refined_result = refine_vol_and_greeks(
1643 spot,
1644 r,
1645 b,
1646 is_call,
1647 k,
1648 t,
1649 target_price,
1650 initial_guess,
1651 multiplier,
1652 );
1653
1654 let implied_result =
1656 imply_vol_and_greeks(spot, r, b, is_call, k, t, target_price, multiplier);
1657
1658 let moneyness = (spot - k) / k;
1661 let is_deep_itm_otm = moneyness.abs() > 0.05;
1662 let is_deep_edge_case = t < 0.1 && is_deep_itm_otm;
1663
1664 let vol_abs_tolerance = 1e-6;
1670 let vol_rel_tolerance = if is_deep_edge_case {
1671 2.0 } else if t < 0.1 {
1674 0.10 } else if t > 1.5 {
1677 if target_vol <= 0.15 {
1679 0.05 } else {
1681 0.01 }
1683 } else {
1684 if target_vol <= 0.15 {
1686 0.05 } else {
1688 0.001 }
1690 };
1691
1692 let refined_vol_error = (refined_result.vol - target_vol).abs();
1693 let implied_vol_error = (implied_result.vol - target_vol).abs();
1694 let refined_vol_rel_error = refined_vol_error / target_vol.max(vol_abs_tolerance);
1695 let implied_vol_rel_error = implied_vol_error / target_vol.max(vol_abs_tolerance);
1696
1697 assert!(
1698 refined_vol_rel_error < vol_rel_tolerance,
1699 "Refined vol mismatch for spot={}, is_call={}, target_vol={}, t={}: refined={:.10}, target={:.10}, abs_error={:.2e}, rel_error={:.2e}",
1700 spot,
1701 is_call,
1702 target_vol,
1703 t,
1704 refined_result.vol,
1705 target_vol,
1706 refined_vol_error,
1707 refined_vol_rel_error
1708 );
1709
1710 let implied_vol_tolerance = if is_deep_edge_case {
1713 2.0 } else if implied_result.vol < 1e-6 {
1716 2.0 } else if t < 0.1 && (implied_result.vol - target_vol).abs() / target_vol.max(1e-6) > 0.5 {
1719 2.0 } else {
1722 vol_rel_tolerance
1723 };
1724
1725 assert!(
1726 implied_vol_rel_error < implied_vol_tolerance,
1727 "Implied vol mismatch for spot={}, is_call={}, target_vol={}, t={}: implied={:.10}, target={:.10}, abs_error={:.2e}, rel_error={:.2e}",
1728 spot,
1729 is_call,
1730 target_vol,
1731 t,
1732 implied_result.vol,
1733 target_vol,
1734 implied_vol_error,
1735 implied_vol_rel_error
1736 );
1737
1738 let greeks_abs_tolerance = 1e-10;
1742
1743 let moneyness = (spot - k) / k;
1745 let is_deep_itm_otm = moneyness.abs() > 0.05;
1746 let is_deep_edge_case = t < 0.1 && is_deep_itm_otm;
1747
1748 let greeks_rel_tolerance = if is_deep_edge_case {
1752 1.0 } else if t < 0.1 {
1755 if target_vol <= 0.15 {
1757 0.10 } else {
1759 0.05 }
1761 } else if t > 1.5 {
1762 if target_vol <= 0.15 {
1764 0.08 } else {
1766 0.01 }
1768 } else {
1769 if target_vol <= 0.15 {
1771 0.05 } else {
1773 2e-3 }
1775 };
1776
1777 let imply_vol_failed = implied_result.vol < 1e-6
1782 || (t < 0.1 && (implied_result.vol - target_vol).abs() / target_vol.max(1e-6) > 0.5)
1783 || is_deep_edge_case;
1784 let effective_greeks_tolerance = if imply_vol_failed || is_deep_edge_case {
1785 1.0 } else {
1787 greeks_rel_tolerance
1788 };
1789
1790 let check_6_sig_figs = |refined: f64, implied: f64, name: &str, is_gamma: bool| {
1791 if (imply_vol_failed || is_deep_edge_case)
1794 && (!implied.is_finite() || implied.abs() < 1e-4 || refined.abs() < 1e-4)
1795 {
1796 return; }
1798
1799 let abs_diff = (refined - implied).abs();
1800 let small_value_threshold = if is_deep_edge_case { 1e-3 } else { 1e-6 };
1803 let rel_diff =
1804 if implied.abs() < small_value_threshold && refined.abs() < small_value_threshold {
1805 0.0 } else {
1807 abs_diff / implied.abs().max(greeks_abs_tolerance)
1808 };
1809 let gamma_multiplier = if (0.1..=1.5).contains(&t) {
1811 if target_vol <= 0.15 { 5.0 } else { 3.0 }
1813 } else {
1814 if target_vol <= 0.15 { 10.0 } else { 5.0 }
1816 };
1817 let tolerance = if is_gamma {
1818 effective_greeks_tolerance * gamma_multiplier
1819 } else {
1820 effective_greeks_tolerance
1821 };
1822 let max_allowed = if is_deep_edge_case && implied.abs() < 1e-3 {
1824 2e-5 } else {
1826 implied.abs().max(greeks_abs_tolerance) * tolerance
1827 };
1828
1829 assert!(
1830 abs_diff < max_allowed,
1831 "{name} mismatch between refine and imply for spot={spot}, is_call={is_call}, target_vol={target_vol}, t={t}: refined={refined:.10}, implied={implied:.10}, abs_diff={abs_diff:.2e}, rel_diff={rel_diff:.2e}, max_allowed={max_allowed:.2e}"
1832 );
1833 };
1834
1835 check_6_sig_figs(refined_result.price, implied_result.price, "Price", false);
1836 check_6_sig_figs(refined_result.delta, implied_result.delta, "Delta", false);
1837 check_6_sig_figs(refined_result.gamma, implied_result.gamma, "Gamma", true);
1838 check_6_sig_figs(refined_result.vega, implied_result.vega, "Vega", false);
1839 check_6_sig_figs(refined_result.theta, implied_result.theta, "Theta", false);
1840 }
1841}