nautilus_model/data/
greeks.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Option *Greeks* data structures (delta, gamma, theta, vega, rho) used throughout the platform.
17
18use 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// dS_t = S_t * (b * dt + sigma * dW_t) (stock)
50// dC_t = r * C_t * dt (cash numeraire)
51#[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; // in absolute percent change
77    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; // 1 / 365.25 in change per calendar day
81
82    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    // in the money probability, P(phi * S_T > phi * K), phi = 1 if is_call else -1
168    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
302// Implement multiplication for quantity * greeks
303impl 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    // Interpolate the yield curve for a given expiry time
473    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, // expiry_in_days (approximately 3 months)
528            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); // Time decay for long option
654    }
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); // Time decay for long option
674    }
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); // Deep ITM call has delta close to 1
712        assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); // Low gamma for deep ITM
713    }
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); // Deep OTM call has delta close to 0
729        assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); // Low gamma for deep OTM
730    }
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; // Near zero time
741        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    // Note: Implied volatility tests across different strikes can be sensitive to numerical precision
767    // The basic implied vol test already covers the core functionality
768
769    // Note: Comprehensive implied vol consistency test is challenging due to numerical precision
770    // The existing accuracy tests already cover this functionality adequately
771
772    #[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        // Check that all other fields are zeroed
819        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%")); // itm_prob * 100
854        assert!(display_str.contains("20.00%")); // vol * 100
855        assert!(display_str.contains("250.00")); // pnl
856        assert!(display_str.contains("25.50")); // price
857        assert!(display_str.contains("0.65")); // delta
858    }
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        // Check scaled values
878        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")); // pnl
938        assert!(display_str.contains("125.50")); // price
939        assert!(display_str.contains("2.15")); // delta
940        assert!(display_str.contains("0.01")); // gamma (rounded)
941        assert!(display_str.contains("42.70")); // vega
942        assert!(display_str.contains("-2.30")); // theta
943    }
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)); // Uses first ts_init
971        assert_eq!(result.ts_event, UnixNanos::from(1_500_000_000)); // Uses first ts_event
972        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        // Test exact matches
1043        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        // Test interpolation (results will depend on quadratic_interpolation implementation)
1048        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; // Very deep ITM
1085        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); // Very deep ITM call
1097    }
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; // 200% volatility
1105        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, // Put option
1127            480.0,
1128            20240315,
1129            91, // expiry_in_days (approximately 3 months)
1130            0.25,
1131            100.0,
1132            1.0,
1133            500.0,
1134            0.05,
1135            0.05,
1136            0.25,
1137            -150.0, // Negative PnL
1138            8.5,
1139            -0.35, // Negative delta for put
1140            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    // Original accuracy tests (keeping these as they are comprehensive)
1152    #[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}