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