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