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