nautilus_common/
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//! Greeks calculator for options and futures.
17
18use std::{cell::RefCell, collections::HashMap, fmt::Debug, rc::Rc};
19
20use anyhow;
21use derive_builder::Builder;
22use nautilus_core::UnixNanos;
23use nautilus_model::{
24    data::greeks::{GreeksData, PortfolioGreeks, black_scholes_greeks, imply_vol_and_greeks},
25    enums::{InstrumentClass, OptionKind, PositionSide, PriceType},
26    identifiers::{InstrumentId, StrategyId, Venue},
27    instruments::Instrument,
28    position::Position,
29};
30
31use crate::{cache::Cache, clock::Clock, msgbus};
32
33/// Type alias for a greeks filter function.
34pub type GreeksFilter = Box<dyn Fn(&GreeksData) -> bool>;
35
36/// Cloneable wrapper for greeks filter functions.
37#[derive(Clone)]
38pub enum GreeksFilterCallback {
39    /// Function pointer (non-capturing closure)
40    Function(fn(&GreeksData) -> bool),
41    /// Boxed closure (may capture variables)
42    Closure(std::rc::Rc<dyn Fn(&GreeksData) -> bool>),
43}
44
45impl GreeksFilterCallback {
46    /// Create a new filter from a function pointer.
47    pub fn from_fn(f: fn(&GreeksData) -> bool) -> Self {
48        Self::Function(f)
49    }
50
51    /// Create a new filter from a closure.
52    pub fn from_closure<F>(f: F) -> Self
53    where
54        F: Fn(&GreeksData) -> bool + 'static,
55    {
56        Self::Closure(std::rc::Rc::new(f))
57    }
58
59    /// Call the filter function.
60    pub fn call(&self, data: &GreeksData) -> bool {
61        match self {
62            Self::Function(f) => f(data),
63            Self::Closure(f) => f(data),
64        }
65    }
66
67    /// Convert to the original GreeksFilter type.
68    pub fn to_greeks_filter(self) -> GreeksFilter {
69        match self {
70            Self::Function(f) => Box::new(f),
71            Self::Closure(f) => {
72                let f_clone = f.clone();
73                Box::new(move |data| f_clone(data))
74            }
75        }
76    }
77}
78
79impl Debug for GreeksFilterCallback {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        match self {
82            Self::Function(_) => f.write_str("GreeksFilterCallback::Function"),
83            Self::Closure(_) => f.write_str("GreeksFilterCallback::Closure"),
84        }
85    }
86}
87
88/// Builder for instrument greeks calculation parameters.
89#[derive(Debug, Builder)]
90#[builder(setter(into), derive(Debug))]
91pub struct InstrumentGreeksParams {
92    /// The instrument ID to calculate greeks for
93    pub instrument_id: InstrumentId,
94    /// Flat interest rate (default: 0.0425)
95    #[builder(default = "0.0425")]
96    pub flat_interest_rate: f64,
97    /// Flat dividend yield
98    #[builder(default)]
99    pub flat_dividend_yield: Option<f64>,
100    /// Spot price shock (default: 0.0)
101    #[builder(default = "0.0")]
102    pub spot_shock: f64,
103    /// Volatility shock (default: 0.0)
104    #[builder(default = "0.0")]
105    pub vol_shock: f64,
106    /// Time to expiry shock (default: 0.0)
107    #[builder(default = "0.0")]
108    pub time_to_expiry_shock: f64,
109    /// Whether to use cached greeks (default: false)
110    #[builder(default = "false")]
111    pub use_cached_greeks: bool,
112    /// Whether to cache greeks (default: false)
113    #[builder(default = "false")]
114    pub cache_greeks: bool,
115    /// Whether to publish greeks (default: false)
116    #[builder(default = "false")]
117    pub publish_greeks: bool,
118    /// Event timestamp
119    #[builder(default)]
120    pub ts_event: Option<UnixNanos>,
121    /// Position for PnL calculation
122    #[builder(default)]
123    pub position: Option<Position>,
124    /// Whether to compute percent greeks (default: false)
125    #[builder(default = "false")]
126    pub percent_greeks: bool,
127    /// Index instrument ID for beta weighting
128    #[builder(default)]
129    pub index_instrument_id: Option<InstrumentId>,
130    /// Beta weights for portfolio calculations
131    #[builder(default)]
132    pub beta_weights: Option<HashMap<InstrumentId, f64>>,
133    /// Base value in days for time-weighting vega
134    #[builder(default)]
135    pub vega_time_weight_base: Option<i32>,
136}
137
138impl InstrumentGreeksParams {
139    /// Calculate instrument greeks using the builder parameters.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if the greeks calculation fails.
144    pub fn calculate(&self, calculator: &GreeksCalculator) -> anyhow::Result<GreeksData> {
145        calculator.instrument_greeks(
146            self.instrument_id,
147            Some(self.flat_interest_rate),
148            self.flat_dividend_yield,
149            Some(self.spot_shock),
150            Some(self.vol_shock),
151            Some(self.time_to_expiry_shock),
152            Some(self.use_cached_greeks),
153            Some(self.cache_greeks),
154            Some(self.publish_greeks),
155            self.ts_event,
156            self.position.clone(),
157            Some(self.percent_greeks),
158            self.index_instrument_id,
159            self.beta_weights.clone(),
160            self.vega_time_weight_base,
161        )
162    }
163}
164
165/// Builder for portfolio greeks calculation parameters.
166#[derive(Builder)]
167#[builder(setter(into))]
168pub struct PortfolioGreeksParams {
169    /// List of underlying symbols to filter by
170    #[builder(default)]
171    pub underlyings: Option<Vec<String>>,
172    /// Venue to filter positions by
173    #[builder(default)]
174    pub venue: Option<Venue>,
175    /// Instrument ID to filter positions by
176    #[builder(default)]
177    pub instrument_id: Option<InstrumentId>,
178    /// Strategy ID to filter positions by
179    #[builder(default)]
180    pub strategy_id: Option<StrategyId>,
181    /// Position side to filter by (default: NoPositionSide)
182    #[builder(default)]
183    pub side: Option<PositionSide>,
184    /// Flat interest rate (default: 0.0425)
185    #[builder(default = "0.0425")]
186    pub flat_interest_rate: f64,
187    /// Flat dividend yield
188    #[builder(default)]
189    pub flat_dividend_yield: Option<f64>,
190    /// Spot price shock (default: 0.0)
191    #[builder(default = "0.0")]
192    pub spot_shock: f64,
193    /// Volatility shock (default: 0.0)
194    #[builder(default = "0.0")]
195    pub vol_shock: f64,
196    /// Time to expiry shock (default: 0.0)
197    #[builder(default = "0.0")]
198    pub time_to_expiry_shock: f64,
199    /// Whether to use cached greeks (default: false)
200    #[builder(default = "false")]
201    pub use_cached_greeks: bool,
202    /// Whether to cache greeks (default: false)
203    #[builder(default = "false")]
204    pub cache_greeks: bool,
205    /// Whether to publish greeks (default: false)
206    #[builder(default = "false")]
207    pub publish_greeks: bool,
208    /// Whether to compute percent greeks (default: false)
209    #[builder(default = "false")]
210    pub percent_greeks: bool,
211    /// Index instrument ID for beta weighting
212    #[builder(default)]
213    pub index_instrument_id: Option<InstrumentId>,
214    /// Beta weights for portfolio calculations
215    #[builder(default)]
216    pub beta_weights: Option<HashMap<InstrumentId, f64>>,
217    /// Filter function for greeks
218    #[builder(default)]
219    pub greeks_filter: Option<GreeksFilterCallback>,
220    /// Base value in days for time-weighting vega
221    #[builder(default)]
222    pub vega_time_weight_base: Option<i32>,
223}
224
225impl std::fmt::Debug for PortfolioGreeksParams {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        f.debug_struct("PortfolioGreeksParams")
228            .field("underlyings", &self.underlyings)
229            .field("venue", &self.venue)
230            .field("instrument_id", &self.instrument_id)
231            .field("strategy_id", &self.strategy_id)
232            .field("side", &self.side)
233            .field("flat_interest_rate", &self.flat_interest_rate)
234            .field("flat_dividend_yield", &self.flat_dividend_yield)
235            .field("spot_shock", &self.spot_shock)
236            .field("vol_shock", &self.vol_shock)
237            .field("time_to_expiry_shock", &self.time_to_expiry_shock)
238            .field("use_cached_greeks", &self.use_cached_greeks)
239            .field("cache_greeks", &self.cache_greeks)
240            .field("publish_greeks", &self.publish_greeks)
241            .field("percent_greeks", &self.percent_greeks)
242            .field("index_instrument_id", &self.index_instrument_id)
243            .field("beta_weights", &self.beta_weights)
244            .field("greeks_filter", &self.greeks_filter)
245            .finish()
246    }
247}
248
249impl PortfolioGreeksParams {
250    /// Calculate portfolio greeks using the builder parameters.
251    ///
252    /// # Errors
253    ///
254    /// Returns an error if the portfolio greeks calculation fails.
255    pub fn calculate(&self, calculator: &GreeksCalculator) -> anyhow::Result<PortfolioGreeks> {
256        let greeks_filter = self
257            .greeks_filter
258            .as_ref()
259            .map(|f| f.clone().to_greeks_filter());
260
261        calculator.portfolio_greeks(
262            self.underlyings.clone(),
263            self.venue,
264            self.instrument_id,
265            self.strategy_id,
266            self.side,
267            Some(self.flat_interest_rate),
268            self.flat_dividend_yield,
269            Some(self.spot_shock),
270            Some(self.vol_shock),
271            Some(self.time_to_expiry_shock),
272            Some(self.use_cached_greeks),
273            Some(self.cache_greeks),
274            Some(self.publish_greeks),
275            Some(self.percent_greeks),
276            self.index_instrument_id,
277            self.beta_weights.clone(),
278            greeks_filter,
279            self.vega_time_weight_base,
280        )
281    }
282}
283
284/// Calculates instrument and portfolio greeks (sensitivities of price moves with respect to market data moves).
285///
286/// Useful for risk management of options and futures portfolios.
287///
288/// Currently implemented greeks are:
289/// - Delta (first derivative of price with respect to spot move).
290/// - Gamma (second derivative of price with respect to spot move).
291/// - Vega (first derivative of price with respect to implied volatility of an option).
292/// - Theta (first derivative of price with respect to time to expiry).
293///
294/// Vega is expressed in terms of absolute percent changes ((dV / dVol) / 100).
295/// Theta is expressed in terms of daily changes ((dV / d(T-t)) / 365.25, where T is the expiry of an option and t is the current time).
296///
297/// Also note that for ease of implementation we consider that american options (for stock options for example) are european for the computation of greeks.
298#[allow(dead_code)]
299#[derive(Debug)]
300pub struct GreeksCalculator {
301    cache: Rc<RefCell<Cache>>,
302    clock: Rc<RefCell<dyn Clock>>,
303}
304
305impl GreeksCalculator {
306    /// Creates a new [`GreeksCalculator`] instance.
307    pub fn new(cache: Rc<RefCell<Cache>>, clock: Rc<RefCell<dyn Clock>>) -> Self {
308        Self { cache, clock }
309    }
310
311    /// Calculates option or underlying greeks for a given instrument and a quantity of 1.
312    ///
313    /// Additional features:
314    /// - Apply shocks to the spot value of the instrument's underlying, implied volatility or time to expiry.
315    /// - Compute percent greeks.
316    /// - Compute beta-weighted delta and gamma with respect to an index.
317    ///
318    /// # Errors
319    ///
320    /// Returns an error if the instrument definition is not found or greeks calculation fails.
321    ///
322    /// # Panics
323    ///
324    /// Panics if the instrument has no underlying identifier.
325    #[allow(clippy::too_many_arguments)]
326    pub fn instrument_greeks(
327        &self,
328        instrument_id: InstrumentId,
329        flat_interest_rate: Option<f64>,
330        flat_dividend_yield: Option<f64>,
331        spot_shock: Option<f64>,
332        vol_shock: Option<f64>,
333        time_to_expiry_shock: Option<f64>,
334        use_cached_greeks: Option<bool>,
335        cache_greeks: Option<bool>,
336        publish_greeks: Option<bool>,
337        ts_event: Option<UnixNanos>,
338        position: Option<Position>,
339        percent_greeks: Option<bool>,
340        index_instrument_id: Option<InstrumentId>,
341        beta_weights: Option<HashMap<InstrumentId, f64>>,
342        vega_time_weight_base: Option<i32>,
343    ) -> anyhow::Result<GreeksData> {
344        // Set default values
345        let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
346        let spot_shock = spot_shock.unwrap_or(0.0);
347        let vol_shock = vol_shock.unwrap_or(0.0);
348        let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
349        let use_cached_greeks = use_cached_greeks.unwrap_or(false);
350        let cache_greeks = cache_greeks.unwrap_or(false);
351        let publish_greeks = publish_greeks.unwrap_or(false);
352        let ts_event = ts_event.unwrap_or_default();
353        let percent_greeks = percent_greeks.unwrap_or(false);
354
355        let cache = self.cache.borrow();
356        let instrument = cache.instrument(&instrument_id);
357        let instrument = match instrument {
358            Some(instrument) => instrument,
359            None => anyhow::bail!(format!(
360                "Instrument definition for {instrument_id} not found."
361            )),
362        };
363
364        if instrument.instrument_class() != InstrumentClass::Option {
365            let multiplier = instrument.multiplier();
366            let underlying_instrument_id = instrument.id();
367            let underlying_price = cache
368                .price(&underlying_instrument_id, PriceType::Last)
369                .unwrap_or_default()
370                .as_f64();
371            let (delta, _, _) = self.modify_greeks(
372                multiplier.as_f64(),
373                0.0,
374                underlying_instrument_id,
375                underlying_price + spot_shock,
376                underlying_price,
377                percent_greeks,
378                index_instrument_id,
379                beta_weights.as_ref(),
380                0.0,
381                0.0,
382                0,
383                None,
384            );
385            let mut greeks_data =
386                GreeksData::from_delta(instrument_id, delta, multiplier.as_f64(), ts_event);
387
388            if let Some(pos) = position {
389                greeks_data.pnl = multiplier * ((underlying_price + spot_shock) - pos.avg_px_open);
390                greeks_data.price = greeks_data.pnl;
391            }
392
393            return Ok(greeks_data);
394        }
395
396        let mut greeks_data = None;
397        let underlying = instrument.underlying().unwrap();
398        let underlying_str = format!("{}.{}", underlying, instrument_id.venue);
399        let underlying_instrument_id = InstrumentId::from(underlying_str.as_str());
400
401        // Use cached greeks if requested
402        if use_cached_greeks && let Some(cached_greeks) = cache.greeks(&instrument_id) {
403            greeks_data = Some(cached_greeks);
404        }
405
406        if greeks_data.is_none() {
407            let utc_now_ns = if ts_event != UnixNanos::default() {
408                ts_event
409            } else {
410                self.clock.borrow().timestamp_ns()
411            };
412
413            let utc_now = utc_now_ns.to_datetime_utc();
414            let expiry_utc = instrument
415                .expiration_ns()
416                .map(|ns| ns.to_datetime_utc())
417                .unwrap_or_default();
418            let expiry_int = expiry_utc
419                .format("%Y%m%d")
420                .to_string()
421                .parse::<i32>()
422                .unwrap_or(0);
423            let expiry_in_days = (expiry_utc - utc_now).num_days().min(1) as i32;
424            let expiry_in_years = expiry_in_days as f64 / 365.25;
425            let currency = instrument.quote_currency().code.to_string();
426            let interest_rate = match cache.yield_curve(&currency) {
427                Some(yield_curve) => yield_curve(expiry_in_years),
428                None => flat_interest_rate,
429            };
430
431            // cost of carry is 0 for futures
432            let mut cost_of_carry = 0.0;
433
434            if let Some(dividend_curve) = cache.yield_curve(&underlying_instrument_id.to_string()) {
435                let dividend_yield = dividend_curve(expiry_in_years);
436                cost_of_carry = interest_rate - dividend_yield;
437            } else if let Some(div_yield) = flat_dividend_yield {
438                // Use a dividend rate of 0. to have a cost of carry of interest rate for options on stocks
439                cost_of_carry = interest_rate - div_yield;
440            }
441
442            let multiplier = instrument.multiplier();
443            let is_call = instrument.option_kind().unwrap_or(OptionKind::Call) == OptionKind::Call;
444            let strike = instrument.strike_price().unwrap_or_default().as_f64();
445            let option_mid_price = cache
446                .price(&instrument_id, PriceType::Mid)
447                .unwrap_or_default()
448                .as_f64();
449            let underlying_price = cache
450                .price(&underlying_instrument_id, PriceType::Last)
451                .unwrap_or_default()
452                .as_f64();
453
454            let greeks = imply_vol_and_greeks(
455                underlying_price,
456                interest_rate,
457                cost_of_carry,
458                is_call,
459                strike,
460                expiry_in_years,
461                option_mid_price,
462                multiplier.as_f64(),
463            );
464            let (delta, gamma, vega) = self.modify_greeks(
465                greeks.delta,
466                greeks.gamma,
467                underlying_instrument_id,
468                underlying_price,
469                underlying_price,
470                percent_greeks,
471                index_instrument_id,
472                beta_weights.as_ref(),
473                greeks.vega,
474                greeks.vol,
475                expiry_in_days,
476                vega_time_weight_base,
477            );
478            greeks_data = Some(GreeksData::new(
479                utc_now_ns,
480                utc_now_ns,
481                instrument_id,
482                is_call,
483                strike,
484                expiry_int,
485                expiry_in_days,
486                expiry_in_years,
487                multiplier.as_f64(),
488                1.0,
489                underlying_price,
490                interest_rate,
491                cost_of_carry,
492                greeks.vol,
493                0.0,
494                greeks.price,
495                delta,
496                gamma,
497                vega,
498                greeks.theta,
499                (greeks.delta / multiplier.as_f64()).abs(),
500            ));
501
502            // Adding greeks to cache if requested
503            if cache_greeks {
504                let mut cache = self.cache.borrow_mut();
505                cache
506                    .add_greeks(greeks_data.clone().unwrap())
507                    .unwrap_or_default();
508            }
509
510            // Publishing greeks on the message bus if requested
511            if publish_greeks {
512                let topic = format!(
513                    "data.GreeksData.instrument_id={}",
514                    instrument_id.symbol.as_str()
515                )
516                .into();
517                msgbus::publish(topic, &greeks_data.clone().unwrap());
518            }
519        }
520
521        let mut greeks_data = greeks_data.unwrap();
522
523        if spot_shock != 0.0 || vol_shock != 0.0 || time_to_expiry_shock != 0.0 {
524            let underlying_price = greeks_data.underlying_price;
525            let shocked_underlying_price = underlying_price + spot_shock;
526            let shocked_vol = greeks_data.vol + vol_shock;
527            let shocked_time_to_expiry = greeks_data.expiry_in_years - time_to_expiry_shock;
528            let shocked_expiry_in_days = (shocked_time_to_expiry * 365.25) as i32;
529
530            let greeks = black_scholes_greeks(
531                shocked_underlying_price,
532                greeks_data.interest_rate,
533                greeks_data.cost_of_carry,
534                shocked_vol,
535                greeks_data.is_call,
536                greeks_data.strike,
537                shocked_time_to_expiry,
538                greeks_data.multiplier,
539            );
540            let (delta, gamma, vega) = self.modify_greeks(
541                greeks.delta,
542                greeks.gamma,
543                underlying_instrument_id,
544                shocked_underlying_price,
545                underlying_price,
546                percent_greeks,
547                index_instrument_id,
548                beta_weights.as_ref(),
549                greeks.vega,
550                shocked_vol,
551                shocked_expiry_in_days,
552                vega_time_weight_base,
553            );
554            greeks_data = GreeksData::new(
555                greeks_data.ts_event,
556                greeks_data.ts_event,
557                greeks_data.instrument_id,
558                greeks_data.is_call,
559                greeks_data.strike,
560                greeks_data.expiry,
561                shocked_expiry_in_days,
562                shocked_time_to_expiry,
563                greeks_data.multiplier,
564                greeks_data.quantity,
565                shocked_underlying_price,
566                greeks_data.interest_rate,
567                greeks_data.cost_of_carry,
568                shocked_vol,
569                0.0,
570                greeks.price,
571                delta,
572                gamma,
573                vega,
574                greeks.theta,
575                (greeks.delta / greeks_data.multiplier).abs(),
576            );
577        }
578
579        if let Some(pos) = position {
580            greeks_data.pnl = greeks_data.price - greeks_data.multiplier * pos.avg_px_open;
581        }
582
583        Ok(greeks_data)
584    }
585
586    /// Modifies delta and gamma based on beta weighting and percentage calculations.
587    ///
588    /// The beta weighting of delta and gamma follows this equation linking the returns of a stock x to the ones of an index I:
589    /// (x - x0) / x0 = alpha + beta (I - I0) / I0 + epsilon
590    ///
591    /// beta can be obtained by linear regression of stock_return = alpha + beta index_return, it's equal to:
592    /// beta = Covariance(stock_returns, index_returns) / Variance(index_returns)
593    ///
594    /// Considering alpha == 0:
595    /// x = x0 + beta x0 / I0 (I-I0)
596    /// I = I0 + 1 / beta I0 / x0 (x - x0)
597    ///
598    /// These two last equations explain the beta weighting below, considering the price of an option is V(x) and delta and gamma
599    /// are the first and second derivatives respectively of V.
600    ///
601    /// Also percent greeks assume a change of variable to percent returns by writing:
602    /// V(x = x0 * (1 + stock_percent_return / 100))
603    /// or V(I = I0 * (1 + index_percent_return / 100))
604    #[allow(clippy::too_many_arguments)]
605    pub fn modify_greeks(
606        &self,
607        delta_input: f64,
608        gamma_input: f64,
609        underlying_instrument_id: InstrumentId,
610        underlying_price: f64,
611        unshocked_underlying_price: f64,
612        percent_greeks: bool,
613        index_instrument_id: Option<InstrumentId>,
614        beta_weights: Option<&HashMap<InstrumentId, f64>>,
615        vega_input: f64,
616        vol: f64,
617        expiry_in_days: i32,
618        vega_time_weight_base: Option<i32>,
619    ) -> (f64, f64, f64) {
620        let mut delta = delta_input;
621        let mut gamma = gamma_input;
622        let mut vega = vega_input;
623
624        let mut index_price = None;
625
626        if let Some(index_id) = index_instrument_id {
627            let cache = self.cache.borrow();
628            index_price = Some(
629                cache
630                    .price(&index_id, PriceType::Last)
631                    .unwrap_or_default()
632                    .as_f64(),
633            );
634
635            let mut beta = 1.0;
636            if let Some(weights) = beta_weights
637                && let Some(&weight) = weights.get(&underlying_instrument_id)
638            {
639                beta = weight;
640            }
641
642            if let Some(ref mut idx_price) = index_price {
643                if underlying_price != unshocked_underlying_price {
644                    *idx_price += 1.0 / beta
645                        * (*idx_price / unshocked_underlying_price)
646                        * (underlying_price - unshocked_underlying_price);
647                }
648
649                let delta_multiplier = beta * underlying_price / *idx_price;
650                delta *= delta_multiplier;
651                gamma *= delta_multiplier.powi(2);
652            }
653        }
654
655        if percent_greeks {
656            if let Some(idx_price) = index_price {
657                delta *= idx_price / 100.0;
658                gamma *= (idx_price / 100.0).powi(2);
659            } else {
660                delta *= underlying_price / 100.0;
661                gamma *= (underlying_price / 100.0).powi(2);
662            }
663
664            // Apply percent vega when percent_greeks is True
665            vega *= vol / 100.0;
666        }
667
668        // Apply time weighting to vega if vega_time_weight_base is provided
669        if let Some(time_base) = vega_time_weight_base
670            && expiry_in_days > 0
671        {
672            let time_weight = (time_base as f64 / expiry_in_days as f64).sqrt();
673            vega *= time_weight;
674        }
675
676        (delta, gamma, vega)
677    }
678
679    /// Calculates the portfolio Greeks for a given set of positions.
680    ///
681    /// Aggregates the Greeks data for all open positions that match the specified criteria.
682    ///
683    /// Additional features:
684    /// - Apply shocks to the spot value of an instrument's underlying, implied volatility or time to expiry.
685    /// - Compute percent greeks.
686    /// - Compute beta-weighted delta and gamma with respect to an index.
687    ///
688    /// # Errors
689    ///
690    /// Returns an error if any underlying greeks calculation fails.
691    ///
692    /// # Panics
693    ///
694    /// Panics if `greeks_filter` is `Some` but the filter function panics when called.
695    #[allow(clippy::too_many_arguments)]
696    pub fn portfolio_greeks(
697        &self,
698        underlyings: Option<Vec<String>>,
699        venue: Option<Venue>,
700        instrument_id: Option<InstrumentId>,
701        strategy_id: Option<StrategyId>,
702        side: Option<PositionSide>,
703        flat_interest_rate: Option<f64>,
704        flat_dividend_yield: Option<f64>,
705        spot_shock: Option<f64>,
706        vol_shock: Option<f64>,
707        time_to_expiry_shock: Option<f64>,
708        use_cached_greeks: Option<bool>,
709        cache_greeks: Option<bool>,
710        publish_greeks: Option<bool>,
711        percent_greeks: Option<bool>,
712        index_instrument_id: Option<InstrumentId>,
713        beta_weights: Option<HashMap<InstrumentId, f64>>,
714        greeks_filter: Option<GreeksFilter>,
715        vega_time_weight_base: Option<i32>,
716    ) -> anyhow::Result<PortfolioGreeks> {
717        let ts_event = self.clock.borrow().timestamp_ns();
718        let mut portfolio_greeks =
719            PortfolioGreeks::new(ts_event, ts_event, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
720
721        // Set default values
722        let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
723        let spot_shock = spot_shock.unwrap_or(0.0);
724        let vol_shock = vol_shock.unwrap_or(0.0);
725        let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
726        let use_cached_greeks = use_cached_greeks.unwrap_or(false);
727        let cache_greeks = cache_greeks.unwrap_or(false);
728        let publish_greeks = publish_greeks.unwrap_or(false);
729        let percent_greeks = percent_greeks.unwrap_or(false);
730        let side = side.unwrap_or(PositionSide::NoPositionSide);
731
732        let cache = self.cache.borrow();
733        let open_positions = cache.positions(
734            venue.as_ref(),
735            instrument_id.as_ref(),
736            strategy_id.as_ref(),
737            Some(side),
738        );
739        let open_positions: Vec<Position> = open_positions.iter().map(|&p| p.clone()).collect();
740
741        for position in open_positions {
742            let position_instrument_id = position.instrument_id;
743
744            if let Some(ref underlyings_list) = underlyings {
745                let mut skip_position = true;
746
747                for underlying in underlyings_list {
748                    if position_instrument_id
749                        .symbol
750                        .as_str()
751                        .starts_with(underlying)
752                    {
753                        skip_position = false;
754                        break;
755                    }
756                }
757
758                if skip_position {
759                    continue;
760                }
761            }
762
763            let quantity = position.signed_qty;
764            let instrument_greeks = self.instrument_greeks(
765                position_instrument_id,
766                Some(flat_interest_rate),
767                flat_dividend_yield,
768                Some(spot_shock),
769                Some(vol_shock),
770                Some(time_to_expiry_shock),
771                Some(use_cached_greeks),
772                Some(cache_greeks),
773                Some(publish_greeks),
774                Some(ts_event),
775                Some(position),
776                Some(percent_greeks),
777                index_instrument_id,
778                beta_weights.clone(),
779                vega_time_weight_base,
780            )?;
781            let position_greeks = (quantity * &instrument_greeks).into();
782
783            // Apply greeks filter if provided
784            if greeks_filter.is_none() || greeks_filter.as_ref().unwrap()(&instrument_greeks) {
785                portfolio_greeks = portfolio_greeks + position_greeks;
786            }
787        }
788
789        Ok(portfolio_greeks)
790    }
791
792    /// Subscribes to Greeks data for a given underlying instrument.
793    ///
794    /// Useful for reading greeks from a backtesting data catalog and caching them for later use.
795    pub fn subscribe_greeks<F>(&self, underlying: &str, handler: Option<F>)
796    where
797        F: Fn(GreeksData) + 'static + Send + Sync,
798    {
799        let pattern = format!("data.GreeksData.instrument_id={underlying}*").into();
800
801        if let Some(custom_handler) = handler {
802            let handler = msgbus::handler::TypedMessageHandler::with_any(
803                move |greeks: &dyn std::any::Any| {
804                    if let Some(greeks_data) = greeks.downcast_ref::<GreeksData>() {
805                        custom_handler(greeks_data.clone());
806                    }
807                },
808            );
809            msgbus::subscribe(
810                pattern,
811                msgbus::handler::ShareableMessageHandler(Rc::new(handler)),
812                None,
813            );
814        } else {
815            let cache_ref = self.cache.clone();
816            let default_handler = msgbus::handler::TypedMessageHandler::with_any(
817                move |greeks: &dyn std::any::Any| {
818                    if let Some(greeks_data) = greeks.downcast_ref::<GreeksData>() {
819                        let mut cache = cache_ref.borrow_mut();
820                        cache.add_greeks(greeks_data.clone()).unwrap_or_default();
821                    }
822                },
823            );
824            msgbus::subscribe(
825                pattern,
826                msgbus::handler::ShareableMessageHandler(Rc::new(default_handler)),
827                None,
828            );
829        }
830    }
831}
832
833#[cfg(test)]
834mod tests {
835    use std::{cell::RefCell, collections::HashMap, rc::Rc};
836
837    use nautilus_model::{
838        enums::PositionSide,
839        identifiers::{InstrumentId, StrategyId, Venue},
840    };
841    use rstest::rstest;
842
843    use super::*;
844    use crate::{cache::Cache, clock::TestClock};
845
846    fn create_test_calculator() -> GreeksCalculator {
847        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
848        let clock = Rc::new(RefCell::new(TestClock::new()));
849        GreeksCalculator::new(cache, clock)
850    }
851
852    #[rstest]
853    fn test_greeks_calculator_creation() {
854        let calculator = create_test_calculator();
855        // Test that the calculator can be created
856        assert!(format!("{calculator:?}").contains("GreeksCalculator"));
857    }
858
859    #[rstest]
860    fn test_greeks_calculator_debug() {
861        let calculator = create_test_calculator();
862        // Test the debug representation
863        let debug_str = format!("{calculator:?}");
864        assert!(debug_str.contains("GreeksCalculator"));
865    }
866
867    #[rstest]
868    fn test_greeks_calculator_has_python_bindings() {
869        // This test just verifies that the GreeksCalculator struct
870        // can be compiled with Python bindings enabled
871        let calculator = create_test_calculator();
872        // The Python methods are only accessible from Python,
873        // but we can verify the struct compiles correctly
874        assert!(format!("{calculator:?}").contains("GreeksCalculator"));
875    }
876
877    #[rstest]
878    fn test_instrument_greeks_params_builder_default() {
879        let instrument_id = InstrumentId::from("AAPL.NASDAQ");
880
881        let params = InstrumentGreeksParamsBuilder::default()
882            .instrument_id(instrument_id)
883            .build()
884            .expect("Failed to build InstrumentGreeksParams");
885
886        assert_eq!(params.instrument_id, instrument_id);
887        assert_eq!(params.flat_interest_rate, 0.0425);
888        assert_eq!(params.flat_dividend_yield, None);
889        assert_eq!(params.spot_shock, 0.0);
890        assert_eq!(params.vol_shock, 0.0);
891        assert_eq!(params.time_to_expiry_shock, 0.0);
892        assert!(!params.use_cached_greeks);
893        assert!(!params.cache_greeks);
894        assert!(!params.publish_greeks);
895        assert_eq!(params.ts_event, None);
896        assert_eq!(params.position, None);
897        assert!(!params.percent_greeks);
898        assert_eq!(params.index_instrument_id, None);
899        assert_eq!(params.beta_weights, None);
900    }
901
902    #[rstest]
903    fn test_instrument_greeks_params_builder_custom_values() {
904        let instrument_id = InstrumentId::from("AAPL.NASDAQ");
905        let index_id = InstrumentId::from("SPY.NASDAQ");
906        let mut beta_weights = HashMap::new();
907        beta_weights.insert(instrument_id, 1.2);
908
909        let params = InstrumentGreeksParamsBuilder::default()
910            .instrument_id(instrument_id)
911            .flat_interest_rate(0.05)
912            .flat_dividend_yield(Some(0.02))
913            .spot_shock(0.01)
914            .vol_shock(0.05)
915            .time_to_expiry_shock(0.1)
916            .use_cached_greeks(true)
917            .cache_greeks(true)
918            .publish_greeks(true)
919            .percent_greeks(true)
920            .index_instrument_id(Some(index_id))
921            .beta_weights(Some(beta_weights.clone()))
922            .build()
923            .expect("Failed to build InstrumentGreeksParams");
924
925        assert_eq!(params.instrument_id, instrument_id);
926        assert_eq!(params.flat_interest_rate, 0.05);
927        assert_eq!(params.flat_dividend_yield, Some(0.02));
928        assert_eq!(params.spot_shock, 0.01);
929        assert_eq!(params.vol_shock, 0.05);
930        assert_eq!(params.time_to_expiry_shock, 0.1);
931        assert!(params.use_cached_greeks);
932        assert!(params.cache_greeks);
933        assert!(params.publish_greeks);
934        assert!(params.percent_greeks);
935        assert_eq!(params.index_instrument_id, Some(index_id));
936        assert_eq!(params.beta_weights, Some(beta_weights));
937    }
938
939    #[rstest]
940    fn test_instrument_greeks_params_debug() {
941        let instrument_id = InstrumentId::from("AAPL.NASDAQ");
942
943        let params = InstrumentGreeksParamsBuilder::default()
944            .instrument_id(instrument_id)
945            .build()
946            .expect("Failed to build InstrumentGreeksParams");
947
948        let debug_str = format!("{params:?}");
949        assert!(debug_str.contains("InstrumentGreeksParams"));
950        assert!(debug_str.contains("AAPL.NASDAQ"));
951    }
952
953    #[rstest]
954    fn test_portfolio_greeks_params_builder_default() {
955        let params = PortfolioGreeksParamsBuilder::default()
956            .build()
957            .expect("Failed to build PortfolioGreeksParams");
958
959        assert_eq!(params.underlyings, None);
960        assert_eq!(params.venue, None);
961        assert_eq!(params.instrument_id, None);
962        assert_eq!(params.strategy_id, None);
963        assert_eq!(params.side, None);
964        assert_eq!(params.flat_interest_rate, 0.0425);
965        assert_eq!(params.flat_dividend_yield, None);
966        assert_eq!(params.spot_shock, 0.0);
967        assert_eq!(params.vol_shock, 0.0);
968        assert_eq!(params.time_to_expiry_shock, 0.0);
969        assert!(!params.use_cached_greeks);
970        assert!(!params.cache_greeks);
971        assert!(!params.publish_greeks);
972        assert!(!params.percent_greeks);
973        assert_eq!(params.index_instrument_id, None);
974        assert_eq!(params.beta_weights, None);
975    }
976
977    #[rstest]
978    fn test_portfolio_greeks_params_builder_custom_values() {
979        let venue = Venue::from("NASDAQ");
980        let instrument_id = InstrumentId::from("AAPL.NASDAQ");
981        let strategy_id = StrategyId::from("test-strategy");
982        let index_id = InstrumentId::from("SPY.NASDAQ");
983        let underlyings = vec!["AAPL".to_string(), "MSFT".to_string()];
984        let mut beta_weights = HashMap::new();
985        beta_weights.insert(instrument_id, 1.2);
986
987        let params = PortfolioGreeksParamsBuilder::default()
988            .underlyings(Some(underlyings.clone()))
989            .venue(Some(venue))
990            .instrument_id(Some(instrument_id))
991            .strategy_id(Some(strategy_id))
992            .side(Some(PositionSide::Long))
993            .flat_interest_rate(0.05)
994            .flat_dividend_yield(Some(0.02))
995            .spot_shock(0.01)
996            .vol_shock(0.05)
997            .time_to_expiry_shock(0.1)
998            .use_cached_greeks(true)
999            .cache_greeks(true)
1000            .publish_greeks(true)
1001            .percent_greeks(true)
1002            .index_instrument_id(Some(index_id))
1003            .beta_weights(Some(beta_weights.clone()))
1004            .build()
1005            .expect("Failed to build PortfolioGreeksParams");
1006
1007        assert_eq!(params.underlyings, Some(underlyings));
1008        assert_eq!(params.venue, Some(venue));
1009        assert_eq!(params.instrument_id, Some(instrument_id));
1010        assert_eq!(params.strategy_id, Some(strategy_id));
1011        assert_eq!(params.side, Some(PositionSide::Long));
1012        assert_eq!(params.flat_interest_rate, 0.05);
1013        assert_eq!(params.flat_dividend_yield, Some(0.02));
1014        assert_eq!(params.spot_shock, 0.01);
1015        assert_eq!(params.vol_shock, 0.05);
1016        assert_eq!(params.time_to_expiry_shock, 0.1);
1017        assert!(params.use_cached_greeks);
1018        assert!(params.cache_greeks);
1019        assert!(params.publish_greeks);
1020        assert!(params.percent_greeks);
1021        assert_eq!(params.index_instrument_id, Some(index_id));
1022        assert_eq!(params.beta_weights, Some(beta_weights));
1023    }
1024
1025    #[rstest]
1026    fn test_portfolio_greeks_params_debug() {
1027        let venue = Venue::from("NASDAQ");
1028
1029        let params = PortfolioGreeksParamsBuilder::default()
1030            .venue(Some(venue))
1031            .build()
1032            .expect("Failed to build PortfolioGreeksParams");
1033
1034        let debug_str = format!("{params:?}");
1035        assert!(debug_str.contains("PortfolioGreeksParams"));
1036        assert!(debug_str.contains("NASDAQ"));
1037    }
1038
1039    #[rstest]
1040    fn test_instrument_greeks_params_builder_missing_required_field() {
1041        // Test that building without required instrument_id fails
1042        let result = InstrumentGreeksParamsBuilder::default().build();
1043        assert!(result.is_err());
1044    }
1045
1046    #[rstest]
1047    fn test_portfolio_greeks_params_builder_fluent_api() {
1048        let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1049
1050        let params = PortfolioGreeksParamsBuilder::default()
1051            .instrument_id(Some(instrument_id))
1052            .flat_interest_rate(0.05)
1053            .spot_shock(0.01)
1054            .percent_greeks(true)
1055            .build()
1056            .expect("Failed to build PortfolioGreeksParams");
1057
1058        assert_eq!(params.instrument_id, Some(instrument_id));
1059        assert_eq!(params.flat_interest_rate, 0.05);
1060        assert_eq!(params.spot_shock, 0.01);
1061        assert!(params.percent_greeks);
1062    }
1063
1064    #[rstest]
1065    fn test_instrument_greeks_params_builder_fluent_chaining() {
1066        let instrument_id = InstrumentId::from("TSLA.NASDAQ");
1067
1068        // Test fluent API chaining
1069        let params = InstrumentGreeksParamsBuilder::default()
1070            .instrument_id(instrument_id)
1071            .flat_interest_rate(0.03)
1072            .spot_shock(0.02)
1073            .vol_shock(0.1)
1074            .use_cached_greeks(true)
1075            .percent_greeks(true)
1076            .build()
1077            .expect("Failed to build InstrumentGreeksParams");
1078
1079        assert_eq!(params.instrument_id, instrument_id);
1080        assert_eq!(params.flat_interest_rate, 0.03);
1081        assert_eq!(params.spot_shock, 0.02);
1082        assert_eq!(params.vol_shock, 0.1);
1083        assert!(params.use_cached_greeks);
1084        assert!(params.percent_greeks);
1085    }
1086
1087    #[rstest]
1088    fn test_portfolio_greeks_params_builder_with_underlyings() {
1089        let underlyings = vec!["AAPL".to_string(), "MSFT".to_string(), "GOOGL".to_string()];
1090
1091        let params = PortfolioGreeksParamsBuilder::default()
1092            .underlyings(Some(underlyings.clone()))
1093            .flat_interest_rate(0.04)
1094            .build()
1095            .expect("Failed to build PortfolioGreeksParams");
1096
1097        assert_eq!(params.underlyings, Some(underlyings));
1098        assert_eq!(params.flat_interest_rate, 0.04);
1099    }
1100
1101    #[rstest]
1102    fn test_builders_with_empty_beta_weights() {
1103        let instrument_id = InstrumentId::from("NVDA.NASDAQ");
1104        let empty_beta_weights = HashMap::new();
1105
1106        let instrument_params = InstrumentGreeksParamsBuilder::default()
1107            .instrument_id(instrument_id)
1108            .beta_weights(Some(empty_beta_weights.clone()))
1109            .build()
1110            .expect("Failed to build InstrumentGreeksParams");
1111
1112        let portfolio_params = PortfolioGreeksParamsBuilder::default()
1113            .beta_weights(Some(empty_beta_weights.clone()))
1114            .build()
1115            .expect("Failed to build PortfolioGreeksParams");
1116
1117        assert_eq!(
1118            instrument_params.beta_weights,
1119            Some(empty_beta_weights.clone())
1120        );
1121        assert_eq!(portfolio_params.beta_weights, Some(empty_beta_weights));
1122    }
1123
1124    #[rstest]
1125    fn test_builders_with_all_shocks() {
1126        let instrument_id = InstrumentId::from("AMD.NASDAQ");
1127
1128        let instrument_params = InstrumentGreeksParamsBuilder::default()
1129            .instrument_id(instrument_id)
1130            .spot_shock(0.05)
1131            .vol_shock(0.1)
1132            .time_to_expiry_shock(0.01)
1133            .build()
1134            .expect("Failed to build InstrumentGreeksParams");
1135
1136        let portfolio_params = PortfolioGreeksParamsBuilder::default()
1137            .spot_shock(0.05)
1138            .vol_shock(0.1)
1139            .time_to_expiry_shock(0.01)
1140            .build()
1141            .expect("Failed to build PortfolioGreeksParams");
1142
1143        assert_eq!(instrument_params.spot_shock, 0.05);
1144        assert_eq!(instrument_params.vol_shock, 0.1);
1145        assert_eq!(instrument_params.time_to_expiry_shock, 0.01);
1146
1147        assert_eq!(portfolio_params.spot_shock, 0.05);
1148        assert_eq!(portfolio_params.vol_shock, 0.1);
1149        assert_eq!(portfolio_params.time_to_expiry_shock, 0.01);
1150    }
1151
1152    #[rstest]
1153    fn test_builders_with_all_boolean_flags() {
1154        let instrument_id = InstrumentId::from("META.NASDAQ");
1155
1156        let instrument_params = InstrumentGreeksParamsBuilder::default()
1157            .instrument_id(instrument_id)
1158            .use_cached_greeks(true)
1159            .cache_greeks(true)
1160            .publish_greeks(true)
1161            .percent_greeks(true)
1162            .build()
1163            .expect("Failed to build InstrumentGreeksParams");
1164
1165        let portfolio_params = PortfolioGreeksParamsBuilder::default()
1166            .use_cached_greeks(true)
1167            .cache_greeks(true)
1168            .publish_greeks(true)
1169            .percent_greeks(true)
1170            .build()
1171            .expect("Failed to build PortfolioGreeksParams");
1172
1173        assert!(instrument_params.use_cached_greeks);
1174        assert!(instrument_params.cache_greeks);
1175        assert!(instrument_params.publish_greeks);
1176        assert!(instrument_params.percent_greeks);
1177
1178        assert!(portfolio_params.use_cached_greeks);
1179        assert!(portfolio_params.cache_greeks);
1180        assert!(portfolio_params.publish_greeks);
1181        assert!(portfolio_params.percent_greeks);
1182    }
1183
1184    #[rstest]
1185    fn test_greeks_filter_callback_function() {
1186        // Test function pointer filter
1187        fn filter_positive_delta(data: &GreeksData) -> bool {
1188            data.delta > 0.0
1189        }
1190
1191        let filter = GreeksFilterCallback::from_fn(filter_positive_delta);
1192
1193        // Create test data
1194        let greeks_data = GreeksData::from_delta(
1195            InstrumentId::from("TEST.NASDAQ"),
1196            0.5,
1197            1.0,
1198            UnixNanos::default(),
1199        );
1200
1201        assert!(filter.call(&greeks_data));
1202
1203        // Test debug formatting
1204        let debug_str = format!("{filter:?}");
1205        assert!(debug_str.contains("GreeksFilterCallback::Function"));
1206    }
1207
1208    #[rstest]
1209    fn test_greeks_filter_callback_closure() {
1210        // Test closure filter that captures a variable
1211        let min_delta = 0.3;
1212        let filter =
1213            GreeksFilterCallback::from_closure(move |data: &GreeksData| data.delta > min_delta);
1214
1215        // Create test data
1216        let greeks_data = GreeksData::from_delta(
1217            InstrumentId::from("TEST.NASDAQ"),
1218            0.5,
1219            1.0,
1220            UnixNanos::default(),
1221        );
1222
1223        assert!(filter.call(&greeks_data));
1224
1225        // Test debug formatting
1226        let debug_str = format!("{filter:?}");
1227        assert!(debug_str.contains("GreeksFilterCallback::Closure"));
1228    }
1229
1230    #[rstest]
1231    fn test_greeks_filter_callback_clone() {
1232        fn filter_fn(data: &GreeksData) -> bool {
1233            data.delta > 0.0
1234        }
1235
1236        let filter1 = GreeksFilterCallback::from_fn(filter_fn);
1237        let filter2 = filter1.clone();
1238
1239        let greeks_data = GreeksData::from_delta(
1240            InstrumentId::from("TEST.NASDAQ"),
1241            0.5,
1242            1.0,
1243            UnixNanos::default(),
1244        );
1245
1246        assert!(filter1.call(&greeks_data));
1247        assert!(filter2.call(&greeks_data));
1248    }
1249
1250    #[rstest]
1251    fn test_portfolio_greeks_params_with_filter() {
1252        fn filter_high_delta(data: &GreeksData) -> bool {
1253            data.delta.abs() > 0.1
1254        }
1255
1256        let filter = GreeksFilterCallback::from_fn(filter_high_delta);
1257
1258        let params = PortfolioGreeksParamsBuilder::default()
1259            .greeks_filter(Some(filter))
1260            .flat_interest_rate(0.05)
1261            .build()
1262            .expect("Failed to build PortfolioGreeksParams");
1263
1264        assert!(params.greeks_filter.is_some());
1265        assert_eq!(params.flat_interest_rate, 0.05);
1266
1267        // Test that the filter can be called
1268        let greeks_data = GreeksData::from_delta(
1269            InstrumentId::from("TEST.NASDAQ"),
1270            0.5,
1271            1.0,
1272            UnixNanos::default(),
1273        );
1274
1275        let filter_ref = params.greeks_filter.as_ref().unwrap();
1276        assert!(filter_ref.call(&greeks_data));
1277    }
1278
1279    #[rstest]
1280    fn test_portfolio_greeks_params_with_closure_filter() {
1281        let min_gamma = 0.01;
1282        let filter =
1283            GreeksFilterCallback::from_closure(move |data: &GreeksData| data.gamma > min_gamma);
1284
1285        let params = PortfolioGreeksParamsBuilder::default()
1286            .greeks_filter(Some(filter))
1287            .build()
1288            .expect("Failed to build PortfolioGreeksParams");
1289
1290        assert!(params.greeks_filter.is_some());
1291
1292        // Test debug formatting includes the filter
1293        let debug_str = format!("{params:?}");
1294        assert!(debug_str.contains("greeks_filter"));
1295    }
1296
1297    #[rstest]
1298    fn test_greeks_filter_to_greeks_filter_conversion() {
1299        fn filter_fn(data: &GreeksData) -> bool {
1300            data.delta > 0.0
1301        }
1302
1303        let callback = GreeksFilterCallback::from_fn(filter_fn);
1304        let greeks_filter = callback.to_greeks_filter();
1305
1306        let greeks_data = GreeksData::from_delta(
1307            InstrumentId::from("TEST.NASDAQ"),
1308            0.5,
1309            1.0,
1310            UnixNanos::default(),
1311        );
1312
1313        assert!(greeks_filter(&greeks_data));
1314    }
1315}