1use std::{cell::RefCell, collections::HashMap, fmt::Debug, rc::Rc};
19
20use ahash::AHashMap;
21use derive_builder::Builder;
22use nautilus_core::UnixNanos;
23use nautilus_model::{
24 data::greeks::{
25 GreeksData, OptionGreekValues, PortfolioGreeks, black_scholes_greeks, imply_vol_and_greeks,
26 refine_vol_and_greeks,
27 },
28 enums::{AssetClass, InstrumentClass, OptionKind, PositionSide, PriceType},
29 identifiers::{InstrumentId, StrategyId, Venue},
30 instruments::{Instrument, any::InstrumentAny},
31 position::Position,
32 types::Price,
33};
34
35use crate::{cache::Cache, clock::Clock, msgbus, msgbus::TypedHandler};
36
37pub type GreeksFilter = Box<dyn Fn(&GreeksData) -> bool>;
39
40#[derive(Clone)]
42pub enum GreeksFilterCallback {
43 Function(fn(&GreeksData) -> bool),
45 Closure(std::rc::Rc<dyn Fn(&GreeksData) -> bool>),
47}
48
49impl GreeksFilterCallback {
50 pub fn from_fn(f: fn(&GreeksData) -> bool) -> Self {
52 Self::Function(f)
53 }
54
55 pub fn from_closure<F>(f: F) -> Self
57 where
58 F: Fn(&GreeksData) -> bool + 'static,
59 {
60 Self::Closure(std::rc::Rc::new(f))
61 }
62
63 pub fn call(&self, data: &GreeksData) -> bool {
65 match self {
66 Self::Function(f) => f(data),
67 Self::Closure(f) => f(data),
68 }
69 }
70
71 pub fn to_greeks_filter(self) -> GreeksFilter {
73 match self {
74 Self::Function(f) => Box::new(f),
75 Self::Closure(f) => {
76 let f_clone = f.clone();
77 Box::new(move |data| f_clone(data))
78 }
79 }
80 }
81}
82
83impl Debug for GreeksFilterCallback {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 Self::Function(_) => f.write_str("GreeksFilterCallback::Function"),
87 Self::Closure(_) => f.write_str("GreeksFilterCallback::Closure"),
88 }
89 }
90}
91
92#[derive(Debug, Builder)]
94#[builder(setter(into), derive(Debug))]
95pub struct InstrumentGreeksParams {
96 pub instrument_id: InstrumentId,
98 #[builder(default = "0.0425")]
100 pub flat_interest_rate: f64,
101 #[builder(default)]
103 pub flat_dividend_yield: Option<f64>,
104 #[builder(default = "0.0")]
106 pub spot_shock: f64,
107 #[builder(default = "0.0")]
109 pub vol_shock: f64,
110 #[builder(default = "0.0")]
112 pub time_to_expiry_shock: f64,
113 #[builder(default = "false")]
115 pub use_cached_greeks: bool,
116 #[builder(default = "false")]
118 pub update_vol: bool,
119 #[builder(default = "false")]
121 pub cache_greeks: bool,
122 #[builder(default = "false")]
124 pub publish_greeks: bool,
125 #[builder(default)]
127 pub ts_event: Option<UnixNanos>,
128 #[builder(default)]
130 pub position: Option<Position>,
131 #[builder(default = "false")]
133 pub percent_greeks: bool,
134 #[builder(default)]
136 pub index_instrument_id: Option<InstrumentId>,
137 #[builder(default)]
139 pub beta_weights: Option<HashMap<InstrumentId, f64>>,
140 #[builder(default)]
142 pub vega_time_weight_base: Option<i32>,
143}
144
145impl InstrumentGreeksParams {
146 pub fn calculate(&self, calculator: &GreeksCalculator) -> anyhow::Result<GreeksData> {
152 calculator.instrument_greeks(
153 self.instrument_id,
154 Some(self.flat_interest_rate),
155 self.flat_dividend_yield,
156 Some(self.spot_shock),
157 Some(self.vol_shock),
158 Some(self.time_to_expiry_shock),
159 Some(self.use_cached_greeks),
160 Some(self.update_vol),
161 Some(self.cache_greeks),
162 Some(self.publish_greeks),
163 self.ts_event,
164 self.position.clone(),
165 Some(self.percent_greeks),
166 self.index_instrument_id,
167 self.beta_weights.as_ref(),
168 self.vega_time_weight_base,
169 )
170 }
171}
172
173#[derive(Builder)]
175#[builder(setter(into))]
176pub struct PortfolioGreeksParams {
177 #[builder(default)]
179 pub underlyings: Option<Vec<String>>,
180 #[builder(default)]
182 pub venue: Option<Venue>,
183 #[builder(default)]
185 pub instrument_id: Option<InstrumentId>,
186 #[builder(default)]
188 pub strategy_id: Option<StrategyId>,
189 #[builder(default)]
191 pub side: Option<PositionSide>,
192 #[builder(default = "0.0425")]
194 pub flat_interest_rate: f64,
195 #[builder(default)]
197 pub flat_dividend_yield: Option<f64>,
198 #[builder(default = "0.0")]
200 pub spot_shock: f64,
201 #[builder(default = "0.0")]
203 pub vol_shock: f64,
204 #[builder(default = "0.0")]
206 pub time_to_expiry_shock: f64,
207 #[builder(default = "false")]
209 pub use_cached_greeks: bool,
210 #[builder(default = "false")]
212 pub update_vol: bool,
213 #[builder(default = "false")]
215 pub cache_greeks: bool,
216 #[builder(default = "false")]
218 pub publish_greeks: bool,
219 #[builder(default = "false")]
221 pub percent_greeks: bool,
222 #[builder(default)]
224 pub index_instrument_id: Option<InstrumentId>,
225 #[builder(default)]
227 pub beta_weights: Option<HashMap<InstrumentId, f64>>,
228 #[builder(default)]
230 pub greeks_filter: Option<GreeksFilterCallback>,
231 #[builder(default)]
233 pub vega_time_weight_base: Option<i32>,
234}
235
236impl Debug for PortfolioGreeksParams {
237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 f.debug_struct(stringify!(PortfolioGreeksParams))
239 .field("underlyings", &self.underlyings)
240 .field("venue", &self.venue)
241 .field("instrument_id", &self.instrument_id)
242 .field("strategy_id", &self.strategy_id)
243 .field("side", &self.side)
244 .field("flat_interest_rate", &self.flat_interest_rate)
245 .field("flat_dividend_yield", &self.flat_dividend_yield)
246 .field("spot_shock", &self.spot_shock)
247 .field("vol_shock", &self.vol_shock)
248 .field("time_to_expiry_shock", &self.time_to_expiry_shock)
249 .field("use_cached_greeks", &self.use_cached_greeks)
250 .field("update_vol", &self.update_vol)
251 .field("cache_greeks", &self.cache_greeks)
252 .field("publish_greeks", &self.publish_greeks)
253 .field("percent_greeks", &self.percent_greeks)
254 .field("index_instrument_id", &self.index_instrument_id)
255 .field("beta_weights", &self.beta_weights)
256 .field("greeks_filter", &self.greeks_filter)
257 .finish()
258 }
259}
260
261impl PortfolioGreeksParams {
262 pub fn calculate(&self, calculator: &GreeksCalculator) -> anyhow::Result<PortfolioGreeks> {
268 let greeks_filter = self
269 .greeks_filter
270 .as_ref()
271 .map(|f| f.clone().to_greeks_filter());
272
273 calculator.portfolio_greeks(
274 self.underlyings.as_deref(),
275 self.venue,
276 self.instrument_id,
277 self.strategy_id,
278 self.side,
279 Some(self.flat_interest_rate),
280 self.flat_dividend_yield,
281 Some(self.spot_shock),
282 Some(self.vol_shock),
283 Some(self.time_to_expiry_shock),
284 Some(self.use_cached_greeks),
285 Some(self.update_vol),
286 Some(self.cache_greeks),
287 Some(self.publish_greeks),
288 Some(self.percent_greeks),
289 self.index_instrument_id,
290 self.beta_weights.as_ref(),
291 greeks_filter.as_ref(),
292 self.vega_time_weight_base,
293 )
294 }
295}
296
297#[allow(dead_code)]
312#[derive(Debug)]
313pub struct GreeksCalculator {
314 cache: Rc<RefCell<Cache>>,
315 clock: Rc<RefCell<dyn Clock>>,
316 cached_futures_spreads: RefCell<AHashMap<InstrumentId, (InstrumentId, Price)>>,
317}
318
319impl GreeksCalculator {
320 pub fn new(cache: Rc<RefCell<Cache>>, clock: Rc<RefCell<dyn Clock>>) -> Self {
322 Self {
323 cache,
324 clock,
325 cached_futures_spreads: RefCell::new(AHashMap::new()),
326 }
327 }
328
329 #[expect(clippy::too_many_arguments)]
344 pub fn instrument_greeks(
345 &self,
346 instrument_id: InstrumentId,
347 flat_interest_rate: Option<f64>,
348 flat_dividend_yield: Option<f64>,
349 spot_shock: Option<f64>,
350 vol_shock: Option<f64>,
351 time_to_expiry_shock: Option<f64>,
352 use_cached_greeks: Option<bool>,
353 update_vol: Option<bool>,
354 cache_greeks: Option<bool>,
355 publish_greeks: Option<bool>,
356 ts_event: Option<UnixNanos>,
357 position: Option<Position>,
358 percent_greeks: Option<bool>,
359 index_instrument_id: Option<InstrumentId>,
360 beta_weights: Option<&HashMap<InstrumentId, f64>>,
361 vega_time_weight_base: Option<i32>,
362 ) -> anyhow::Result<GreeksData> {
363 let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
365 let spot_shock = spot_shock.unwrap_or(0.0);
366 let vol_shock = vol_shock.unwrap_or(0.0);
367 let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
368 let use_cached_greeks = use_cached_greeks.unwrap_or(false);
369 let update_vol = update_vol.unwrap_or(false);
370 let cache_greeks = cache_greeks.unwrap_or(false);
371 let publish_greeks = publish_greeks.unwrap_or(false);
372 let ts_event = ts_event.unwrap_or_default();
373 let percent_greeks = percent_greeks.unwrap_or(false);
374
375 let instrument = {
376 let cache = self.cache.borrow();
377 match cache.instrument(&instrument_id) {
378 Some(instrument) => instrument.clone(),
379 None => anyhow::bail!("Instrument definition for {instrument_id} not found"),
380 }
381 };
382
383 if instrument.instrument_class() != InstrumentClass::Option {
384 return self.calculate_non_option_greeks(
385 &instrument,
386 instrument_id,
387 spot_shock,
388 ts_event,
389 position,
390 percent_greeks,
391 index_instrument_id,
392 beta_weights,
393 );
394 }
395
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 let mut greeks_data = self.calculate_option_greeks(
400 &instrument,
401 instrument_id,
402 underlying_instrument_id,
403 flat_interest_rate,
404 flat_dividend_yield,
405 use_cached_greeks,
406 update_vol,
407 cache_greeks,
408 publish_greeks,
409 ts_event,
410 percent_greeks,
411 index_instrument_id,
412 beta_weights,
413 vega_time_weight_base,
414 )?;
415
416 if spot_shock != 0.0 || vol_shock != 0.0 || time_to_expiry_shock != 0.0 {
417 greeks_data = self.apply_option_greeks_shocks(
418 &greeks_data,
419 underlying_instrument_id,
420 spot_shock,
421 vol_shock,
422 time_to_expiry_shock,
423 percent_greeks,
424 index_instrument_id,
425 beta_weights,
426 vega_time_weight_base,
427 );
428 }
429
430 if let Some(pos) = position {
431 greeks_data.pnl = greeks_data.price - pos.avg_px_open;
432 }
433
434 Ok(greeks_data)
435 }
436
437 #[expect(clippy::too_many_arguments)]
438 fn calculate_non_option_greeks(
439 &self,
440 instrument: &InstrumentAny,
441 instrument_id: InstrumentId,
442 spot_shock: f64,
443 ts_event: UnixNanos,
444 position: Option<Position>,
445 percent_greeks: bool,
446 index_instrument_id: Option<InstrumentId>,
447 beta_weights: Option<&HashMap<InstrumentId, f64>>,
448 ) -> anyhow::Result<GreeksData> {
449 let multiplier = instrument.multiplier();
450 let underlying_instrument_id = instrument.id();
451 let underlying_price = self
452 .get_price(&underlying_instrument_id)
453 .ok_or_else(|| anyhow::anyhow!("No price available for {underlying_instrument_id}"))?;
454 let (delta, _, _) = self.modify_greeks(
455 1.0,
456 0.0,
457 underlying_instrument_id,
458 underlying_price + spot_shock,
459 underlying_price,
460 percent_greeks,
461 index_instrument_id,
462 beta_weights,
463 0.0,
464 0.0,
465 0,
466 None,
467 );
468 let mut greeks_data =
469 GreeksData::from_delta(instrument_id, delta, multiplier.as_f64(), ts_event);
470
471 if let Some(pos) = position {
472 greeks_data.pnl = (underlying_price + spot_shock) - pos.avg_px_open;
473 greeks_data.price = greeks_data.pnl;
474 }
475
476 Ok(greeks_data)
477 }
478
479 #[expect(clippy::too_many_arguments)]
480 fn calculate_option_greeks(
481 &self,
482 instrument: &InstrumentAny,
483 instrument_id: InstrumentId,
484 underlying_instrument_id: InstrumentId,
485 flat_interest_rate: f64,
486 flat_dividend_yield: Option<f64>,
487 use_cached_greeks: bool,
488 update_vol: bool,
489 cache_greeks: bool,
490 publish_greeks: bool,
491 ts_event: UnixNanos,
492 percent_greeks: bool,
493 index_instrument_id: Option<InstrumentId>,
494 beta_weights: Option<&HashMap<InstrumentId, f64>>,
495 vega_time_weight_base: Option<i32>,
496 ) -> anyhow::Result<GreeksData> {
497 if use_cached_greeks {
498 let cache = self.cache.borrow();
499 if let Some(cached_greeks) = cache.greeks(&instrument_id) {
500 return Ok(cached_greeks);
501 }
502 }
503
504 let utc_now_ns = if ts_event == UnixNanos::default() {
505 self.clock.borrow().timestamp_ns()
506 } else {
507 ts_event
508 };
509 let utc_now = utc_now_ns.to_datetime_utc();
510 let expiry_utc = instrument
511 .expiration_ns()
512 .map(|ns| ns.to_datetime_utc())
513 .unwrap_or_default();
514 let expiry_int = expiry_utc
515 .format("%Y%m%d")
516 .to_string()
517 .parse::<i32>()
518 .unwrap_or(0);
519 let raw_days = (expiry_utc - utc_now).num_days();
520 let expiry_in_days = raw_days.max(1) as i32;
521 let expiry_in_years = expiry_in_days as f64 / 365.25;
522 let currency = instrument.quote_currency().code.to_string();
523
524 let cache = self.cache.borrow();
525 let yield_curve = cache.yield_curve(¤cy);
526 let interest_rate = match yield_curve {
527 Some(yield_curve) => yield_curve(expiry_in_years),
528 None => flat_interest_rate,
529 };
530 let dividend_curve = cache.yield_curve(&underlying_instrument_id.to_string());
531 drop(cache);
532
533 let mut cost_of_carry = 0.0;
534
535 if let Some(dividend_curve) = dividend_curve {
536 cost_of_carry = interest_rate - dividend_curve(expiry_in_years);
537 } else if let Some(div_yield) = flat_dividend_yield {
538 cost_of_carry = interest_rate - div_yield;
539 }
540
541 let multiplier = instrument.multiplier();
542 let is_call = instrument.option_kind().unwrap_or(OptionKind::Call) == OptionKind::Call;
543 let strike = instrument.strike_price().unwrap_or_default().as_f64();
544 let option_price = self
545 .get_price(&instrument_id)
546 .ok_or_else(|| anyhow::anyhow!("No price available for {instrument_id}"))?;
547 let underlying_price = self.get_underlying_price(&underlying_instrument_id)?;
548 let greeks = if update_vol {
549 let cached_greeks = self.cache.borrow().greeks(&instrument_id);
550 match cached_greeks {
551 Some(cached_greeks) => refine_vol_and_greeks(
552 underlying_price,
553 interest_rate,
554 cost_of_carry,
555 is_call,
556 strike,
557 expiry_in_years,
558 option_price,
559 cached_greeks.vol,
560 ),
561 None => imply_vol_and_greeks(
562 underlying_price,
563 interest_rate,
564 cost_of_carry,
565 is_call,
566 strike,
567 expiry_in_years,
568 option_price,
569 ),
570 }
571 } else {
572 imply_vol_and_greeks(
573 underlying_price,
574 interest_rate,
575 cost_of_carry,
576 is_call,
577 strike,
578 expiry_in_years,
579 option_price,
580 )
581 };
582 let (delta, gamma, vega) = self.modify_greeks(
583 greeks.delta,
584 greeks.gamma,
585 underlying_instrument_id,
586 underlying_price,
587 underlying_price,
588 percent_greeks,
589 index_instrument_id,
590 beta_weights,
591 greeks.vega,
592 greeks.vol,
593 expiry_in_days,
594 vega_time_weight_base,
595 );
596 let greeks_data = GreeksData::new(
597 utc_now_ns,
598 utc_now_ns,
599 instrument_id,
600 is_call,
601 strike,
602 expiry_int,
603 expiry_in_days,
604 expiry_in_years,
605 multiplier.as_f64(),
606 1.0,
607 underlying_price,
608 interest_rate,
609 cost_of_carry,
610 greeks.vol,
611 0.0,
612 greeks.price,
613 OptionGreekValues {
614 delta,
615 gamma,
616 vega,
617 theta: greeks.theta,
618 rho: 0.0,
619 },
620 greeks.itm_prob,
621 );
622
623 if cache_greeks {
624 let mut cache = self.cache.borrow_mut();
625 cache.add_greeks(greeks_data.clone()).unwrap_or_default();
626 }
627
628 if publish_greeks {
629 let topic = format!(
630 "data.GreeksData.instrument_id={}",
631 instrument_id.symbol.as_str()
632 )
633 .into();
634 msgbus::publish_greeks(topic, &greeks_data);
635 }
636
637 Ok(greeks_data)
638 }
639
640 #[expect(clippy::too_many_arguments)]
641 fn apply_option_greeks_shocks(
642 &self,
643 greeks_data: &GreeksData,
644 underlying_instrument_id: InstrumentId,
645 spot_shock: f64,
646 vol_shock: f64,
647 time_to_expiry_shock: f64,
648 percent_greeks: bool,
649 index_instrument_id: Option<InstrumentId>,
650 beta_weights: Option<&HashMap<InstrumentId, f64>>,
651 vega_time_weight_base: Option<i32>,
652 ) -> GreeksData {
653 let underlying_price = greeks_data.underlying_price;
654 let shocked_underlying_price = underlying_price + spot_shock;
655 let shocked_vol = greeks_data.vol + vol_shock;
656 let shocked_time_to_expiry = greeks_data.expiry_in_years - time_to_expiry_shock;
657 let shocked_expiry_in_days = (shocked_time_to_expiry * 365.25) as i32;
658
659 let greeks = black_scholes_greeks(
660 shocked_underlying_price,
661 greeks_data.interest_rate,
662 greeks_data.cost_of_carry,
663 shocked_vol,
664 greeks_data.is_call,
665 greeks_data.strike,
666 shocked_time_to_expiry,
667 );
668 let (delta, gamma, vega) = self.modify_greeks(
669 greeks.delta,
670 greeks.gamma,
671 underlying_instrument_id,
672 shocked_underlying_price,
673 underlying_price,
674 percent_greeks,
675 index_instrument_id,
676 beta_weights,
677 greeks.vega,
678 shocked_vol,
679 shocked_expiry_in_days,
680 vega_time_weight_base,
681 );
682 GreeksData::new(
683 greeks_data.ts_event,
684 greeks_data.ts_event,
685 greeks_data.instrument_id,
686 greeks_data.is_call,
687 greeks_data.strike,
688 greeks_data.expiry,
689 shocked_expiry_in_days,
690 shocked_time_to_expiry,
691 greeks_data.multiplier,
692 greeks_data.quantity,
693 shocked_underlying_price,
694 greeks_data.interest_rate,
695 greeks_data.cost_of_carry,
696 shocked_vol,
697 0.0,
698 greeks.price,
699 OptionGreekValues {
700 delta,
701 gamma,
702 vega,
703 theta: greeks.theta,
704 rho: 0.0,
705 },
706 greeks.itm_prob,
707 )
708 }
709
710 fn get_underlying_price(&self, underlying_instrument_id: &InstrumentId) -> anyhow::Result<f64> {
711 if let Some(underlying_price) = self.get_price(underlying_instrument_id) {
712 return Ok(underlying_price);
713 }
714
715 let is_future_or_absent = {
718 let cache = self.cache.borrow();
719 cache
720 .instrument(underlying_instrument_id)
721 .is_none_or(|inst| inst.instrument_class() == InstrumentClass::Future)
722 };
723
724 if is_future_or_absent
725 && let Some(underlying_price) =
726 self.get_cached_futures_spread_price(*underlying_instrument_id)
727 {
728 return Ok(underlying_price.as_f64());
729 }
730
731 anyhow::bail!("No price available for {underlying_instrument_id}")
732 }
733
734 #[expect(clippy::too_many_arguments)]
753 pub fn modify_greeks(
754 &self,
755 delta_input: f64,
756 gamma_input: f64,
757 underlying_instrument_id: InstrumentId,
758 underlying_price: f64,
759 unshocked_underlying_price: f64,
760 percent_greeks: bool,
761 index_instrument_id: Option<InstrumentId>,
762 beta_weights: Option<&HashMap<InstrumentId, f64>>,
763 vega_input: f64,
764 vol: f64,
765 expiry_in_days: i32,
766 vega_time_weight_base: Option<i32>,
767 ) -> (f64, f64, f64) {
768 let mut delta = delta_input;
769 let mut gamma = gamma_input;
770 let mut vega = vega_input;
771
772 let mut index_price = None;
773
774 if let Some(index_id) = index_instrument_id {
775 let cache = self.cache.borrow();
776 index_price = Some(
777 cache
778 .price(&index_id, PriceType::Last)
779 .unwrap_or_default()
780 .as_f64(),
781 );
782
783 let mut beta = 1.0;
784
785 if let Some(weights) = beta_weights
786 && let Some(&weight) = weights.get(&underlying_instrument_id)
787 {
788 beta = weight;
789 }
790
791 if let Some(ref mut idx_price) = index_price {
792 if underlying_price != unshocked_underlying_price {
793 *idx_price += 1.0 / beta
794 * (*idx_price / unshocked_underlying_price)
795 * (underlying_price - unshocked_underlying_price);
796 }
797
798 let delta_multiplier = beta * underlying_price / *idx_price;
799 delta *= delta_multiplier;
800 gamma *= delta_multiplier.powi(2);
801 }
802 }
803
804 if percent_greeks {
805 if let Some(idx_price) = index_price {
806 delta *= idx_price / 100.0;
807 gamma *= (idx_price / 100.0).powi(2);
808 } else {
809 delta *= underlying_price / 100.0;
810 gamma *= (underlying_price / 100.0).powi(2);
811 }
812
813 vega *= vol / 100.0;
815 }
816
817 if let Some(time_base) = vega_time_weight_base
819 && expiry_in_days > 0
820 {
821 let time_weight = (time_base as f64 / expiry_in_days as f64).sqrt();
822 vega *= time_weight;
823 }
824
825 (delta, gamma, vega)
826 }
827
828 #[expect(clippy::too_many_arguments)]
842 #[expect(clippy::missing_panics_doc)] pub fn portfolio_greeks(
844 &self,
845 underlyings: Option<&[String]>,
846 venue: Option<Venue>,
847 instrument_id: Option<InstrumentId>,
848 strategy_id: Option<StrategyId>,
849 side: Option<PositionSide>,
850 flat_interest_rate: Option<f64>,
851 flat_dividend_yield: Option<f64>,
852 spot_shock: Option<f64>,
853 vol_shock: Option<f64>,
854 time_to_expiry_shock: Option<f64>,
855 use_cached_greeks: Option<bool>,
856 update_vol: Option<bool>,
857 cache_greeks: Option<bool>,
858 publish_greeks: Option<bool>,
859 percent_greeks: Option<bool>,
860 index_instrument_id: Option<InstrumentId>,
861 beta_weights: Option<&HashMap<InstrumentId, f64>>,
862 greeks_filter: Option<&GreeksFilter>,
863 vega_time_weight_base: Option<i32>,
864 ) -> anyhow::Result<PortfolioGreeks> {
865 let ts_event = self.clock.borrow().timestamp_ns();
866 let mut portfolio_greeks =
867 PortfolioGreeks::new(ts_event, ts_event, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
868
869 let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
871 let spot_shock = spot_shock.unwrap_or(0.0);
872 let vol_shock = vol_shock.unwrap_or(0.0);
873 let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
874 let use_cached_greeks = use_cached_greeks.unwrap_or(false);
875 let update_vol = update_vol.unwrap_or(false);
876 let cache_greeks = cache_greeks.unwrap_or(false);
877 let publish_greeks = publish_greeks.unwrap_or(false);
878 let percent_greeks = percent_greeks.unwrap_or(false);
879 let side = side.unwrap_or(PositionSide::NoPositionSide);
880
881 let cache = self.cache.borrow();
882 let open_positions = cache.positions(
883 venue.as_ref(),
884 instrument_id.as_ref(),
885 strategy_id.as_ref(),
886 None, Some(side),
888 );
889 let open_positions: Vec<Position> = open_positions.iter().map(|&p| p.clone()).collect();
890
891 for position in open_positions {
892 let position_instrument_id = position.instrument_id;
893
894 if let Some(underlyings_list) = underlyings {
895 let mut skip_position = true;
896
897 for underlying in underlyings_list {
898 if position_instrument_id
899 .symbol
900 .as_str()
901 .starts_with(underlying)
902 {
903 skip_position = false;
904 break;
905 }
906 }
907
908 if skip_position {
909 continue;
910 }
911 }
912
913 let quantity = position.signed_qty;
914 let instrument_greeks = self.instrument_greeks(
915 position_instrument_id,
916 Some(flat_interest_rate),
917 flat_dividend_yield,
918 Some(spot_shock),
919 Some(vol_shock),
920 Some(time_to_expiry_shock),
921 Some(use_cached_greeks),
922 Some(update_vol),
923 Some(cache_greeks),
924 Some(publish_greeks),
925 Some(ts_event),
926 Some(position),
927 Some(percent_greeks),
928 index_instrument_id,
929 beta_weights,
930 vega_time_weight_base,
931 )?;
932 let position_greeks = quantity * &instrument_greeks;
933
934 if greeks_filter.is_none() || greeks_filter.unwrap()(&position_greeks) {
936 portfolio_greeks = portfolio_greeks + PortfolioGreeks::from(position_greeks);
937 }
938 }
939
940 Ok(portfolio_greeks)
941 }
942
943 pub fn cache_futures_spread(
949 &self,
950 call_instrument_id: InstrumentId,
951 put_instrument_id: InstrumentId,
952 futures_instrument_id: InstrumentId,
953 ) -> anyhow::Result<Price> {
954 let cache = self.cache.borrow();
955 let call_instrument = cache.instrument(&call_instrument_id).cloned();
956 let put_instrument = cache.instrument(&put_instrument_id).cloned();
957 let reference_future_instrument = cache.instrument(&futures_instrument_id).cloned();
958 drop(cache);
959
960 let Some(call_instrument) = call_instrument else {
961 anyhow::bail!(
962 "Cannot cache futures spread: missing option instrument {call_instrument_id}"
963 );
964 };
965 let Some(put_instrument) = put_instrument else {
966 anyhow::bail!(
967 "Cannot cache futures spread: missing option instrument {put_instrument_id}"
968 );
969 };
970 let Some(reference_future_instrument) = reference_future_instrument else {
971 anyhow::bail!(
972 "Cannot cache futures spread: no reference futures instrument for {futures_instrument_id}"
973 );
974 };
975
976 if call_instrument.instrument_class() != InstrumentClass::Option
977 || put_instrument.instrument_class() != InstrumentClass::Option
978 {
979 anyhow::bail!(
980 "Cannot cache futures spread: non-option instruments provided call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
981 );
982 }
983
984 if call_instrument.option_kind() != Some(OptionKind::Call)
985 || put_instrument.option_kind() != Some(OptionKind::Put)
986 {
987 anyhow::bail!(
988 "Cannot cache futures spread: expected call/put pair call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
989 );
990 }
991
992 let Some(call_underlying) = call_instrument.underlying() else {
993 anyhow::bail!(
994 "Cannot cache futures spread: missing call underlying for {call_instrument_id}"
995 );
996 };
997 let Some(put_underlying) = put_instrument.underlying() else {
998 anyhow::bail!(
999 "Cannot cache futures spread: missing put underlying for {put_instrument_id}"
1000 );
1001 };
1002
1003 if call_underlying != put_underlying {
1004 anyhow::bail!(
1005 "Cannot cache futures spread: option underlyings differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
1006 );
1007 }
1008
1009 if call_instrument.strike_price() != put_instrument.strike_price() {
1010 anyhow::bail!(
1011 "Cannot cache futures spread: strike prices differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
1012 );
1013 }
1014
1015 if call_instrument.expiration_ns() != put_instrument.expiration_ns() {
1016 anyhow::bail!(
1017 "Cannot cache futures spread: expiration dates differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
1018 );
1019 }
1020
1021 let reference_future_price = self.get_price_object(&futures_instrument_id).ok_or_else(|| {
1022 anyhow::anyhow!(
1023 "Cannot cache futures spread: no reference futures price for {futures_instrument_id}"
1024 )
1025 })?;
1026 let call_price = self.get_price(&call_instrument_id).ok_or_else(|| {
1027 anyhow::anyhow!(
1028 "Cannot cache futures spread: missing option price for {call_instrument_id}"
1029 )
1030 })?;
1031 let put_price = self.get_price(&put_instrument_id).ok_or_else(|| {
1032 anyhow::anyhow!(
1033 "Cannot cache futures spread: missing option price for {put_instrument_id}"
1034 )
1035 })?;
1036
1037 let underlying_instrument_id =
1038 InstrumentId::from(format!("{call_underlying}.{}", call_instrument_id.venue));
1039
1040 {
1042 let cache = self.cache.borrow();
1043 if let Some(underlying) = cache.instrument(&underlying_instrument_id)
1044 && underlying.instrument_class() != InstrumentClass::Future
1045 {
1046 anyhow::bail!(
1047 "Cannot cache futures spread: underlying {underlying_instrument_id} is not a futures contract"
1048 );
1049 }
1050 }
1051
1052 let implied_future_price =
1053 self.calculate_implied_future_price(&call_instrument, call_price, put_price);
1054 let spread = implied_future_price - reference_future_price.as_f64();
1055 let spread_price = reference_future_instrument.make_price(spread);
1056
1057 self.cached_futures_spreads.borrow_mut().insert(
1058 underlying_instrument_id,
1059 (futures_instrument_id, spread_price),
1060 );
1061
1062 Ok(reference_future_price + spread_price)
1063 }
1064
1065 fn calculate_implied_future_price(
1066 &self,
1067 call_instrument: &InstrumentAny,
1068 call_price: f64,
1069 put_price: f64,
1070 ) -> f64 {
1071 let expiry_utc = call_instrument
1072 .expiration_ns()
1073 .map(|ns| ns.to_datetime_utc())
1074 .unwrap_or_default();
1075 let expiry_in_days = (expiry_utc - self.clock.borrow().timestamp_ns().to_datetime_utc())
1076 .num_days()
1077 .max(1) as i32;
1078 let expiry_in_years = expiry_in_days as f64 / 365.25;
1079 let currency = call_instrument.quote_currency().code.to_string();
1080 let interest_rate = self
1081 .cache
1082 .borrow()
1083 .yield_curve(¤cy)
1084 .map_or(0.0425, |yield_curve| yield_curve(expiry_in_years));
1085 let strike = call_instrument.strike_price().unwrap_or_default().as_f64();
1086
1087 strike + (interest_rate * expiry_in_years).exp() * (call_price - put_price)
1088 }
1089
1090 #[must_use]
1092 pub fn get_cached_futures_spread_price(
1093 &self,
1094 underlying_instrument_id: InstrumentId,
1095 ) -> Option<Price> {
1096 let (futures_instrument_id, spread) = self
1097 .cached_futures_spreads
1098 .borrow()
1099 .get(&underlying_instrument_id)
1100 .copied()?;
1101 let reference_future_price = self.get_price_object(&futures_instrument_id)?;
1102
1103 Some(reference_future_price + spread)
1104 }
1105
1106 fn get_price_object(&self, instrument_id: &InstrumentId) -> Option<Price> {
1107 let cache = self.cache.borrow();
1108
1109 if let Some(instrument) = cache.instrument(instrument_id)
1113 && instrument.asset_class() == AssetClass::Index
1114 {
1115 if instrument.instrument_class() == InstrumentClass::Future {
1116 let tradable = cache
1117 .price(instrument_id, PriceType::Mid)
1118 .or_else(|| cache.price(instrument_id, PriceType::Last));
1119
1120 if tradable.is_some() {
1121 return tradable;
1122 }
1123 }
1124
1125 if let Some(index_price) = cache.index_price(instrument_id) {
1126 return Some(index_price.value);
1127 }
1128 }
1129
1130 cache
1131 .price(instrument_id, PriceType::Mid)
1132 .or_else(|| cache.price(instrument_id, PriceType::Last))
1133 }
1134
1135 fn get_price(&self, instrument_id: &InstrumentId) -> Option<f64> {
1136 self.get_price_object(instrument_id)
1137 .map(|price| price.as_f64())
1138 }
1139
1140 pub fn subscribe_greeks<F>(&self, underlying: &str, handler: Option<F>)
1144 where
1145 F: Fn(&GreeksData) + 'static,
1146 {
1147 let pattern = format!("data.GreeksData.instrument_id={underlying}*").into();
1148
1149 if let Some(custom_handler) = handler {
1150 let typed_handler = TypedHandler::from(custom_handler);
1151 msgbus::subscribe_greeks(pattern, typed_handler, None);
1152 } else {
1153 let cache_ref = self.cache.clone();
1154 let typed_handler = TypedHandler::from(move |greeks: &GreeksData| {
1155 let mut cache = cache_ref.borrow_mut();
1156 cache.add_greeks(greeks.clone()).unwrap_or_default();
1157 });
1158 msgbus::subscribe_greeks(pattern, typed_handler, None);
1159 }
1160 }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165 use std::{cell::RefCell, collections::HashMap, rc::Rc};
1166
1167 use chrono::{TimeZone, Utc};
1168 use nautilus_model::{
1169 data::{IndexPriceUpdate, QuoteTick},
1170 enums::{AssetClass, OptionKind, PositionSide},
1171 identifiers::{InstrumentId, StrategyId, Symbol, Venue},
1172 instruments::{Equity, FuturesContract, OptionContract, any::InstrumentAny},
1173 types::{Currency, Price, Quantity},
1174 };
1175 use rstest::rstest;
1176 use ustr::Ustr;
1177
1178 use super::*;
1179 use crate::{cache::Cache, clock::TestClock};
1180
1181 fn create_test_calculator() -> GreeksCalculator {
1182 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1183 let clock = Rc::new(RefCell::new(TestClock::new()));
1184 GreeksCalculator::new(cache, clock)
1185 }
1186
1187 #[rstest]
1188 fn test_greeks_calculator_creation() {
1189 let calculator = create_test_calculator();
1190 assert!(format!("{calculator:?}").contains("GreeksCalculator"));
1192 }
1193
1194 #[rstest]
1195 fn test_greeks_calculator_debug() {
1196 let calculator = create_test_calculator();
1197 let debug_str = format!("{calculator:?}");
1199 assert!(debug_str.contains("GreeksCalculator"));
1200 }
1201
1202 #[rstest]
1203 fn test_greeks_calculator_has_python_bindings() {
1204 let calculator = create_test_calculator();
1207 assert!(format!("{calculator:?}").contains("GreeksCalculator"));
1210 }
1211
1212 #[rstest]
1213 fn test_instrument_greeks_params_builder_default() {
1214 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1215
1216 let params = InstrumentGreeksParamsBuilder::default()
1217 .instrument_id(instrument_id)
1218 .build()
1219 .expect("Failed to build InstrumentGreeksParams");
1220
1221 assert_eq!(params.instrument_id, instrument_id);
1222 assert_eq!(params.flat_interest_rate, 0.0425);
1223 assert_eq!(params.flat_dividend_yield, None);
1224 assert_eq!(params.spot_shock, 0.0);
1225 assert_eq!(params.vol_shock, 0.0);
1226 assert_eq!(params.time_to_expiry_shock, 0.0);
1227 assert!(!params.use_cached_greeks);
1228 assert!(!params.cache_greeks);
1229 assert!(!params.publish_greeks);
1230 assert_eq!(params.ts_event, None);
1231 assert_eq!(params.position, None);
1232 assert!(!params.percent_greeks);
1233 assert_eq!(params.index_instrument_id, None);
1234 assert_eq!(params.beta_weights, None);
1235 }
1236
1237 #[rstest]
1238 fn test_instrument_greeks_params_builder_custom_values() {
1239 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1240 let index_id = InstrumentId::from("SPY.NASDAQ");
1241 let mut beta_weights = HashMap::new();
1242 beta_weights.insert(instrument_id, 1.2);
1243
1244 let params = InstrumentGreeksParamsBuilder::default()
1245 .instrument_id(instrument_id)
1246 .flat_interest_rate(0.05)
1247 .flat_dividend_yield(Some(0.02))
1248 .spot_shock(0.01)
1249 .vol_shock(0.05)
1250 .time_to_expiry_shock(0.1)
1251 .use_cached_greeks(true)
1252 .cache_greeks(true)
1253 .publish_greeks(true)
1254 .percent_greeks(true)
1255 .index_instrument_id(Some(index_id))
1256 .beta_weights(Some(beta_weights.clone()))
1257 .build()
1258 .expect("Failed to build InstrumentGreeksParams");
1259
1260 assert_eq!(params.instrument_id, instrument_id);
1261 assert_eq!(params.flat_interest_rate, 0.05);
1262 assert_eq!(params.flat_dividend_yield, Some(0.02));
1263 assert_eq!(params.spot_shock, 0.01);
1264 assert_eq!(params.vol_shock, 0.05);
1265 assert_eq!(params.time_to_expiry_shock, 0.1);
1266 assert!(params.use_cached_greeks);
1267 assert!(params.cache_greeks);
1268 assert!(params.publish_greeks);
1269 assert!(params.percent_greeks);
1270 assert_eq!(params.index_instrument_id, Some(index_id));
1271 assert_eq!(params.beta_weights, Some(beta_weights));
1272 }
1273
1274 #[rstest]
1275 fn test_instrument_greeks_params_debug() {
1276 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1277
1278 let params = InstrumentGreeksParamsBuilder::default()
1279 .instrument_id(instrument_id)
1280 .build()
1281 .expect("Failed to build InstrumentGreeksParams");
1282
1283 let debug_str = format!("{params:?}");
1284 assert!(debug_str.contains("InstrumentGreeksParams"));
1285 assert!(debug_str.contains("AAPL.NASDAQ"));
1286 }
1287
1288 #[rstest]
1289 fn test_portfolio_greeks_params_builder_default() {
1290 let params = PortfolioGreeksParamsBuilder::default()
1291 .build()
1292 .expect("Failed to build PortfolioGreeksParams");
1293
1294 assert_eq!(params.underlyings, None);
1295 assert_eq!(params.venue, None);
1296 assert_eq!(params.instrument_id, None);
1297 assert_eq!(params.strategy_id, None);
1298 assert_eq!(params.side, None);
1299 assert_eq!(params.flat_interest_rate, 0.0425);
1300 assert_eq!(params.flat_dividend_yield, None);
1301 assert_eq!(params.spot_shock, 0.0);
1302 assert_eq!(params.vol_shock, 0.0);
1303 assert_eq!(params.time_to_expiry_shock, 0.0);
1304 assert!(!params.use_cached_greeks);
1305 assert!(!params.cache_greeks);
1306 assert!(!params.publish_greeks);
1307 assert!(!params.percent_greeks);
1308 assert_eq!(params.index_instrument_id, None);
1309 assert_eq!(params.beta_weights, None);
1310 }
1311
1312 #[rstest]
1313 fn test_portfolio_greeks_params_builder_custom_values() {
1314 let venue = Venue::from("NASDAQ");
1315 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1316 let strategy_id = StrategyId::from("test-strategy");
1317 let index_id = InstrumentId::from("SPY.NASDAQ");
1318 let underlyings = vec!["AAPL".to_string(), "MSFT".to_string()];
1319 let mut beta_weights = HashMap::new();
1320 beta_weights.insert(instrument_id, 1.2);
1321
1322 let params = PortfolioGreeksParamsBuilder::default()
1323 .underlyings(Some(underlyings.clone()))
1324 .venue(Some(venue))
1325 .instrument_id(Some(instrument_id))
1326 .strategy_id(Some(strategy_id))
1327 .side(Some(PositionSide::Long))
1328 .flat_interest_rate(0.05)
1329 .flat_dividend_yield(Some(0.02))
1330 .spot_shock(0.01)
1331 .vol_shock(0.05)
1332 .time_to_expiry_shock(0.1)
1333 .use_cached_greeks(true)
1334 .cache_greeks(true)
1335 .publish_greeks(true)
1336 .percent_greeks(true)
1337 .index_instrument_id(Some(index_id))
1338 .beta_weights(Some(beta_weights.clone()))
1339 .build()
1340 .expect("Failed to build PortfolioGreeksParams");
1341
1342 assert_eq!(params.underlyings, Some(underlyings));
1343 assert_eq!(params.venue, Some(venue));
1344 assert_eq!(params.instrument_id, Some(instrument_id));
1345 assert_eq!(params.strategy_id, Some(strategy_id));
1346 assert_eq!(params.side, Some(PositionSide::Long));
1347 assert_eq!(params.flat_interest_rate, 0.05);
1348 assert_eq!(params.flat_dividend_yield, Some(0.02));
1349 assert_eq!(params.spot_shock, 0.01);
1350 assert_eq!(params.vol_shock, 0.05);
1351 assert_eq!(params.time_to_expiry_shock, 0.1);
1352 assert!(params.use_cached_greeks);
1353 assert!(params.cache_greeks);
1354 assert!(params.publish_greeks);
1355 assert!(params.percent_greeks);
1356 assert_eq!(params.index_instrument_id, Some(index_id));
1357 assert_eq!(params.beta_weights, Some(beta_weights));
1358 }
1359
1360 #[rstest]
1361 fn test_portfolio_greeks_params_debug() {
1362 let venue = Venue::from("NASDAQ");
1363
1364 let params = PortfolioGreeksParamsBuilder::default()
1365 .venue(Some(venue))
1366 .build()
1367 .expect("Failed to build PortfolioGreeksParams");
1368
1369 let debug_str = format!("{params:?}");
1370 assert!(debug_str.contains("PortfolioGreeksParams"));
1371 assert!(debug_str.contains("NASDAQ"));
1372 }
1373
1374 #[rstest]
1375 fn test_instrument_greeks_params_builder_missing_required_field() {
1376 let result = InstrumentGreeksParamsBuilder::default().build();
1378 assert!(result.is_err());
1379 }
1380
1381 #[rstest]
1382 fn test_portfolio_greeks_params_builder_fluent_api() {
1383 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1384
1385 let params = PortfolioGreeksParamsBuilder::default()
1386 .instrument_id(Some(instrument_id))
1387 .flat_interest_rate(0.05)
1388 .spot_shock(0.01)
1389 .percent_greeks(true)
1390 .build()
1391 .expect("Failed to build PortfolioGreeksParams");
1392
1393 assert_eq!(params.instrument_id, Some(instrument_id));
1394 assert_eq!(params.flat_interest_rate, 0.05);
1395 assert_eq!(params.spot_shock, 0.01);
1396 assert!(params.percent_greeks);
1397 }
1398
1399 #[rstest]
1400 fn test_instrument_greeks_params_builder_fluent_chaining() {
1401 let instrument_id = InstrumentId::from("TSLA.NASDAQ");
1402
1403 let params = InstrumentGreeksParamsBuilder::default()
1405 .instrument_id(instrument_id)
1406 .flat_interest_rate(0.03)
1407 .spot_shock(0.02)
1408 .vol_shock(0.1)
1409 .use_cached_greeks(true)
1410 .percent_greeks(true)
1411 .build()
1412 .expect("Failed to build InstrumentGreeksParams");
1413
1414 assert_eq!(params.instrument_id, instrument_id);
1415 assert_eq!(params.flat_interest_rate, 0.03);
1416 assert_eq!(params.spot_shock, 0.02);
1417 assert_eq!(params.vol_shock, 0.1);
1418 assert!(params.use_cached_greeks);
1419 assert!(params.percent_greeks);
1420 }
1421
1422 #[rstest]
1423 fn test_portfolio_greeks_params_builder_with_underlyings() {
1424 let underlyings = vec!["AAPL".to_string(), "MSFT".to_string(), "GOOGL".to_string()];
1425
1426 let params = PortfolioGreeksParamsBuilder::default()
1427 .underlyings(Some(underlyings.clone()))
1428 .flat_interest_rate(0.04)
1429 .build()
1430 .expect("Failed to build PortfolioGreeksParams");
1431
1432 assert_eq!(params.underlyings, Some(underlyings));
1433 assert_eq!(params.flat_interest_rate, 0.04);
1434 }
1435
1436 #[rstest]
1437 fn test_builders_with_empty_beta_weights() {
1438 let instrument_id = InstrumentId::from("NVDA.NASDAQ");
1439 let empty_beta_weights = HashMap::new();
1440
1441 let instrument_params = InstrumentGreeksParamsBuilder::default()
1442 .instrument_id(instrument_id)
1443 .beta_weights(Some(empty_beta_weights.clone()))
1444 .build()
1445 .expect("Failed to build InstrumentGreeksParams");
1446
1447 let portfolio_params = PortfolioGreeksParamsBuilder::default()
1448 .beta_weights(Some(empty_beta_weights.clone()))
1449 .build()
1450 .expect("Failed to build PortfolioGreeksParams");
1451
1452 assert_eq!(
1453 instrument_params.beta_weights,
1454 Some(empty_beta_weights.clone())
1455 );
1456 assert_eq!(portfolio_params.beta_weights, Some(empty_beta_weights));
1457 }
1458
1459 #[rstest]
1460 fn test_builders_with_all_shocks() {
1461 let instrument_id = InstrumentId::from("AMD.NASDAQ");
1462
1463 let instrument_params = InstrumentGreeksParamsBuilder::default()
1464 .instrument_id(instrument_id)
1465 .spot_shock(0.05)
1466 .vol_shock(0.1)
1467 .time_to_expiry_shock(0.01)
1468 .build()
1469 .expect("Failed to build InstrumentGreeksParams");
1470
1471 let portfolio_params = PortfolioGreeksParamsBuilder::default()
1472 .spot_shock(0.05)
1473 .vol_shock(0.1)
1474 .time_to_expiry_shock(0.01)
1475 .build()
1476 .expect("Failed to build PortfolioGreeksParams");
1477
1478 assert_eq!(instrument_params.spot_shock, 0.05);
1479 assert_eq!(instrument_params.vol_shock, 0.1);
1480 assert_eq!(instrument_params.time_to_expiry_shock, 0.01);
1481
1482 assert_eq!(portfolio_params.spot_shock, 0.05);
1483 assert_eq!(portfolio_params.vol_shock, 0.1);
1484 assert_eq!(portfolio_params.time_to_expiry_shock, 0.01);
1485 }
1486
1487 #[rstest]
1488 fn test_builders_with_all_boolean_flags() {
1489 let instrument_id = InstrumentId::from("META.NASDAQ");
1490
1491 let instrument_params = InstrumentGreeksParamsBuilder::default()
1492 .instrument_id(instrument_id)
1493 .use_cached_greeks(true)
1494 .cache_greeks(true)
1495 .publish_greeks(true)
1496 .percent_greeks(true)
1497 .build()
1498 .expect("Failed to build InstrumentGreeksParams");
1499
1500 let portfolio_params = PortfolioGreeksParamsBuilder::default()
1501 .use_cached_greeks(true)
1502 .cache_greeks(true)
1503 .publish_greeks(true)
1504 .percent_greeks(true)
1505 .build()
1506 .expect("Failed to build PortfolioGreeksParams");
1507
1508 assert!(instrument_params.use_cached_greeks);
1509 assert!(instrument_params.cache_greeks);
1510 assert!(instrument_params.publish_greeks);
1511 assert!(instrument_params.percent_greeks);
1512
1513 assert!(portfolio_params.use_cached_greeks);
1514 assert!(portfolio_params.cache_greeks);
1515 assert!(portfolio_params.publish_greeks);
1516 assert!(portfolio_params.percent_greeks);
1517 }
1518
1519 #[rstest]
1520 fn test_greeks_filter_callback_function() {
1521 fn filter_positive_delta(data: &GreeksData) -> bool {
1523 data.delta > 0.0
1524 }
1525
1526 let filter = GreeksFilterCallback::from_fn(filter_positive_delta);
1527
1528 let greeks_data = GreeksData::from_delta(
1530 InstrumentId::from("TEST.NASDAQ"),
1531 0.5,
1532 1.0,
1533 UnixNanos::default(),
1534 );
1535
1536 assert!(filter.call(&greeks_data));
1537
1538 let debug_str = format!("{filter:?}");
1540 assert!(debug_str.contains("GreeksFilterCallback::Function"));
1541 }
1542
1543 #[rstest]
1544 fn test_greeks_filter_callback_closure() {
1545 let min_delta = 0.3;
1547 let filter =
1548 GreeksFilterCallback::from_closure(move |data: &GreeksData| data.delta > min_delta);
1549
1550 let greeks_data = GreeksData::from_delta(
1552 InstrumentId::from("TEST.NASDAQ"),
1553 0.5,
1554 1.0,
1555 UnixNanos::default(),
1556 );
1557
1558 assert!(filter.call(&greeks_data));
1559
1560 let debug_str = format!("{filter:?}");
1562 assert!(debug_str.contains("GreeksFilterCallback::Closure"));
1563 }
1564
1565 #[rstest]
1566 fn test_greeks_filter_callback_clone() {
1567 fn filter_fn(data: &GreeksData) -> bool {
1568 data.delta > 0.0
1569 }
1570
1571 let filter1 = GreeksFilterCallback::from_fn(filter_fn);
1572 let filter2 = filter1.clone();
1573
1574 let greeks_data = GreeksData::from_delta(
1575 InstrumentId::from("TEST.NASDAQ"),
1576 0.5,
1577 1.0,
1578 UnixNanos::default(),
1579 );
1580
1581 assert!(filter1.call(&greeks_data));
1582 assert!(filter2.call(&greeks_data));
1583 }
1584
1585 #[rstest]
1586 fn test_portfolio_greeks_params_with_filter() {
1587 fn filter_high_delta(data: &GreeksData) -> bool {
1588 data.delta.abs() > 0.1
1589 }
1590
1591 let filter = GreeksFilterCallback::from_fn(filter_high_delta);
1592
1593 let params = PortfolioGreeksParamsBuilder::default()
1594 .greeks_filter(Some(filter))
1595 .flat_interest_rate(0.05)
1596 .build()
1597 .expect("Failed to build PortfolioGreeksParams");
1598
1599 assert!(params.greeks_filter.is_some());
1600 assert_eq!(params.flat_interest_rate, 0.05);
1601
1602 let greeks_data = GreeksData::from_delta(
1604 InstrumentId::from("TEST.NASDAQ"),
1605 0.5,
1606 1.0,
1607 UnixNanos::default(),
1608 );
1609
1610 let filter_ref = params.greeks_filter.as_ref().unwrap();
1611 assert!(filter_ref.call(&greeks_data));
1612 }
1613
1614 #[rstest]
1615 fn test_portfolio_greeks_params_with_closure_filter() {
1616 let min_gamma = 0.01;
1617 let filter =
1618 GreeksFilterCallback::from_closure(move |data: &GreeksData| data.gamma > min_gamma);
1619
1620 let params = PortfolioGreeksParamsBuilder::default()
1621 .greeks_filter(Some(filter))
1622 .build()
1623 .expect("Failed to build PortfolioGreeksParams");
1624
1625 assert!(params.greeks_filter.is_some());
1626
1627 let debug_str = format!("{params:?}");
1629 assert!(debug_str.contains("greeks_filter"));
1630 }
1631
1632 #[rstest]
1633 fn test_greeks_filter_to_greeks_filter_conversion() {
1634 fn filter_fn(data: &GreeksData) -> bool {
1635 data.delta > 0.0
1636 }
1637
1638 let callback = GreeksFilterCallback::from_fn(filter_fn);
1639 let greeks_filter = callback.to_greeks_filter();
1640
1641 let greeks_data = GreeksData::from_delta(
1642 InstrumentId::from("TEST.NASDAQ"),
1643 0.5,
1644 1.0,
1645 UnixNanos::default(),
1646 );
1647
1648 assert!(greeks_filter(&greeks_data));
1649 }
1650
1651 fn option_with_expiration(instrument_id: &str, expiration_ns: UnixNanos) -> OptionContract {
1652 let activation_ns = UnixNanos::from(Utc.with_ymd_and_hms(2021, 9, 17, 0, 0, 0).unwrap());
1653 OptionContract::new(
1654 InstrumentId::from(instrument_id),
1655 Symbol::from("AAPL211217C00150000"),
1656 AssetClass::Equity,
1657 Some(Ustr::from("GMNI")),
1658 Ustr::from("AAPL"),
1659 OptionKind::Call,
1660 Price::from("149.0"),
1661 Currency::from("USD"),
1662 activation_ns,
1663 expiration_ns,
1664 2,
1665 Price::from("0.01"),
1666 Quantity::from(100),
1667 Quantity::from(1),
1668 None,
1669 None,
1670 None,
1671 None,
1672 None,
1673 None,
1674 None,
1675 None,
1676 None,
1677 UnixNanos::default(),
1678 UnixNanos::default(),
1679 )
1680 }
1681
1682 fn equity_aapl_opra() -> Equity {
1683 Equity::new(
1684 InstrumentId::from("AAPL.OPRA"),
1685 Symbol::from("AAPL"),
1686 Some(Ustr::from("US0378331005")),
1687 Currency::from("USD"),
1688 2,
1689 Price::from("0.01"),
1690 None,
1691 None,
1692 None,
1693 None,
1694 None,
1695 None,
1696 None,
1697 None,
1698 None,
1699 None,
1700 UnixNanos::default(),
1701 UnixNanos::default(),
1702 )
1703 }
1704
1705 fn future_with_expiration(
1706 instrument_id: &str,
1707 underlying: &str,
1708 expiration_ns: UnixNanos,
1709 ) -> FuturesContract {
1710 FuturesContract::new(
1711 InstrumentId::from(instrument_id),
1712 Symbol::from(underlying),
1713 AssetClass::Index,
1714 Some(Ustr::from("XCME")),
1715 Ustr::from(underlying),
1716 UnixNanos::default(),
1717 expiration_ns,
1718 Currency::from("USD"),
1719 2,
1720 Price::from("0.25"),
1721 Quantity::from(1),
1722 Quantity::from(1),
1723 None,
1724 None,
1725 None,
1726 None,
1727 None,
1728 None,
1729 None,
1730 None,
1731 None,
1732 UnixNanos::default(),
1733 UnixNanos::default(),
1734 )
1735 }
1736
1737 fn future_option_with_expiration(
1738 instrument_id: &str,
1739 raw_symbol: &str,
1740 underlying: &str,
1741 option_kind: OptionKind,
1742 strike: &str,
1743 expiration_ns: UnixNanos,
1744 ) -> OptionContract {
1745 OptionContract::new(
1746 InstrumentId::from(instrument_id),
1747 Symbol::from(raw_symbol),
1748 AssetClass::Index,
1749 Some(Ustr::from("XCME")),
1750 Ustr::from(underlying),
1751 option_kind,
1752 Price::from(strike),
1753 Currency::from("USD"),
1754 UnixNanos::default(),
1755 expiration_ns,
1756 2,
1757 Price::from("0.01"),
1758 Quantity::from(1),
1759 Quantity::from(1),
1760 None,
1761 None,
1762 None,
1763 None,
1764 None,
1765 None,
1766 None,
1767 None,
1768 None,
1769 UnixNanos::default(),
1770 UnixNanos::default(),
1771 )
1772 }
1773
1774 fn setup_cache_with_option_and_quotes(
1775 option: OptionContract,
1776 underlying_id: InstrumentId,
1777 now_ns: UnixNanos,
1778 ) -> Rc<RefCell<Cache>> {
1779 let option_id = option.id();
1780 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1781 cache
1782 .borrow_mut()
1783 .add_instrument(InstrumentAny::OptionContract(option))
1784 .unwrap();
1785 cache
1786 .borrow_mut()
1787 .add_instrument(InstrumentAny::Equity(equity_aapl_opra()))
1788 .unwrap();
1789 let option_quote = QuoteTick::new(
1790 option_id,
1791 Price::from("10.50"),
1792 Price::from("10.60"),
1793 Quantity::from(100),
1794 Quantity::from(100),
1795 now_ns,
1796 now_ns,
1797 );
1798 let underlying_quote = QuoteTick::new(
1799 underlying_id,
1800 Price::from("150.00"),
1801 Price::from("150.10"),
1802 Quantity::from(100),
1803 Quantity::from(100),
1804 now_ns,
1805 now_ns,
1806 );
1807 cache.borrow_mut().add_quote(option_quote).unwrap();
1808 cache.borrow_mut().add_quote(underlying_quote).unwrap();
1809 cache
1810 }
1811
1812 #[rstest]
1813 fn test_expiry_in_days_multi_day_unchanged() {
1814 let now = Utc.with_ymd_and_hms(2025, 3, 8, 12, 0, 0).unwrap();
1815 let expiry = now + chrono::Duration::days(30);
1816 let now_ns = UnixNanos::from(now);
1817 let expiry_ns = UnixNanos::from(expiry);
1818 let option = option_with_expiration("AAPL250417C00150000.OPRA", expiry_ns);
1819 let option_id = option.id();
1820 let underlying_id = InstrumentId::from("AAPL.OPRA");
1821 let cache = setup_cache_with_option_and_quotes(option, underlying_id, now_ns);
1822 let clock = Rc::new(RefCell::new(TestClock::new()));
1823 let calculator = GreeksCalculator::new(cache, clock);
1824
1825 let greeks = calculator
1826 .instrument_greeks(
1827 option_id,
1828 None,
1829 None,
1830 None,
1831 None,
1832 None,
1833 None,
1834 None,
1835 None,
1836 None,
1837 Some(now_ns),
1838 None,
1839 None,
1840 None,
1841 None,
1842 None,
1843 )
1844 .unwrap();
1845
1846 assert_eq!(greeks.expiry_in_days, 30);
1847 assert!((greeks.expiry_in_years - 30.0 / 365.25).abs() < 1e-9);
1848 }
1849
1850 #[rstest]
1851 fn test_expiry_in_days_same_day_clamped_to_one() {
1852 let now = Utc.with_ymd_and_hms(2025, 3, 8, 12, 0, 0).unwrap();
1853 let expiry_same_day = Utc.with_ymd_and_hms(2025, 3, 8, 18, 0, 0).unwrap();
1854 let now_ns = UnixNanos::from(now);
1855 let expiry_ns = UnixNanos::from(expiry_same_day);
1856 let option = option_with_expiration("AAPL250308C00150000.OPRA", expiry_ns);
1857 let option_id = option.id();
1858 let underlying_id = InstrumentId::from("AAPL.OPRA");
1859 let cache = setup_cache_with_option_and_quotes(option, underlying_id, now_ns);
1860 let clock = Rc::new(RefCell::new(TestClock::new()));
1861 let calculator = GreeksCalculator::new(cache, clock);
1862
1863 let greeks = calculator
1864 .instrument_greeks(
1865 option_id,
1866 None,
1867 None,
1868 None,
1869 None,
1870 None,
1871 None,
1872 None,
1873 None,
1874 None,
1875 Some(now_ns),
1876 None,
1877 None,
1878 None,
1879 None,
1880 None,
1881 )
1882 .unwrap();
1883
1884 assert_eq!(greeks.expiry_in_days, 1);
1885 assert!((greeks.expiry_in_years - 1.0 / 365.25).abs() < 1e-9);
1886 }
1887
1888 #[rstest]
1889 fn test_instrument_greeks_errors_when_future_underlying_price_missing_without_cached_spread() {
1890 let now = Utc.with_ymd_and_hms(2024, 2, 14, 16, 0, 0).unwrap();
1891 let expiry = Utc.with_ymd_and_hms(2024, 3, 15, 16, 0, 0).unwrap();
1892 let now_ns = UnixNanos::from(now);
1893 let expiry_ns = UnixNanos::from(expiry);
1894
1895 let future = future_with_expiration("ESH4.GLBX", "ESH4", expiry_ns);
1896 let call_option = future_option_with_expiration(
1897 "ESH4C150.GLBX",
1898 "ESH4C150",
1899 "ESH4",
1900 OptionKind::Call,
1901 "150.00",
1902 expiry_ns,
1903 );
1904 let put_option = future_option_with_expiration(
1905 "ESH4P150.GLBX",
1906 "ESH4P150",
1907 "ESH4",
1908 OptionKind::Put,
1909 "150.00",
1910 expiry_ns,
1911 );
1912
1913 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1914 cache
1915 .borrow_mut()
1916 .add_instrument(InstrumentAny::FuturesContract(future))
1917 .unwrap();
1918 cache
1919 .borrow_mut()
1920 .add_instrument(InstrumentAny::OptionContract(call_option.clone()))
1921 .unwrap();
1922 cache
1923 .borrow_mut()
1924 .add_instrument(InstrumentAny::OptionContract(put_option.clone()))
1925 .unwrap();
1926
1927 let call_quote = QuoteTick::new(
1928 call_option.id(),
1929 Price::from("8.50"),
1930 Price::from("8.50"),
1931 Quantity::from(100),
1932 Quantity::from(100),
1933 now_ns,
1934 now_ns,
1935 );
1936 let put_quote = QuoteTick::new(
1937 put_option.id(),
1938 Price::from("3.33"),
1939 Price::from("3.33"),
1940 Quantity::from(100),
1941 Quantity::from(100),
1942 now_ns,
1943 now_ns,
1944 );
1945 cache.borrow_mut().add_quote(call_quote).unwrap();
1946 cache.borrow_mut().add_quote(put_quote).unwrap();
1947
1948 let clock = Rc::new(RefCell::new(TestClock::new()));
1949 clock.borrow_mut().set_time(now_ns);
1950 let calculator = GreeksCalculator::new(cache, clock);
1951
1952 let error = calculator
1953 .instrument_greeks(
1954 call_option.id(),
1955 Some(0.0425),
1956 None,
1957 None,
1958 None,
1959 None,
1960 None,
1961 None,
1962 None,
1963 None,
1964 Some(now_ns),
1965 None,
1966 None,
1967 None,
1968 None,
1969 None,
1970 )
1971 .unwrap_err();
1972
1973 assert_eq!(error.to_string(), "No price available for ESH4.GLBX");
1974 }
1975
1976 #[rstest]
1977 fn test_cache_futures_spread_returns_price_to_reference_future() {
1978 let now = Utc.with_ymd_and_hms(2024, 2, 14, 16, 0, 0).unwrap();
1979 let expiry = Utc.with_ymd_and_hms(2024, 3, 15, 16, 0, 0).unwrap();
1980 let now_ns = UnixNanos::from(now);
1981 let expiry_ns = UnixNanos::from(expiry);
1982
1983 let future = future_with_expiration("ESH4.GLBX", "ESH4", expiry_ns);
1984 let reference_future = future_with_expiration("ESM4.GLBX", "ESM4", expiry_ns);
1985 let call_option = future_option_with_expiration(
1986 "ESH4C150.GLBX",
1987 "ESH4C150",
1988 "ESH4",
1989 OptionKind::Call,
1990 "150.00",
1991 expiry_ns,
1992 );
1993 let put_option = future_option_with_expiration(
1994 "ESH4P150.GLBX",
1995 "ESH4P150",
1996 "ESH4",
1997 OptionKind::Put,
1998 "150.00",
1999 expiry_ns,
2000 );
2001
2002 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
2003 cache
2004 .borrow_mut()
2005 .add_instrument(InstrumentAny::FuturesContract(future))
2006 .unwrap();
2007 cache
2008 .borrow_mut()
2009 .add_instrument(InstrumentAny::FuturesContract(reference_future.clone()))
2010 .unwrap();
2011 cache
2012 .borrow_mut()
2013 .add_instrument(InstrumentAny::OptionContract(call_option.clone()))
2014 .unwrap();
2015 cache
2016 .borrow_mut()
2017 .add_instrument(InstrumentAny::OptionContract(put_option.clone()))
2018 .unwrap();
2019
2020 let call_quote = QuoteTick::new(
2021 call_option.id(),
2022 Price::from("8.50"),
2023 Price::from("8.50"),
2024 Quantity::from(100),
2025 Quantity::from(100),
2026 now_ns,
2027 now_ns,
2028 );
2029 let put_quote = QuoteTick::new(
2030 put_option.id(),
2031 Price::from("3.33"),
2032 Price::from("3.33"),
2033 Quantity::from(100),
2034 Quantity::from(100),
2035 now_ns,
2036 now_ns,
2037 );
2038 let reference_future_quote = QuoteTick::new(
2039 reference_future.id(),
2040 Price::from("155.00"),
2041 Price::from("155.00"),
2042 Quantity::from(100),
2043 Quantity::from(100),
2044 now_ns,
2045 now_ns,
2046 );
2047 cache.borrow_mut().add_quote(call_quote).unwrap();
2048 cache.borrow_mut().add_quote(put_quote).unwrap();
2049 cache
2050 .borrow_mut()
2051 .add_quote(reference_future_quote)
2052 .unwrap();
2053
2054 let clock = Rc::new(RefCell::new(TestClock::new()));
2055 clock.borrow_mut().set_time(now_ns);
2056 let calculator = GreeksCalculator::new(cache, clock);
2057
2058 let cached_future_price = calculator
2059 .cache_futures_spread(call_option.id(), put_option.id(), reference_future.id())
2060 .unwrap();
2061
2062 let expected_underlying = 150.0 + (0.0425_f64 * (30.0 / 365.25)).exp() * (8.50 - 3.33);
2063 let expected_cached_underlying = reference_future.make_price(expected_underlying);
2064 assert_eq!(cached_future_price, expected_cached_underlying);
2065 assert_eq!(
2066 calculator.get_cached_futures_spread_price(InstrumentId::from("ESH4.GLBX")),
2067 Some(expected_cached_underlying)
2068 );
2069 }
2070
2071 #[rstest]
2072 fn test_instrument_greeks_uses_cached_futures_spread_when_underlying_price_missing() {
2073 let now = Utc.with_ymd_and_hms(2024, 2, 14, 16, 0, 0).unwrap();
2074 let expiry = Utc.with_ymd_and_hms(2024, 3, 15, 16, 0, 0).unwrap();
2075 let now_ns = UnixNanos::from(now);
2076 let expiry_ns = UnixNanos::from(expiry);
2077
2078 let future = future_with_expiration("ESH4.GLBX", "ESH4", expiry_ns);
2079 let reference_future = future_with_expiration("ESM4.GLBX", "ESM4", expiry_ns);
2080 let call_option = future_option_with_expiration(
2081 "ESH4C150.GLBX",
2082 "ESH4C150",
2083 "ESH4",
2084 OptionKind::Call,
2085 "150.00",
2086 expiry_ns,
2087 );
2088 let put_option = future_option_with_expiration(
2089 "ESH4P150.GLBX",
2090 "ESH4P150",
2091 "ESH4",
2092 OptionKind::Put,
2093 "150.00",
2094 expiry_ns,
2095 );
2096 let target_call_option = future_option_with_expiration(
2097 "ESH4C152.GLBX",
2098 "ESH4C152",
2099 "ESH4",
2100 OptionKind::Call,
2101 "152.00",
2102 expiry_ns,
2103 );
2104
2105 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
2106 cache
2107 .borrow_mut()
2108 .add_instrument(InstrumentAny::FuturesContract(future))
2109 .unwrap();
2110 cache
2111 .borrow_mut()
2112 .add_instrument(InstrumentAny::FuturesContract(reference_future.clone()))
2113 .unwrap();
2114 cache
2115 .borrow_mut()
2116 .add_instrument(InstrumentAny::OptionContract(call_option.clone()))
2117 .unwrap();
2118 cache
2119 .borrow_mut()
2120 .add_instrument(InstrumentAny::OptionContract(put_option.clone()))
2121 .unwrap();
2122 cache
2123 .borrow_mut()
2124 .add_instrument(InstrumentAny::OptionContract(target_call_option.clone()))
2125 .unwrap();
2126
2127 let call_quote = QuoteTick::new(
2128 call_option.id(),
2129 Price::from("8.50"),
2130 Price::from("8.50"),
2131 Quantity::from(100),
2132 Quantity::from(100),
2133 now_ns,
2134 now_ns,
2135 );
2136 let put_quote = QuoteTick::new(
2137 put_option.id(),
2138 Price::from("3.33"),
2139 Price::from("3.33"),
2140 Quantity::from(100),
2141 Quantity::from(100),
2142 now_ns,
2143 now_ns,
2144 );
2145 let target_call_quote = QuoteTick::new(
2146 target_call_option.id(),
2147 Price::from("6.75"),
2148 Price::from("6.75"),
2149 Quantity::from(100),
2150 Quantity::from(100),
2151 now_ns,
2152 now_ns,
2153 );
2154 let reference_future_quote = QuoteTick::new(
2155 reference_future.id(),
2156 Price::from("155.00"),
2157 Price::from("155.00"),
2158 Quantity::from(100),
2159 Quantity::from(100),
2160 now_ns,
2161 now_ns,
2162 );
2163 cache.borrow_mut().add_quote(call_quote).unwrap();
2164 cache.borrow_mut().add_quote(put_quote).unwrap();
2165 cache.borrow_mut().add_quote(target_call_quote).unwrap();
2166 cache
2167 .borrow_mut()
2168 .add_quote(reference_future_quote)
2169 .unwrap();
2170
2171 let clock = Rc::new(RefCell::new(TestClock::new()));
2172 clock.borrow_mut().set_time(now_ns);
2173 let calculator = GreeksCalculator::new(cache, clock);
2174 calculator
2175 .cache_futures_spread(call_option.id(), put_option.id(), reference_future.id())
2176 .unwrap();
2177
2178 let greeks = calculator
2179 .instrument_greeks(
2180 target_call_option.id(),
2181 Some(0.0425),
2182 None,
2183 None,
2184 None,
2185 None,
2186 None,
2187 None,
2188 None,
2189 None,
2190 Some(now_ns),
2191 None,
2192 None,
2193 None,
2194 None,
2195 None,
2196 )
2197 .unwrap();
2198
2199 let expected_underlying = reference_future
2200 .make_price(150.0 + (0.0425_f64 * (30.0 / 365.25)).exp() * (8.50 - 3.33))
2201 .as_f64();
2202 assert_eq!(greeks.underlying_price, expected_underlying);
2203 }
2204
2205 #[rstest]
2206 fn test_instrument_greeks_uses_index_price_for_index_underlying() {
2207 let now = Utc.with_ymd_and_hms(2024, 2, 14, 16, 0, 0).unwrap();
2208 let expiry = Utc.with_ymd_and_hms(2024, 3, 15, 16, 0, 0).unwrap();
2209 let now_ns = UnixNanos::from(now);
2210 let expiry_ns = UnixNanos::from(expiry);
2211
2212 let future = future_with_expiration("ESH4.GLBX", "ESH4", expiry_ns);
2213 let call_option = future_option_with_expiration(
2214 "ESH4C150.GLBX",
2215 "ESH4C150",
2216 "ESH4",
2217 OptionKind::Call,
2218 "150.00",
2219 expiry_ns,
2220 );
2221
2222 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
2223 cache
2224 .borrow_mut()
2225 .add_instrument(InstrumentAny::FuturesContract(future))
2226 .unwrap();
2227 cache
2228 .borrow_mut()
2229 .add_instrument(InstrumentAny::OptionContract(call_option.clone()))
2230 .unwrap();
2231
2232 let call_quote = QuoteTick::new(
2233 call_option.id(),
2234 Price::from("8.50"),
2235 Price::from("8.50"),
2236 Quantity::from(100),
2237 Quantity::from(100),
2238 now_ns,
2239 now_ns,
2240 );
2241 cache.borrow_mut().add_quote(call_quote).unwrap();
2242 cache
2243 .borrow_mut()
2244 .add_index_price(IndexPriceUpdate::new(
2245 InstrumentId::from("ESH4.GLBX"),
2246 Price::from("157.25"),
2247 now_ns,
2248 now_ns,
2249 ))
2250 .unwrap();
2251
2252 let clock = Rc::new(RefCell::new(TestClock::new()));
2253 clock.borrow_mut().set_time(now_ns);
2254 let calculator = GreeksCalculator::new(cache, clock);
2255
2256 let greeks = calculator
2257 .instrument_greeks(
2258 call_option.id(),
2259 Some(0.0425),
2260 None,
2261 None,
2262 None,
2263 None,
2264 None,
2265 None,
2266 None,
2267 None,
2268 Some(now_ns),
2269 None,
2270 None,
2271 None,
2272 None,
2273 None,
2274 )
2275 .unwrap();
2276
2277 assert_eq!(greeks.underlying_price, 157.25);
2278 }
2279
2280 #[rstest]
2281 fn test_instrument_greeks_prefers_quote_over_index_price_for_index_future() {
2282 let now = Utc.with_ymd_and_hms(2024, 2, 14, 16, 0, 0).unwrap();
2283 let expiry = Utc.with_ymd_and_hms(2024, 3, 15, 16, 0, 0).unwrap();
2284 let now_ns = UnixNanos::from(now);
2285 let expiry_ns = UnixNanos::from(expiry);
2286
2287 let future = future_with_expiration("ESH4.GLBX", "ESH4", expiry_ns);
2288 let call_option = future_option_with_expiration(
2289 "ESH4C150.GLBX",
2290 "ESH4C150",
2291 "ESH4",
2292 OptionKind::Call,
2293 "150.00",
2294 expiry_ns,
2295 );
2296
2297 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
2298 cache
2299 .borrow_mut()
2300 .add_instrument(InstrumentAny::FuturesContract(future))
2301 .unwrap();
2302 cache
2303 .borrow_mut()
2304 .add_instrument(InstrumentAny::OptionContract(call_option.clone()))
2305 .unwrap();
2306
2307 let future_quote = QuoteTick::new(
2309 InstrumentId::from("ESH4.GLBX"),
2310 Price::from("158.50"),
2311 Price::from("159.50"),
2312 Quantity::from(100),
2313 Quantity::from(100),
2314 now_ns,
2315 now_ns,
2316 );
2317 cache.borrow_mut().add_quote(future_quote).unwrap();
2318 cache
2319 .borrow_mut()
2320 .add_index_price(IndexPriceUpdate::new(
2321 InstrumentId::from("ESH4.GLBX"),
2322 Price::from("157.25"),
2323 now_ns,
2324 now_ns,
2325 ))
2326 .unwrap();
2327
2328 let call_quote = QuoteTick::new(
2329 call_option.id(),
2330 Price::from("8.50"),
2331 Price::from("8.50"),
2332 Quantity::from(100),
2333 Quantity::from(100),
2334 now_ns,
2335 now_ns,
2336 );
2337 cache.borrow_mut().add_quote(call_quote).unwrap();
2338
2339 let clock = Rc::new(RefCell::new(TestClock::new()));
2340 clock.borrow_mut().set_time(now_ns);
2341 let calculator = GreeksCalculator::new(cache, clock);
2342
2343 let greeks = calculator
2344 .instrument_greeks(
2345 call_option.id(),
2346 Some(0.0425),
2347 None,
2348 None,
2349 None,
2350 None,
2351 None,
2352 None,
2353 None,
2354 None,
2355 Some(now_ns),
2356 None,
2357 None,
2358 None,
2359 None,
2360 None,
2361 )
2362 .unwrap();
2363
2364 assert_eq!(greeks.underlying_price, 159.0);
2366 }
2367}