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