1use std::{cell::RefCell, collections::HashMap, fmt::Debug, rc::Rc};
19
20use ahash::AHashMap;
21use nautilus_core::UnixNanos;
22use nautilus_model::{
23 data::greeks::{
24 GreeksData, OptionGreekValues, PortfolioGreeks, black_scholes_greeks, imply_vol_and_greeks,
25 refine_vol_and_greeks,
26 },
27 enums::{AssetClass, InstrumentClass, OptionKind, PositionSide, PriceType},
28 identifiers::{InstrumentId, StrategyId, Venue},
29 instruments::{Instrument, any::InstrumentAny},
30 position::Position,
31 types::Price,
32};
33
34use crate::{
35 actor::DataActorNative,
36 cache::{Cache, refs::PositionRef},
37 clock::Clock,
38 msgbus,
39 msgbus::TypedHandler,
40};
41
42pub type GreeksFilter = Box<dyn Fn(&GreeksData) -> bool>;
44
45#[derive(Clone)]
47pub enum GreeksFilterCallback {
48 Function(fn(&GreeksData) -> bool),
50 Closure(std::rc::Rc<dyn Fn(&GreeksData) -> bool>),
52}
53
54impl GreeksFilterCallback {
55 pub fn from_fn(f: fn(&GreeksData) -> bool) -> Self {
57 Self::Function(f)
58 }
59
60 pub fn from_closure<F>(f: F) -> Self
62 where
63 F: Fn(&GreeksData) -> bool + 'static,
64 {
65 Self::Closure(std::rc::Rc::new(f))
66 }
67
68 pub fn call(&self, data: &GreeksData) -> bool {
70 match self {
71 Self::Function(f) => f(data),
72 Self::Closure(f) => f(data),
73 }
74 }
75
76 pub fn to_greeks_filter(self) -> GreeksFilter {
78 match self {
79 Self::Function(f) => Box::new(f),
80 Self::Closure(f) => {
81 let f_clone = f.clone();
82 Box::new(move |data| f_clone(data))
83 }
84 }
85 }
86}
87
88impl Debug for GreeksFilterCallback {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 Self::Function(_) => f.write_str("GreeksFilterCallback::Function"),
92 Self::Closure(_) => f.write_str("GreeksFilterCallback::Closure"),
93 }
94 }
95}
96
97#[derive(Debug, bon::Builder)]
99pub struct InstrumentGreeksParams {
100 pub instrument_id: InstrumentId,
102 #[builder(default = 0.0425)]
104 pub flat_interest_rate: f64,
105 pub flat_dividend_yield: Option<f64>,
107 #[builder(default = 0.0)]
109 pub spot_shock: f64,
110 #[builder(default = 0.0)]
112 pub vol_shock: f64,
113 #[builder(default = 0.0)]
115 pub time_to_expiry_shock: f64,
116 #[builder(default = false)]
118 pub use_cached_greeks: bool,
119 #[builder(default = false)]
121 pub update_vol: bool,
122 #[builder(default = false)]
124 pub cache_greeks: bool,
125 #[builder(default = false)]
127 pub publish_greeks: bool,
128 pub ts_event: Option<UnixNanos>,
130 pub position: Option<Position>,
132 #[builder(default = false)]
134 pub percent_greeks: bool,
135 pub index_instrument_id: Option<InstrumentId>,
137 pub beta_weights: Option<HashMap<InstrumentId, f64>>,
139 pub vega_time_weight_base: Option<i32>,
141 pub vol_index_instrument_id: Option<InstrumentId>,
143 pub vol_beta_weights: Option<HashMap<InstrumentId, f64>>,
145}
146
147impl InstrumentGreeksParams {
148 pub fn calculate(&self, calculator: &GreeksCalculator) -> anyhow::Result<GreeksData> {
154 calculator.instrument_greeks(
155 self.instrument_id,
156 Some(self.flat_interest_rate),
157 self.flat_dividend_yield,
158 Some(self.spot_shock),
159 Some(self.vol_shock),
160 Some(self.time_to_expiry_shock),
161 Some(self.use_cached_greeks),
162 Some(self.update_vol),
163 Some(self.cache_greeks),
164 Some(self.publish_greeks),
165 self.ts_event,
166 self.position.clone(),
167 Some(self.percent_greeks),
168 self.index_instrument_id,
169 self.beta_weights.as_ref(),
170 self.vega_time_weight_base,
171 self.vol_index_instrument_id,
172 self.vol_beta_weights.as_ref(),
173 )
174 }
175}
176
177#[derive(bon::Builder)]
179pub struct PortfolioGreeksParams {
180 pub underlyings: Option<Vec<String>>,
182 pub venue: Option<Venue>,
184 pub instrument_id: Option<InstrumentId>,
186 pub strategy_id: Option<StrategyId>,
188 pub side: Option<PositionSide>,
190 #[builder(default = 0.0425)]
192 pub flat_interest_rate: f64,
193 pub flat_dividend_yield: Option<f64>,
195 #[builder(default = 0.0)]
197 pub spot_shock: f64,
198 #[builder(default = 0.0)]
200 pub vol_shock: f64,
201 #[builder(default = 0.0)]
203 pub time_to_expiry_shock: f64,
204 #[builder(default = false)]
206 pub use_cached_greeks: bool,
207 #[builder(default = false)]
209 pub update_vol: bool,
210 #[builder(default = false)]
212 pub cache_greeks: bool,
213 #[builder(default = false)]
215 pub publish_greeks: bool,
216 #[builder(default = false)]
218 pub percent_greeks: bool,
219 pub index_instrument_id: Option<InstrumentId>,
221 pub beta_weights: Option<HashMap<InstrumentId, f64>>,
223 pub greeks_filter: Option<GreeksFilterCallback>,
225 pub vega_time_weight_base: Option<i32>,
227 pub vol_index_instrument_id: Option<InstrumentId>,
229 pub vol_beta_weights: Option<HashMap<InstrumentId, f64>>,
231}
232
233impl Debug for PortfolioGreeksParams {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 f.debug_struct(stringify!(PortfolioGreeksParams))
236 .field("underlyings", &self.underlyings)
237 .field("venue", &self.venue)
238 .field("instrument_id", &self.instrument_id)
239 .field("strategy_id", &self.strategy_id)
240 .field("side", &self.side)
241 .field("flat_interest_rate", &self.flat_interest_rate)
242 .field("flat_dividend_yield", &self.flat_dividend_yield)
243 .field("spot_shock", &self.spot_shock)
244 .field("vol_shock", &self.vol_shock)
245 .field("time_to_expiry_shock", &self.time_to_expiry_shock)
246 .field("use_cached_greeks", &self.use_cached_greeks)
247 .field("update_vol", &self.update_vol)
248 .field("cache_greeks", &self.cache_greeks)
249 .field("publish_greeks", &self.publish_greeks)
250 .field("percent_greeks", &self.percent_greeks)
251 .field("index_instrument_id", &self.index_instrument_id)
252 .field("beta_weights", &self.beta_weights)
253 .field("greeks_filter", &self.greeks_filter)
254 .field("vega_time_weight_base", &self.vega_time_weight_base)
255 .field("vol_index_instrument_id", &self.vol_index_instrument_id)
256 .field("vol_beta_weights", &self.vol_beta_weights)
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 self.vol_index_instrument_id,
294 self.vol_beta_weights.as_ref(),
295 )
296 }
297}
298
299#[allow(dead_code)]
314#[derive(Debug)]
315pub struct GreeksCalculator {
316 cache: Rc<RefCell<Cache>>,
317 clock: Rc<RefCell<dyn Clock>>,
318 cached_futures_spreads: RefCell<AHashMap<InstrumentId, (InstrumentId, Price)>>,
319}
320
321impl GreeksCalculator {
322 pub fn new(cache: Rc<RefCell<Cache>>, clock: Rc<RefCell<dyn Clock>>) -> Self {
324 Self {
325 cache,
326 clock,
327 cached_futures_spreads: RefCell::new(AHashMap::new()),
328 }
329 }
330
331 pub fn from_actor(actor: &impl DataActorNative) -> Self {
337 Self::new(actor.cache_rc(), actor.clock_rc())
338 }
339
340 #[expect(clippy::too_many_arguments)]
352 pub fn instrument_greeks(
353 &self,
354 instrument_id: InstrumentId,
355 flat_interest_rate: Option<f64>,
356 flat_dividend_yield: Option<f64>,
357 spot_shock: Option<f64>,
358 vol_shock: Option<f64>,
359 time_to_expiry_shock: Option<f64>,
360 use_cached_greeks: Option<bool>,
361 update_vol: Option<bool>,
362 cache_greeks: Option<bool>,
363 publish_greeks: Option<bool>,
364 ts_event: Option<UnixNanos>,
365 position: Option<Position>,
366 percent_greeks: Option<bool>,
367 index_instrument_id: Option<InstrumentId>,
368 beta_weights: Option<&HashMap<InstrumentId, f64>>,
369 vega_time_weight_base: Option<i32>,
370 vol_index_instrument_id: Option<InstrumentId>,
371 vol_beta_weights: Option<&HashMap<InstrumentId, f64>>,
372 ) -> anyhow::Result<GreeksData> {
373 let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
375 let spot_shock = spot_shock.unwrap_or(0.0);
376 let vol_shock = vol_shock.unwrap_or(0.0);
377 let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
378 let use_cached_greeks = use_cached_greeks.unwrap_or(false);
379 let update_vol = update_vol.unwrap_or(false);
380 let cache_greeks = cache_greeks.unwrap_or(false);
381 let publish_greeks = publish_greeks.unwrap_or(false);
382 let ts_event = ts_event.unwrap_or_default();
383 let percent_greeks = percent_greeks.unwrap_or(false);
384
385 let instrument = {
386 let cache = self.cache.borrow();
387 cache.try_instrument(&instrument_id)?.clone()
388 };
389
390 if instrument.instrument_class() != InstrumentClass::Option {
391 return self.calculate_non_option_greeks(
392 &instrument,
393 instrument_id,
394 spot_shock,
395 ts_event,
396 position,
397 percent_greeks,
398 index_instrument_id,
399 beta_weights,
400 );
401 }
402
403 let underlying_instrument_id =
404 Self::resolve_underlying_instrument_id(&instrument, instrument_id)?;
405 let mut greeks_data = self.calculate_option_greeks(
406 &instrument,
407 instrument_id,
408 underlying_instrument_id,
409 flat_interest_rate,
410 flat_dividend_yield,
411 use_cached_greeks,
412 update_vol,
413 cache_greeks,
414 publish_greeks,
415 ts_event,
416 percent_greeks,
417 index_instrument_id,
418 beta_weights,
419 vega_time_weight_base,
420 vol_index_instrument_id,
421 vol_beta_weights,
422 )?;
423
424 if spot_shock != 0.0 || vol_shock != 0.0 || time_to_expiry_shock != 0.0 {
425 greeks_data = self.apply_option_greeks_shocks(
426 &greeks_data,
427 underlying_instrument_id,
428 spot_shock,
429 vol_shock,
430 time_to_expiry_shock,
431 percent_greeks,
432 index_instrument_id,
433 beta_weights,
434 vega_time_weight_base,
435 vol_index_instrument_id,
436 vol_beta_weights,
437 )?;
438 }
439
440 if let Some(pos) = position {
441 greeks_data.pnl = greeks_data.price - pos.avg_px_open;
442 }
443
444 Ok(greeks_data)
445 }
446
447 fn resolve_underlying_instrument_id(
448 instrument: &InstrumentAny,
449 instrument_id: InstrumentId,
450 ) -> anyhow::Result<InstrumentId> {
451 let Some(underlying) = instrument.underlying() else {
452 anyhow::bail!("Instrument {instrument_id} has no underlying identifier");
453 };
454
455 Ok(InstrumentId::from(format!(
456 "{}.{}",
457 underlying, instrument_id.venue
458 )))
459 }
460
461 #[expect(clippy::too_many_arguments)]
462 fn calculate_non_option_greeks(
463 &self,
464 instrument: &InstrumentAny,
465 instrument_id: InstrumentId,
466 spot_shock: f64,
467 ts_event: UnixNanos,
468 position: Option<Position>,
469 percent_greeks: bool,
470 index_instrument_id: Option<InstrumentId>,
471 beta_weights: Option<&HashMap<InstrumentId, f64>>,
472 ) -> anyhow::Result<GreeksData> {
473 let multiplier = instrument.multiplier();
474 let underlying_instrument_id = instrument.id();
475 let underlying_price = self
476 .get_price(&underlying_instrument_id)
477 .ok_or_else(|| anyhow::anyhow!("No price available for {underlying_instrument_id}"))?;
478 let (delta, _, _) = self.modify_greeks(
479 1.0,
480 0.0,
481 underlying_instrument_id,
482 underlying_price + spot_shock,
483 underlying_price,
484 percent_greeks,
485 index_instrument_id,
486 beta_weights,
487 0.0,
488 0.0,
489 0,
490 None,
491 0.0,
492 None,
493 None,
494 None,
495 None,
496 )?;
497 let mut greeks_data =
498 GreeksData::from_delta(instrument_id, delta, multiplier.as_f64(), ts_event);
499
500 if let Some(pos) = position {
501 greeks_data.pnl = (underlying_price + spot_shock) - pos.avg_px_open;
502 greeks_data.price = greeks_data.pnl;
503 }
504
505 Ok(greeks_data)
506 }
507
508 #[expect(clippy::too_many_arguments)]
509 fn calculate_option_greeks(
510 &self,
511 instrument: &InstrumentAny,
512 instrument_id: InstrumentId,
513 underlying_instrument_id: InstrumentId,
514 flat_interest_rate: f64,
515 flat_dividend_yield: Option<f64>,
516 use_cached_greeks: bool,
517 update_vol: bool,
518 cache_greeks: bool,
519 publish_greeks: bool,
520 ts_event: UnixNanos,
521 percent_greeks: bool,
522 index_instrument_id: Option<InstrumentId>,
523 beta_weights: Option<&HashMap<InstrumentId, f64>>,
524 vega_time_weight_base: Option<i32>,
525 vol_index_instrument_id: Option<InstrumentId>,
526 vol_beta_weights: Option<&HashMap<InstrumentId, f64>>,
527 ) -> anyhow::Result<GreeksData> {
528 if use_cached_greeks {
529 let cache = self.cache.borrow();
530 if let Some(cached_greeks) = cache.greeks(&instrument_id) {
531 return Ok(cached_greeks);
532 }
533 }
534
535 let utc_now_ns = if ts_event == UnixNanos::default() {
536 self.clock.borrow().timestamp_ns()
537 } else {
538 ts_event
539 };
540 let utc_now = utc_now_ns.to_datetime_utc();
541 let expiry_utc = instrument
542 .expiration_ns()
543 .map(|ns| ns.to_datetime_utc())
544 .unwrap_or_default();
545 let expiry_int = expiry_utc
546 .strftime("%Y%m%d")
547 .to_string()
548 .parse::<i32>()
549 .unwrap_or(0);
550 let raw_days = utc_now.duration_until(expiry_utc).as_hours() / 24;
551 let expiry_in_days = raw_days.max(1) as i32;
552 let expiry_in_years = expiry_in_days as f64 / 365.25;
553 let currency = instrument.quote_currency().code.to_string();
554
555 let cache = self.cache.borrow();
556 let yield_curve = cache.yield_curve(¤cy);
557 let interest_rate = match yield_curve {
558 Some(yield_curve) => yield_curve(expiry_in_years),
559 None => flat_interest_rate,
560 };
561 let dividend_curve = cache.yield_curve(&underlying_instrument_id.to_string());
562 drop(cache);
563
564 let mut cost_of_carry = 0.0;
565
566 if let Some(dividend_curve) = dividend_curve {
567 cost_of_carry = interest_rate - dividend_curve(expiry_in_years);
568 } else if let Some(div_yield) = flat_dividend_yield {
569 cost_of_carry = interest_rate - div_yield;
570 }
571
572 let multiplier = instrument.multiplier();
573 let is_call = instrument.option_kind().unwrap_or(OptionKind::Call) == OptionKind::Call;
574 let strike = instrument.strike_price().unwrap_or_default().as_f64();
575 let option_price = self
576 .get_price(&instrument_id)
577 .ok_or_else(|| anyhow::anyhow!("No price available for {instrument_id}"))?;
578 let underlying_price = self.get_underlying_price(&underlying_instrument_id)?;
579
580 if let Some(vol_index_id) = vol_index_instrument_id {
581 self.get_price(&vol_index_id)
582 .ok_or_else(|| anyhow::anyhow!("No price available for {vol_index_id}"))?;
583 }
584 let greeks = if update_vol {
585 let cached_greeks = self.cache.borrow().greeks(&instrument_id);
586 match cached_greeks {
587 Some(cached_greeks) => refine_vol_and_greeks(
588 underlying_price,
589 interest_rate,
590 cost_of_carry,
591 is_call,
592 strike,
593 expiry_in_years,
594 option_price,
595 cached_greeks.vol,
596 ),
597 None => imply_vol_and_greeks(
598 underlying_price,
599 interest_rate,
600 cost_of_carry,
601 is_call,
602 strike,
603 expiry_in_years,
604 option_price,
605 ),
606 }
607 } else {
608 imply_vol_and_greeks(
609 underlying_price,
610 interest_rate,
611 cost_of_carry,
612 is_call,
613 strike,
614 expiry_in_years,
615 option_price,
616 )
617 };
618 let (delta, gamma, vega) = self.modify_greeks(
619 greeks.delta,
620 greeks.gamma,
621 underlying_instrument_id,
622 underlying_price,
623 underlying_price,
624 percent_greeks,
625 index_instrument_id,
626 beta_weights,
627 greeks.vega,
628 greeks.vol,
629 expiry_in_days,
630 vega_time_weight_base,
631 greeks.vol,
632 vol_index_instrument_id,
633 vol_beta_weights,
634 None,
635 None,
636 )?;
637 let greeks_data = GreeksData::new(
638 utc_now_ns,
639 utc_now_ns,
640 instrument_id,
641 is_call,
642 strike,
643 expiry_int,
644 expiry_in_days,
645 expiry_in_years,
646 multiplier.as_f64(),
647 1.0,
648 underlying_price,
649 interest_rate,
650 cost_of_carry,
651 greeks.vol,
652 0.0,
653 greeks.price,
654 OptionGreekValues {
655 delta,
656 gamma,
657 vega,
658 theta: greeks.theta,
659 rho: 0.0,
660 },
661 greeks.itm_prob,
662 );
663
664 if cache_greeks {
665 let mut cache = self.cache.borrow_mut();
666 cache.add_greeks(greeks_data.clone()).unwrap_or_default();
667 }
668
669 if publish_greeks {
670 let topic = format!(
671 "data.GreeksData.instrument_id={}",
672 instrument_id.symbol.as_str()
673 )
674 .into();
675 msgbus::publish_greeks(topic, &greeks_data);
676 }
677
678 Ok(greeks_data)
679 }
680
681 #[expect(clippy::too_many_arguments)]
682 fn apply_option_greeks_shocks(
683 &self,
684 greeks_data: &GreeksData,
685 underlying_instrument_id: InstrumentId,
686 spot_shock: f64,
687 vol_shock: f64,
688 time_to_expiry_shock: f64,
689 percent_greeks: bool,
690 index_instrument_id: Option<InstrumentId>,
691 beta_weights: Option<&HashMap<InstrumentId, f64>>,
692 vega_time_weight_base: Option<i32>,
693 vol_index_instrument_id: Option<InstrumentId>,
694 vol_beta_weights: Option<&HashMap<InstrumentId, f64>>,
695 ) -> anyhow::Result<GreeksData> {
696 let underlying_price = greeks_data.underlying_price;
697 let shocked_underlying_price = underlying_price + spot_shock;
698 let shocked_vol = greeks_data.vol + vol_shock;
699 let shocked_time_to_expiry = greeks_data.expiry_in_years - time_to_expiry_shock;
700 let shocked_expiry_in_days = (shocked_time_to_expiry * 365.25) as i32;
701
702 let greeks = black_scholes_greeks(
703 shocked_underlying_price,
704 greeks_data.interest_rate,
705 greeks_data.cost_of_carry,
706 shocked_vol,
707 greeks_data.is_call,
708 greeks_data.strike,
709 shocked_time_to_expiry,
710 );
711 let (delta, gamma, vega) = self.modify_greeks(
712 greeks.delta,
713 greeks.gamma,
714 underlying_instrument_id,
715 shocked_underlying_price,
716 underlying_price,
717 percent_greeks,
718 index_instrument_id,
719 beta_weights,
720 greeks.vega,
721 shocked_vol,
722 shocked_expiry_in_days,
723 vega_time_weight_base,
724 greeks_data.vol,
725 vol_index_instrument_id,
726 vol_beta_weights,
727 None,
728 None,
729 )?;
730 Ok(GreeksData::new(
731 greeks_data.ts_event,
732 greeks_data.ts_event,
733 greeks_data.instrument_id,
734 greeks_data.is_call,
735 greeks_data.strike,
736 greeks_data.expiry,
737 shocked_expiry_in_days,
738 shocked_time_to_expiry,
739 greeks_data.multiplier,
740 greeks_data.quantity,
741 shocked_underlying_price,
742 greeks_data.interest_rate,
743 greeks_data.cost_of_carry,
744 shocked_vol,
745 0.0,
746 greeks.price,
747 OptionGreekValues {
748 delta,
749 gamma,
750 vega,
751 theta: greeks.theta,
752 rho: 0.0,
753 },
754 greeks.itm_prob,
755 ))
756 }
757
758 fn get_underlying_price(&self, underlying_instrument_id: &InstrumentId) -> anyhow::Result<f64> {
759 if let Some(underlying_price) = self.get_price(underlying_instrument_id) {
760 return Ok(underlying_price);
761 }
762
763 let is_future_or_absent = {
766 let cache = self.cache.borrow();
767 cache
768 .instrument(underlying_instrument_id)
769 .is_none_or(|inst| inst.instrument_class() == InstrumentClass::Future)
770 };
771
772 if is_future_or_absent
773 && let Some(underlying_price) =
774 self.get_cached_futures_spread_price(*underlying_instrument_id)
775 {
776 return Ok(underlying_price.as_f64());
777 }
778
779 anyhow::bail!("No price available for {underlying_instrument_id}")
780 }
781
782 #[expect(clippy::too_many_arguments)]
808 pub fn modify_greeks(
809 &self,
810 delta_input: f64,
811 gamma_input: f64,
812 underlying_instrument_id: InstrumentId,
813 underlying_price: f64,
814 unshocked_underlying_price: f64,
815 percent_greeks: bool,
816 index_instrument_id: Option<InstrumentId>,
817 beta_weights: Option<&HashMap<InstrumentId, f64>>,
818 vega_input: f64,
819 vol: f64,
820 expiry_in_days: i32,
821 vega_time_weight_base: Option<i32>,
822 unshocked_vol: f64,
823 vol_index_instrument_id: Option<InstrumentId>,
824 vol_beta_weights: Option<&HashMap<InstrumentId, f64>>,
825 index_price: Option<f64>,
826 vol_index_price: Option<f64>,
827 ) -> anyhow::Result<(f64, f64, f64)> {
828 let mut delta = delta_input;
829 let mut gamma = gamma_input;
830 let mut vega = vega_input;
831
832 let mut used_index_price = index_price
833 .or_else(|| index_instrument_id.and_then(|index_id| self.get_price(&index_id)));
834 let mut used_index_vol = vol_index_price;
835 if used_index_vol.is_none()
836 && let Some(vol_index_id) = vol_index_instrument_id
837 {
838 used_index_vol = Some(
839 self.get_price(&vol_index_id)
840 .ok_or_else(|| anyhow::anyhow!("No price available for {vol_index_id}"))?,
841 );
842 }
843
844 if used_index_price.is_some() {
845 let mut beta = 1.0;
846
847 if let Some(weights) = beta_weights
848 && let Some(&weight) = weights.get(&underlying_instrument_id)
849 {
850 beta = weight;
851 }
852
853 if let Some(ref mut idx_price) = used_index_price {
854 #[expect(clippy::float_cmp, reason = "exact-equality baseline check")]
855 if underlying_price != unshocked_underlying_price {
856 *idx_price += 1.0 / beta
857 * (*idx_price / unshocked_underlying_price)
858 * (underlying_price - unshocked_underlying_price);
859 }
860
861 let delta_multiplier = beta * underlying_price / *idx_price;
862 delta *= delta_multiplier;
863 gamma *= delta_multiplier.powi(2);
864 }
865 }
866
867 if used_index_vol.is_some() {
868 let mut vega_beta = 1.0;
869 let used_vol = if unshocked_vol == 0.0 {
870 vol
871 } else {
872 unshocked_vol
873 };
874
875 if let Some(weights) = vol_beta_weights
876 && let Some(&weight) = weights.get(&underlying_instrument_id)
877 {
878 vega_beta = weight;
879 }
880
881 if let Some(ref mut idx_vol) = used_index_vol {
882 *idx_vol *= 0.01;
883
884 #[expect(clippy::float_cmp, reason = "exact-equality baseline check")]
885 if vol != used_vol && used_vol != 0.0 {
886 *idx_vol += 1.0 / vega_beta * (*idx_vol / used_vol) * (vol - used_vol);
887 }
888
889 if *idx_vol != 0.0 {
890 vega *= vega_beta * vol / *idx_vol;
891 }
892 }
893 }
894
895 if percent_greeks {
896 if let Some(idx_price) = used_index_price {
897 delta *= idx_price / 100.0;
898 gamma *= (idx_price / 100.0).powi(2);
899 } else {
900 delta *= underlying_price / 100.0;
901 gamma *= (underlying_price / 100.0).powi(2);
902 }
903
904 if let Some(idx_vol) = used_index_vol {
905 vega *= idx_vol / 100.0;
906 } else {
907 vega *= vol / 100.0;
908 }
909 }
910
911 if let Some(time_base) = vega_time_weight_base
913 && expiry_in_days > 0
914 {
915 let time_weight = (time_base as f64 / expiry_in_days as f64).sqrt();
916 vega *= time_weight;
917 }
918
919 Ok((delta, gamma, vega))
920 }
921
922 #[expect(clippy::too_many_arguments)]
935 pub fn portfolio_greeks(
936 &self,
937 underlyings: Option<&[String]>,
938 venue: Option<Venue>,
939 instrument_id: Option<InstrumentId>,
940 strategy_id: Option<StrategyId>,
941 side: Option<PositionSide>,
942 flat_interest_rate: Option<f64>,
943 flat_dividend_yield: Option<f64>,
944 spot_shock: Option<f64>,
945 vol_shock: Option<f64>,
946 time_to_expiry_shock: Option<f64>,
947 use_cached_greeks: Option<bool>,
948 update_vol: Option<bool>,
949 cache_greeks: Option<bool>,
950 publish_greeks: Option<bool>,
951 percent_greeks: Option<bool>,
952 index_instrument_id: Option<InstrumentId>,
953 beta_weights: Option<&HashMap<InstrumentId, f64>>,
954 greeks_filter: Option<&GreeksFilter>,
955 vega_time_weight_base: Option<i32>,
956 vol_index_instrument_id: Option<InstrumentId>,
957 vol_beta_weights: Option<&HashMap<InstrumentId, f64>>,
958 ) -> anyhow::Result<PortfolioGreeks> {
959 let ts_event = self.clock.borrow().timestamp_ns();
960 let mut portfolio_greeks =
961 PortfolioGreeks::new(ts_event, ts_event, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
962
963 let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
965 let spot_shock = spot_shock.unwrap_or(0.0);
966 let vol_shock = vol_shock.unwrap_or(0.0);
967 let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
968 let use_cached_greeks = use_cached_greeks.unwrap_or(false);
969 let update_vol = update_vol.unwrap_or(false);
970 let cache_greeks = cache_greeks.unwrap_or(false);
971 let publish_greeks = publish_greeks.unwrap_or(false);
972 let percent_greeks = percent_greeks.unwrap_or(false);
973 let cache = self.cache.borrow();
974 let open_positions = cache.positions_open(
975 venue.as_ref(),
976 instrument_id.as_ref(),
977 strategy_id.as_ref(),
978 None, side,
980 );
981 let open_positions: Vec<Position> =
982 open_positions.iter().map(PositionRef::cloned).collect();
983
984 for position in open_positions {
985 let position_instrument_id = position.instrument_id;
986
987 if let Some(underlyings_list) = underlyings {
988 let mut skip_position = true;
989
990 for underlying in underlyings_list {
991 if position_instrument_id
992 .symbol
993 .as_str()
994 .starts_with(underlying)
995 {
996 skip_position = false;
997 break;
998 }
999 }
1000
1001 if skip_position {
1002 continue;
1003 }
1004 }
1005
1006 let quantity = position.signed_qty;
1007 let instrument_greeks = self.instrument_greeks(
1008 position_instrument_id,
1009 Some(flat_interest_rate),
1010 flat_dividend_yield,
1011 Some(spot_shock),
1012 Some(vol_shock),
1013 Some(time_to_expiry_shock),
1014 Some(use_cached_greeks),
1015 Some(update_vol),
1016 Some(cache_greeks),
1017 Some(publish_greeks),
1018 Some(ts_event),
1019 Some(position),
1020 Some(percent_greeks),
1021 index_instrument_id,
1022 beta_weights,
1023 vega_time_weight_base,
1024 vol_index_instrument_id,
1025 vol_beta_weights,
1026 )?;
1027 let position_greeks = quantity * &instrument_greeks;
1028
1029 if greeks_filter.is_none_or(|filter| filter(&position_greeks)) {
1031 portfolio_greeks = portfolio_greeks + PortfolioGreeks::from(position_greeks);
1032 }
1033 }
1034
1035 Ok(portfolio_greeks)
1036 }
1037
1038 pub fn cache_futures_spread(
1044 &self,
1045 call_instrument_id: InstrumentId,
1046 put_instrument_id: InstrumentId,
1047 futures_instrument_id: InstrumentId,
1048 ) -> anyhow::Result<Price> {
1049 let cache = self.cache.borrow();
1050 let call_instrument = cache.instrument(&call_instrument_id).cloned();
1051 let put_instrument = cache.instrument(&put_instrument_id).cloned();
1052 let reference_future_instrument = cache.instrument(&futures_instrument_id).cloned();
1053 drop(cache);
1054
1055 let Some(call_instrument) = call_instrument else {
1056 anyhow::bail!(
1057 "Cannot cache futures spread: missing option instrument {call_instrument_id}"
1058 );
1059 };
1060 let Some(put_instrument) = put_instrument else {
1061 anyhow::bail!(
1062 "Cannot cache futures spread: missing option instrument {put_instrument_id}"
1063 );
1064 };
1065 let Some(reference_future_instrument) = reference_future_instrument else {
1066 anyhow::bail!(
1067 "Cannot cache futures spread: no reference futures instrument for {futures_instrument_id}"
1068 );
1069 };
1070
1071 if call_instrument.instrument_class() != InstrumentClass::Option
1072 || put_instrument.instrument_class() != InstrumentClass::Option
1073 {
1074 anyhow::bail!(
1075 "Cannot cache futures spread: non-option instruments provided call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
1076 );
1077 }
1078
1079 if call_instrument.option_kind() != Some(OptionKind::Call)
1080 || put_instrument.option_kind() != Some(OptionKind::Put)
1081 {
1082 anyhow::bail!(
1083 "Cannot cache futures spread: expected call/put pair call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
1084 );
1085 }
1086
1087 let Some(call_underlying) = call_instrument.underlying() else {
1088 anyhow::bail!(
1089 "Cannot cache futures spread: missing call underlying for {call_instrument_id}"
1090 );
1091 };
1092 let Some(put_underlying) = put_instrument.underlying() else {
1093 anyhow::bail!(
1094 "Cannot cache futures spread: missing put underlying for {put_instrument_id}"
1095 );
1096 };
1097
1098 if call_underlying != put_underlying {
1099 anyhow::bail!(
1100 "Cannot cache futures spread: option underlyings differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
1101 );
1102 }
1103
1104 if call_instrument.strike_price() != put_instrument.strike_price() {
1105 anyhow::bail!(
1106 "Cannot cache futures spread: strike prices differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
1107 );
1108 }
1109
1110 if call_instrument.expiration_ns() != put_instrument.expiration_ns() {
1111 anyhow::bail!(
1112 "Cannot cache futures spread: expiration dates differ call_instrument_id={call_instrument_id} put_instrument_id={put_instrument_id}"
1113 );
1114 }
1115
1116 let reference_future_price = self.get_price_object(&futures_instrument_id).ok_or_else(|| {
1117 anyhow::anyhow!(
1118 "Cannot cache futures spread: no reference futures price for {futures_instrument_id}"
1119 )
1120 })?;
1121 let call_price = self.get_price(&call_instrument_id).ok_or_else(|| {
1122 anyhow::anyhow!(
1123 "Cannot cache futures spread: missing option price for {call_instrument_id}"
1124 )
1125 })?;
1126 let put_price = self.get_price(&put_instrument_id).ok_or_else(|| {
1127 anyhow::anyhow!(
1128 "Cannot cache futures spread: missing option price for {put_instrument_id}"
1129 )
1130 })?;
1131
1132 let underlying_instrument_id =
1133 InstrumentId::from(format!("{call_underlying}.{}", call_instrument_id.venue));
1134
1135 {
1137 let cache = self.cache.borrow();
1138 if let Some(underlying) = cache.instrument(&underlying_instrument_id)
1139 && underlying.instrument_class() != InstrumentClass::Future
1140 {
1141 anyhow::bail!(
1142 "Cannot cache futures spread: underlying {underlying_instrument_id} is not a futures contract"
1143 );
1144 }
1145 }
1146
1147 let implied_future_price =
1148 self.calculate_implied_future_price(&call_instrument, call_price, put_price);
1149 let spread = implied_future_price - reference_future_price.as_f64();
1150 let spread_price = reference_future_instrument.make_price(spread);
1151
1152 self.cached_futures_spreads.borrow_mut().insert(
1153 underlying_instrument_id,
1154 (futures_instrument_id, spread_price),
1155 );
1156
1157 Ok(reference_future_price + spread_price)
1158 }
1159
1160 fn calculate_implied_future_price(
1161 &self,
1162 call_instrument: &InstrumentAny,
1163 call_price: f64,
1164 put_price: f64,
1165 ) -> f64 {
1166 let expiry_utc = call_instrument
1167 .expiration_ns()
1168 .map(|ns| ns.to_datetime_utc())
1169 .unwrap_or_default();
1170 let now = self.clock.borrow().timestamp_ns().to_datetime_utc();
1171 let expiry_in_days = (now.duration_until(expiry_utc).as_hours() / 24).max(1) as i32;
1172 let expiry_in_years = expiry_in_days as f64 / 365.25;
1173 let currency = call_instrument.quote_currency().code.to_string();
1174 let interest_rate = self
1175 .cache
1176 .borrow()
1177 .yield_curve(¤cy)
1178 .map_or(0.0425, |yield_curve| yield_curve(expiry_in_years));
1179 let strike = call_instrument.strike_price().unwrap_or_default().as_f64();
1180
1181 strike + (interest_rate * expiry_in_years).exp() * (call_price - put_price)
1182 }
1183
1184 #[must_use]
1186 pub fn get_cached_futures_spread_price(
1187 &self,
1188 underlying_instrument_id: InstrumentId,
1189 ) -> Option<Price> {
1190 let (futures_instrument_id, spread) = self
1191 .cached_futures_spreads
1192 .borrow()
1193 .get(&underlying_instrument_id)
1194 .copied()?;
1195 let reference_future_price = self.get_price_object(&futures_instrument_id)?;
1196
1197 Some(reference_future_price + spread)
1198 }
1199
1200 fn get_price_object(&self, instrument_id: &InstrumentId) -> Option<Price> {
1201 let cache = self.cache.borrow();
1202 let price = cache
1203 .price(instrument_id, PriceType::Mid)
1204 .or_else(|| cache.price(instrument_id, PriceType::Last));
1205
1206 if let Some(instrument) = cache.instrument(instrument_id)
1210 && instrument.asset_class() == AssetClass::Index
1211 {
1212 if instrument.instrument_class() == InstrumentClass::Future && price.is_some() {
1213 return price;
1214 }
1215
1216 if let Some(index_price) = cache.index_price(instrument_id) {
1217 return Some(index_price.value);
1218 }
1219 }
1220
1221 price
1222 }
1223
1224 fn get_price(&self, instrument_id: &InstrumentId) -> Option<f64> {
1225 self.get_price_object(instrument_id)
1226 .map(|price| price.as_f64())
1227 }
1228
1229 pub fn subscribe_greeks<F>(&self, underlying: &str, handler: Option<F>)
1233 where
1234 F: Fn(&GreeksData) + 'static,
1235 {
1236 let pattern = format!("data.GreeksData.instrument_id={underlying}*").into();
1237
1238 if let Some(custom_handler) = handler {
1239 let typed_handler = TypedHandler::from(custom_handler);
1240 msgbus::subscribe_greeks(pattern, typed_handler, None);
1241 } else {
1242 let cache_ref = self.cache.clone();
1243 let typed_handler = TypedHandler::from(move |greeks: &GreeksData| {
1244 let mut cache = cache_ref.borrow_mut();
1245 cache.add_greeks(greeks.clone()).unwrap_or_default();
1246 });
1247 msgbus::subscribe_greeks(pattern, typed_handler, None);
1248 }
1249 }
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use std::{cell::RefCell, collections::HashMap, rc::Rc};
1255
1256 use jiff::{Timestamp, civil::Date, tz::Offset};
1257 use nautilus_model::{
1258 data::{IndexPriceUpdate, QuoteTick},
1259 enums::{AssetClass, OmsType, OptionKind, OrderSide, PositionSide},
1260 events::order::spec::OrderFilledSpec,
1261 identifiers::{
1262 ClientOrderId, InstrumentId, PositionId, StrategyId, Symbol, TradeId, Venue,
1263 },
1264 instruments::{Equity, FuturesContract, OptionContract, any::InstrumentAny},
1265 types::{Currency, Price, Quantity},
1266 };
1267 use rstest::rstest;
1268 use ustr::Ustr;
1269
1270 use super::*;
1271 use crate::{cache::Cache, clock::TestClock};
1272
1273 fn utc_timestamp(year: i16, month: i8, day: i8, hour: i8, minute: i8, second: i8) -> Timestamp {
1274 Offset::UTC
1275 .to_timestamp(
1276 Date::new(year, month, day)
1277 .unwrap()
1278 .at(hour, minute, second, 0),
1279 )
1280 .unwrap()
1281 }
1282
1283 fn create_test_calculator() -> GreeksCalculator {
1284 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1285 let clock = Rc::new(RefCell::new(TestClock::new()));
1286 GreeksCalculator::new(cache, clock)
1287 }
1288
1289 #[rstest]
1290 fn test_greeks_calculator_creation() {
1291 let calculator = create_test_calculator();
1292 assert!(format!("{calculator:?}").contains("GreeksCalculator"));
1294 }
1295
1296 #[rstest]
1297 fn test_greeks_calculator_debug() {
1298 let calculator = create_test_calculator();
1299 let debug_str = format!("{calculator:?}");
1301 assert!(debug_str.contains("GreeksCalculator"));
1302 }
1303
1304 #[rstest]
1305 fn test_greeks_calculator_has_python_bindings() {
1306 let calculator = create_test_calculator();
1309 assert!(format!("{calculator:?}").contains("GreeksCalculator"));
1312 }
1313
1314 #[rstest]
1315 fn test_instrument_greeks_params_builder_default() {
1316 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1317
1318 let params = InstrumentGreeksParams::builder()
1319 .instrument_id(instrument_id)
1320 .build();
1321
1322 assert_eq!(params.instrument_id, instrument_id);
1323 assert_eq!(params.flat_interest_rate, 0.0425);
1324 assert_eq!(params.flat_dividend_yield, None);
1325 assert_eq!(params.spot_shock, 0.0);
1326 assert_eq!(params.vol_shock, 0.0);
1327 assert_eq!(params.time_to_expiry_shock, 0.0);
1328 assert!(!params.use_cached_greeks);
1329 assert!(!params.cache_greeks);
1330 assert!(!params.publish_greeks);
1331 assert_eq!(params.ts_event, None);
1332 assert_eq!(params.position, None);
1333 assert!(!params.percent_greeks);
1334 assert_eq!(params.index_instrument_id, None);
1335 assert_eq!(params.beta_weights, None);
1336 assert_eq!(params.vol_index_instrument_id, None);
1337 assert_eq!(params.vol_beta_weights, None);
1338 }
1339
1340 #[rstest]
1341 fn test_instrument_greeks_params_builder_custom_values() {
1342 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1343 let index_id = InstrumentId::from("SPY.NASDAQ");
1344 let vol_index_id = InstrumentId::from("VIX.XCBF");
1345 let mut beta_weights = HashMap::new();
1346 beta_weights.insert(instrument_id, 1.2);
1347 let mut vol_beta_weights = HashMap::new();
1348 vol_beta_weights.insert(instrument_id, 0.8);
1349
1350 let params = InstrumentGreeksParams::builder()
1351 .instrument_id(instrument_id)
1352 .flat_interest_rate(0.05)
1353 .flat_dividend_yield(0.02)
1354 .spot_shock(0.01)
1355 .vol_shock(0.05)
1356 .time_to_expiry_shock(0.1)
1357 .use_cached_greeks(true)
1358 .cache_greeks(true)
1359 .publish_greeks(true)
1360 .percent_greeks(true)
1361 .index_instrument_id(index_id)
1362 .beta_weights(beta_weights.clone())
1363 .vol_index_instrument_id(vol_index_id)
1364 .vol_beta_weights(vol_beta_weights.clone())
1365 .build();
1366
1367 assert_eq!(params.instrument_id, instrument_id);
1368 assert_eq!(params.flat_interest_rate, 0.05);
1369 assert_eq!(params.flat_dividend_yield, Some(0.02));
1370 assert_eq!(params.spot_shock, 0.01);
1371 assert_eq!(params.vol_shock, 0.05);
1372 assert_eq!(params.time_to_expiry_shock, 0.1);
1373 assert!(params.use_cached_greeks);
1374 assert!(params.cache_greeks);
1375 assert!(params.publish_greeks);
1376 assert!(params.percent_greeks);
1377 assert_eq!(params.index_instrument_id, Some(index_id));
1378 assert_eq!(params.beta_weights, Some(beta_weights));
1379 assert_eq!(params.vol_index_instrument_id, Some(vol_index_id));
1380 assert_eq!(params.vol_beta_weights, Some(vol_beta_weights));
1381 }
1382
1383 #[rstest]
1384 fn test_instrument_greeks_params_debug() {
1385 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1386
1387 let params = InstrumentGreeksParams::builder()
1388 .instrument_id(instrument_id)
1389 .build();
1390
1391 let debug_str = format!("{params:?}");
1392 assert!(debug_str.contains("InstrumentGreeksParams"));
1393 assert!(debug_str.contains("AAPL.NASDAQ"));
1394 }
1395
1396 #[rstest]
1397 fn test_portfolio_greeks_params_builder_default() {
1398 let params = PortfolioGreeksParams::builder().build();
1399
1400 assert_eq!(params.underlyings, None);
1401 assert_eq!(params.venue, None);
1402 assert_eq!(params.instrument_id, None);
1403 assert_eq!(params.strategy_id, None);
1404 assert_eq!(params.side, None);
1405 assert_eq!(params.flat_interest_rate, 0.0425);
1406 assert_eq!(params.flat_dividend_yield, None);
1407 assert_eq!(params.spot_shock, 0.0);
1408 assert_eq!(params.vol_shock, 0.0);
1409 assert_eq!(params.time_to_expiry_shock, 0.0);
1410 assert!(!params.use_cached_greeks);
1411 assert!(!params.cache_greeks);
1412 assert!(!params.publish_greeks);
1413 assert!(!params.percent_greeks);
1414 assert_eq!(params.index_instrument_id, None);
1415 assert_eq!(params.beta_weights, None);
1416 assert_eq!(params.vol_index_instrument_id, None);
1417 assert_eq!(params.vol_beta_weights, None);
1418 }
1419
1420 #[rstest]
1421 fn test_portfolio_greeks_params_builder_custom_values() {
1422 let venue = Venue::from("NASDAQ");
1423 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1424 let strategy_id = StrategyId::from("test-strategy");
1425 let index_id = InstrumentId::from("SPY.NASDAQ");
1426 let vol_index_id = InstrumentId::from("VIX.XCBF");
1427 let underlyings = vec!["AAPL".to_string(), "MSFT".to_string()];
1428 let mut beta_weights = HashMap::new();
1429 beta_weights.insert(instrument_id, 1.2);
1430 let mut vol_beta_weights = HashMap::new();
1431 vol_beta_weights.insert(instrument_id, 0.8);
1432
1433 let params = PortfolioGreeksParams::builder()
1434 .underlyings(underlyings.clone())
1435 .venue(venue)
1436 .instrument_id(instrument_id)
1437 .strategy_id(strategy_id)
1438 .side(PositionSide::Long)
1439 .flat_interest_rate(0.05)
1440 .flat_dividend_yield(0.02)
1441 .spot_shock(0.01)
1442 .vol_shock(0.05)
1443 .time_to_expiry_shock(0.1)
1444 .use_cached_greeks(true)
1445 .cache_greeks(true)
1446 .publish_greeks(true)
1447 .percent_greeks(true)
1448 .index_instrument_id(index_id)
1449 .beta_weights(beta_weights.clone())
1450 .vol_index_instrument_id(vol_index_id)
1451 .vol_beta_weights(vol_beta_weights.clone())
1452 .build();
1453
1454 assert_eq!(params.underlyings, Some(underlyings));
1455 assert_eq!(params.venue, Some(venue));
1456 assert_eq!(params.instrument_id, Some(instrument_id));
1457 assert_eq!(params.strategy_id, Some(strategy_id));
1458 assert_eq!(params.side, Some(PositionSide::Long));
1459 assert_eq!(params.flat_interest_rate, 0.05);
1460 assert_eq!(params.flat_dividend_yield, Some(0.02));
1461 assert_eq!(params.spot_shock, 0.01);
1462 assert_eq!(params.vol_shock, 0.05);
1463 assert_eq!(params.time_to_expiry_shock, 0.1);
1464 assert!(params.use_cached_greeks);
1465 assert!(params.cache_greeks);
1466 assert!(params.publish_greeks);
1467 assert!(params.percent_greeks);
1468 assert_eq!(params.index_instrument_id, Some(index_id));
1469 assert_eq!(params.beta_weights, Some(beta_weights));
1470 assert_eq!(params.vol_index_instrument_id, Some(vol_index_id));
1471 assert_eq!(params.vol_beta_weights, Some(vol_beta_weights));
1472 }
1473
1474 #[rstest]
1475 fn test_portfolio_greeks_params_debug() {
1476 let venue = Venue::from("NASDAQ");
1477
1478 let params = PortfolioGreeksParams::builder().venue(venue).build();
1479
1480 let debug_str = format!("{params:?}");
1481 assert!(debug_str.contains("PortfolioGreeksParams"));
1482 assert!(debug_str.contains("NASDAQ"));
1483 }
1484
1485 #[rstest]
1486 fn test_portfolio_greeks_params_builder_fluent_api() {
1487 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1488
1489 let params = PortfolioGreeksParams::builder()
1490 .instrument_id(instrument_id)
1491 .flat_interest_rate(0.05)
1492 .spot_shock(0.01)
1493 .percent_greeks(true)
1494 .build();
1495
1496 assert_eq!(params.instrument_id, Some(instrument_id));
1497 assert_eq!(params.flat_interest_rate, 0.05);
1498 assert_eq!(params.spot_shock, 0.01);
1499 assert!(params.percent_greeks);
1500 }
1501
1502 #[rstest]
1503 fn test_instrument_greeks_params_builder_fluent_chaining() {
1504 let instrument_id = InstrumentId::from("TSLA.NASDAQ");
1505
1506 let params = InstrumentGreeksParams::builder()
1508 .instrument_id(instrument_id)
1509 .flat_interest_rate(0.03)
1510 .spot_shock(0.02)
1511 .vol_shock(0.1)
1512 .use_cached_greeks(true)
1513 .percent_greeks(true)
1514 .build();
1515
1516 assert_eq!(params.instrument_id, instrument_id);
1517 assert_eq!(params.flat_interest_rate, 0.03);
1518 assert_eq!(params.spot_shock, 0.02);
1519 assert_eq!(params.vol_shock, 0.1);
1520 assert!(params.use_cached_greeks);
1521 assert!(params.percent_greeks);
1522 }
1523
1524 #[rstest]
1525 fn test_portfolio_greeks_params_builder_with_underlyings() {
1526 let underlyings = vec!["AAPL".to_string(), "MSFT".to_string(), "GOOGL".to_string()];
1527
1528 let params = PortfolioGreeksParams::builder()
1529 .underlyings(underlyings.clone())
1530 .flat_interest_rate(0.04)
1531 .build();
1532
1533 assert_eq!(params.underlyings, Some(underlyings));
1534 assert_eq!(params.flat_interest_rate, 0.04);
1535 }
1536
1537 #[rstest]
1538 fn test_builders_with_empty_beta_weights() {
1539 let instrument_id = InstrumentId::from("NVDA.NASDAQ");
1540 let empty_beta_weights = HashMap::new();
1541
1542 let instrument_params = InstrumentGreeksParams::builder()
1543 .instrument_id(instrument_id)
1544 .beta_weights(empty_beta_weights.clone())
1545 .vol_beta_weights(empty_beta_weights.clone())
1546 .build();
1547
1548 let portfolio_params = PortfolioGreeksParams::builder()
1549 .beta_weights(empty_beta_weights.clone())
1550 .vol_beta_weights(empty_beta_weights.clone())
1551 .build();
1552
1553 assert_eq!(
1554 instrument_params.beta_weights,
1555 Some(empty_beta_weights.clone())
1556 );
1557 assert_eq!(portfolio_params.beta_weights, Some(empty_beta_weights));
1558 assert_eq!(instrument_params.vol_beta_weights, Some(HashMap::new()));
1559 assert_eq!(portfolio_params.vol_beta_weights, Some(HashMap::new()));
1560 }
1561
1562 #[rstest]
1563 fn test_builders_with_all_shocks() {
1564 let instrument_id = InstrumentId::from("AMD.NASDAQ");
1565
1566 let instrument_params = InstrumentGreeksParams::builder()
1567 .instrument_id(instrument_id)
1568 .spot_shock(0.05)
1569 .vol_shock(0.1)
1570 .time_to_expiry_shock(0.01)
1571 .build();
1572
1573 let portfolio_params = PortfolioGreeksParams::builder()
1574 .spot_shock(0.05)
1575 .vol_shock(0.1)
1576 .time_to_expiry_shock(0.01)
1577 .build();
1578
1579 assert_eq!(instrument_params.spot_shock, 0.05);
1580 assert_eq!(instrument_params.vol_shock, 0.1);
1581 assert_eq!(instrument_params.time_to_expiry_shock, 0.01);
1582
1583 assert_eq!(portfolio_params.spot_shock, 0.05);
1584 assert_eq!(portfolio_params.vol_shock, 0.1);
1585 assert_eq!(portfolio_params.time_to_expiry_shock, 0.01);
1586 }
1587
1588 #[rstest]
1589 fn test_builders_with_all_boolean_flags() {
1590 let instrument_id = InstrumentId::from("META.NASDAQ");
1591
1592 let instrument_params = InstrumentGreeksParams::builder()
1593 .instrument_id(instrument_id)
1594 .use_cached_greeks(true)
1595 .cache_greeks(true)
1596 .publish_greeks(true)
1597 .percent_greeks(true)
1598 .build();
1599
1600 let portfolio_params = PortfolioGreeksParams::builder()
1601 .use_cached_greeks(true)
1602 .cache_greeks(true)
1603 .publish_greeks(true)
1604 .percent_greeks(true)
1605 .build();
1606
1607 assert!(instrument_params.use_cached_greeks);
1608 assert!(instrument_params.cache_greeks);
1609 assert!(instrument_params.publish_greeks);
1610 assert!(instrument_params.percent_greeks);
1611
1612 assert!(portfolio_params.use_cached_greeks);
1613 assert!(portfolio_params.cache_greeks);
1614 assert!(portfolio_params.publish_greeks);
1615 assert!(portfolio_params.percent_greeks);
1616 }
1617
1618 #[rstest]
1619 fn test_greeks_filter_callback_function() {
1620 fn filter_positive_delta(data: &GreeksData) -> bool {
1622 data.delta > 0.0
1623 }
1624
1625 let filter = GreeksFilterCallback::from_fn(filter_positive_delta);
1626
1627 let greeks_data = GreeksData::from_delta(
1629 InstrumentId::from("TEST.NASDAQ"),
1630 0.5,
1631 1.0,
1632 UnixNanos::default(),
1633 );
1634
1635 assert!(filter.call(&greeks_data));
1636
1637 let debug_str = format!("{filter:?}");
1639 assert!(debug_str.contains("GreeksFilterCallback::Function"));
1640 }
1641
1642 #[rstest]
1643 fn test_greeks_filter_callback_closure() {
1644 let min_delta = 0.3;
1646 let filter =
1647 GreeksFilterCallback::from_closure(move |data: &GreeksData| data.delta > min_delta);
1648
1649 let greeks_data = GreeksData::from_delta(
1651 InstrumentId::from("TEST.NASDAQ"),
1652 0.5,
1653 1.0,
1654 UnixNanos::default(),
1655 );
1656
1657 assert!(filter.call(&greeks_data));
1658
1659 let debug_str = format!("{filter:?}");
1661 assert!(debug_str.contains("GreeksFilterCallback::Closure"));
1662 }
1663
1664 #[rstest]
1665 fn test_greeks_filter_callback_clone() {
1666 fn filter_fn(data: &GreeksData) -> bool {
1667 data.delta > 0.0
1668 }
1669
1670 let filter1 = GreeksFilterCallback::from_fn(filter_fn);
1671 let filter2 = filter1.clone();
1672
1673 let greeks_data = GreeksData::from_delta(
1674 InstrumentId::from("TEST.NASDAQ"),
1675 0.5,
1676 1.0,
1677 UnixNanos::default(),
1678 );
1679
1680 assert!(filter1.call(&greeks_data));
1681 assert!(filter2.call(&greeks_data));
1682 }
1683
1684 #[rstest]
1685 fn test_portfolio_greeks_params_with_filter() {
1686 fn filter_high_delta(data: &GreeksData) -> bool {
1687 data.delta.abs() > 0.1
1688 }
1689
1690 let filter = GreeksFilterCallback::from_fn(filter_high_delta);
1691
1692 let params = PortfolioGreeksParams::builder()
1693 .greeks_filter(filter)
1694 .flat_interest_rate(0.05)
1695 .build();
1696
1697 assert!(params.greeks_filter.is_some());
1698 assert_eq!(params.flat_interest_rate, 0.05);
1699
1700 let greeks_data = GreeksData::from_delta(
1702 InstrumentId::from("TEST.NASDAQ"),
1703 0.5,
1704 1.0,
1705 UnixNanos::default(),
1706 );
1707
1708 let filter_ref = params.greeks_filter.as_ref().unwrap();
1709 assert!(filter_ref.call(&greeks_data));
1710 }
1711
1712 #[rstest]
1713 fn test_portfolio_greeks_params_with_closure_filter() {
1714 let min_gamma = 0.01;
1715 let filter =
1716 GreeksFilterCallback::from_closure(move |data: &GreeksData| data.gamma > min_gamma);
1717
1718 let params = PortfolioGreeksParams::builder()
1719 .greeks_filter(filter)
1720 .build();
1721
1722 assert!(params.greeks_filter.is_some());
1723
1724 let debug_str = format!("{params:?}");
1726 assert!(debug_str.contains("greeks_filter"));
1727 }
1728
1729 #[rstest]
1730 fn test_greeks_filter_to_greeks_filter_conversion() {
1731 fn filter_fn(data: &GreeksData) -> bool {
1732 data.delta > 0.0
1733 }
1734
1735 let callback = GreeksFilterCallback::from_fn(filter_fn);
1736 let greeks_filter = callback.to_greeks_filter();
1737
1738 let greeks_data = GreeksData::from_delta(
1739 InstrumentId::from("TEST.NASDAQ"),
1740 0.5,
1741 1.0,
1742 UnixNanos::default(),
1743 );
1744
1745 assert!(greeks_filter(&greeks_data));
1746 }
1747
1748 fn option_with_expiration(instrument_id: &str, expiration_ns: UnixNanos) -> OptionContract {
1749 let activation_ns = UnixNanos::from(utc_timestamp(2021, 9, 17, 0, 0, 0));
1750 OptionContract::builder()
1751 .instrument_id(InstrumentId::from(instrument_id))
1752 .raw_symbol(Symbol::from("AAPL211217C00150000"))
1753 .asset_class(AssetClass::Equity)
1754 .exchange(Ustr::from("GMNI"))
1755 .underlying(Ustr::from("AAPL"))
1756 .option_kind(OptionKind::Call)
1757 .strike_price(Price::from("149.0"))
1758 .currency(Currency::from("USD"))
1759 .activation_ns(activation_ns)
1760 .expiration_ns(expiration_ns)
1761 .price_precision(2)
1762 .price_increment(Price::from("0.01"))
1763 .multiplier(Quantity::from(100))
1764 .lot_size(Quantity::from(1))
1765 .ts_event(UnixNanos::default())
1766 .ts_init(UnixNanos::default())
1767 .build()
1768 .unwrap()
1769 }
1770
1771 fn equity_aapl_opra() -> Equity {
1772 Equity::builder()
1773 .instrument_id(InstrumentId::from("AAPL.OPRA"))
1774 .raw_symbol(Symbol::from("AAPL"))
1775 .isin(Ustr::from("US0378331005"))
1776 .currency(Currency::from("USD"))
1777 .price_precision(2)
1778 .price_increment(Price::from("0.01"))
1779 .ts_event(UnixNanos::default())
1780 .ts_init(UnixNanos::default())
1781 .build()
1782 .unwrap()
1783 }
1784
1785 #[rstest]
1786 fn test_resolve_underlying_instrument_id_errors_without_underlying() {
1787 let instrument = InstrumentAny::Equity(equity_aapl_opra());
1788 let error = GreeksCalculator::resolve_underlying_instrument_id(
1789 &instrument,
1790 InstrumentId::from("AAPL.OPRA"),
1791 )
1792 .unwrap_err();
1793
1794 assert_eq!(
1795 error.to_string(),
1796 "Instrument AAPL.OPRA has no underlying identifier"
1797 );
1798 }
1799
1800 fn future_with_expiration(
1801 instrument_id: &str,
1802 underlying: &str,
1803 expiration_ns: UnixNanos,
1804 ) -> FuturesContract {
1805 FuturesContract::builder()
1806 .instrument_id(InstrumentId::from(instrument_id))
1807 .raw_symbol(Symbol::from(underlying))
1808 .asset_class(AssetClass::Index)
1809 .exchange(Ustr::from("XCME"))
1810 .underlying(Ustr::from(underlying))
1811 .activation_ns(UnixNanos::default())
1812 .expiration_ns(expiration_ns)
1813 .currency(Currency::from("USD"))
1814 .price_precision(2)
1815 .price_increment(Price::from("0.25"))
1816 .multiplier(Quantity::from(1))
1817 .lot_size(Quantity::from(1))
1818 .ts_event(UnixNanos::default())
1819 .ts_init(UnixNanos::default())
1820 .build()
1821 .unwrap()
1822 }
1823
1824 fn future_option_with_expiration(
1825 instrument_id: &str,
1826 raw_symbol: &str,
1827 underlying: &str,
1828 option_kind: OptionKind,
1829 strike: &str,
1830 expiration_ns: UnixNanos,
1831 ) -> OptionContract {
1832 OptionContract::builder()
1833 .instrument_id(InstrumentId::from(instrument_id))
1834 .raw_symbol(Symbol::from(raw_symbol))
1835 .asset_class(AssetClass::Index)
1836 .exchange(Ustr::from("XCME"))
1837 .underlying(Ustr::from(underlying))
1838 .option_kind(option_kind)
1839 .strike_price(Price::from(strike))
1840 .currency(Currency::from("USD"))
1841 .activation_ns(UnixNanos::default())
1842 .expiration_ns(expiration_ns)
1843 .price_precision(2)
1844 .price_increment(Price::from("0.01"))
1845 .multiplier(Quantity::from(1))
1846 .lot_size(Quantity::from(1))
1847 .ts_event(UnixNanos::default())
1848 .ts_init(UnixNanos::default())
1849 .build()
1850 .unwrap()
1851 }
1852
1853 fn setup_cache_with_option_and_quotes(
1854 option: OptionContract,
1855 underlying_id: InstrumentId,
1856 now_ns: UnixNanos,
1857 ) -> Rc<RefCell<Cache>> {
1858 let option_id = option.id();
1859 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
1860 cache
1861 .borrow_mut()
1862 .add_instrument(InstrumentAny::OptionContract(option))
1863 .unwrap();
1864 cache
1865 .borrow_mut()
1866 .add_instrument(InstrumentAny::Equity(equity_aapl_opra()))
1867 .unwrap();
1868 let option_quote = QuoteTick::new(
1869 option_id,
1870 Price::from("10.50"),
1871 Price::from("10.60"),
1872 Quantity::from(100),
1873 Quantity::from(100),
1874 now_ns,
1875 now_ns,
1876 );
1877 let underlying_quote = QuoteTick::new(
1878 underlying_id,
1879 Price::from("150.00"),
1880 Price::from("150.10"),
1881 Quantity::from(100),
1882 Quantity::from(100),
1883 now_ns,
1884 now_ns,
1885 );
1886 cache.borrow_mut().add_quote(option_quote).unwrap();
1887 cache.borrow_mut().add_quote(underlying_quote).unwrap();
1888 cache
1889 }
1890
1891 fn position_from_fill(
1892 instrument: &InstrumentAny,
1893 position_id: &str,
1894 client_order_id: &str,
1895 trade_id: &str,
1896 side: OrderSide,
1897 quantity: u64,
1898 price: &str,
1899 ) -> Position {
1900 let fill = OrderFilledSpec::builder()
1901 .instrument_id(instrument.id())
1902 .client_order_id(ClientOrderId::from(client_order_id))
1903 .trade_id(TradeId::from(trade_id))
1904 .order_side(side)
1905 .last_qty(Quantity::from(quantity))
1906 .last_px(Price::from(price))
1907 .currency(Currency::USD())
1908 .position_id(PositionId::from(position_id))
1909 .build();
1910 Position::new(instrument, fill)
1911 }
1912
1913 fn calculate_portfolio_greeks(
1914 calculator: &GreeksCalculator,
1915 side: Option<PositionSide>,
1916 ) -> anyhow::Result<PortfolioGreeks> {
1917 calculator.portfolio_greeks(
1918 None, None, None, None, side, None, None, None, None, None, None, None, None, None,
1919 None, None, None, None, None, None, None,
1920 )
1921 }
1922
1923 fn assert_portfolio_greeks_eq(actual: &PortfolioGreeks, expected: &PortfolioGreeks) {
1924 assert_eq!(actual.ts_init, expected.ts_init);
1925 assert_eq!(actual.ts_event, expected.ts_event);
1926 assert_eq!(actual.pnl, expected.pnl);
1927 assert_eq!(actual.price, expected.price);
1928 assert_eq!(actual.delta, expected.delta);
1929 assert_eq!(actual.gamma, expected.gamma);
1930 assert_eq!(actual.vega, expected.vega);
1931 assert_eq!(actual.theta, expected.theta);
1932 assert_eq!(actual.rho, expected.rho);
1933 }
1934
1935 #[rstest]
1936 fn test_portfolio_greeks_ignores_closed_position_with_missing_price() {
1937 let now = utc_timestamp(2025, 3, 8, 12, 0, 0);
1938 let expiry = now + jiff::SignedDuration::from_hours(24 * 30);
1939 let now_ns = UnixNanos::from(now);
1940 let expiry_ns = UnixNanos::from(expiry);
1941 let open_option = option_with_expiration("AAPL250417C00150000.OPRA", expiry_ns);
1942 let open_option_id = open_option.id();
1943 let underlying_id = InstrumentId::from("AAPL.OPRA");
1944 let cache = setup_cache_with_option_and_quotes(open_option.clone(), underlying_id, now_ns);
1945 let closed_future = future_with_expiration("CLOSED.GLBX", "CLOSED", expiry_ns);
1946 let closed_future_id = closed_future.id();
1947 let open_instrument = InstrumentAny::OptionContract(open_option);
1948 let closed_instrument = InstrumentAny::FuturesContract(closed_future);
1949
1950 let open_position = position_from_fill(
1951 &open_instrument,
1952 "P-OPEN",
1953 "O-OPEN",
1954 "T-OPEN",
1955 OrderSide::Buy,
1956 2,
1957 "10.50",
1958 );
1959 let mut closed_position = position_from_fill(
1960 &closed_instrument,
1961 "P-CLOSED",
1962 "O-CLOSED-OPEN",
1963 "T-CLOSED-OPEN",
1964 OrderSide::Buy,
1965 1,
1966 "100.00",
1967 );
1968 cache
1969 .borrow_mut()
1970 .add_instrument(closed_instrument)
1971 .unwrap();
1972 cache
1973 .borrow_mut()
1974 .add_position(&open_position, OmsType::Hedging)
1975 .unwrap();
1976 cache
1977 .borrow_mut()
1978 .add_position(&closed_position, OmsType::Hedging)
1979 .unwrap();
1980 let closing_fill = OrderFilledSpec::builder()
1981 .instrument_id(closed_future_id)
1982 .client_order_id(ClientOrderId::from("O-CLOSED-CLOSE"))
1983 .trade_id(TradeId::from("T-CLOSED-CLOSE"))
1984 .order_side(OrderSide::Sell)
1985 .last_qty(Quantity::from(1))
1986 .last_px(Price::from("101.00"))
1987 .currency(Currency::USD())
1988 .position_id(PositionId::from("P-CLOSED"))
1989 .build();
1990 closed_position.apply(&closing_fill);
1991 cache
1992 .borrow_mut()
1993 .update_position(&closed_position)
1994 .unwrap();
1995
1996 assert!(closed_position.is_closed());
2000 assert_eq!(
2001 cache
2002 .borrow()
2003 .positions_open(None, None, None, None, None)
2004 .len(),
2005 1
2006 );
2007
2008 let clock = Rc::new(RefCell::new(TestClock::new()));
2009 clock.borrow_mut().set_time(now_ns);
2010 let calculator = GreeksCalculator::new(cache, clock);
2011 let expected = calculator
2012 .instrument_greeks(
2013 open_option_id,
2014 None,
2015 None,
2016 None,
2017 None,
2018 None,
2019 None,
2020 None,
2021 None,
2022 None,
2023 Some(now_ns),
2024 Some(open_position.clone()),
2025 None,
2026 None,
2027 None,
2028 None,
2029 None,
2030 None,
2031 )
2032 .unwrap();
2033 let expected = PortfolioGreeks::from(open_position.signed_qty * &expected);
2034
2035 assert_ne!(expected.delta, 0.0);
2036 assert_portfolio_greeks_eq(
2037 &calculate_portfolio_greeks(&calculator, None).unwrap(),
2038 &expected,
2039 );
2040 assert_portfolio_greeks_eq(
2041 &calculate_portfolio_greeks(&calculator, Some(PositionSide::Flat)).unwrap(),
2042 &PortfolioGreeks::new(now_ns, now_ns, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
2043 );
2044 }
2045
2046 #[rstest]
2047 fn test_portfolio_greeks_preserves_open_position_aggregate_and_side_filters() {
2048 let now = utc_timestamp(2025, 3, 8, 12, 0, 0);
2049 let expiry = now + jiff::SignedDuration::from_hours(24 * 30);
2050 let now_ns = UnixNanos::from(now);
2051 let expiry_ns = UnixNanos::from(expiry);
2052 let long_option = option_with_expiration("AAPL250417C00145000.OPRA", expiry_ns);
2053 let short_option = option_with_expiration("AAPL250417C00155000.OPRA", expiry_ns);
2054 let long_instrument = InstrumentAny::OptionContract(long_option.clone());
2055 let short_instrument = InstrumentAny::OptionContract(short_option.clone());
2056 let underlying_id = InstrumentId::from("AAPL.OPRA");
2057 let cache = setup_cache_with_option_and_quotes(long_option, underlying_id, now_ns);
2058 cache
2059 .borrow_mut()
2060 .add_instrument(short_instrument.clone())
2061 .unwrap();
2062 cache
2063 .borrow_mut()
2064 .add_quote(QuoteTick::new(
2065 short_option.id(),
2066 Price::from("3.50"),
2067 Price::from("3.60"),
2068 Quantity::from(100),
2069 Quantity::from(100),
2070 now_ns,
2071 now_ns,
2072 ))
2073 .unwrap();
2074 let long_position = position_from_fill(
2075 &long_instrument,
2076 "P-LONG",
2077 "O-LONG",
2078 "T-LONG",
2079 OrderSide::Buy,
2080 3,
2081 "10.50",
2082 );
2083 let short_position = position_from_fill(
2084 &short_instrument,
2085 "P-SHORT",
2086 "O-SHORT",
2087 "T-SHORT",
2088 OrderSide::Sell,
2089 2,
2090 "3.50",
2091 );
2092 cache
2093 .borrow_mut()
2094 .add_position(&long_position, OmsType::Hedging)
2095 .unwrap();
2096 cache
2097 .borrow_mut()
2098 .add_position(&short_position, OmsType::Hedging)
2099 .unwrap();
2100
2101 let clock = Rc::new(RefCell::new(TestClock::new()));
2102 clock.borrow_mut().set_time(now_ns);
2103 let calculator = GreeksCalculator::new(cache, clock);
2104 let long_greeks = calculator
2105 .instrument_greeks(
2106 long_instrument.id(),
2107 None,
2108 None,
2109 None,
2110 None,
2111 None,
2112 None,
2113 None,
2114 None,
2115 None,
2116 Some(now_ns),
2117 Some(long_position.clone()),
2118 None,
2119 None,
2120 None,
2121 None,
2122 None,
2123 None,
2124 )
2125 .unwrap();
2126 let short_greeks = calculator
2127 .instrument_greeks(
2128 short_instrument.id(),
2129 None,
2130 None,
2131 None,
2132 None,
2133 None,
2134 None,
2135 None,
2136 None,
2137 None,
2138 Some(now_ns),
2139 Some(short_position.clone()),
2140 None,
2141 None,
2142 None,
2143 None,
2144 None,
2145 None,
2146 )
2147 .unwrap();
2148 let expected_long = PortfolioGreeks::from(long_position.signed_qty * &long_greeks);
2149 let expected_short = PortfolioGreeks::from(short_position.signed_qty * &short_greeks);
2150 let expected = expected_long + expected_short;
2151
2152 assert_ne!(expected.pnl, 0.0);
2153 assert_ne!(expected.price, 0.0);
2154 assert_ne!(expected.delta, 0.0);
2155 assert_ne!(expected.gamma, 0.0);
2156 assert_ne!(expected.vega, 0.0);
2157 assert_ne!(expected.theta, 0.0);
2158 assert_portfolio_greeks_eq(
2159 &calculate_portfolio_greeks(&calculator, None).unwrap(),
2160 &expected,
2161 );
2162 assert_portfolio_greeks_eq(
2163 &calculate_portfolio_greeks(&calculator, Some(PositionSide::Long)).unwrap(),
2164 &PortfolioGreeks::from(long_position.signed_qty * &long_greeks),
2165 );
2166 assert_portfolio_greeks_eq(
2167 &calculate_portfolio_greeks(&calculator, Some(PositionSide::Short)).unwrap(),
2168 &PortfolioGreeks::from(short_position.signed_qty * &short_greeks),
2169 );
2170 }
2171
2172 #[rstest]
2173 fn test_expiry_in_days_multi_day_unchanged() {
2174 let now = utc_timestamp(2025, 3, 8, 12, 0, 0);
2175 let expiry = now + jiff::SignedDuration::from_hours(24 * (30));
2176 let now_ns = UnixNanos::from(now);
2177 let expiry_ns = UnixNanos::from(expiry);
2178 let option = option_with_expiration("AAPL250417C00150000.OPRA", expiry_ns);
2179 let option_id = option.id();
2180 let underlying_id = InstrumentId::from("AAPL.OPRA");
2181 let cache = setup_cache_with_option_and_quotes(option, underlying_id, now_ns);
2182 let clock = Rc::new(RefCell::new(TestClock::new()));
2183 let calculator = GreeksCalculator::new(cache, clock);
2184
2185 let greeks = calculator
2186 .instrument_greeks(
2187 option_id,
2188 None,
2189 None,
2190 None,
2191 None,
2192 None,
2193 None,
2194 None,
2195 None,
2196 None,
2197 Some(now_ns),
2198 None,
2199 None,
2200 None,
2201 None,
2202 None,
2203 None,
2204 None,
2205 )
2206 .unwrap();
2207
2208 assert_eq!(greeks.expiry_in_days, 30);
2209 assert!((greeks.expiry_in_years - 30.0 / 365.25).abs() < 1e-9);
2210 }
2211
2212 #[rstest]
2213 fn test_expiry_in_days_same_day_clamped_to_one() {
2214 let now = utc_timestamp(2025, 3, 8, 12, 0, 0);
2215 let expiry_same_day = utc_timestamp(2025, 3, 8, 18, 0, 0);
2216 let now_ns = UnixNanos::from(now);
2217 let expiry_ns = UnixNanos::from(expiry_same_day);
2218 let option = option_with_expiration("AAPL250308C00150000.OPRA", expiry_ns);
2219 let option_id = option.id();
2220 let underlying_id = InstrumentId::from("AAPL.OPRA");
2221 let cache = setup_cache_with_option_and_quotes(option, underlying_id, now_ns);
2222 let clock = Rc::new(RefCell::new(TestClock::new()));
2223 let calculator = GreeksCalculator::new(cache, clock);
2224
2225 let greeks = calculator
2226 .instrument_greeks(
2227 option_id,
2228 None,
2229 None,
2230 None,
2231 None,
2232 None,
2233 None,
2234 None,
2235 None,
2236 None,
2237 Some(now_ns),
2238 None,
2239 None,
2240 None,
2241 None,
2242 None,
2243 None,
2244 None,
2245 )
2246 .unwrap();
2247
2248 assert_eq!(greeks.expiry_in_days, 1);
2249 assert!((greeks.expiry_in_years - 1.0 / 365.25).abs() < 1e-9);
2250 }
2251
2252 #[rstest]
2253 fn test_instrument_greeks_beta_weights_vega_to_vol_index() {
2254 let now = utc_timestamp(2025, 3, 8, 12, 0, 0);
2255 let expiry = now + jiff::SignedDuration::from_hours(24 * (30));
2256 let now_ns = UnixNanos::from(now);
2257 let expiry_ns = UnixNanos::from(expiry);
2258 let option = option_with_expiration("AAPL250417C00150000.OPRA", expiry_ns);
2259 let option_id = option.id();
2260 let underlying_id = InstrumentId::from("AAPL.OPRA");
2261 let vol_index_id = InstrumentId::from("VIX.XCBF");
2262 let cache = setup_cache_with_option_and_quotes(option, underlying_id, now_ns);
2263 cache
2264 .borrow_mut()
2265 .add_quote(QuoteTick::new(
2266 vol_index_id,
2267 Price::from("25.00"),
2268 Price::from("25.00"),
2269 Quantity::from(100),
2270 Quantity::from(100),
2271 now_ns,
2272 now_ns,
2273 ))
2274 .unwrap();
2275
2276 let clock = Rc::new(RefCell::new(TestClock::new()));
2277 let calculator = GreeksCalculator::new(cache, clock);
2278 let greeks = calculator
2279 .instrument_greeks(
2280 option_id,
2281 None,
2282 None,
2283 None,
2284 None,
2285 None,
2286 None,
2287 None,
2288 None,
2289 None,
2290 Some(now_ns),
2291 None,
2292 None,
2293 None,
2294 None,
2295 None,
2296 None,
2297 None,
2298 )
2299 .unwrap();
2300
2301 let mut vol_beta_weights = HashMap::new();
2302 vol_beta_weights.insert(underlying_id, 0.75);
2303 let vol_weighted_greeks = calculator
2304 .instrument_greeks(
2305 option_id,
2306 None,
2307 None,
2308 None,
2309 None,
2310 None,
2311 None,
2312 None,
2313 None,
2314 None,
2315 Some(now_ns),
2316 None,
2317 None,
2318 None,
2319 None,
2320 None,
2321 Some(vol_index_id),
2322 Some(&vol_beta_weights),
2323 )
2324 .unwrap();
2325
2326 let expected_vega = greeks.vega * 0.75 * (greeks.vol * 100.0) / 25.0;
2327 assert_eq!(
2328 (vol_weighted_greeks.delta * 1e12).round(),
2329 (greeks.delta * 1e12).round()
2330 );
2331 assert_eq!(
2332 (vol_weighted_greeks.gamma * 1e12).round(),
2333 (greeks.gamma * 1e12).round()
2334 );
2335 assert_eq!(
2336 (vol_weighted_greeks.vega * 1e12).round(),
2337 (expected_vega * 1e12).round()
2338 );
2339 }
2340
2341 #[rstest]
2342 fn test_instrument_greeks_errors_when_vol_index_price_missing() {
2343 let now = utc_timestamp(2025, 3, 8, 12, 0, 0);
2344 let expiry = now + jiff::SignedDuration::from_hours(24 * (30));
2345 let now_ns = UnixNanos::from(now);
2346 let expiry_ns = UnixNanos::from(expiry);
2347 let option = option_with_expiration("AAPL250417C00150000.OPRA", expiry_ns);
2348 let option_id = option.id();
2349 let underlying_id = InstrumentId::from("AAPL.OPRA");
2350 let vol_index_id = InstrumentId::from("VIX.XCBF");
2351 let cache = setup_cache_with_option_and_quotes(option, underlying_id, now_ns);
2352
2353 let clock = Rc::new(RefCell::new(TestClock::new()));
2354 let calculator = GreeksCalculator::new(cache, clock);
2355 let error = calculator
2356 .instrument_greeks(
2357 option_id,
2358 None,
2359 None,
2360 None,
2361 None,
2362 None,
2363 None,
2364 None,
2365 None,
2366 None,
2367 Some(now_ns),
2368 None,
2369 None,
2370 None,
2371 None,
2372 None,
2373 Some(vol_index_id),
2374 None,
2375 )
2376 .unwrap_err();
2377
2378 assert_eq!(error.to_string(), "No price available for VIX.XCBF");
2379 }
2380
2381 #[rstest]
2382 fn test_modify_greeks_errors_when_vol_index_price_missing() {
2383 let calculator = create_test_calculator();
2384 let underlying_id = InstrumentId::from("AAPL.OPRA");
2385 let vol_index_id = InstrumentId::from("VIX.XCBF");
2386
2387 let error = calculator
2388 .modify_greeks(
2389 1.0,
2390 2.0,
2391 underlying_id,
2392 150.0,
2393 150.0,
2394 false,
2395 None,
2396 None,
2397 2.0,
2398 0.30,
2399 0,
2400 None,
2401 0.0,
2402 Some(vol_index_id),
2403 None,
2404 None,
2405 None,
2406 )
2407 .unwrap_err();
2408
2409 assert_eq!(error.to_string(), "No price available for VIX.XCBF");
2410 }
2411
2412 #[rstest]
2413 fn test_modify_greeks_accepts_explicit_index_prices() {
2414 let calculator = create_test_calculator();
2415 let underlying_id = InstrumentId::from("AAPL.OPRA");
2416 let mut beta_weights = HashMap::new();
2417 beta_weights.insert(underlying_id, 0.5);
2418 let mut vol_beta_weights = HashMap::new();
2419 vol_beta_weights.insert(underlying_id, 0.75);
2420
2421 let (delta, gamma, vega) = calculator
2422 .modify_greeks(
2423 1.0,
2424 2.0,
2425 underlying_id,
2426 150.0,
2427 150.0,
2428 false,
2429 None,
2430 Some(&beta_weights),
2431 2.0,
2432 0.30,
2433 0,
2434 None,
2435 0.0,
2436 None,
2437 Some(&vol_beta_weights),
2438 Some(200.0),
2439 Some(25.0),
2440 )
2441 .unwrap();
2442
2443 assert_eq!((delta * 1e12).round(), 375_000_000_000.0);
2444 assert_eq!((gamma * 1e12).round(), 281_250_000_000.0);
2445 assert_eq!((vega * 1e12).round(), 1_800_000_000_000.0);
2446
2447 let (delta, gamma, vega) = calculator
2448 .modify_greeks(
2449 1.0,
2450 2.0,
2451 underlying_id,
2452 150.0,
2453 150.0,
2454 true,
2455 None,
2456 Some(&beta_weights),
2457 2.0,
2458 0.30,
2459 0,
2460 None,
2461 0.0,
2462 None,
2463 Some(&vol_beta_weights),
2464 Some(200.0),
2465 Some(25.0),
2466 )
2467 .unwrap();
2468
2469 assert_eq!((delta * 1e12).round(), 750_000_000_000.0);
2470 assert_eq!((gamma * 1e12).round(), 1_125_000_000_000.0);
2471 assert_eq!((vega * 1e12).round(), 4_500_000_000.0);
2472 }
2473
2474 #[rstest]
2475 fn test_instrument_greeks_errors_when_future_underlying_price_missing_without_cached_spread() {
2476 let now = utc_timestamp(2024, 2, 14, 16, 0, 0);
2477 let expiry = utc_timestamp(2024, 3, 15, 16, 0, 0);
2478 let now_ns = UnixNanos::from(now);
2479 let expiry_ns = UnixNanos::from(expiry);
2480
2481 let future = future_with_expiration("ESH4.GLBX", "ESH4", expiry_ns);
2482 let call_option = future_option_with_expiration(
2483 "ESH4C150.GLBX",
2484 "ESH4C150",
2485 "ESH4",
2486 OptionKind::Call,
2487 "150.00",
2488 expiry_ns,
2489 );
2490 let put_option = future_option_with_expiration(
2491 "ESH4P150.GLBX",
2492 "ESH4P150",
2493 "ESH4",
2494 OptionKind::Put,
2495 "150.00",
2496 expiry_ns,
2497 );
2498
2499 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
2500 cache
2501 .borrow_mut()
2502 .add_instrument(InstrumentAny::FuturesContract(future))
2503 .unwrap();
2504 cache
2505 .borrow_mut()
2506 .add_instrument(InstrumentAny::OptionContract(call_option.clone()))
2507 .unwrap();
2508 cache
2509 .borrow_mut()
2510 .add_instrument(InstrumentAny::OptionContract(put_option.clone()))
2511 .unwrap();
2512
2513 let call_quote = QuoteTick::new(
2514 call_option.id(),
2515 Price::from("8.50"),
2516 Price::from("8.50"),
2517 Quantity::from(100),
2518 Quantity::from(100),
2519 now_ns,
2520 now_ns,
2521 );
2522 let put_quote = QuoteTick::new(
2523 put_option.id(),
2524 Price::from("3.33"),
2525 Price::from("3.33"),
2526 Quantity::from(100),
2527 Quantity::from(100),
2528 now_ns,
2529 now_ns,
2530 );
2531 cache.borrow_mut().add_quote(call_quote).unwrap();
2532 cache.borrow_mut().add_quote(put_quote).unwrap();
2533
2534 let clock = Rc::new(RefCell::new(TestClock::new()));
2535 clock.borrow_mut().set_time(now_ns);
2536 let calculator = GreeksCalculator::new(cache, clock);
2537
2538 let error = calculator
2539 .instrument_greeks(
2540 call_option.id(),
2541 Some(0.0425),
2542 None,
2543 None,
2544 None,
2545 None,
2546 None,
2547 None,
2548 None,
2549 None,
2550 Some(now_ns),
2551 None,
2552 None,
2553 None,
2554 None,
2555 None,
2556 None,
2557 None,
2558 )
2559 .unwrap_err();
2560
2561 assert_eq!(error.to_string(), "No price available for ESH4.GLBX");
2562 }
2563
2564 #[rstest]
2565 fn test_cache_futures_spread_returns_price_to_reference_future() {
2566 let now = utc_timestamp(2024, 2, 14, 16, 0, 0);
2567 let expiry = utc_timestamp(2024, 3, 15, 16, 0, 0);
2568 let now_ns = UnixNanos::from(now);
2569 let expiry_ns = UnixNanos::from(expiry);
2570
2571 let future = future_with_expiration("ESH4.GLBX", "ESH4", expiry_ns);
2572 let reference_future = future_with_expiration("ESM4.GLBX", "ESM4", expiry_ns);
2573 let call_option = future_option_with_expiration(
2574 "ESH4C150.GLBX",
2575 "ESH4C150",
2576 "ESH4",
2577 OptionKind::Call,
2578 "150.00",
2579 expiry_ns,
2580 );
2581 let put_option = future_option_with_expiration(
2582 "ESH4P150.GLBX",
2583 "ESH4P150",
2584 "ESH4",
2585 OptionKind::Put,
2586 "150.00",
2587 expiry_ns,
2588 );
2589
2590 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
2591 cache
2592 .borrow_mut()
2593 .add_instrument(InstrumentAny::FuturesContract(future))
2594 .unwrap();
2595 cache
2596 .borrow_mut()
2597 .add_instrument(InstrumentAny::FuturesContract(reference_future.clone()))
2598 .unwrap();
2599 cache
2600 .borrow_mut()
2601 .add_instrument(InstrumentAny::OptionContract(call_option.clone()))
2602 .unwrap();
2603 cache
2604 .borrow_mut()
2605 .add_instrument(InstrumentAny::OptionContract(put_option.clone()))
2606 .unwrap();
2607
2608 let call_quote = QuoteTick::new(
2609 call_option.id(),
2610 Price::from("8.50"),
2611 Price::from("8.50"),
2612 Quantity::from(100),
2613 Quantity::from(100),
2614 now_ns,
2615 now_ns,
2616 );
2617 let put_quote = QuoteTick::new(
2618 put_option.id(),
2619 Price::from("3.33"),
2620 Price::from("3.33"),
2621 Quantity::from(100),
2622 Quantity::from(100),
2623 now_ns,
2624 now_ns,
2625 );
2626 let reference_future_quote = QuoteTick::new(
2627 reference_future.id(),
2628 Price::from("155.00"),
2629 Price::from("155.00"),
2630 Quantity::from(100),
2631 Quantity::from(100),
2632 now_ns,
2633 now_ns,
2634 );
2635 cache.borrow_mut().add_quote(call_quote).unwrap();
2636 cache.borrow_mut().add_quote(put_quote).unwrap();
2637 cache
2638 .borrow_mut()
2639 .add_quote(reference_future_quote)
2640 .unwrap();
2641
2642 let clock = Rc::new(RefCell::new(TestClock::new()));
2643 clock.borrow_mut().set_time(now_ns);
2644 let calculator = GreeksCalculator::new(cache, clock);
2645
2646 let cached_future_price = calculator
2647 .cache_futures_spread(call_option.id(), put_option.id(), reference_future.id())
2648 .unwrap();
2649
2650 let expected_underlying = 150.0 + (0.0425_f64 * (30.0 / 365.25)).exp() * (8.50 - 3.33);
2651 let expected_cached_underlying = reference_future.make_price(expected_underlying);
2652 assert_eq!(cached_future_price, expected_cached_underlying);
2653 assert_eq!(
2654 calculator.get_cached_futures_spread_price(InstrumentId::from("ESH4.GLBX")),
2655 Some(expected_cached_underlying)
2656 );
2657 }
2658
2659 #[rstest]
2660 fn test_instrument_greeks_uses_cached_futures_spread_when_underlying_price_missing() {
2661 let now = utc_timestamp(2024, 2, 14, 16, 0, 0);
2662 let expiry = utc_timestamp(2024, 3, 15, 16, 0, 0);
2663 let now_ns = UnixNanos::from(now);
2664 let expiry_ns = UnixNanos::from(expiry);
2665
2666 let future = future_with_expiration("ESH4.GLBX", "ESH4", expiry_ns);
2667 let reference_future = future_with_expiration("ESM4.GLBX", "ESM4", expiry_ns);
2668 let call_option = future_option_with_expiration(
2669 "ESH4C150.GLBX",
2670 "ESH4C150",
2671 "ESH4",
2672 OptionKind::Call,
2673 "150.00",
2674 expiry_ns,
2675 );
2676 let put_option = future_option_with_expiration(
2677 "ESH4P150.GLBX",
2678 "ESH4P150",
2679 "ESH4",
2680 OptionKind::Put,
2681 "150.00",
2682 expiry_ns,
2683 );
2684 let target_call_option = future_option_with_expiration(
2685 "ESH4C152.GLBX",
2686 "ESH4C152",
2687 "ESH4",
2688 OptionKind::Call,
2689 "152.00",
2690 expiry_ns,
2691 );
2692
2693 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
2694 cache
2695 .borrow_mut()
2696 .add_instrument(InstrumentAny::FuturesContract(future))
2697 .unwrap();
2698 cache
2699 .borrow_mut()
2700 .add_instrument(InstrumentAny::FuturesContract(reference_future.clone()))
2701 .unwrap();
2702 cache
2703 .borrow_mut()
2704 .add_instrument(InstrumentAny::OptionContract(call_option.clone()))
2705 .unwrap();
2706 cache
2707 .borrow_mut()
2708 .add_instrument(InstrumentAny::OptionContract(put_option.clone()))
2709 .unwrap();
2710 cache
2711 .borrow_mut()
2712 .add_instrument(InstrumentAny::OptionContract(target_call_option.clone()))
2713 .unwrap();
2714
2715 let call_quote = QuoteTick::new(
2716 call_option.id(),
2717 Price::from("8.50"),
2718 Price::from("8.50"),
2719 Quantity::from(100),
2720 Quantity::from(100),
2721 now_ns,
2722 now_ns,
2723 );
2724 let put_quote = QuoteTick::new(
2725 put_option.id(),
2726 Price::from("3.33"),
2727 Price::from("3.33"),
2728 Quantity::from(100),
2729 Quantity::from(100),
2730 now_ns,
2731 now_ns,
2732 );
2733 let target_call_quote = QuoteTick::new(
2734 target_call_option.id(),
2735 Price::from("6.75"),
2736 Price::from("6.75"),
2737 Quantity::from(100),
2738 Quantity::from(100),
2739 now_ns,
2740 now_ns,
2741 );
2742 let reference_future_quote = QuoteTick::new(
2743 reference_future.id(),
2744 Price::from("155.00"),
2745 Price::from("155.00"),
2746 Quantity::from(100),
2747 Quantity::from(100),
2748 now_ns,
2749 now_ns,
2750 );
2751 cache.borrow_mut().add_quote(call_quote).unwrap();
2752 cache.borrow_mut().add_quote(put_quote).unwrap();
2753 cache.borrow_mut().add_quote(target_call_quote).unwrap();
2754 cache
2755 .borrow_mut()
2756 .add_quote(reference_future_quote)
2757 .unwrap();
2758
2759 let clock = Rc::new(RefCell::new(TestClock::new()));
2760 clock.borrow_mut().set_time(now_ns);
2761 let calculator = GreeksCalculator::new(cache, clock);
2762 calculator
2763 .cache_futures_spread(call_option.id(), put_option.id(), reference_future.id())
2764 .unwrap();
2765
2766 let greeks = calculator
2767 .instrument_greeks(
2768 target_call_option.id(),
2769 Some(0.0425),
2770 None,
2771 None,
2772 None,
2773 None,
2774 None,
2775 None,
2776 None,
2777 None,
2778 Some(now_ns),
2779 None,
2780 None,
2781 None,
2782 None,
2783 None,
2784 None,
2785 None,
2786 )
2787 .unwrap();
2788
2789 let expected_underlying = reference_future
2790 .make_price(150.0 + (0.0425_f64 * (30.0 / 365.25)).exp() * (8.50 - 3.33))
2791 .as_f64();
2792 assert_eq!(greeks.underlying_price, expected_underlying);
2793 }
2794
2795 #[rstest]
2796 fn test_instrument_greeks_uses_index_price_for_index_underlying() {
2797 let now = utc_timestamp(2024, 2, 14, 16, 0, 0);
2798 let expiry = utc_timestamp(2024, 3, 15, 16, 0, 0);
2799 let now_ns = UnixNanos::from(now);
2800 let expiry_ns = UnixNanos::from(expiry);
2801
2802 let future = future_with_expiration("ESH4.GLBX", "ESH4", expiry_ns);
2803 let call_option = future_option_with_expiration(
2804 "ESH4C150.GLBX",
2805 "ESH4C150",
2806 "ESH4",
2807 OptionKind::Call,
2808 "150.00",
2809 expiry_ns,
2810 );
2811
2812 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
2813 cache
2814 .borrow_mut()
2815 .add_instrument(InstrumentAny::FuturesContract(future))
2816 .unwrap();
2817 cache
2818 .borrow_mut()
2819 .add_instrument(InstrumentAny::OptionContract(call_option.clone()))
2820 .unwrap();
2821
2822 let call_quote = QuoteTick::new(
2823 call_option.id(),
2824 Price::from("8.50"),
2825 Price::from("8.50"),
2826 Quantity::from(100),
2827 Quantity::from(100),
2828 now_ns,
2829 now_ns,
2830 );
2831 cache.borrow_mut().add_quote(call_quote).unwrap();
2832 cache
2833 .borrow_mut()
2834 .add_index_price(IndexPriceUpdate::new(
2835 InstrumentId::from("ESH4.GLBX"),
2836 Price::from("157.25"),
2837 now_ns,
2838 now_ns,
2839 ))
2840 .unwrap();
2841
2842 let clock = Rc::new(RefCell::new(TestClock::new()));
2843 clock.borrow_mut().set_time(now_ns);
2844 let calculator = GreeksCalculator::new(cache, clock);
2845
2846 let greeks = calculator
2847 .instrument_greeks(
2848 call_option.id(),
2849 Some(0.0425),
2850 None,
2851 None,
2852 None,
2853 None,
2854 None,
2855 None,
2856 None,
2857 None,
2858 Some(now_ns),
2859 None,
2860 None,
2861 None,
2862 None,
2863 None,
2864 None,
2865 None,
2866 )
2867 .unwrap();
2868
2869 assert_eq!(greeks.underlying_price, 157.25);
2870 }
2871
2872 #[rstest]
2873 fn test_instrument_greeks_prefers_quote_over_index_price_for_index_future() {
2874 let now = utc_timestamp(2024, 2, 14, 16, 0, 0);
2875 let expiry = utc_timestamp(2024, 3, 15, 16, 0, 0);
2876 let now_ns = UnixNanos::from(now);
2877 let expiry_ns = UnixNanos::from(expiry);
2878
2879 let future = future_with_expiration("ESH4.GLBX", "ESH4", expiry_ns);
2880 let call_option = future_option_with_expiration(
2881 "ESH4C150.GLBX",
2882 "ESH4C150",
2883 "ESH4",
2884 OptionKind::Call,
2885 "150.00",
2886 expiry_ns,
2887 );
2888
2889 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
2890 cache
2891 .borrow_mut()
2892 .add_instrument(InstrumentAny::FuturesContract(future))
2893 .unwrap();
2894 cache
2895 .borrow_mut()
2896 .add_instrument(InstrumentAny::OptionContract(call_option.clone()))
2897 .unwrap();
2898
2899 let future_quote = QuoteTick::new(
2901 InstrumentId::from("ESH4.GLBX"),
2902 Price::from("158.50"),
2903 Price::from("159.50"),
2904 Quantity::from(100),
2905 Quantity::from(100),
2906 now_ns,
2907 now_ns,
2908 );
2909 cache.borrow_mut().add_quote(future_quote).unwrap();
2910 cache
2911 .borrow_mut()
2912 .add_index_price(IndexPriceUpdate::new(
2913 InstrumentId::from("ESH4.GLBX"),
2914 Price::from("157.25"),
2915 now_ns,
2916 now_ns,
2917 ))
2918 .unwrap();
2919
2920 let call_quote = QuoteTick::new(
2921 call_option.id(),
2922 Price::from("8.50"),
2923 Price::from("8.50"),
2924 Quantity::from(100),
2925 Quantity::from(100),
2926 now_ns,
2927 now_ns,
2928 );
2929 cache.borrow_mut().add_quote(call_quote).unwrap();
2930
2931 let clock = Rc::new(RefCell::new(TestClock::new()));
2932 clock.borrow_mut().set_time(now_ns);
2933 let calculator = GreeksCalculator::new(cache, clock);
2934
2935 let greeks = calculator
2936 .instrument_greeks(
2937 call_option.id(),
2938 Some(0.0425),
2939 None,
2940 None,
2941 None,
2942 None,
2943 None,
2944 None,
2945 None,
2946 None,
2947 Some(now_ns),
2948 None,
2949 None,
2950 None,
2951 None,
2952 None,
2953 None,
2954 None,
2955 )
2956 .unwrap();
2957
2958 assert_eq!(greeks.underlying_price, 159.0);
2960 }
2961}