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