Skip to main content

nautilus_common/
greeks.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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, msgbus::TypedHandler};
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 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                1.0,
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 = (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);
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                self.clock.borrow().timestamp_ns()
408            } else {
409                ts_event
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            );
462            let (delta, gamma, vega) = self.modify_greeks(
463                greeks.delta,
464                greeks.gamma,
465                underlying_instrument_id,
466                underlying_price,
467                underlying_price,
468                percent_greeks,
469                index_instrument_id,
470                beta_weights.as_ref(),
471                greeks.vega,
472                greeks.vol,
473                expiry_in_days,
474                vega_time_weight_base,
475            );
476            greeks_data = Some(GreeksData::new(
477                utc_now_ns,
478                utc_now_ns,
479                instrument_id,
480                is_call,
481                strike,
482                expiry_int,
483                expiry_in_days,
484                expiry_in_years,
485                multiplier.as_f64(),
486                1.0,
487                underlying_price,
488                interest_rate,
489                cost_of_carry,
490                greeks.vol,
491                0.0,
492                greeks.price,
493                delta,
494                gamma,
495                vega,
496                greeks.theta,
497                greeks.itm_prob,
498            ));
499
500            // Adding greeks to cache if requested
501            if cache_greeks {
502                let mut cache = self.cache.borrow_mut();
503                cache
504                    .add_greeks(greeks_data.clone().unwrap())
505                    .unwrap_or_default();
506            }
507
508            // Publishing greeks on the message bus if requested
509            if publish_greeks {
510                let topic = format!(
511                    "data.GreeksData.instrument_id={}",
512                    instrument_id.symbol.as_str()
513                )
514                .into();
515                msgbus::publish_greeks(topic, &greeks_data.clone().unwrap());
516            }
517        }
518
519        let mut greeks_data = greeks_data.unwrap();
520
521        if spot_shock != 0.0 || vol_shock != 0.0 || time_to_expiry_shock != 0.0 {
522            let underlying_price = greeks_data.underlying_price;
523            let shocked_underlying_price = underlying_price + spot_shock;
524            let shocked_vol = greeks_data.vol + vol_shock;
525            let shocked_time_to_expiry = greeks_data.expiry_in_years - time_to_expiry_shock;
526            let shocked_expiry_in_days = (shocked_time_to_expiry * 365.25) as i32;
527
528            let greeks = black_scholes_greeks(
529                shocked_underlying_price,
530                greeks_data.interest_rate,
531                greeks_data.cost_of_carry,
532                shocked_vol,
533                greeks_data.is_call,
534                greeks_data.strike,
535                shocked_time_to_expiry,
536            );
537            let (delta, gamma, vega) = self.modify_greeks(
538                greeks.delta,
539                greeks.gamma,
540                underlying_instrument_id,
541                shocked_underlying_price,
542                underlying_price,
543                percent_greeks,
544                index_instrument_id,
545                beta_weights.as_ref(),
546                greeks.vega,
547                shocked_vol,
548                shocked_expiry_in_days,
549                vega_time_weight_base,
550            );
551            greeks_data = GreeksData::new(
552                greeks_data.ts_event,
553                greeks_data.ts_event,
554                greeks_data.instrument_id,
555                greeks_data.is_call,
556                greeks_data.strike,
557                greeks_data.expiry,
558                shocked_expiry_in_days,
559                shocked_time_to_expiry,
560                greeks_data.multiplier,
561                greeks_data.quantity,
562                shocked_underlying_price,
563                greeks_data.interest_rate,
564                greeks_data.cost_of_carry,
565                shocked_vol,
566                0.0,
567                greeks.price,
568                delta,
569                gamma,
570                vega,
571                greeks.theta,
572                greeks.itm_prob,
573            );
574        }
575
576        if let Some(pos) = position {
577            greeks_data.pnl = greeks_data.price - greeks_data.multiplier * pos.avg_px_open;
578        }
579
580        Ok(greeks_data)
581    }
582
583    /// Modifies delta and gamma based on beta weighting and percentage calculations.
584    ///
585    /// The beta weighting of delta and gamma follows this equation linking the returns of a stock x to the ones of an index I:
586    /// (x - x0) / x0 = alpha + beta (I - I0) / I0 + epsilon
587    ///
588    /// beta can be obtained by linear regression of stock_return = alpha + beta index_return, it's equal to:
589    /// beta = Covariance(stock_returns, index_returns) / Variance(index_returns)
590    ///
591    /// Considering alpha == 0:
592    /// x = x0 + beta x0 / I0 (I-I0)
593    /// I = I0 + 1 / beta I0 / x0 (x - x0)
594    ///
595    /// These two last equations explain the beta weighting below, considering the price of an option is V(x) and delta and gamma
596    /// are the first and second derivatives respectively of V.
597    ///
598    /// Also percent greeks assume a change of variable to percent returns by writing:
599    /// V(x = x0 * (1 + stock_percent_return / 100))
600    /// or V(I = I0 * (1 + index_percent_return / 100))
601    #[allow(clippy::too_many_arguments)]
602    pub fn modify_greeks(
603        &self,
604        delta_input: f64,
605        gamma_input: f64,
606        underlying_instrument_id: InstrumentId,
607        underlying_price: f64,
608        unshocked_underlying_price: f64,
609        percent_greeks: bool,
610        index_instrument_id: Option<InstrumentId>,
611        beta_weights: Option<&HashMap<InstrumentId, f64>>,
612        vega_input: f64,
613        vol: f64,
614        expiry_in_days: i32,
615        vega_time_weight_base: Option<i32>,
616    ) -> (f64, f64, f64) {
617        let mut delta = delta_input;
618        let mut gamma = gamma_input;
619        let mut vega = vega_input;
620
621        let mut index_price = None;
622
623        if let Some(index_id) = index_instrument_id {
624            let cache = self.cache.borrow();
625            index_price = Some(
626                cache
627                    .price(&index_id, PriceType::Last)
628                    .unwrap_or_default()
629                    .as_f64(),
630            );
631
632            let mut beta = 1.0;
633            if let Some(weights) = beta_weights
634                && let Some(&weight) = weights.get(&underlying_instrument_id)
635            {
636                beta = weight;
637            }
638
639            if let Some(ref mut idx_price) = index_price {
640                if underlying_price != unshocked_underlying_price {
641                    *idx_price += 1.0 / beta
642                        * (*idx_price / unshocked_underlying_price)
643                        * (underlying_price - unshocked_underlying_price);
644                }
645
646                let delta_multiplier = beta * underlying_price / *idx_price;
647                delta *= delta_multiplier;
648                gamma *= delta_multiplier.powi(2);
649            }
650        }
651
652        if percent_greeks {
653            if let Some(idx_price) = index_price {
654                delta *= idx_price / 100.0;
655                gamma *= (idx_price / 100.0).powi(2);
656            } else {
657                delta *= underlying_price / 100.0;
658                gamma *= (underlying_price / 100.0).powi(2);
659            }
660
661            // Apply percent vega when percent_greeks is True
662            vega *= vol / 100.0;
663        }
664
665        // Apply time weighting to vega if vega_time_weight_base is provided
666        if let Some(time_base) = vega_time_weight_base
667            && expiry_in_days > 0
668        {
669            let time_weight = (time_base as f64 / expiry_in_days as f64).sqrt();
670            vega *= time_weight;
671        }
672
673        (delta, gamma, vega)
674    }
675
676    /// Calculates the portfolio Greeks for a given set of positions.
677    ///
678    /// Aggregates the Greeks data for all open positions that match the specified criteria.
679    ///
680    /// Additional features:
681    /// - Apply shocks to the spot value of an instrument's underlying, implied volatility or time to expiry.
682    /// - Compute percent greeks.
683    /// - Compute beta-weighted delta and gamma with respect to an index.
684    ///
685    /// # Errors
686    ///
687    /// Returns an error if any underlying greeks calculation fails.
688    ///
689    /// # Panics
690    ///
691    /// Panics if `greeks_filter` is `Some` but the filter function panics when called.
692    #[allow(clippy::too_many_arguments)]
693    pub fn portfolio_greeks(
694        &self,
695        underlyings: Option<Vec<String>>,
696        venue: Option<Venue>,
697        instrument_id: Option<InstrumentId>,
698        strategy_id: Option<StrategyId>,
699        side: Option<PositionSide>,
700        flat_interest_rate: Option<f64>,
701        flat_dividend_yield: Option<f64>,
702        spot_shock: Option<f64>,
703        vol_shock: Option<f64>,
704        time_to_expiry_shock: Option<f64>,
705        use_cached_greeks: Option<bool>,
706        cache_greeks: Option<bool>,
707        publish_greeks: Option<bool>,
708        percent_greeks: Option<bool>,
709        index_instrument_id: Option<InstrumentId>,
710        beta_weights: Option<HashMap<InstrumentId, f64>>,
711        greeks_filter: Option<GreeksFilter>,
712        vega_time_weight_base: Option<i32>,
713    ) -> anyhow::Result<PortfolioGreeks> {
714        let ts_event = self.clock.borrow().timestamp_ns();
715        let mut portfolio_greeks =
716            PortfolioGreeks::new(ts_event, ts_event, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
717
718        // Set default values
719        let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
720        let spot_shock = spot_shock.unwrap_or(0.0);
721        let vol_shock = vol_shock.unwrap_or(0.0);
722        let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
723        let use_cached_greeks = use_cached_greeks.unwrap_or(false);
724        let cache_greeks = cache_greeks.unwrap_or(false);
725        let publish_greeks = publish_greeks.unwrap_or(false);
726        let percent_greeks = percent_greeks.unwrap_or(false);
727        let side = side.unwrap_or(PositionSide::NoPositionSide);
728
729        let cache = self.cache.borrow();
730        let open_positions = cache.positions(
731            venue.as_ref(),
732            instrument_id.as_ref(),
733            strategy_id.as_ref(),
734            None, // account_id
735            Some(side),
736        );
737        let open_positions: Vec<Position> = open_positions.iter().map(|&p| p.clone()).collect();
738
739        for position in open_positions {
740            let position_instrument_id = position.instrument_id;
741
742            if let Some(ref underlyings_list) = underlyings {
743                let mut skip_position = true;
744
745                for underlying in underlyings_list {
746                    if position_instrument_id
747                        .symbol
748                        .as_str()
749                        .starts_with(underlying)
750                    {
751                        skip_position = false;
752                        break;
753                    }
754                }
755
756                if skip_position {
757                    continue;
758                }
759            }
760
761            let quantity = position.signed_qty;
762            let instrument_greeks = self.instrument_greeks(
763                position_instrument_id,
764                Some(flat_interest_rate),
765                flat_dividend_yield,
766                Some(spot_shock),
767                Some(vol_shock),
768                Some(time_to_expiry_shock),
769                Some(use_cached_greeks),
770                Some(cache_greeks),
771                Some(publish_greeks),
772                Some(ts_event),
773                Some(position),
774                Some(percent_greeks),
775                index_instrument_id,
776                beta_weights.clone(),
777                vega_time_weight_base,
778            )?;
779            let position_greeks = (quantity * &instrument_greeks).into();
780
781            // Apply greeks filter if provided
782            if greeks_filter.is_none() || greeks_filter.as_ref().unwrap()(&instrument_greeks) {
783                portfolio_greeks = portfolio_greeks + position_greeks;
784            }
785        }
786
787        Ok(portfolio_greeks)
788    }
789
790    /// Subscribes to Greeks data for a given underlying instrument.
791    ///
792    /// Useful for reading greeks from a backtesting data catalog and caching them for later use.
793    pub fn subscribe_greeks<F>(&self, underlying: &str, handler: Option<F>)
794    where
795        F: Fn(&GreeksData) + 'static,
796    {
797        let pattern = format!("data.GreeksData.instrument_id={underlying}*").into();
798
799        if let Some(custom_handler) = handler {
800            let typed_handler = TypedHandler::from(custom_handler);
801            msgbus::subscribe_greeks(pattern, typed_handler, None);
802        } else {
803            let cache_ref = self.cache.clone();
804            let typed_handler = TypedHandler::from(move |greeks: &GreeksData| {
805                let mut cache = cache_ref.borrow_mut();
806                cache.add_greeks(greeks.clone()).unwrap_or_default();
807            });
808            msgbus::subscribe_greeks(pattern, typed_handler, None);
809        }
810    }
811}
812
813#[cfg(test)]
814mod tests {
815    use std::{cell::RefCell, collections::HashMap, rc::Rc};
816
817    use nautilus_model::{
818        enums::PositionSide,
819        identifiers::{InstrumentId, StrategyId, Venue},
820    };
821    use rstest::rstest;
822
823    use super::*;
824    use crate::{cache::Cache, clock::TestClock};
825
826    fn create_test_calculator() -> GreeksCalculator {
827        let cache = Rc::new(RefCell::new(Cache::new(None, None)));
828        let clock = Rc::new(RefCell::new(TestClock::new()));
829        GreeksCalculator::new(cache, clock)
830    }
831
832    #[rstest]
833    fn test_greeks_calculator_creation() {
834        let calculator = create_test_calculator();
835        // Test that the calculator can be created
836        assert!(format!("{calculator:?}").contains("GreeksCalculator"));
837    }
838
839    #[rstest]
840    fn test_greeks_calculator_debug() {
841        let calculator = create_test_calculator();
842        // Test the debug representation
843        let debug_str = format!("{calculator:?}");
844        assert!(debug_str.contains("GreeksCalculator"));
845    }
846
847    #[rstest]
848    fn test_greeks_calculator_has_python_bindings() {
849        // This test just verifies that the GreeksCalculator struct
850        // can be compiled with Python bindings enabled
851        let calculator = create_test_calculator();
852        // The Python methods are only accessible from Python,
853        // but we can verify the struct compiles correctly
854        assert!(format!("{calculator:?}").contains("GreeksCalculator"));
855    }
856
857    #[rstest]
858    fn test_instrument_greeks_params_builder_default() {
859        let instrument_id = InstrumentId::from("AAPL.NASDAQ");
860
861        let params = InstrumentGreeksParamsBuilder::default()
862            .instrument_id(instrument_id)
863            .build()
864            .expect("Failed to build InstrumentGreeksParams");
865
866        assert_eq!(params.instrument_id, instrument_id);
867        assert_eq!(params.flat_interest_rate, 0.0425);
868        assert_eq!(params.flat_dividend_yield, None);
869        assert_eq!(params.spot_shock, 0.0);
870        assert_eq!(params.vol_shock, 0.0);
871        assert_eq!(params.time_to_expiry_shock, 0.0);
872        assert!(!params.use_cached_greeks);
873        assert!(!params.cache_greeks);
874        assert!(!params.publish_greeks);
875        assert_eq!(params.ts_event, None);
876        assert_eq!(params.position, None);
877        assert!(!params.percent_greeks);
878        assert_eq!(params.index_instrument_id, None);
879        assert_eq!(params.beta_weights, None);
880    }
881
882    #[rstest]
883    fn test_instrument_greeks_params_builder_custom_values() {
884        let instrument_id = InstrumentId::from("AAPL.NASDAQ");
885        let index_id = InstrumentId::from("SPY.NASDAQ");
886        let mut beta_weights = HashMap::new();
887        beta_weights.insert(instrument_id, 1.2);
888
889        let params = InstrumentGreeksParamsBuilder::default()
890            .instrument_id(instrument_id)
891            .flat_interest_rate(0.05)
892            .flat_dividend_yield(Some(0.02))
893            .spot_shock(0.01)
894            .vol_shock(0.05)
895            .time_to_expiry_shock(0.1)
896            .use_cached_greeks(true)
897            .cache_greeks(true)
898            .publish_greeks(true)
899            .percent_greeks(true)
900            .index_instrument_id(Some(index_id))
901            .beta_weights(Some(beta_weights.clone()))
902            .build()
903            .expect("Failed to build InstrumentGreeksParams");
904
905        assert_eq!(params.instrument_id, instrument_id);
906        assert_eq!(params.flat_interest_rate, 0.05);
907        assert_eq!(params.flat_dividend_yield, Some(0.02));
908        assert_eq!(params.spot_shock, 0.01);
909        assert_eq!(params.vol_shock, 0.05);
910        assert_eq!(params.time_to_expiry_shock, 0.1);
911        assert!(params.use_cached_greeks);
912        assert!(params.cache_greeks);
913        assert!(params.publish_greeks);
914        assert!(params.percent_greeks);
915        assert_eq!(params.index_instrument_id, Some(index_id));
916        assert_eq!(params.beta_weights, Some(beta_weights));
917    }
918
919    #[rstest]
920    fn test_instrument_greeks_params_debug() {
921        let instrument_id = InstrumentId::from("AAPL.NASDAQ");
922
923        let params = InstrumentGreeksParamsBuilder::default()
924            .instrument_id(instrument_id)
925            .build()
926            .expect("Failed to build InstrumentGreeksParams");
927
928        let debug_str = format!("{params:?}");
929        assert!(debug_str.contains("InstrumentGreeksParams"));
930        assert!(debug_str.contains("AAPL.NASDAQ"));
931    }
932
933    #[rstest]
934    fn test_portfolio_greeks_params_builder_default() {
935        let params = PortfolioGreeksParamsBuilder::default()
936            .build()
937            .expect("Failed to build PortfolioGreeksParams");
938
939        assert_eq!(params.underlyings, None);
940        assert_eq!(params.venue, None);
941        assert_eq!(params.instrument_id, None);
942        assert_eq!(params.strategy_id, None);
943        assert_eq!(params.side, None);
944        assert_eq!(params.flat_interest_rate, 0.0425);
945        assert_eq!(params.flat_dividend_yield, None);
946        assert_eq!(params.spot_shock, 0.0);
947        assert_eq!(params.vol_shock, 0.0);
948        assert_eq!(params.time_to_expiry_shock, 0.0);
949        assert!(!params.use_cached_greeks);
950        assert!(!params.cache_greeks);
951        assert!(!params.publish_greeks);
952        assert!(!params.percent_greeks);
953        assert_eq!(params.index_instrument_id, None);
954        assert_eq!(params.beta_weights, None);
955    }
956
957    #[rstest]
958    fn test_portfolio_greeks_params_builder_custom_values() {
959        let venue = Venue::from("NASDAQ");
960        let instrument_id = InstrumentId::from("AAPL.NASDAQ");
961        let strategy_id = StrategyId::from("test-strategy");
962        let index_id = InstrumentId::from("SPY.NASDAQ");
963        let underlyings = vec!["AAPL".to_string(), "MSFT".to_string()];
964        let mut beta_weights = HashMap::new();
965        beta_weights.insert(instrument_id, 1.2);
966
967        let params = PortfolioGreeksParamsBuilder::default()
968            .underlyings(Some(underlyings.clone()))
969            .venue(Some(venue))
970            .instrument_id(Some(instrument_id))
971            .strategy_id(Some(strategy_id))
972            .side(Some(PositionSide::Long))
973            .flat_interest_rate(0.05)
974            .flat_dividend_yield(Some(0.02))
975            .spot_shock(0.01)
976            .vol_shock(0.05)
977            .time_to_expiry_shock(0.1)
978            .use_cached_greeks(true)
979            .cache_greeks(true)
980            .publish_greeks(true)
981            .percent_greeks(true)
982            .index_instrument_id(Some(index_id))
983            .beta_weights(Some(beta_weights.clone()))
984            .build()
985            .expect("Failed to build PortfolioGreeksParams");
986
987        assert_eq!(params.underlyings, Some(underlyings));
988        assert_eq!(params.venue, Some(venue));
989        assert_eq!(params.instrument_id, Some(instrument_id));
990        assert_eq!(params.strategy_id, Some(strategy_id));
991        assert_eq!(params.side, Some(PositionSide::Long));
992        assert_eq!(params.flat_interest_rate, 0.05);
993        assert_eq!(params.flat_dividend_yield, Some(0.02));
994        assert_eq!(params.spot_shock, 0.01);
995        assert_eq!(params.vol_shock, 0.05);
996        assert_eq!(params.time_to_expiry_shock, 0.1);
997        assert!(params.use_cached_greeks);
998        assert!(params.cache_greeks);
999        assert!(params.publish_greeks);
1000        assert!(params.percent_greeks);
1001        assert_eq!(params.index_instrument_id, Some(index_id));
1002        assert_eq!(params.beta_weights, Some(beta_weights));
1003    }
1004
1005    #[rstest]
1006    fn test_portfolio_greeks_params_debug() {
1007        let venue = Venue::from("NASDAQ");
1008
1009        let params = PortfolioGreeksParamsBuilder::default()
1010            .venue(Some(venue))
1011            .build()
1012            .expect("Failed to build PortfolioGreeksParams");
1013
1014        let debug_str = format!("{params:?}");
1015        assert!(debug_str.contains("PortfolioGreeksParams"));
1016        assert!(debug_str.contains("NASDAQ"));
1017    }
1018
1019    #[rstest]
1020    fn test_instrument_greeks_params_builder_missing_required_field() {
1021        // Test that building without required instrument_id fails
1022        let result = InstrumentGreeksParamsBuilder::default().build();
1023        assert!(result.is_err());
1024    }
1025
1026    #[rstest]
1027    fn test_portfolio_greeks_params_builder_fluent_api() {
1028        let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1029
1030        let params = PortfolioGreeksParamsBuilder::default()
1031            .instrument_id(Some(instrument_id))
1032            .flat_interest_rate(0.05)
1033            .spot_shock(0.01)
1034            .percent_greeks(true)
1035            .build()
1036            .expect("Failed to build PortfolioGreeksParams");
1037
1038        assert_eq!(params.instrument_id, Some(instrument_id));
1039        assert_eq!(params.flat_interest_rate, 0.05);
1040        assert_eq!(params.spot_shock, 0.01);
1041        assert!(params.percent_greeks);
1042    }
1043
1044    #[rstest]
1045    fn test_instrument_greeks_params_builder_fluent_chaining() {
1046        let instrument_id = InstrumentId::from("TSLA.NASDAQ");
1047
1048        // Test fluent API chaining
1049        let params = InstrumentGreeksParamsBuilder::default()
1050            .instrument_id(instrument_id)
1051            .flat_interest_rate(0.03)
1052            .spot_shock(0.02)
1053            .vol_shock(0.1)
1054            .use_cached_greeks(true)
1055            .percent_greeks(true)
1056            .build()
1057            .expect("Failed to build InstrumentGreeksParams");
1058
1059        assert_eq!(params.instrument_id, instrument_id);
1060        assert_eq!(params.flat_interest_rate, 0.03);
1061        assert_eq!(params.spot_shock, 0.02);
1062        assert_eq!(params.vol_shock, 0.1);
1063        assert!(params.use_cached_greeks);
1064        assert!(params.percent_greeks);
1065    }
1066
1067    #[rstest]
1068    fn test_portfolio_greeks_params_builder_with_underlyings() {
1069        let underlyings = vec!["AAPL".to_string(), "MSFT".to_string(), "GOOGL".to_string()];
1070
1071        let params = PortfolioGreeksParamsBuilder::default()
1072            .underlyings(Some(underlyings.clone()))
1073            .flat_interest_rate(0.04)
1074            .build()
1075            .expect("Failed to build PortfolioGreeksParams");
1076
1077        assert_eq!(params.underlyings, Some(underlyings));
1078        assert_eq!(params.flat_interest_rate, 0.04);
1079    }
1080
1081    #[rstest]
1082    fn test_builders_with_empty_beta_weights() {
1083        let instrument_id = InstrumentId::from("NVDA.NASDAQ");
1084        let empty_beta_weights = HashMap::new();
1085
1086        let instrument_params = InstrumentGreeksParamsBuilder::default()
1087            .instrument_id(instrument_id)
1088            .beta_weights(Some(empty_beta_weights.clone()))
1089            .build()
1090            .expect("Failed to build InstrumentGreeksParams");
1091
1092        let portfolio_params = PortfolioGreeksParamsBuilder::default()
1093            .beta_weights(Some(empty_beta_weights.clone()))
1094            .build()
1095            .expect("Failed to build PortfolioGreeksParams");
1096
1097        assert_eq!(
1098            instrument_params.beta_weights,
1099            Some(empty_beta_weights.clone())
1100        );
1101        assert_eq!(portfolio_params.beta_weights, Some(empty_beta_weights));
1102    }
1103
1104    #[rstest]
1105    fn test_builders_with_all_shocks() {
1106        let instrument_id = InstrumentId::from("AMD.NASDAQ");
1107
1108        let instrument_params = InstrumentGreeksParamsBuilder::default()
1109            .instrument_id(instrument_id)
1110            .spot_shock(0.05)
1111            .vol_shock(0.1)
1112            .time_to_expiry_shock(0.01)
1113            .build()
1114            .expect("Failed to build InstrumentGreeksParams");
1115
1116        let portfolio_params = PortfolioGreeksParamsBuilder::default()
1117            .spot_shock(0.05)
1118            .vol_shock(0.1)
1119            .time_to_expiry_shock(0.01)
1120            .build()
1121            .expect("Failed to build PortfolioGreeksParams");
1122
1123        assert_eq!(instrument_params.spot_shock, 0.05);
1124        assert_eq!(instrument_params.vol_shock, 0.1);
1125        assert_eq!(instrument_params.time_to_expiry_shock, 0.01);
1126
1127        assert_eq!(portfolio_params.spot_shock, 0.05);
1128        assert_eq!(portfolio_params.vol_shock, 0.1);
1129        assert_eq!(portfolio_params.time_to_expiry_shock, 0.01);
1130    }
1131
1132    #[rstest]
1133    fn test_builders_with_all_boolean_flags() {
1134        let instrument_id = InstrumentId::from("META.NASDAQ");
1135
1136        let instrument_params = InstrumentGreeksParamsBuilder::default()
1137            .instrument_id(instrument_id)
1138            .use_cached_greeks(true)
1139            .cache_greeks(true)
1140            .publish_greeks(true)
1141            .percent_greeks(true)
1142            .build()
1143            .expect("Failed to build InstrumentGreeksParams");
1144
1145        let portfolio_params = PortfolioGreeksParamsBuilder::default()
1146            .use_cached_greeks(true)
1147            .cache_greeks(true)
1148            .publish_greeks(true)
1149            .percent_greeks(true)
1150            .build()
1151            .expect("Failed to build PortfolioGreeksParams");
1152
1153        assert!(instrument_params.use_cached_greeks);
1154        assert!(instrument_params.cache_greeks);
1155        assert!(instrument_params.publish_greeks);
1156        assert!(instrument_params.percent_greeks);
1157
1158        assert!(portfolio_params.use_cached_greeks);
1159        assert!(portfolio_params.cache_greeks);
1160        assert!(portfolio_params.publish_greeks);
1161        assert!(portfolio_params.percent_greeks);
1162    }
1163
1164    #[rstest]
1165    fn test_greeks_filter_callback_function() {
1166        // Test function pointer filter
1167        fn filter_positive_delta(data: &GreeksData) -> bool {
1168            data.delta > 0.0
1169        }
1170
1171        let filter = GreeksFilterCallback::from_fn(filter_positive_delta);
1172
1173        // Create test data
1174        let greeks_data = GreeksData::from_delta(
1175            InstrumentId::from("TEST.NASDAQ"),
1176            0.5,
1177            1.0,
1178            UnixNanos::default(),
1179        );
1180
1181        assert!(filter.call(&greeks_data));
1182
1183        // Test debug formatting
1184        let debug_str = format!("{filter:?}");
1185        assert!(debug_str.contains("GreeksFilterCallback::Function"));
1186    }
1187
1188    #[rstest]
1189    fn test_greeks_filter_callback_closure() {
1190        // Test closure filter that captures a variable
1191        let min_delta = 0.3;
1192        let filter =
1193            GreeksFilterCallback::from_closure(move |data: &GreeksData| data.delta > min_delta);
1194
1195        // Create test data
1196        let greeks_data = GreeksData::from_delta(
1197            InstrumentId::from("TEST.NASDAQ"),
1198            0.5,
1199            1.0,
1200            UnixNanos::default(),
1201        );
1202
1203        assert!(filter.call(&greeks_data));
1204
1205        // Test debug formatting
1206        let debug_str = format!("{filter:?}");
1207        assert!(debug_str.contains("GreeksFilterCallback::Closure"));
1208    }
1209
1210    #[rstest]
1211    fn test_greeks_filter_callback_clone() {
1212        fn filter_fn(data: &GreeksData) -> bool {
1213            data.delta > 0.0
1214        }
1215
1216        let filter1 = GreeksFilterCallback::from_fn(filter_fn);
1217        let filter2 = filter1.clone();
1218
1219        let greeks_data = GreeksData::from_delta(
1220            InstrumentId::from("TEST.NASDAQ"),
1221            0.5,
1222            1.0,
1223            UnixNanos::default(),
1224        );
1225
1226        assert!(filter1.call(&greeks_data));
1227        assert!(filter2.call(&greeks_data));
1228    }
1229
1230    #[rstest]
1231    fn test_portfolio_greeks_params_with_filter() {
1232        fn filter_high_delta(data: &GreeksData) -> bool {
1233            data.delta.abs() > 0.1
1234        }
1235
1236        let filter = GreeksFilterCallback::from_fn(filter_high_delta);
1237
1238        let params = PortfolioGreeksParamsBuilder::default()
1239            .greeks_filter(Some(filter))
1240            .flat_interest_rate(0.05)
1241            .build()
1242            .expect("Failed to build PortfolioGreeksParams");
1243
1244        assert!(params.greeks_filter.is_some());
1245        assert_eq!(params.flat_interest_rate, 0.05);
1246
1247        // Test that the filter can be called
1248        let greeks_data = GreeksData::from_delta(
1249            InstrumentId::from("TEST.NASDAQ"),
1250            0.5,
1251            1.0,
1252            UnixNanos::default(),
1253        );
1254
1255        let filter_ref = params.greeks_filter.as_ref().unwrap();
1256        assert!(filter_ref.call(&greeks_data));
1257    }
1258
1259    #[rstest]
1260    fn test_portfolio_greeks_params_with_closure_filter() {
1261        let min_gamma = 0.01;
1262        let filter =
1263            GreeksFilterCallback::from_closure(move |data: &GreeksData| data.gamma > min_gamma);
1264
1265        let params = PortfolioGreeksParamsBuilder::default()
1266            .greeks_filter(Some(filter))
1267            .build()
1268            .expect("Failed to build PortfolioGreeksParams");
1269
1270        assert!(params.greeks_filter.is_some());
1271
1272        // Test debug formatting includes the filter
1273        let debug_str = format!("{params:?}");
1274        assert!(debug_str.contains("greeks_filter"));
1275    }
1276
1277    #[rstest]
1278    fn test_greeks_filter_to_greeks_filter_conversion() {
1279        fn filter_fn(data: &GreeksData) -> bool {
1280            data.delta > 0.0
1281        }
1282
1283        let callback = GreeksFilterCallback::from_fn(filter_fn);
1284        let greeks_filter = callback.to_greeks_filter();
1285
1286        let greeks_data = GreeksData::from_delta(
1287            InstrumentId::from("TEST.NASDAQ"),
1288            0.5,
1289            1.0,
1290            UnixNanos::default(),
1291        );
1292
1293        assert!(greeks_filter(&greeks_data));
1294    }
1295}