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