1use std::{
19 fmt::Display,
20 ops::{Add, Mul},
21};
22
23use implied_vol::{DefaultSpecialFn, ImpliedBlackVolatility, SpecialFn};
24use nautilus_core::{UnixNanos, datetime::unix_nanos_to_iso8601, math::quadratic_interpolation};
25
26use crate::{data::HasTsInit, identifiers::InstrumentId};
27
28const FRAC_SQRT_2_PI: f64 = f64::from_bits(0x3fd9884533d43651);
29
30#[inline(always)]
31fn norm_pdf(x: f64) -> f64 {
32 FRAC_SQRT_2_PI * (-0.5 * x * x).exp()
33}
34
35#[repr(C)]
36#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
37#[cfg_attr(
38 feature = "python",
39 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
40)]
41pub struct BlackScholesGreeksResult {
42 pub price: f64,
43 pub delta: f64,
44 pub gamma: f64,
45 pub vega: f64,
46 pub theta: f64,
47}
48
49#[allow(clippy::too_many_arguments)]
52pub fn black_scholes_greeks(
53 s: f64,
54 r: f64,
55 b: f64,
56 sigma: f64,
57 is_call: bool,
58 k: f64,
59 t: f64,
60 multiplier: f64,
61) -> BlackScholesGreeksResult {
62 let phi = if is_call { 1.0 } else { -1.0 };
63 let scaled_vol = sigma * t.sqrt();
64 let d1 = ((s / k).ln() + (b + 0.5 * sigma.powi(2)) * t) / scaled_vol;
65 let d2 = d1 - scaled_vol;
66 let cdf_phi_d1 = DefaultSpecialFn::norm_cdf(phi * d1);
67 let cdf_phi_d2 = DefaultSpecialFn::norm_cdf(phi * d2);
68 let dist_d1 = norm_pdf(d1);
69 let df = ((b - r) * t).exp();
70 let s_t = s * df;
71 let k_t = k * (-r * t).exp();
72
73 let price = multiplier * phi * (s_t * cdf_phi_d1 - k_t * cdf_phi_d2);
74 let delta = multiplier * phi * df * cdf_phi_d1;
75 let gamma = multiplier * df * dist_d1 / (s * scaled_vol);
76 let vega = multiplier * s_t * t.sqrt() * dist_d1 * 0.01; let theta = multiplier
78 * (s_t * (-dist_d1 * sigma / (2.0 * t.sqrt()) - phi * (b - r) * cdf_phi_d1)
79 - phi * r * k_t * cdf_phi_d2)
80 * 0.0027378507871321013; BlackScholesGreeksResult {
83 price,
84 delta,
85 gamma,
86 vega,
87 theta,
88 }
89}
90
91pub fn imply_vol(s: f64, r: f64, b: f64, is_call: bool, k: f64, t: f64, price: f64) -> f64 {
92 let forward = s * b.exp();
93 let forward_price = price * (r * t).exp();
94
95 ImpliedBlackVolatility::builder()
96 .option_price(forward_price)
97 .forward(forward)
98 .strike(k)
99 .expiry(t)
100 .is_call(is_call)
101 .build_unchecked()
102 .calculate::<DefaultSpecialFn>()
103 .unwrap_or(0.0)
104}
105
106#[repr(C)]
107#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
108#[cfg_attr(
109 feature = "python",
110 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
111)]
112pub struct ImplyVolAndGreeksResult {
113 pub vol: f64,
114 pub price: f64,
115 pub delta: f64,
116 pub gamma: f64,
117 pub vega: f64,
118 pub theta: f64,
119}
120
121#[allow(clippy::too_many_arguments)]
122pub fn imply_vol_and_greeks(
123 s: f64,
124 r: f64,
125 b: f64,
126 is_call: bool,
127 k: f64,
128 t: f64,
129 price: f64,
130 multiplier: f64,
131) -> ImplyVolAndGreeksResult {
132 let vol = imply_vol(s, r, b, is_call, k, t, price);
133 let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t, multiplier);
134
135 ImplyVolAndGreeksResult {
136 vol,
137 price: greeks.price,
138 delta: greeks.delta,
139 gamma: greeks.gamma,
140 vega: greeks.vega,
141 theta: greeks.theta,
142 }
143}
144
145#[derive(Debug, Clone)]
146pub struct GreeksData {
147 pub ts_init: UnixNanos,
148 pub ts_event: UnixNanos,
149 pub instrument_id: InstrumentId,
150 pub is_call: bool,
151 pub strike: f64,
152 pub expiry: i32,
153 pub expiry_in_days: i32,
154 pub expiry_in_years: f64,
155 pub multiplier: f64,
156 pub quantity: f64,
157 pub underlying_price: f64,
158 pub interest_rate: f64,
159 pub cost_of_carry: f64,
160 pub vol: f64,
161 pub pnl: f64,
162 pub price: f64,
163 pub delta: f64,
164 pub gamma: f64,
165 pub vega: f64,
166 pub theta: f64,
167 pub itm_prob: f64,
169}
170
171impl GreeksData {
172 #[allow(clippy::too_many_arguments)]
173 pub fn new(
174 ts_init: UnixNanos,
175 ts_event: UnixNanos,
176 instrument_id: InstrumentId,
177 is_call: bool,
178 strike: f64,
179 expiry: i32,
180 expiry_in_days: i32,
181 expiry_in_years: f64,
182 multiplier: f64,
183 quantity: f64,
184 underlying_price: f64,
185 interest_rate: f64,
186 cost_of_carry: f64,
187 vol: f64,
188 pnl: f64,
189 price: f64,
190 delta: f64,
191 gamma: f64,
192 vega: f64,
193 theta: f64,
194 itm_prob: f64,
195 ) -> Self {
196 Self {
197 ts_init,
198 ts_event,
199 instrument_id,
200 is_call,
201 strike,
202 expiry,
203 expiry_in_days,
204 expiry_in_years,
205 multiplier,
206 quantity,
207 underlying_price,
208 interest_rate,
209 cost_of_carry,
210 vol,
211 pnl,
212 price,
213 delta,
214 gamma,
215 vega,
216 theta,
217 itm_prob,
218 }
219 }
220
221 pub fn from_delta(
222 instrument_id: InstrumentId,
223 delta: f64,
224 multiplier: f64,
225 ts_event: UnixNanos,
226 ) -> Self {
227 Self {
228 ts_init: ts_event,
229 ts_event,
230 instrument_id,
231 is_call: true,
232 strike: 0.0,
233 expiry: 0,
234 expiry_in_days: 0,
235 expiry_in_years: 0.0,
236 multiplier,
237 quantity: 1.0,
238 underlying_price: 0.0,
239 interest_rate: 0.0,
240 cost_of_carry: 0.0,
241 vol: 0.0,
242 pnl: 0.0,
243 price: 0.0,
244 delta,
245 gamma: 0.0,
246 vega: 0.0,
247 theta: 0.0,
248 itm_prob: 0.0,
249 }
250 }
251}
252
253impl Default for GreeksData {
254 fn default() -> Self {
255 Self {
256 ts_init: UnixNanos::default(),
257 ts_event: UnixNanos::default(),
258 instrument_id: InstrumentId::from("ES.GLBX"),
259 is_call: true,
260 strike: 0.0,
261 expiry: 0,
262 expiry_in_days: 0,
263 expiry_in_years: 0.0,
264 multiplier: 0.0,
265 quantity: 0.0,
266 underlying_price: 0.0,
267 interest_rate: 0.0,
268 cost_of_carry: 0.0,
269 vol: 0.0,
270 pnl: 0.0,
271 price: 0.0,
272 delta: 0.0,
273 gamma: 0.0,
274 vega: 0.0,
275 theta: 0.0,
276 itm_prob: 0.0,
277 }
278 }
279}
280
281impl Display for GreeksData {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 write!(
284 f,
285 "GreeksData(instrument_id={}, expiry={}, itm_prob={:.2}%, vol={:.2}%, pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, quantity={}, ts_init={})",
286 self.instrument_id,
287 self.expiry,
288 self.itm_prob * 100.0,
289 self.vol * 100.0,
290 self.pnl,
291 self.price,
292 self.delta,
293 self.gamma,
294 self.vega,
295 self.theta,
296 self.quantity,
297 unix_nanos_to_iso8601(self.ts_init)
298 )
299 }
300}
301
302impl Mul<&GreeksData> for f64 {
304 type Output = GreeksData;
305
306 fn mul(self, greeks: &GreeksData) -> GreeksData {
307 GreeksData {
308 ts_init: greeks.ts_init,
309 ts_event: greeks.ts_event,
310 instrument_id: greeks.instrument_id,
311 is_call: greeks.is_call,
312 strike: greeks.strike,
313 expiry: greeks.expiry,
314 expiry_in_days: greeks.expiry_in_days,
315 expiry_in_years: greeks.expiry_in_years,
316 multiplier: greeks.multiplier,
317 quantity: greeks.quantity,
318 underlying_price: greeks.underlying_price,
319 interest_rate: greeks.interest_rate,
320 cost_of_carry: greeks.cost_of_carry,
321 vol: greeks.vol,
322 pnl: self * greeks.pnl,
323 price: self * greeks.price,
324 delta: self * greeks.delta,
325 gamma: self * greeks.gamma,
326 vega: self * greeks.vega,
327 theta: self * greeks.theta,
328 itm_prob: greeks.itm_prob,
329 }
330 }
331}
332
333impl HasTsInit for GreeksData {
334 fn ts_init(&self) -> UnixNanos {
335 self.ts_init
336 }
337}
338
339#[derive(Debug, Clone)]
340pub struct PortfolioGreeks {
341 pub ts_init: UnixNanos,
342 pub ts_event: UnixNanos,
343 pub pnl: f64,
344 pub price: f64,
345 pub delta: f64,
346 pub gamma: f64,
347 pub vega: f64,
348 pub theta: f64,
349}
350
351impl PortfolioGreeks {
352 #[allow(clippy::too_many_arguments)]
353 pub fn new(
354 ts_init: UnixNanos,
355 ts_event: UnixNanos,
356 pnl: f64,
357 price: f64,
358 delta: f64,
359 gamma: f64,
360 vega: f64,
361 theta: f64,
362 ) -> Self {
363 Self {
364 ts_init,
365 ts_event,
366 pnl,
367 price,
368 delta,
369 gamma,
370 vega,
371 theta,
372 }
373 }
374}
375
376impl Default for PortfolioGreeks {
377 fn default() -> Self {
378 Self {
379 ts_init: UnixNanos::default(),
380 ts_event: UnixNanos::default(),
381 pnl: 0.0,
382 price: 0.0,
383 delta: 0.0,
384 gamma: 0.0,
385 vega: 0.0,
386 theta: 0.0,
387 }
388 }
389}
390
391impl Display for PortfolioGreeks {
392 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393 write!(
394 f,
395 "PortfolioGreeks(pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, ts_event={}, ts_init={})",
396 self.pnl,
397 self.price,
398 self.delta,
399 self.gamma,
400 self.vega,
401 self.theta,
402 unix_nanos_to_iso8601(self.ts_event),
403 unix_nanos_to_iso8601(self.ts_init)
404 )
405 }
406}
407
408impl Add for PortfolioGreeks {
409 type Output = Self;
410
411 fn add(self, other: Self) -> Self {
412 Self {
413 ts_init: self.ts_init,
414 ts_event: self.ts_event,
415 pnl: self.pnl + other.pnl,
416 price: self.price + other.price,
417 delta: self.delta + other.delta,
418 gamma: self.gamma + other.gamma,
419 vega: self.vega + other.vega,
420 theta: self.theta + other.theta,
421 }
422 }
423}
424
425impl From<GreeksData> for PortfolioGreeks {
426 fn from(greeks: GreeksData) -> Self {
427 Self {
428 ts_init: greeks.ts_init,
429 ts_event: greeks.ts_event,
430 pnl: greeks.pnl,
431 price: greeks.price,
432 delta: greeks.delta,
433 gamma: greeks.gamma,
434 vega: greeks.vega,
435 theta: greeks.theta,
436 }
437 }
438}
439
440impl HasTsInit for PortfolioGreeks {
441 fn ts_init(&self) -> UnixNanos {
442 self.ts_init
443 }
444}
445
446#[derive(Debug, Clone)]
447pub struct YieldCurveData {
448 pub ts_init: UnixNanos,
449 pub ts_event: UnixNanos,
450 pub curve_name: String,
451 pub tenors: Vec<f64>,
452 pub interest_rates: Vec<f64>,
453}
454
455impl YieldCurveData {
456 pub fn new(
457 ts_init: UnixNanos,
458 ts_event: UnixNanos,
459 curve_name: String,
460 tenors: Vec<f64>,
461 interest_rates: Vec<f64>,
462 ) -> Self {
463 Self {
464 ts_init,
465 ts_event,
466 curve_name,
467 tenors,
468 interest_rates,
469 }
470 }
471
472 pub fn get_rate(&self, expiry_in_years: f64) -> f64 {
474 if self.interest_rates.len() == 1 {
475 return self.interest_rates[0];
476 }
477
478 quadratic_interpolation(expiry_in_years, &self.tenors, &self.interest_rates)
479 }
480}
481
482impl Display for YieldCurveData {
483 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
484 write!(
485 f,
486 "InterestRateCurve(curve_name={}, ts_event={}, ts_init={})",
487 self.curve_name,
488 unix_nanos_to_iso8601(self.ts_event),
489 unix_nanos_to_iso8601(self.ts_init)
490 )
491 }
492}
493
494impl HasTsInit for YieldCurveData {
495 fn ts_init(&self) -> UnixNanos {
496 self.ts_init
497 }
498}
499
500impl Default for YieldCurveData {
501 fn default() -> Self {
502 Self {
503 ts_init: UnixNanos::default(),
504 ts_event: UnixNanos::default(),
505 curve_name: "USD".to_string(),
506 tenors: vec![0.5, 1.0, 1.5, 2.0, 2.5],
507 interest_rates: vec![0.04, 0.04, 0.04, 0.04, 0.04],
508 }
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use rstest::rstest;
515
516 use super::*;
517 use crate::identifiers::InstrumentId;
518
519 fn create_test_greeks_data() -> GreeksData {
520 GreeksData::new(
521 UnixNanos::from(1_000_000_000),
522 UnixNanos::from(1_500_000_000),
523 InstrumentId::from("SPY240315C00500000.OPRA"),
524 true,
525 500.0,
526 20240315,
527 91, 0.25,
529 100.0,
530 1.0,
531 520.0,
532 0.05,
533 0.05,
534 0.2,
535 250.0,
536 25.5,
537 0.65,
538 0.003,
539 15.2,
540 -0.08,
541 0.75,
542 )
543 }
544
545 fn create_test_portfolio_greeks() -> PortfolioGreeks {
546 PortfolioGreeks::new(
547 UnixNanos::from(1_000_000_000),
548 UnixNanos::from(1_500_000_000),
549 1500.0,
550 125.5,
551 2.15,
552 0.008,
553 42.7,
554 -2.3,
555 )
556 }
557
558 fn create_test_yield_curve() -> YieldCurveData {
559 YieldCurveData::new(
560 UnixNanos::from(1_000_000_000),
561 UnixNanos::from(1_500_000_000),
562 "USD".to_string(),
563 vec![0.25, 0.5, 1.0, 2.0, 5.0],
564 vec![0.025, 0.03, 0.035, 0.04, 0.045],
565 )
566 }
567
568 #[rstest]
569 fn test_black_scholes_greeks_result_creation() {
570 let result = BlackScholesGreeksResult {
571 price: 25.5,
572 delta: 0.65,
573 gamma: 0.003,
574 vega: 15.2,
575 theta: -0.08,
576 };
577
578 assert_eq!(result.price, 25.5);
579 assert_eq!(result.delta, 0.65);
580 assert_eq!(result.gamma, 0.003);
581 assert_eq!(result.vega, 15.2);
582 assert_eq!(result.theta, -0.08);
583 }
584
585 #[rstest]
586 fn test_black_scholes_greeks_result_clone_and_copy() {
587 let result1 = BlackScholesGreeksResult {
588 price: 25.5,
589 delta: 0.65,
590 gamma: 0.003,
591 vega: 15.2,
592 theta: -0.08,
593 };
594 let result2 = result1;
595 let result3 = result1;
596
597 assert_eq!(result1, result2);
598 assert_eq!(result1, result3);
599 }
600
601 #[rstest]
602 fn test_black_scholes_greeks_result_debug() {
603 let result = BlackScholesGreeksResult {
604 price: 25.5,
605 delta: 0.65,
606 gamma: 0.003,
607 vega: 15.2,
608 theta: -0.08,
609 };
610 let debug_str = format!("{result:?}");
611
612 assert!(debug_str.contains("BlackScholesGreeksResult"));
613 assert!(debug_str.contains("25.5"));
614 assert!(debug_str.contains("0.65"));
615 }
616
617 #[rstest]
618 fn test_imply_vol_and_greeks_result_creation() {
619 let result = ImplyVolAndGreeksResult {
620 vol: 0.2,
621 price: 25.5,
622 delta: 0.65,
623 gamma: 0.003,
624 vega: 15.2,
625 theta: -0.08,
626 };
627
628 assert_eq!(result.vol, 0.2);
629 assert_eq!(result.price, 25.5);
630 assert_eq!(result.delta, 0.65);
631 assert_eq!(result.gamma, 0.003);
632 assert_eq!(result.vega, 15.2);
633 assert_eq!(result.theta, -0.08);
634 }
635
636 #[rstest]
637 fn test_black_scholes_greeks_basic_call() {
638 let s = 100.0;
639 let r = 0.05;
640 let b = 0.05;
641 let sigma = 0.2;
642 let is_call = true;
643 let k = 100.0;
644 let t = 1.0;
645 let multiplier = 1.0;
646
647 let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, multiplier);
648
649 assert!(greeks.price > 0.0);
650 assert!(greeks.delta > 0.0 && greeks.delta < 1.0);
651 assert!(greeks.gamma > 0.0);
652 assert!(greeks.vega > 0.0);
653 assert!(greeks.theta < 0.0); }
655
656 #[rstest]
657 fn test_black_scholes_greeks_basic_put() {
658 let s = 100.0;
659 let r = 0.05;
660 let b = 0.05;
661 let sigma = 0.2;
662 let is_call = false;
663 let k = 100.0;
664 let t = 1.0;
665 let multiplier = 1.0;
666
667 let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, multiplier);
668
669 assert!(greeks.price > 0.0);
670 assert!(greeks.delta < 0.0 && greeks.delta > -1.0);
671 assert!(greeks.gamma > 0.0);
672 assert!(greeks.vega > 0.0);
673 assert!(greeks.theta < 0.0); }
675
676 #[rstest]
677 fn test_black_scholes_greeks_with_multiplier() {
678 let s = 100.0;
679 let r = 0.05;
680 let b = 0.05;
681 let sigma = 0.2;
682 let is_call = true;
683 let k = 100.0;
684 let t = 1.0;
685 let multiplier = 100.0;
686
687 let greeks_1x = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
688 let greeks_100x = black_scholes_greeks(s, r, b, sigma, is_call, k, t, multiplier);
689
690 let tolerance = 1e-10;
691 assert!((greeks_100x.price - greeks_1x.price * 100.0).abs() < tolerance);
692 assert!((greeks_100x.delta - greeks_1x.delta * 100.0).abs() < tolerance);
693 assert!((greeks_100x.gamma - greeks_1x.gamma * 100.0).abs() < tolerance);
694 assert!((greeks_100x.vega - greeks_1x.vega * 100.0).abs() < tolerance);
695 assert!((greeks_100x.theta - greeks_1x.theta * 100.0).abs() < tolerance);
696 }
697
698 #[rstest]
699 fn test_black_scholes_greeks_deep_itm_call() {
700 let s = 150.0;
701 let r = 0.05;
702 let b = 0.05;
703 let sigma = 0.2;
704 let is_call = true;
705 let k = 100.0;
706 let t = 1.0;
707 let multiplier = 1.0;
708
709 let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, multiplier);
710
711 assert!(greeks.delta > 0.9); assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); }
714
715 #[rstest]
716 fn test_black_scholes_greeks_deep_otm_call() {
717 let s = 50.0;
718 let r = 0.05;
719 let b = 0.05;
720 let sigma = 0.2;
721 let is_call = true;
722 let k = 100.0;
723 let t = 1.0;
724 let multiplier = 1.0;
725
726 let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, multiplier);
727
728 assert!(greeks.delta < 0.1); assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); }
731
732 #[rstest]
733 fn test_black_scholes_greeks_zero_time() {
734 let s = 100.0;
735 let r = 0.05;
736 let b = 0.05;
737 let sigma = 0.2;
738 let is_call = true;
739 let k = 100.0;
740 let t = 0.0001; let multiplier = 1.0;
742
743 let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, multiplier);
744
745 assert!(greeks.price >= 0.0);
746 assert!(greeks.theta.is_finite());
747 }
748
749 #[rstest]
750 fn test_imply_vol_basic() {
751 let s = 100.0;
752 let r = 0.05;
753 let b = 0.05;
754 let sigma = 0.2;
755 let is_call = true;
756 let k = 100.0;
757 let t = 1.0;
758
759 let theoretical_price = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0).price;
760 let implied_vol = imply_vol(s, r, b, is_call, k, t, theoretical_price);
761
762 let tolerance = 1e-6;
763 assert!((implied_vol - sigma).abs() < tolerance);
764 }
765
766 #[rstest]
773 fn test_greeks_data_new() {
774 let greeks = create_test_greeks_data();
775
776 assert_eq!(greeks.ts_init, UnixNanos::from(1_000_000_000));
777 assert_eq!(greeks.ts_event, UnixNanos::from(1_500_000_000));
778 assert_eq!(
779 greeks.instrument_id,
780 InstrumentId::from("SPY240315C00500000.OPRA")
781 );
782 assert!(greeks.is_call);
783 assert_eq!(greeks.strike, 500.0);
784 assert_eq!(greeks.expiry, 20240315);
785 assert_eq!(greeks.expiry_in_years, 0.25);
786 assert_eq!(greeks.multiplier, 100.0);
787 assert_eq!(greeks.quantity, 1.0);
788 assert_eq!(greeks.underlying_price, 520.0);
789 assert_eq!(greeks.interest_rate, 0.05);
790 assert_eq!(greeks.cost_of_carry, 0.05);
791 assert_eq!(greeks.vol, 0.2);
792 assert_eq!(greeks.pnl, 250.0);
793 assert_eq!(greeks.price, 25.5);
794 assert_eq!(greeks.delta, 0.65);
795 assert_eq!(greeks.gamma, 0.003);
796 assert_eq!(greeks.vega, 15.2);
797 assert_eq!(greeks.theta, -0.08);
798 assert_eq!(greeks.itm_prob, 0.75);
799 }
800
801 #[rstest]
802 fn test_greeks_data_from_delta() {
803 let delta = 0.5;
804 let multiplier = 100.0;
805 let ts_event = UnixNanos::from(2_000_000_000);
806 let instrument_id = InstrumentId::from("AAPL240315C00180000.OPRA");
807
808 let greeks = GreeksData::from_delta(instrument_id, delta, multiplier, ts_event);
809
810 assert_eq!(greeks.ts_init, ts_event);
811 assert_eq!(greeks.ts_event, ts_event);
812 assert_eq!(greeks.instrument_id, instrument_id);
813 assert!(greeks.is_call);
814 assert_eq!(greeks.delta, delta);
815 assert_eq!(greeks.multiplier, multiplier);
816 assert_eq!(greeks.quantity, 1.0);
817
818 assert_eq!(greeks.strike, 0.0);
820 assert_eq!(greeks.expiry, 0);
821 assert_eq!(greeks.price, 0.0);
822 assert_eq!(greeks.gamma, 0.0);
823 assert_eq!(greeks.vega, 0.0);
824 assert_eq!(greeks.theta, 0.0);
825 }
826
827 #[rstest]
828 fn test_greeks_data_default() {
829 let greeks = GreeksData::default();
830
831 assert_eq!(greeks.ts_init, UnixNanos::default());
832 assert_eq!(greeks.ts_event, UnixNanos::default());
833 assert_eq!(greeks.instrument_id, InstrumentId::from("ES.GLBX"));
834 assert!(greeks.is_call);
835 assert_eq!(greeks.strike, 0.0);
836 assert_eq!(greeks.expiry, 0);
837 assert_eq!(greeks.multiplier, 0.0);
838 assert_eq!(greeks.quantity, 0.0);
839 assert_eq!(greeks.delta, 0.0);
840 assert_eq!(greeks.gamma, 0.0);
841 assert_eq!(greeks.vega, 0.0);
842 assert_eq!(greeks.theta, 0.0);
843 }
844
845 #[rstest]
846 fn test_greeks_data_display() {
847 let greeks = create_test_greeks_data();
848 let display_str = format!("{greeks}");
849
850 assert!(display_str.contains("GreeksData"));
851 assert!(display_str.contains("SPY240315C00500000.OPRA"));
852 assert!(display_str.contains("20240315"));
853 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")); }
859
860 #[rstest]
861 fn test_greeks_data_multiplication() {
862 let greeks = create_test_greeks_data();
863 let quantity = 5.0;
864 let scaled_greeks = quantity * &greeks;
865
866 assert_eq!(scaled_greeks.ts_init, greeks.ts_init);
867 assert_eq!(scaled_greeks.ts_event, greeks.ts_event);
868 assert_eq!(scaled_greeks.instrument_id, greeks.instrument_id);
869 assert_eq!(scaled_greeks.is_call, greeks.is_call);
870 assert_eq!(scaled_greeks.strike, greeks.strike);
871 assert_eq!(scaled_greeks.expiry, greeks.expiry);
872 assert_eq!(scaled_greeks.multiplier, greeks.multiplier);
873 assert_eq!(scaled_greeks.quantity, greeks.quantity);
874 assert_eq!(scaled_greeks.vol, greeks.vol);
875 assert_eq!(scaled_greeks.itm_prob, greeks.itm_prob);
876
877 assert_eq!(scaled_greeks.pnl, quantity * greeks.pnl);
879 assert_eq!(scaled_greeks.price, quantity * greeks.price);
880 assert_eq!(scaled_greeks.delta, quantity * greeks.delta);
881 assert_eq!(scaled_greeks.gamma, quantity * greeks.gamma);
882 assert_eq!(scaled_greeks.vega, quantity * greeks.vega);
883 assert_eq!(scaled_greeks.theta, quantity * greeks.theta);
884 }
885
886 #[rstest]
887 fn test_greeks_data_has_ts_init() {
888 let greeks = create_test_greeks_data();
889 assert_eq!(greeks.ts_init(), UnixNanos::from(1_000_000_000));
890 }
891
892 #[rstest]
893 fn test_greeks_data_clone() {
894 let greeks1 = create_test_greeks_data();
895 let greeks2 = greeks1.clone();
896
897 assert_eq!(greeks1.ts_init, greeks2.ts_init);
898 assert_eq!(greeks1.instrument_id, greeks2.instrument_id);
899 assert_eq!(greeks1.delta, greeks2.delta);
900 assert_eq!(greeks1.gamma, greeks2.gamma);
901 }
902
903 #[rstest]
904 fn test_portfolio_greeks_new() {
905 let portfolio_greeks = create_test_portfolio_greeks();
906
907 assert_eq!(portfolio_greeks.ts_init, UnixNanos::from(1_000_000_000));
908 assert_eq!(portfolio_greeks.ts_event, UnixNanos::from(1_500_000_000));
909 assert_eq!(portfolio_greeks.pnl, 1500.0);
910 assert_eq!(portfolio_greeks.price, 125.5);
911 assert_eq!(portfolio_greeks.delta, 2.15);
912 assert_eq!(portfolio_greeks.gamma, 0.008);
913 assert_eq!(portfolio_greeks.vega, 42.7);
914 assert_eq!(portfolio_greeks.theta, -2.3);
915 }
916
917 #[rstest]
918 fn test_portfolio_greeks_default() {
919 let portfolio_greeks = PortfolioGreeks::default();
920
921 assert_eq!(portfolio_greeks.ts_init, UnixNanos::default());
922 assert_eq!(portfolio_greeks.ts_event, UnixNanos::default());
923 assert_eq!(portfolio_greeks.pnl, 0.0);
924 assert_eq!(portfolio_greeks.price, 0.0);
925 assert_eq!(portfolio_greeks.delta, 0.0);
926 assert_eq!(portfolio_greeks.gamma, 0.0);
927 assert_eq!(portfolio_greeks.vega, 0.0);
928 assert_eq!(portfolio_greeks.theta, 0.0);
929 }
930
931 #[rstest]
932 fn test_portfolio_greeks_display() {
933 let portfolio_greeks = create_test_portfolio_greeks();
934 let display_str = format!("{portfolio_greeks}");
935
936 assert!(display_str.contains("PortfolioGreeks"));
937 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")); }
944
945 #[rstest]
946 fn test_portfolio_greeks_addition() {
947 let greeks1 = PortfolioGreeks::new(
948 UnixNanos::from(1_000_000_000),
949 UnixNanos::from(1_500_000_000),
950 100.0,
951 50.0,
952 1.0,
953 0.005,
954 20.0,
955 -1.0,
956 );
957 let greeks2 = PortfolioGreeks::new(
958 UnixNanos::from(2_000_000_000),
959 UnixNanos::from(2_500_000_000),
960 200.0,
961 75.0,
962 1.5,
963 0.003,
964 25.0,
965 -1.5,
966 );
967
968 let result = greeks1 + greeks2;
969
970 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);
973 assert_eq!(result.price, 125.0);
974 assert_eq!(result.delta, 2.5);
975 assert_eq!(result.gamma, 0.008);
976 assert_eq!(result.vega, 45.0);
977 assert_eq!(result.theta, -2.5);
978 }
979
980 #[rstest]
981 fn test_portfolio_greeks_from_greeks_data() {
982 let greeks_data = create_test_greeks_data();
983 let portfolio_greeks: PortfolioGreeks = greeks_data.clone().into();
984
985 assert_eq!(portfolio_greeks.ts_init, greeks_data.ts_init);
986 assert_eq!(portfolio_greeks.ts_event, greeks_data.ts_event);
987 assert_eq!(portfolio_greeks.pnl, greeks_data.pnl);
988 assert_eq!(portfolio_greeks.price, greeks_data.price);
989 assert_eq!(portfolio_greeks.delta, greeks_data.delta);
990 assert_eq!(portfolio_greeks.gamma, greeks_data.gamma);
991 assert_eq!(portfolio_greeks.vega, greeks_data.vega);
992 assert_eq!(portfolio_greeks.theta, greeks_data.theta);
993 }
994
995 #[rstest]
996 fn test_portfolio_greeks_has_ts_init() {
997 let portfolio_greeks = create_test_portfolio_greeks();
998 assert_eq!(portfolio_greeks.ts_init(), UnixNanos::from(1_000_000_000));
999 }
1000
1001 #[rstest]
1002 fn test_yield_curve_data_new() {
1003 let curve = create_test_yield_curve();
1004
1005 assert_eq!(curve.ts_init, UnixNanos::from(1_000_000_000));
1006 assert_eq!(curve.ts_event, UnixNanos::from(1_500_000_000));
1007 assert_eq!(curve.curve_name, "USD");
1008 assert_eq!(curve.tenors, vec![0.25, 0.5, 1.0, 2.0, 5.0]);
1009 assert_eq!(curve.interest_rates, vec![0.025, 0.03, 0.035, 0.04, 0.045]);
1010 }
1011
1012 #[rstest]
1013 fn test_yield_curve_data_default() {
1014 let curve = YieldCurveData::default();
1015
1016 assert_eq!(curve.ts_init, UnixNanos::default());
1017 assert_eq!(curve.ts_event, UnixNanos::default());
1018 assert_eq!(curve.curve_name, "USD");
1019 assert_eq!(curve.tenors, vec![0.5, 1.0, 1.5, 2.0, 2.5]);
1020 assert_eq!(curve.interest_rates, vec![0.04, 0.04, 0.04, 0.04, 0.04]);
1021 }
1022
1023 #[rstest]
1024 fn test_yield_curve_data_get_rate_single_point() {
1025 let curve = YieldCurveData::new(
1026 UnixNanos::default(),
1027 UnixNanos::default(),
1028 "USD".to_string(),
1029 vec![1.0],
1030 vec![0.05],
1031 );
1032
1033 assert_eq!(curve.get_rate(0.5), 0.05);
1034 assert_eq!(curve.get_rate(1.0), 0.05);
1035 assert_eq!(curve.get_rate(2.0), 0.05);
1036 }
1037
1038 #[rstest]
1039 fn test_yield_curve_data_get_rate_interpolation() {
1040 let curve = create_test_yield_curve();
1041
1042 assert_eq!(curve.get_rate(0.25), 0.025);
1044 assert_eq!(curve.get_rate(1.0), 0.035);
1045 assert_eq!(curve.get_rate(5.0), 0.045);
1046
1047 let rate_0_75 = curve.get_rate(0.75);
1049 assert!(rate_0_75 > 0.025 && rate_0_75 < 0.045);
1050 }
1051
1052 #[rstest]
1053 fn test_yield_curve_data_display() {
1054 let curve = create_test_yield_curve();
1055 let display_str = format!("{curve}");
1056
1057 assert!(display_str.contains("InterestRateCurve"));
1058 assert!(display_str.contains("USD"));
1059 }
1060
1061 #[rstest]
1062 fn test_yield_curve_data_has_ts_init() {
1063 let curve = create_test_yield_curve();
1064 assert_eq!(curve.ts_init(), UnixNanos::from(1_000_000_000));
1065 }
1066
1067 #[rstest]
1068 fn test_yield_curve_data_clone() {
1069 let curve1 = create_test_yield_curve();
1070 let curve2 = curve1.clone();
1071
1072 assert_eq!(curve1.curve_name, curve2.curve_name);
1073 assert_eq!(curve1.tenors, curve2.tenors);
1074 assert_eq!(curve1.interest_rates, curve2.interest_rates);
1075 }
1076
1077 #[rstest]
1078 fn test_black_scholes_greeks_extreme_values() {
1079 let s = 1000.0;
1080 let r = 0.1;
1081 let b = 0.1;
1082 let sigma = 0.5;
1083 let is_call = true;
1084 let k = 10.0; let t = 0.1;
1086 let multiplier = 1.0;
1087
1088 let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, multiplier);
1089
1090 assert!(greeks.price.is_finite());
1091 assert!(greeks.delta.is_finite());
1092 assert!(greeks.gamma.is_finite());
1093 assert!(greeks.vega.is_finite());
1094 assert!(greeks.theta.is_finite());
1095 assert!(greeks.price > 0.0);
1096 assert!(greeks.delta > 0.99); }
1098
1099 #[rstest]
1100 fn test_black_scholes_greeks_high_volatility() {
1101 let s = 100.0;
1102 let r = 0.05;
1103 let b = 0.05;
1104 let sigma = 2.0; let is_call = true;
1106 let k = 100.0;
1107 let t = 1.0;
1108 let multiplier = 1.0;
1109
1110 let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, multiplier);
1111
1112 assert!(greeks.price.is_finite());
1113 assert!(greeks.delta.is_finite());
1114 assert!(greeks.gamma.is_finite());
1115 assert!(greeks.vega.is_finite());
1116 assert!(greeks.theta.is_finite());
1117 assert!(greeks.price > 0.0);
1118 }
1119
1120 #[rstest]
1121 fn test_greeks_data_put_option() {
1122 let greeks = GreeksData::new(
1123 UnixNanos::from(1_000_000_000),
1124 UnixNanos::from(1_500_000_000),
1125 InstrumentId::from("SPY240315P00480000.OPRA"),
1126 false, 480.0,
1128 20240315,
1129 91, 0.25,
1131 100.0,
1132 1.0,
1133 500.0,
1134 0.05,
1135 0.05,
1136 0.25,
1137 -150.0, 8.5,
1139 -0.35, 0.002,
1141 12.8,
1142 -0.06,
1143 0.25,
1144 );
1145
1146 assert!(!greeks.is_call);
1147 assert!(greeks.delta < 0.0);
1148 assert_eq!(greeks.pnl, -150.0);
1149 }
1150
1151 #[rstest]
1153 fn test_greeks_accuracy_call() {
1154 let s = 100.0;
1155 let k = 100.1;
1156 let t = 1.0;
1157 let r = 0.01;
1158 let b = 0.005;
1159 let sigma = 0.2;
1160 let is_call = true;
1161 let eps = 1e-3;
1162
1163 let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
1164
1165 let price0 = |s: f64| black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0).price;
1166
1167 let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
1168 let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
1169 let vega_bnr = (black_scholes_greeks(s, r, b, sigma + eps, is_call, k, t, 1.0).price
1170 - black_scholes_greeks(s, r, b, sigma - eps, is_call, k, t, 1.0).price)
1171 / (2.0 * eps)
1172 / 100.0;
1173 let theta_bnr = (black_scholes_greeks(s, r, b, sigma, is_call, k, t - eps, 1.0).price
1174 - black_scholes_greeks(s, r, b, sigma, is_call, k, t + eps, 1.0).price)
1175 / (2.0 * eps)
1176 / 365.25;
1177
1178 let tolerance = 1e-5;
1179 assert!(
1180 (greeks.delta - delta_bnr).abs() < tolerance,
1181 "Delta difference exceeds tolerance"
1182 );
1183 assert!(
1184 (greeks.gamma - gamma_bnr).abs() < tolerance,
1185 "Gamma difference exceeds tolerance"
1186 );
1187 assert!(
1188 (greeks.vega - vega_bnr).abs() < tolerance,
1189 "Vega difference exceeds tolerance"
1190 );
1191 assert!(
1192 (greeks.theta - theta_bnr).abs() < tolerance,
1193 "Theta difference exceeds tolerance"
1194 );
1195 }
1196
1197 #[rstest]
1198 fn test_greeks_accuracy_put() {
1199 let s = 100.0;
1200 let k = 100.1;
1201 let t = 1.0;
1202 let r = 0.01;
1203 let b = 0.005;
1204 let sigma = 0.2;
1205 let is_call = false;
1206 let eps = 1e-3;
1207
1208 let greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
1209
1210 let price0 = |s: f64| black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0).price;
1211
1212 let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
1213 let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
1214 let vega_bnr = (black_scholes_greeks(s, r, b, sigma + eps, is_call, k, t, 1.0).price
1215 - black_scholes_greeks(s, r, b, sigma - eps, is_call, k, t, 1.0).price)
1216 / (2.0 * eps)
1217 / 100.0;
1218 let theta_bnr = (black_scholes_greeks(s, r, b, sigma, is_call, k, t - eps, 1.0).price
1219 - black_scholes_greeks(s, r, b, sigma, is_call, k, t + eps, 1.0).price)
1220 / (2.0 * eps)
1221 / 365.25;
1222
1223 let tolerance = 1e-5;
1224 assert!(
1225 (greeks.delta - delta_bnr).abs() < tolerance,
1226 "Delta difference exceeds tolerance"
1227 );
1228 assert!(
1229 (greeks.gamma - gamma_bnr).abs() < tolerance,
1230 "Gamma difference exceeds tolerance"
1231 );
1232 assert!(
1233 (greeks.vega - vega_bnr).abs() < tolerance,
1234 "Vega difference exceeds tolerance"
1235 );
1236 assert!(
1237 (greeks.theta - theta_bnr).abs() < tolerance,
1238 "Theta difference exceeds tolerance"
1239 );
1240 }
1241
1242 #[rstest]
1243 fn test_imply_vol_and_greeks_accuracy_call() {
1244 let s = 100.0;
1245 let k = 100.1;
1246 let t = 1.0;
1247 let r = 0.01;
1248 let b = 0.005;
1249 let sigma = 0.2;
1250 let is_call = true;
1251
1252 let base_greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
1253 let price = base_greeks.price;
1254
1255 let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price, 1.0);
1256
1257 let tolerance = 1e-5;
1258 assert!(
1259 (implied_result.vol - sigma).abs() < tolerance,
1260 "Vol difference exceeds tolerance"
1261 );
1262 assert!(
1263 (implied_result.price - base_greeks.price).abs() < tolerance,
1264 "Price difference exceeds tolerance"
1265 );
1266 assert!(
1267 (implied_result.delta - base_greeks.delta).abs() < tolerance,
1268 "Delta difference exceeds tolerance"
1269 );
1270 assert!(
1271 (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
1272 "Gamma difference exceeds tolerance"
1273 );
1274 assert!(
1275 (implied_result.vega - base_greeks.vega).abs() < tolerance,
1276 "Vega difference exceeds tolerance"
1277 );
1278 assert!(
1279 (implied_result.theta - base_greeks.theta).abs() < tolerance,
1280 "Theta difference exceeds tolerance"
1281 );
1282 }
1283
1284 #[rstest]
1285 fn test_imply_vol_and_greeks_accuracy_put() {
1286 let s = 100.0;
1287 let k = 100.1;
1288 let t = 1.0;
1289 let r = 0.01;
1290 let b = 0.005;
1291 let sigma = 0.2;
1292 let is_call = false;
1293
1294 let base_greeks = black_scholes_greeks(s, r, b, sigma, is_call, k, t, 1.0);
1295 let price = base_greeks.price;
1296
1297 let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price, 1.0);
1298
1299 let tolerance = 1e-5;
1300 assert!(
1301 (implied_result.vol - sigma).abs() < tolerance,
1302 "Vol difference exceeds tolerance"
1303 );
1304 assert!(
1305 (implied_result.price - base_greeks.price).abs() < tolerance,
1306 "Price difference exceeds tolerance"
1307 );
1308 assert!(
1309 (implied_result.delta - base_greeks.delta).abs() < tolerance,
1310 "Delta difference exceeds tolerance"
1311 );
1312 assert!(
1313 (implied_result.gamma - base_greeks.gamma).abs() < tolerance,
1314 "Gamma difference exceeds tolerance"
1315 );
1316 assert!(
1317 (implied_result.vega - base_greeks.vega).abs() < tolerance,
1318 "Vega difference exceeds tolerance"
1319 );
1320 assert!(
1321 (implied_result.theta - base_greeks.theta).abs() < tolerance,
1322 "Theta difference exceeds tolerance"
1323 );
1324 }
1325}