nautilus_common/
greeks.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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, rc::Rc};
19
20use anyhow;
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/// Calculates instrument and portfolio greeks (sensitivities of price moves with respect to market data moves).
33///
34/// Useful for risk management of options and futures portfolios.
35///
36/// Currently implemented greeks are:
37/// - Delta (first derivative of price with respect to spot move).
38/// - Gamma (second derivative of price with respect to spot move).
39/// - Vega (first derivative of price with respect to implied volatility of an option).
40/// - Theta (first derivative of price with respect to time to expiry).
41///
42/// Vega is expressed in terms of absolute percent changes ((dV / dVol) / 100).
43/// 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).
44///
45/// Also note that for ease of implementation we consider that american options (for stock options for example) are european for the computation of greeks.
46#[allow(dead_code)]
47#[derive(Debug)]
48pub struct GreeksCalculator {
49    cache: Rc<RefCell<Cache>>,
50    clock: Rc<RefCell<dyn Clock>>,
51}
52
53impl GreeksCalculator {
54    /// Creates a new [`GreeksCalculator`] instance.
55    pub fn new(cache: Rc<RefCell<Cache>>, clock: Rc<RefCell<dyn Clock>>) -> Self {
56        Self { cache, clock }
57    }
58
59    /// Calculates option or underlying greeks for a given instrument and a quantity of 1.
60    ///
61    /// Additional features:
62    /// - Apply shocks to the spot value of the instrument's underlying, implied volatility or time to expiry.
63    /// - Compute percent greeks.
64    /// - Compute beta-weighted delta and gamma with respect to an index.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the instrument definition is not found or greeks calculation fails.
69    ///
70    /// # Panics
71    ///
72    /// Panics if the instrument has no underlying identifier.
73    #[allow(clippy::too_many_arguments)]
74    pub fn instrument_greeks(
75        &self,
76        instrument_id: InstrumentId,
77        flat_interest_rate: Option<f64>,
78        flat_dividend_yield: Option<f64>,
79        spot_shock: Option<f64>,
80        vol_shock: Option<f64>,
81        time_to_expiry_shock: Option<f64>,
82        use_cached_greeks: Option<bool>,
83        cache_greeks: Option<bool>,
84        publish_greeks: Option<bool>,
85        ts_event: Option<UnixNanos>,
86        position: Option<Position>,
87        percent_greeks: Option<bool>,
88        index_instrument_id: Option<InstrumentId>,
89        beta_weights: Option<HashMap<InstrumentId, f64>>,
90    ) -> anyhow::Result<GreeksData> {
91        // Set default values
92        let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
93        let spot_shock = spot_shock.unwrap_or(0.0);
94        let vol_shock = vol_shock.unwrap_or(0.0);
95        let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
96        let use_cached_greeks = use_cached_greeks.unwrap_or(false);
97        let cache_greeks = cache_greeks.unwrap_or(false);
98        let publish_greeks = publish_greeks.unwrap_or(false);
99        let ts_event = ts_event.unwrap_or_default();
100        let percent_greeks = percent_greeks.unwrap_or(false);
101
102        let cache = self.cache.borrow();
103        let instrument = cache.instrument(&instrument_id);
104        let instrument = match instrument {
105            Some(instrument) => instrument,
106            None => anyhow::bail!(format!(
107                "Instrument definition for {instrument_id} not found."
108            )),
109        };
110
111        if instrument.instrument_class() != InstrumentClass::Option {
112            let multiplier = instrument.multiplier();
113            let underlying_instrument_id = instrument.id();
114            let underlying_price = cache
115                .price(&underlying_instrument_id, PriceType::Last)
116                .unwrap_or_default()
117                .as_f64();
118            let (delta, _) = self.modify_greeks(
119                multiplier.as_f64(),
120                0.0,
121                underlying_instrument_id,
122                underlying_price + spot_shock,
123                underlying_price,
124                percent_greeks,
125                index_instrument_id,
126                beta_weights.as_ref(),
127            );
128            let mut greeks_data =
129                GreeksData::from_delta(instrument_id, delta, multiplier.as_f64(), ts_event);
130
131            if let Some(pos) = position {
132                greeks_data.pnl = multiplier * ((underlying_price + spot_shock) - pos.avg_px_open);
133                greeks_data.price = greeks_data.pnl;
134            }
135
136            return Ok(greeks_data);
137        }
138
139        let mut greeks_data = None;
140        let underlying = instrument.underlying().unwrap();
141        let underlying_str = format!("{}.{}", underlying, instrument_id.venue);
142        let underlying_instrument_id = InstrumentId::from(underlying_str.as_str());
143
144        // Use cached greeks if requested
145        if use_cached_greeks {
146            if let Some(cached_greeks) = cache.greeks(&instrument_id) {
147                greeks_data = Some(cached_greeks);
148            }
149        }
150
151        if greeks_data.is_none() {
152            let utc_now_ns = if ts_event != UnixNanos::default() {
153                ts_event
154            } else {
155                self.clock.borrow().timestamp_ns()
156            };
157
158            let utc_now = utc_now_ns.to_datetime_utc();
159            let expiry_utc = instrument
160                .expiration_ns()
161                .map(|ns| ns.to_datetime_utc())
162                .unwrap_or_default();
163            let expiry_int = expiry_utc
164                .format("%Y%m%d")
165                .to_string()
166                .parse::<i32>()
167                .unwrap_or(0);
168            let expiry_in_years = (expiry_utc - utc_now).num_days().min(1) as f64 / 365.25;
169            let currency = instrument.quote_currency().code.to_string();
170            let interest_rate = match cache.yield_curve(&currency) {
171                Some(yield_curve) => yield_curve(expiry_in_years),
172                None => flat_interest_rate,
173            };
174
175            // cost of carry is 0 for futures
176            let mut cost_of_carry = 0.0;
177
178            if let Some(dividend_curve) = cache.yield_curve(&underlying_instrument_id.to_string()) {
179                let dividend_yield = dividend_curve(expiry_in_years);
180                cost_of_carry = interest_rate - dividend_yield;
181            } else if let Some(div_yield) = flat_dividend_yield {
182                // Use a dividend rate of 0. to have a cost of carry of interest rate for options on stocks
183                cost_of_carry = interest_rate - div_yield;
184            }
185
186            let multiplier = instrument.multiplier();
187            let is_call = instrument.option_kind().unwrap_or(OptionKind::Call) == OptionKind::Call;
188            let strike = instrument.strike_price().unwrap_or_default().as_f64();
189            let option_mid_price = cache
190                .price(&instrument_id, PriceType::Mid)
191                .unwrap_or_default()
192                .as_f64();
193            let underlying_price = cache
194                .price(&underlying_instrument_id, PriceType::Last)
195                .unwrap_or_default()
196                .as_f64();
197
198            let greeks = imply_vol_and_greeks(
199                underlying_price,
200                interest_rate,
201                cost_of_carry,
202                is_call,
203                strike,
204                expiry_in_years,
205                option_mid_price,
206                multiplier.as_f64(),
207            );
208            let (delta, gamma) = self.modify_greeks(
209                greeks.delta,
210                greeks.gamma,
211                underlying_instrument_id,
212                underlying_price,
213                underlying_price,
214                percent_greeks,
215                index_instrument_id,
216                beta_weights.as_ref(),
217            );
218            greeks_data = Some(GreeksData::new(
219                utc_now_ns,
220                utc_now_ns,
221                instrument_id,
222                is_call,
223                strike,
224                expiry_int,
225                expiry_in_years,
226                multiplier.as_f64(),
227                1.0,
228                underlying_price,
229                interest_rate,
230                cost_of_carry,
231                greeks.vol,
232                0.0,
233                greeks.price,
234                delta,
235                gamma,
236                greeks.vega,
237                greeks.theta,
238                (greeks.delta / multiplier.as_f64()).abs(),
239            ));
240
241            // Adding greeks to cache if requested
242            if cache_greeks {
243                let mut cache = self.cache.borrow_mut();
244                cache
245                    .add_greeks(greeks_data.clone().unwrap())
246                    .unwrap_or_default();
247            }
248
249            // Publishing greeks on the message bus if requested
250            if publish_greeks {
251                let topic = format!(
252                    "data.GreeksData.instrument_id={}",
253                    instrument_id.symbol.as_str()
254                )
255                .into();
256                msgbus::publish(topic, &greeks_data.clone().unwrap());
257            }
258        }
259
260        let mut greeks_data = greeks_data.unwrap();
261
262        if spot_shock != 0.0 || vol_shock != 0.0 || time_to_expiry_shock != 0.0 {
263            let underlying_price = greeks_data.underlying_price;
264            let shocked_underlying_price = underlying_price + spot_shock;
265            let shocked_vol = greeks_data.vol + vol_shock;
266            let shocked_time_to_expiry = greeks_data.expiry_in_years - time_to_expiry_shock;
267
268            let greeks = black_scholes_greeks(
269                shocked_underlying_price,
270                greeks_data.interest_rate,
271                greeks_data.cost_of_carry,
272                shocked_vol,
273                greeks_data.is_call,
274                greeks_data.strike,
275                shocked_time_to_expiry,
276                greeks_data.multiplier,
277            );
278            let (delta, gamma) = self.modify_greeks(
279                greeks.delta,
280                greeks.gamma,
281                underlying_instrument_id,
282                shocked_underlying_price,
283                underlying_price,
284                percent_greeks,
285                index_instrument_id,
286                beta_weights.as_ref(),
287            );
288            greeks_data = GreeksData::new(
289                greeks_data.ts_event,
290                greeks_data.ts_event,
291                greeks_data.instrument_id,
292                greeks_data.is_call,
293                greeks_data.strike,
294                greeks_data.expiry,
295                shocked_time_to_expiry,
296                greeks_data.multiplier,
297                greeks_data.quantity,
298                shocked_underlying_price,
299                greeks_data.interest_rate,
300                greeks_data.cost_of_carry,
301                shocked_vol,
302                0.0,
303                greeks.price,
304                delta,
305                gamma,
306                greeks.vega,
307                greeks.theta,
308                (greeks.delta / greeks_data.multiplier).abs(),
309            );
310        }
311
312        if let Some(pos) = position {
313            greeks_data.pnl = greeks_data.price - greeks_data.multiplier * pos.avg_px_open;
314        }
315
316        Ok(greeks_data)
317    }
318
319    /// Modifies delta and gamma based on beta weighting and percentage calculations.
320    ///
321    /// The beta weighting of delta and gamma follows this equation linking the returns of a stock x to the ones of an index I:
322    /// (x - x0) / x0 = alpha + beta (I - I0) / I0 + epsilon
323    ///
324    /// beta can be obtained by linear regression of stock_return = alpha + beta index_return, it's equal to:
325    /// beta = Covariance(stock_returns, index_returns) / Variance(index_returns)
326    ///
327    /// Considering alpha == 0:
328    /// x = x0 + beta x0 / I0 (I-I0)
329    /// I = I0 + 1 / beta I0 / x0 (x - x0)
330    ///
331    /// These two last equations explain the beta weighting below, considering the price of an option is V(x) and delta and gamma
332    /// are the first and second derivatives respectively of V.
333    ///
334    /// Also percent greeks assume a change of variable to percent returns by writing:
335    /// V(x = x0 * (1 + stock_percent_return / 100))
336    /// or V(I = I0 * (1 + index_percent_return / 100))
337    #[allow(clippy::too_many_arguments)]
338    pub fn modify_greeks(
339        &self,
340        delta_input: f64,
341        gamma_input: f64,
342        underlying_instrument_id: InstrumentId,
343        underlying_price: f64,
344        unshocked_underlying_price: f64,
345        percent_greeks: bool,
346        index_instrument_id: Option<InstrumentId>,
347        beta_weights: Option<&HashMap<InstrumentId, f64>>,
348    ) -> (f64, f64) {
349        let mut delta = delta_input;
350        let mut gamma = gamma_input;
351
352        let mut index_price = None;
353
354        if let Some(index_id) = index_instrument_id {
355            let cache = self.cache.borrow();
356            index_price = Some(
357                cache
358                    .price(&index_id, PriceType::Last)
359                    .unwrap_or_default()
360                    .as_f64(),
361            );
362
363            let mut beta = 1.0;
364            if let Some(weights) = beta_weights {
365                if let Some(&weight) = weights.get(&underlying_instrument_id) {
366                    beta = weight;
367                }
368            }
369
370            if let Some(ref mut idx_price) = index_price {
371                if underlying_price != unshocked_underlying_price {
372                    *idx_price += 1.0 / beta
373                        * (*idx_price / unshocked_underlying_price)
374                        * (underlying_price - unshocked_underlying_price);
375                }
376
377                let delta_multiplier = beta * underlying_price / *idx_price;
378                delta *= delta_multiplier;
379                gamma *= delta_multiplier.powi(2);
380            }
381        }
382
383        if percent_greeks {
384            if let Some(idx_price) = index_price {
385                delta *= idx_price / 100.0;
386                gamma *= (idx_price / 100.0).powi(2);
387            } else {
388                delta *= underlying_price / 100.0;
389                gamma *= (underlying_price / 100.0).powi(2);
390            }
391        }
392
393        (delta, gamma)
394    }
395
396    /// Calculates the portfolio Greeks for a given set of positions.
397    ///
398    /// Aggregates the Greeks data for all open positions that match the specified criteria.
399    ///
400    /// Additional features:
401    /// - Apply shocks to the spot value of an instrument's underlying, implied volatility or time to expiry.
402    /// - Compute percent greeks.
403    /// - Compute beta-weighted delta and gamma with respect to an index.
404    ///
405    /// # Errors
406    ///
407    /// Returns an error if any underlying greeks calculation fails.
408    #[allow(clippy::too_many_arguments)]
409    pub fn portfolio_greeks(
410        &self,
411        underlyings: Option<Vec<String>>,
412        venue: Option<Venue>,
413        instrument_id: Option<InstrumentId>,
414        strategy_id: Option<StrategyId>,
415        side: Option<PositionSide>,
416        flat_interest_rate: Option<f64>,
417        flat_dividend_yield: Option<f64>,
418        spot_shock: Option<f64>,
419        vol_shock: Option<f64>,
420        time_to_expiry_shock: Option<f64>,
421        use_cached_greeks: Option<bool>,
422        cache_greeks: Option<bool>,
423        publish_greeks: Option<bool>,
424        percent_greeks: Option<bool>,
425        index_instrument_id: Option<InstrumentId>,
426        beta_weights: Option<HashMap<InstrumentId, f64>>,
427    ) -> anyhow::Result<PortfolioGreeks> {
428        let ts_event = self.clock.borrow().timestamp_ns();
429        let mut portfolio_greeks =
430            PortfolioGreeks::new(ts_event, ts_event, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
431
432        // Set default values
433        let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
434        let spot_shock = spot_shock.unwrap_or(0.0);
435        let vol_shock = vol_shock.unwrap_or(0.0);
436        let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
437        let use_cached_greeks = use_cached_greeks.unwrap_or(false);
438        let cache_greeks = cache_greeks.unwrap_or(false);
439        let publish_greeks = publish_greeks.unwrap_or(false);
440        let percent_greeks = percent_greeks.unwrap_or(false);
441        let side = side.unwrap_or(PositionSide::NoPositionSide);
442
443        let cache = self.cache.borrow();
444        let open_positions = cache.positions(
445            venue.as_ref(),
446            instrument_id.as_ref(),
447            strategy_id.as_ref(),
448            Some(side),
449        );
450        let open_positions: Vec<Position> = open_positions.iter().map(|&p| p.clone()).collect();
451
452        for position in open_positions {
453            let position_instrument_id = position.instrument_id;
454
455            if let Some(ref underlyings_list) = underlyings {
456                let mut skip_position = true;
457
458                for underlying in underlyings_list {
459                    if position_instrument_id
460                        .symbol
461                        .as_str()
462                        .starts_with(underlying)
463                    {
464                        skip_position = false;
465                        break;
466                    }
467                }
468
469                if skip_position {
470                    continue;
471                }
472            }
473
474            let quantity = position.signed_qty;
475            let instrument_greeks = self.instrument_greeks(
476                position_instrument_id,
477                Some(flat_interest_rate),
478                flat_dividend_yield,
479                Some(spot_shock),
480                Some(vol_shock),
481                Some(time_to_expiry_shock),
482                Some(use_cached_greeks),
483                Some(cache_greeks),
484                Some(publish_greeks),
485                Some(ts_event),
486                Some(position),
487                Some(percent_greeks),
488                index_instrument_id,
489                beta_weights.clone(),
490            )?;
491            portfolio_greeks = portfolio_greeks + (quantity * &instrument_greeks).into();
492        }
493
494        Ok(portfolio_greeks)
495    }
496
497    /// Subscribes to Greeks data for a given underlying instrument.
498    ///
499    /// Useful for reading greeks from a backtesting data catalog and caching them for later use.
500    pub fn subscribe_greeks<F>(&self, underlying: &str, handler: Option<F>)
501    where
502        F: Fn(GreeksData) + 'static + Send + Sync,
503    {
504        let pattern = format!("data.GreeksData.instrument_id={}*", underlying).into();
505
506        if let Some(custom_handler) = handler {
507            let handler = msgbus::handler::TypedMessageHandler::with_any(
508                move |greeks: &dyn std::any::Any| {
509                    if let Some(greeks_data) = greeks.downcast_ref::<GreeksData>() {
510                        custom_handler(greeks_data.clone());
511                    }
512                },
513            );
514            msgbus::subscribe(
515                pattern,
516                msgbus::handler::ShareableMessageHandler(Rc::new(handler)),
517                None,
518            );
519        } else {
520            let cache_ref = self.cache.clone();
521            let default_handler = msgbus::handler::TypedMessageHandler::with_any(
522                move |greeks: &dyn std::any::Any| {
523                    if let Some(greeks_data) = greeks.downcast_ref::<GreeksData>() {
524                        let mut cache = cache_ref.borrow_mut();
525                        cache.add_greeks(greeks_data.clone()).unwrap_or_default();
526                    }
527                },
528            );
529            msgbus::subscribe(
530                pattern,
531                msgbus::handler::ShareableMessageHandler(Rc::new(default_handler)),
532                None,
533            );
534        }
535    }
536}