1use 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#[allow(dead_code)]
48pub struct GreeksCalculator {
49 cache: Rc<RefCell<Cache>>,
50 clock: Rc<RefCell<dyn Clock>>,
51}
52
53impl GreeksCalculator {
54 pub fn new(cache: Rc<RefCell<Cache>>, clock: Rc<RefCell<dyn Clock>>) -> Self {
56 Self { cache, clock }
57 }
58
59 #[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 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 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(¤cy) {
163 Some(yield_curve) => yield_curve(expiry_in_years),
164 None => flat_interest_rate,
165 };
166
167 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 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 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 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 #[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 #[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 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 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}