1use std::{cell::RefCell, collections::HashMap, fmt::Debug, rc::Rc};
19
20use anyhow;
21use derive_builder::Builder;
22use nautilus_core::UnixNanos;
23use nautilus_model::{
24 data::greeks::{GreeksData, PortfolioGreeks, black_scholes_greeks, imply_vol_and_greeks},
25 enums::{InstrumentClass, OptionKind, PositionSide, PriceType},
26 identifiers::{InstrumentId, StrategyId, Venue},
27 instruments::Instrument,
28 position::Position,
29};
30
31use crate::{cache::Cache, clock::Clock, msgbus};
32
33pub type GreeksFilter = Box<dyn Fn(&GreeksData) -> bool>;
35
36#[derive(Clone)]
38pub enum GreeksFilterCallback {
39 Function(fn(&GreeksData) -> bool),
41 Closure(std::rc::Rc<dyn Fn(&GreeksData) -> bool>),
43}
44
45impl GreeksFilterCallback {
46 pub fn from_fn(f: fn(&GreeksData) -> bool) -> Self {
48 Self::Function(f)
49 }
50
51 pub fn from_closure<F>(f: F) -> Self
53 where
54 F: Fn(&GreeksData) -> bool + 'static,
55 {
56 Self::Closure(std::rc::Rc::new(f))
57 }
58
59 pub fn call(&self, data: &GreeksData) -> bool {
61 match self {
62 Self::Function(f) => f(data),
63 Self::Closure(f) => f(data),
64 }
65 }
66
67 pub fn to_greeks_filter(self) -> GreeksFilter {
69 match self {
70 Self::Function(f) => Box::new(f),
71 Self::Closure(f) => {
72 let f_clone = f.clone();
73 Box::new(move |data| f_clone(data))
74 }
75 }
76 }
77}
78
79impl Debug for GreeksFilterCallback {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 Self::Function(_) => f.write_str("GreeksFilterCallback::Function"),
83 Self::Closure(_) => f.write_str("GreeksFilterCallback::Closure"),
84 }
85 }
86}
87
88#[derive(Debug, Builder)]
90#[builder(setter(into), derive(Debug))]
91pub struct InstrumentGreeksParams {
92 pub instrument_id: InstrumentId,
94 #[builder(default = "0.0425")]
96 pub flat_interest_rate: f64,
97 #[builder(default)]
99 pub flat_dividend_yield: Option<f64>,
100 #[builder(default = "0.0")]
102 pub spot_shock: f64,
103 #[builder(default = "0.0")]
105 pub vol_shock: f64,
106 #[builder(default = "0.0")]
108 pub time_to_expiry_shock: f64,
109 #[builder(default = "false")]
111 pub use_cached_greeks: bool,
112 #[builder(default = "false")]
114 pub cache_greeks: bool,
115 #[builder(default = "false")]
117 pub publish_greeks: bool,
118 #[builder(default)]
120 pub ts_event: Option<UnixNanos>,
121 #[builder(default)]
123 pub position: Option<Position>,
124 #[builder(default = "false")]
126 pub percent_greeks: bool,
127 #[builder(default)]
129 pub index_instrument_id: Option<InstrumentId>,
130 #[builder(default)]
132 pub beta_weights: Option<HashMap<InstrumentId, f64>>,
133 #[builder(default)]
135 pub vega_time_weight_base: Option<i32>,
136}
137
138impl InstrumentGreeksParams {
139 pub fn calculate(&self, calculator: &GreeksCalculator) -> anyhow::Result<GreeksData> {
145 calculator.instrument_greeks(
146 self.instrument_id,
147 Some(self.flat_interest_rate),
148 self.flat_dividend_yield,
149 Some(self.spot_shock),
150 Some(self.vol_shock),
151 Some(self.time_to_expiry_shock),
152 Some(self.use_cached_greeks),
153 Some(self.cache_greeks),
154 Some(self.publish_greeks),
155 self.ts_event,
156 self.position.clone(),
157 Some(self.percent_greeks),
158 self.index_instrument_id,
159 self.beta_weights.clone(),
160 self.vega_time_weight_base,
161 )
162 }
163}
164
165#[derive(Builder)]
167#[builder(setter(into))]
168pub struct PortfolioGreeksParams {
169 #[builder(default)]
171 pub underlyings: Option<Vec<String>>,
172 #[builder(default)]
174 pub venue: Option<Venue>,
175 #[builder(default)]
177 pub instrument_id: Option<InstrumentId>,
178 #[builder(default)]
180 pub strategy_id: Option<StrategyId>,
181 #[builder(default)]
183 pub side: Option<PositionSide>,
184 #[builder(default = "0.0425")]
186 pub flat_interest_rate: f64,
187 #[builder(default)]
189 pub flat_dividend_yield: Option<f64>,
190 #[builder(default = "0.0")]
192 pub spot_shock: f64,
193 #[builder(default = "0.0")]
195 pub vol_shock: f64,
196 #[builder(default = "0.0")]
198 pub time_to_expiry_shock: f64,
199 #[builder(default = "false")]
201 pub use_cached_greeks: bool,
202 #[builder(default = "false")]
204 pub cache_greeks: bool,
205 #[builder(default = "false")]
207 pub publish_greeks: bool,
208 #[builder(default = "false")]
210 pub percent_greeks: bool,
211 #[builder(default)]
213 pub index_instrument_id: Option<InstrumentId>,
214 #[builder(default)]
216 pub beta_weights: Option<HashMap<InstrumentId, f64>>,
217 #[builder(default)]
219 pub greeks_filter: Option<GreeksFilterCallback>,
220 #[builder(default)]
222 pub vega_time_weight_base: Option<i32>,
223}
224
225impl std::fmt::Debug for PortfolioGreeksParams {
226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227 f.debug_struct("PortfolioGreeksParams")
228 .field("underlyings", &self.underlyings)
229 .field("venue", &self.venue)
230 .field("instrument_id", &self.instrument_id)
231 .field("strategy_id", &self.strategy_id)
232 .field("side", &self.side)
233 .field("flat_interest_rate", &self.flat_interest_rate)
234 .field("flat_dividend_yield", &self.flat_dividend_yield)
235 .field("spot_shock", &self.spot_shock)
236 .field("vol_shock", &self.vol_shock)
237 .field("time_to_expiry_shock", &self.time_to_expiry_shock)
238 .field("use_cached_greeks", &self.use_cached_greeks)
239 .field("cache_greeks", &self.cache_greeks)
240 .field("publish_greeks", &self.publish_greeks)
241 .field("percent_greeks", &self.percent_greeks)
242 .field("index_instrument_id", &self.index_instrument_id)
243 .field("beta_weights", &self.beta_weights)
244 .field("greeks_filter", &self.greeks_filter)
245 .finish()
246 }
247}
248
249impl PortfolioGreeksParams {
250 pub fn calculate(&self, calculator: &GreeksCalculator) -> anyhow::Result<PortfolioGreeks> {
256 let greeks_filter = self
257 .greeks_filter
258 .as_ref()
259 .map(|f| f.clone().to_greeks_filter());
260
261 calculator.portfolio_greeks(
262 self.underlyings.clone(),
263 self.venue,
264 self.instrument_id,
265 self.strategy_id,
266 self.side,
267 Some(self.flat_interest_rate),
268 self.flat_dividend_yield,
269 Some(self.spot_shock),
270 Some(self.vol_shock),
271 Some(self.time_to_expiry_shock),
272 Some(self.use_cached_greeks),
273 Some(self.cache_greeks),
274 Some(self.publish_greeks),
275 Some(self.percent_greeks),
276 self.index_instrument_id,
277 self.beta_weights.clone(),
278 greeks_filter,
279 self.vega_time_weight_base,
280 )
281 }
282}
283
284#[allow(dead_code)]
299#[derive(Debug)]
300pub struct GreeksCalculator {
301 cache: Rc<RefCell<Cache>>,
302 clock: Rc<RefCell<dyn Clock>>,
303}
304
305impl GreeksCalculator {
306 pub fn new(cache: Rc<RefCell<Cache>>, clock: Rc<RefCell<dyn Clock>>) -> Self {
308 Self { cache, clock }
309 }
310
311 #[allow(clippy::too_many_arguments)]
326 pub fn instrument_greeks(
327 &self,
328 instrument_id: InstrumentId,
329 flat_interest_rate: Option<f64>,
330 flat_dividend_yield: Option<f64>,
331 spot_shock: Option<f64>,
332 vol_shock: Option<f64>,
333 time_to_expiry_shock: Option<f64>,
334 use_cached_greeks: Option<bool>,
335 cache_greeks: Option<bool>,
336 publish_greeks: Option<bool>,
337 ts_event: Option<UnixNanos>,
338 position: Option<Position>,
339 percent_greeks: Option<bool>,
340 index_instrument_id: Option<InstrumentId>,
341 beta_weights: Option<HashMap<InstrumentId, f64>>,
342 vega_time_weight_base: Option<i32>,
343 ) -> anyhow::Result<GreeksData> {
344 let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
346 let spot_shock = spot_shock.unwrap_or(0.0);
347 let vol_shock = vol_shock.unwrap_or(0.0);
348 let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
349 let use_cached_greeks = use_cached_greeks.unwrap_or(false);
350 let cache_greeks = cache_greeks.unwrap_or(false);
351 let publish_greeks = publish_greeks.unwrap_or(false);
352 let ts_event = ts_event.unwrap_or_default();
353 let percent_greeks = percent_greeks.unwrap_or(false);
354
355 let cache = self.cache.borrow();
356 let instrument = cache.instrument(&instrument_id);
357 let instrument = match instrument {
358 Some(instrument) => instrument,
359 None => anyhow::bail!(format!(
360 "Instrument definition for {instrument_id} not found."
361 )),
362 };
363
364 if instrument.instrument_class() != InstrumentClass::Option {
365 let multiplier = instrument.multiplier();
366 let underlying_instrument_id = instrument.id();
367 let underlying_price = cache
368 .price(&underlying_instrument_id, PriceType::Last)
369 .unwrap_or_default()
370 .as_f64();
371 let (delta, _, _) = self.modify_greeks(
372 multiplier.as_f64(),
373 0.0,
374 underlying_instrument_id,
375 underlying_price + spot_shock,
376 underlying_price,
377 percent_greeks,
378 index_instrument_id,
379 beta_weights.as_ref(),
380 0.0,
381 0.0,
382 0,
383 None,
384 );
385 let mut greeks_data =
386 GreeksData::from_delta(instrument_id, delta, multiplier.as_f64(), ts_event);
387
388 if let Some(pos) = position {
389 greeks_data.pnl = multiplier * ((underlying_price + spot_shock) - pos.avg_px_open);
390 greeks_data.price = greeks_data.pnl;
391 }
392
393 return Ok(greeks_data);
394 }
395
396 let mut greeks_data = None;
397 let underlying = instrument.underlying().unwrap();
398 let underlying_str = format!("{}.{}", underlying, instrument_id.venue);
399 let underlying_instrument_id = InstrumentId::from(underlying_str.as_str());
400
401 if use_cached_greeks && let Some(cached_greeks) = cache.greeks(&instrument_id) {
403 greeks_data = Some(cached_greeks);
404 }
405
406 if greeks_data.is_none() {
407 let utc_now_ns = if ts_event != UnixNanos::default() {
408 ts_event
409 } else {
410 self.clock.borrow().timestamp_ns()
411 };
412
413 let utc_now = utc_now_ns.to_datetime_utc();
414 let expiry_utc = instrument
415 .expiration_ns()
416 .map(|ns| ns.to_datetime_utc())
417 .unwrap_or_default();
418 let expiry_int = expiry_utc
419 .format("%Y%m%d")
420 .to_string()
421 .parse::<i32>()
422 .unwrap_or(0);
423 let expiry_in_days = (expiry_utc - utc_now).num_days().min(1) as i32;
424 let expiry_in_years = expiry_in_days as f64 / 365.25;
425 let currency = instrument.quote_currency().code.to_string();
426 let interest_rate = match cache.yield_curve(¤cy) {
427 Some(yield_curve) => yield_curve(expiry_in_years),
428 None => flat_interest_rate,
429 };
430
431 let mut cost_of_carry = 0.0;
433
434 if let Some(dividend_curve) = cache.yield_curve(&underlying_instrument_id.to_string()) {
435 let dividend_yield = dividend_curve(expiry_in_years);
436 cost_of_carry = interest_rate - dividend_yield;
437 } else if let Some(div_yield) = flat_dividend_yield {
438 cost_of_carry = interest_rate - div_yield;
440 }
441
442 let multiplier = instrument.multiplier();
443 let is_call = instrument.option_kind().unwrap_or(OptionKind::Call) == OptionKind::Call;
444 let strike = instrument.strike_price().unwrap_or_default().as_f64();
445 let option_mid_price = cache
446 .price(&instrument_id, PriceType::Mid)
447 .unwrap_or_default()
448 .as_f64();
449 let underlying_price = cache
450 .price(&underlying_instrument_id, PriceType::Last)
451 .unwrap_or_default()
452 .as_f64();
453
454 let greeks = imply_vol_and_greeks(
455 underlying_price,
456 interest_rate,
457 cost_of_carry,
458 is_call,
459 strike,
460 expiry_in_years,
461 option_mid_price,
462 multiplier.as_f64(),
463 );
464 let (delta, gamma, vega) = self.modify_greeks(
465 greeks.delta,
466 greeks.gamma,
467 underlying_instrument_id,
468 underlying_price,
469 underlying_price,
470 percent_greeks,
471 index_instrument_id,
472 beta_weights.as_ref(),
473 greeks.vega,
474 greeks.vol,
475 expiry_in_days,
476 vega_time_weight_base,
477 );
478 greeks_data = Some(GreeksData::new(
479 utc_now_ns,
480 utc_now_ns,
481 instrument_id,
482 is_call,
483 strike,
484 expiry_int,
485 expiry_in_days,
486 expiry_in_years,
487 multiplier.as_f64(),
488 1.0,
489 underlying_price,
490 interest_rate,
491 cost_of_carry,
492 greeks.vol,
493 0.0,
494 greeks.price,
495 delta,
496 gamma,
497 vega,
498 greeks.theta,
499 (greeks.delta / multiplier.as_f64()).abs(),
500 ));
501
502 if cache_greeks {
504 let mut cache = self.cache.borrow_mut();
505 cache
506 .add_greeks(greeks_data.clone().unwrap())
507 .unwrap_or_default();
508 }
509
510 if publish_greeks {
512 let topic = format!(
513 "data.GreeksData.instrument_id={}",
514 instrument_id.symbol.as_str()
515 )
516 .into();
517 msgbus::publish(topic, &greeks_data.clone().unwrap());
518 }
519 }
520
521 let mut greeks_data = greeks_data.unwrap();
522
523 if spot_shock != 0.0 || vol_shock != 0.0 || time_to_expiry_shock != 0.0 {
524 let underlying_price = greeks_data.underlying_price;
525 let shocked_underlying_price = underlying_price + spot_shock;
526 let shocked_vol = greeks_data.vol + vol_shock;
527 let shocked_time_to_expiry = greeks_data.expiry_in_years - time_to_expiry_shock;
528 let shocked_expiry_in_days = (shocked_time_to_expiry * 365.25) as i32;
529
530 let greeks = black_scholes_greeks(
531 shocked_underlying_price,
532 greeks_data.interest_rate,
533 greeks_data.cost_of_carry,
534 shocked_vol,
535 greeks_data.is_call,
536 greeks_data.strike,
537 shocked_time_to_expiry,
538 greeks_data.multiplier,
539 );
540 let (delta, gamma, vega) = self.modify_greeks(
541 greeks.delta,
542 greeks.gamma,
543 underlying_instrument_id,
544 shocked_underlying_price,
545 underlying_price,
546 percent_greeks,
547 index_instrument_id,
548 beta_weights.as_ref(),
549 greeks.vega,
550 shocked_vol,
551 shocked_expiry_in_days,
552 vega_time_weight_base,
553 );
554 greeks_data = GreeksData::new(
555 greeks_data.ts_event,
556 greeks_data.ts_event,
557 greeks_data.instrument_id,
558 greeks_data.is_call,
559 greeks_data.strike,
560 greeks_data.expiry,
561 shocked_expiry_in_days,
562 shocked_time_to_expiry,
563 greeks_data.multiplier,
564 greeks_data.quantity,
565 shocked_underlying_price,
566 greeks_data.interest_rate,
567 greeks_data.cost_of_carry,
568 shocked_vol,
569 0.0,
570 greeks.price,
571 delta,
572 gamma,
573 vega,
574 greeks.theta,
575 (greeks.delta / greeks_data.multiplier).abs(),
576 );
577 }
578
579 if let Some(pos) = position {
580 greeks_data.pnl = greeks_data.price - greeks_data.multiplier * pos.avg_px_open;
581 }
582
583 Ok(greeks_data)
584 }
585
586 #[allow(clippy::too_many_arguments)]
605 pub fn modify_greeks(
606 &self,
607 delta_input: f64,
608 gamma_input: f64,
609 underlying_instrument_id: InstrumentId,
610 underlying_price: f64,
611 unshocked_underlying_price: f64,
612 percent_greeks: bool,
613 index_instrument_id: Option<InstrumentId>,
614 beta_weights: Option<&HashMap<InstrumentId, f64>>,
615 vega_input: f64,
616 vol: f64,
617 expiry_in_days: i32,
618 vega_time_weight_base: Option<i32>,
619 ) -> (f64, f64, f64) {
620 let mut delta = delta_input;
621 let mut gamma = gamma_input;
622 let mut vega = vega_input;
623
624 let mut index_price = None;
625
626 if let Some(index_id) = index_instrument_id {
627 let cache = self.cache.borrow();
628 index_price = Some(
629 cache
630 .price(&index_id, PriceType::Last)
631 .unwrap_or_default()
632 .as_f64(),
633 );
634
635 let mut beta = 1.0;
636 if let Some(weights) = beta_weights
637 && let Some(&weight) = weights.get(&underlying_instrument_id)
638 {
639 beta = weight;
640 }
641
642 if let Some(ref mut idx_price) = index_price {
643 if underlying_price != unshocked_underlying_price {
644 *idx_price += 1.0 / beta
645 * (*idx_price / unshocked_underlying_price)
646 * (underlying_price - unshocked_underlying_price);
647 }
648
649 let delta_multiplier = beta * underlying_price / *idx_price;
650 delta *= delta_multiplier;
651 gamma *= delta_multiplier.powi(2);
652 }
653 }
654
655 if percent_greeks {
656 if let Some(idx_price) = index_price {
657 delta *= idx_price / 100.0;
658 gamma *= (idx_price / 100.0).powi(2);
659 } else {
660 delta *= underlying_price / 100.0;
661 gamma *= (underlying_price / 100.0).powi(2);
662 }
663
664 vega *= vol / 100.0;
666 }
667
668 if let Some(time_base) = vega_time_weight_base
670 && expiry_in_days > 0
671 {
672 let time_weight = (time_base as f64 / expiry_in_days as f64).sqrt();
673 vega *= time_weight;
674 }
675
676 (delta, gamma, vega)
677 }
678
679 #[allow(clippy::too_many_arguments)]
696 pub fn portfolio_greeks(
697 &self,
698 underlyings: Option<Vec<String>>,
699 venue: Option<Venue>,
700 instrument_id: Option<InstrumentId>,
701 strategy_id: Option<StrategyId>,
702 side: Option<PositionSide>,
703 flat_interest_rate: Option<f64>,
704 flat_dividend_yield: Option<f64>,
705 spot_shock: Option<f64>,
706 vol_shock: Option<f64>,
707 time_to_expiry_shock: Option<f64>,
708 use_cached_greeks: Option<bool>,
709 cache_greeks: Option<bool>,
710 publish_greeks: Option<bool>,
711 percent_greeks: Option<bool>,
712 index_instrument_id: Option<InstrumentId>,
713 beta_weights: Option<HashMap<InstrumentId, f64>>,
714 greeks_filter: Option<GreeksFilter>,
715 vega_time_weight_base: Option<i32>,
716 ) -> anyhow::Result<PortfolioGreeks> {
717 let ts_event = self.clock.borrow().timestamp_ns();
718 let mut portfolio_greeks =
719 PortfolioGreeks::new(ts_event, ts_event, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
720
721 let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
723 let spot_shock = spot_shock.unwrap_or(0.0);
724 let vol_shock = vol_shock.unwrap_or(0.0);
725 let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
726 let use_cached_greeks = use_cached_greeks.unwrap_or(false);
727 let cache_greeks = cache_greeks.unwrap_or(false);
728 let publish_greeks = publish_greeks.unwrap_or(false);
729 let percent_greeks = percent_greeks.unwrap_or(false);
730 let side = side.unwrap_or(PositionSide::NoPositionSide);
731
732 let cache = self.cache.borrow();
733 let open_positions = cache.positions(
734 venue.as_ref(),
735 instrument_id.as_ref(),
736 strategy_id.as_ref(),
737 Some(side),
738 );
739 let open_positions: Vec<Position> = open_positions.iter().map(|&p| p.clone()).collect();
740
741 for position in open_positions {
742 let position_instrument_id = position.instrument_id;
743
744 if let Some(ref underlyings_list) = underlyings {
745 let mut skip_position = true;
746
747 for underlying in underlyings_list {
748 if position_instrument_id
749 .symbol
750 .as_str()
751 .starts_with(underlying)
752 {
753 skip_position = false;
754 break;
755 }
756 }
757
758 if skip_position {
759 continue;
760 }
761 }
762
763 let quantity = position.signed_qty;
764 let instrument_greeks = self.instrument_greeks(
765 position_instrument_id,
766 Some(flat_interest_rate),
767 flat_dividend_yield,
768 Some(spot_shock),
769 Some(vol_shock),
770 Some(time_to_expiry_shock),
771 Some(use_cached_greeks),
772 Some(cache_greeks),
773 Some(publish_greeks),
774 Some(ts_event),
775 Some(position),
776 Some(percent_greeks),
777 index_instrument_id,
778 beta_weights.clone(),
779 vega_time_weight_base,
780 )?;
781 let position_greeks = (quantity * &instrument_greeks).into();
782
783 if greeks_filter.is_none() || greeks_filter.as_ref().unwrap()(&instrument_greeks) {
785 portfolio_greeks = portfolio_greeks + position_greeks;
786 }
787 }
788
789 Ok(portfolio_greeks)
790 }
791
792 pub fn subscribe_greeks<F>(&self, underlying: &str, handler: Option<F>)
796 where
797 F: Fn(GreeksData) + 'static + Send + Sync,
798 {
799 let pattern = format!("data.GreeksData.instrument_id={underlying}*").into();
800
801 if let Some(custom_handler) = handler {
802 let handler = msgbus::handler::TypedMessageHandler::with_any(
803 move |greeks: &dyn std::any::Any| {
804 if let Some(greeks_data) = greeks.downcast_ref::<GreeksData>() {
805 custom_handler(greeks_data.clone());
806 }
807 },
808 );
809 msgbus::subscribe(
810 pattern,
811 msgbus::handler::ShareableMessageHandler(Rc::new(handler)),
812 None,
813 );
814 } else {
815 let cache_ref = self.cache.clone();
816 let default_handler = msgbus::handler::TypedMessageHandler::with_any(
817 move |greeks: &dyn std::any::Any| {
818 if let Some(greeks_data) = greeks.downcast_ref::<GreeksData>() {
819 let mut cache = cache_ref.borrow_mut();
820 cache.add_greeks(greeks_data.clone()).unwrap_or_default();
821 }
822 },
823 );
824 msgbus::subscribe(
825 pattern,
826 msgbus::handler::ShareableMessageHandler(Rc::new(default_handler)),
827 None,
828 );
829 }
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use std::{cell::RefCell, collections::HashMap, rc::Rc};
836
837 use nautilus_model::{
838 enums::PositionSide,
839 identifiers::{InstrumentId, StrategyId, Venue},
840 };
841 use rstest::rstest;
842
843 use super::*;
844 use crate::{cache::Cache, clock::TestClock};
845
846 fn create_test_calculator() -> GreeksCalculator {
847 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
848 let clock = Rc::new(RefCell::new(TestClock::new()));
849 GreeksCalculator::new(cache, clock)
850 }
851
852 #[rstest]
853 fn test_greeks_calculator_creation() {
854 let calculator = create_test_calculator();
855 assert!(format!("{calculator:?}").contains("GreeksCalculator"));
857 }
858
859 #[rstest]
860 fn test_greeks_calculator_debug() {
861 let calculator = create_test_calculator();
862 let debug_str = format!("{calculator:?}");
864 assert!(debug_str.contains("GreeksCalculator"));
865 }
866
867 #[rstest]
868 fn test_greeks_calculator_has_python_bindings() {
869 let calculator = create_test_calculator();
872 assert!(format!("{calculator:?}").contains("GreeksCalculator"));
875 }
876
877 #[rstest]
878 fn test_instrument_greeks_params_builder_default() {
879 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
880
881 let params = InstrumentGreeksParamsBuilder::default()
882 .instrument_id(instrument_id)
883 .build()
884 .expect("Failed to build InstrumentGreeksParams");
885
886 assert_eq!(params.instrument_id, instrument_id);
887 assert_eq!(params.flat_interest_rate, 0.0425);
888 assert_eq!(params.flat_dividend_yield, None);
889 assert_eq!(params.spot_shock, 0.0);
890 assert_eq!(params.vol_shock, 0.0);
891 assert_eq!(params.time_to_expiry_shock, 0.0);
892 assert!(!params.use_cached_greeks);
893 assert!(!params.cache_greeks);
894 assert!(!params.publish_greeks);
895 assert_eq!(params.ts_event, None);
896 assert_eq!(params.position, None);
897 assert!(!params.percent_greeks);
898 assert_eq!(params.index_instrument_id, None);
899 assert_eq!(params.beta_weights, None);
900 }
901
902 #[rstest]
903 fn test_instrument_greeks_params_builder_custom_values() {
904 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
905 let index_id = InstrumentId::from("SPY.NASDAQ");
906 let mut beta_weights = HashMap::new();
907 beta_weights.insert(instrument_id, 1.2);
908
909 let params = InstrumentGreeksParamsBuilder::default()
910 .instrument_id(instrument_id)
911 .flat_interest_rate(0.05)
912 .flat_dividend_yield(Some(0.02))
913 .spot_shock(0.01)
914 .vol_shock(0.05)
915 .time_to_expiry_shock(0.1)
916 .use_cached_greeks(true)
917 .cache_greeks(true)
918 .publish_greeks(true)
919 .percent_greeks(true)
920 .index_instrument_id(Some(index_id))
921 .beta_weights(Some(beta_weights.clone()))
922 .build()
923 .expect("Failed to build InstrumentGreeksParams");
924
925 assert_eq!(params.instrument_id, instrument_id);
926 assert_eq!(params.flat_interest_rate, 0.05);
927 assert_eq!(params.flat_dividend_yield, Some(0.02));
928 assert_eq!(params.spot_shock, 0.01);
929 assert_eq!(params.vol_shock, 0.05);
930 assert_eq!(params.time_to_expiry_shock, 0.1);
931 assert!(params.use_cached_greeks);
932 assert!(params.cache_greeks);
933 assert!(params.publish_greeks);
934 assert!(params.percent_greeks);
935 assert_eq!(params.index_instrument_id, Some(index_id));
936 assert_eq!(params.beta_weights, Some(beta_weights));
937 }
938
939 #[rstest]
940 fn test_instrument_greeks_params_debug() {
941 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
942
943 let params = InstrumentGreeksParamsBuilder::default()
944 .instrument_id(instrument_id)
945 .build()
946 .expect("Failed to build InstrumentGreeksParams");
947
948 let debug_str = format!("{params:?}");
949 assert!(debug_str.contains("InstrumentGreeksParams"));
950 assert!(debug_str.contains("AAPL.NASDAQ"));
951 }
952
953 #[rstest]
954 fn test_portfolio_greeks_params_builder_default() {
955 let params = PortfolioGreeksParamsBuilder::default()
956 .build()
957 .expect("Failed to build PortfolioGreeksParams");
958
959 assert_eq!(params.underlyings, None);
960 assert_eq!(params.venue, None);
961 assert_eq!(params.instrument_id, None);
962 assert_eq!(params.strategy_id, None);
963 assert_eq!(params.side, None);
964 assert_eq!(params.flat_interest_rate, 0.0425);
965 assert_eq!(params.flat_dividend_yield, None);
966 assert_eq!(params.spot_shock, 0.0);
967 assert_eq!(params.vol_shock, 0.0);
968 assert_eq!(params.time_to_expiry_shock, 0.0);
969 assert!(!params.use_cached_greeks);
970 assert!(!params.cache_greeks);
971 assert!(!params.publish_greeks);
972 assert!(!params.percent_greeks);
973 assert_eq!(params.index_instrument_id, None);
974 assert_eq!(params.beta_weights, None);
975 }
976
977 #[rstest]
978 fn test_portfolio_greeks_params_builder_custom_values() {
979 let venue = Venue::from("NASDAQ");
980 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
981 let strategy_id = StrategyId::from("test-strategy");
982 let index_id = InstrumentId::from("SPY.NASDAQ");
983 let underlyings = vec!["AAPL".to_string(), "MSFT".to_string()];
984 let mut beta_weights = HashMap::new();
985 beta_weights.insert(instrument_id, 1.2);
986
987 let params = PortfolioGreeksParamsBuilder::default()
988 .underlyings(Some(underlyings.clone()))
989 .venue(Some(venue))
990 .instrument_id(Some(instrument_id))
991 .strategy_id(Some(strategy_id))
992 .side(Some(PositionSide::Long))
993 .flat_interest_rate(0.05)
994 .flat_dividend_yield(Some(0.02))
995 .spot_shock(0.01)
996 .vol_shock(0.05)
997 .time_to_expiry_shock(0.1)
998 .use_cached_greeks(true)
999 .cache_greeks(true)
1000 .publish_greeks(true)
1001 .percent_greeks(true)
1002 .index_instrument_id(Some(index_id))
1003 .beta_weights(Some(beta_weights.clone()))
1004 .build()
1005 .expect("Failed to build PortfolioGreeksParams");
1006
1007 assert_eq!(params.underlyings, Some(underlyings));
1008 assert_eq!(params.venue, Some(venue));
1009 assert_eq!(params.instrument_id, Some(instrument_id));
1010 assert_eq!(params.strategy_id, Some(strategy_id));
1011 assert_eq!(params.side, Some(PositionSide::Long));
1012 assert_eq!(params.flat_interest_rate, 0.05);
1013 assert_eq!(params.flat_dividend_yield, Some(0.02));
1014 assert_eq!(params.spot_shock, 0.01);
1015 assert_eq!(params.vol_shock, 0.05);
1016 assert_eq!(params.time_to_expiry_shock, 0.1);
1017 assert!(params.use_cached_greeks);
1018 assert!(params.cache_greeks);
1019 assert!(params.publish_greeks);
1020 assert!(params.percent_greeks);
1021 assert_eq!(params.index_instrument_id, Some(index_id));
1022 assert_eq!(params.beta_weights, Some(beta_weights));
1023 }
1024
1025 #[rstest]
1026 fn test_portfolio_greeks_params_debug() {
1027 let venue = Venue::from("NASDAQ");
1028
1029 let params = PortfolioGreeksParamsBuilder::default()
1030 .venue(Some(venue))
1031 .build()
1032 .expect("Failed to build PortfolioGreeksParams");
1033
1034 let debug_str = format!("{params:?}");
1035 assert!(debug_str.contains("PortfolioGreeksParams"));
1036 assert!(debug_str.contains("NASDAQ"));
1037 }
1038
1039 #[rstest]
1040 fn test_instrument_greeks_params_builder_missing_required_field() {
1041 let result = InstrumentGreeksParamsBuilder::default().build();
1043 assert!(result.is_err());
1044 }
1045
1046 #[rstest]
1047 fn test_portfolio_greeks_params_builder_fluent_api() {
1048 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1049
1050 let params = PortfolioGreeksParamsBuilder::default()
1051 .instrument_id(Some(instrument_id))
1052 .flat_interest_rate(0.05)
1053 .spot_shock(0.01)
1054 .percent_greeks(true)
1055 .build()
1056 .expect("Failed to build PortfolioGreeksParams");
1057
1058 assert_eq!(params.instrument_id, Some(instrument_id));
1059 assert_eq!(params.flat_interest_rate, 0.05);
1060 assert_eq!(params.spot_shock, 0.01);
1061 assert!(params.percent_greeks);
1062 }
1063
1064 #[rstest]
1065 fn test_instrument_greeks_params_builder_fluent_chaining() {
1066 let instrument_id = InstrumentId::from("TSLA.NASDAQ");
1067
1068 let params = InstrumentGreeksParamsBuilder::default()
1070 .instrument_id(instrument_id)
1071 .flat_interest_rate(0.03)
1072 .spot_shock(0.02)
1073 .vol_shock(0.1)
1074 .use_cached_greeks(true)
1075 .percent_greeks(true)
1076 .build()
1077 .expect("Failed to build InstrumentGreeksParams");
1078
1079 assert_eq!(params.instrument_id, instrument_id);
1080 assert_eq!(params.flat_interest_rate, 0.03);
1081 assert_eq!(params.spot_shock, 0.02);
1082 assert_eq!(params.vol_shock, 0.1);
1083 assert!(params.use_cached_greeks);
1084 assert!(params.percent_greeks);
1085 }
1086
1087 #[rstest]
1088 fn test_portfolio_greeks_params_builder_with_underlyings() {
1089 let underlyings = vec!["AAPL".to_string(), "MSFT".to_string(), "GOOGL".to_string()];
1090
1091 let params = PortfolioGreeksParamsBuilder::default()
1092 .underlyings(Some(underlyings.clone()))
1093 .flat_interest_rate(0.04)
1094 .build()
1095 .expect("Failed to build PortfolioGreeksParams");
1096
1097 assert_eq!(params.underlyings, Some(underlyings));
1098 assert_eq!(params.flat_interest_rate, 0.04);
1099 }
1100
1101 #[rstest]
1102 fn test_builders_with_empty_beta_weights() {
1103 let instrument_id = InstrumentId::from("NVDA.NASDAQ");
1104 let empty_beta_weights = HashMap::new();
1105
1106 let instrument_params = InstrumentGreeksParamsBuilder::default()
1107 .instrument_id(instrument_id)
1108 .beta_weights(Some(empty_beta_weights.clone()))
1109 .build()
1110 .expect("Failed to build InstrumentGreeksParams");
1111
1112 let portfolio_params = PortfolioGreeksParamsBuilder::default()
1113 .beta_weights(Some(empty_beta_weights.clone()))
1114 .build()
1115 .expect("Failed to build PortfolioGreeksParams");
1116
1117 assert_eq!(
1118 instrument_params.beta_weights,
1119 Some(empty_beta_weights.clone())
1120 );
1121 assert_eq!(portfolio_params.beta_weights, Some(empty_beta_weights));
1122 }
1123
1124 #[rstest]
1125 fn test_builders_with_all_shocks() {
1126 let instrument_id = InstrumentId::from("AMD.NASDAQ");
1127
1128 let instrument_params = InstrumentGreeksParamsBuilder::default()
1129 .instrument_id(instrument_id)
1130 .spot_shock(0.05)
1131 .vol_shock(0.1)
1132 .time_to_expiry_shock(0.01)
1133 .build()
1134 .expect("Failed to build InstrumentGreeksParams");
1135
1136 let portfolio_params = PortfolioGreeksParamsBuilder::default()
1137 .spot_shock(0.05)
1138 .vol_shock(0.1)
1139 .time_to_expiry_shock(0.01)
1140 .build()
1141 .expect("Failed to build PortfolioGreeksParams");
1142
1143 assert_eq!(instrument_params.spot_shock, 0.05);
1144 assert_eq!(instrument_params.vol_shock, 0.1);
1145 assert_eq!(instrument_params.time_to_expiry_shock, 0.01);
1146
1147 assert_eq!(portfolio_params.spot_shock, 0.05);
1148 assert_eq!(portfolio_params.vol_shock, 0.1);
1149 assert_eq!(portfolio_params.time_to_expiry_shock, 0.01);
1150 }
1151
1152 #[rstest]
1153 fn test_builders_with_all_boolean_flags() {
1154 let instrument_id = InstrumentId::from("META.NASDAQ");
1155
1156 let instrument_params = InstrumentGreeksParamsBuilder::default()
1157 .instrument_id(instrument_id)
1158 .use_cached_greeks(true)
1159 .cache_greeks(true)
1160 .publish_greeks(true)
1161 .percent_greeks(true)
1162 .build()
1163 .expect("Failed to build InstrumentGreeksParams");
1164
1165 let portfolio_params = PortfolioGreeksParamsBuilder::default()
1166 .use_cached_greeks(true)
1167 .cache_greeks(true)
1168 .publish_greeks(true)
1169 .percent_greeks(true)
1170 .build()
1171 .expect("Failed to build PortfolioGreeksParams");
1172
1173 assert!(instrument_params.use_cached_greeks);
1174 assert!(instrument_params.cache_greeks);
1175 assert!(instrument_params.publish_greeks);
1176 assert!(instrument_params.percent_greeks);
1177
1178 assert!(portfolio_params.use_cached_greeks);
1179 assert!(portfolio_params.cache_greeks);
1180 assert!(portfolio_params.publish_greeks);
1181 assert!(portfolio_params.percent_greeks);
1182 }
1183
1184 #[rstest]
1185 fn test_greeks_filter_callback_function() {
1186 fn filter_positive_delta(data: &GreeksData) -> bool {
1188 data.delta > 0.0
1189 }
1190
1191 let filter = GreeksFilterCallback::from_fn(filter_positive_delta);
1192
1193 let greeks_data = GreeksData::from_delta(
1195 InstrumentId::from("TEST.NASDAQ"),
1196 0.5,
1197 1.0,
1198 UnixNanos::default(),
1199 );
1200
1201 assert!(filter.call(&greeks_data));
1202
1203 let debug_str = format!("{filter:?}");
1205 assert!(debug_str.contains("GreeksFilterCallback::Function"));
1206 }
1207
1208 #[rstest]
1209 fn test_greeks_filter_callback_closure() {
1210 let min_delta = 0.3;
1212 let filter =
1213 GreeksFilterCallback::from_closure(move |data: &GreeksData| data.delta > min_delta);
1214
1215 let greeks_data = GreeksData::from_delta(
1217 InstrumentId::from("TEST.NASDAQ"),
1218 0.5,
1219 1.0,
1220 UnixNanos::default(),
1221 );
1222
1223 assert!(filter.call(&greeks_data));
1224
1225 let debug_str = format!("{filter:?}");
1227 assert!(debug_str.contains("GreeksFilterCallback::Closure"));
1228 }
1229
1230 #[rstest]
1231 fn test_greeks_filter_callback_clone() {
1232 fn filter_fn(data: &GreeksData) -> bool {
1233 data.delta > 0.0
1234 }
1235
1236 let filter1 = GreeksFilterCallback::from_fn(filter_fn);
1237 let filter2 = filter1.clone();
1238
1239 let greeks_data = GreeksData::from_delta(
1240 InstrumentId::from("TEST.NASDAQ"),
1241 0.5,
1242 1.0,
1243 UnixNanos::default(),
1244 );
1245
1246 assert!(filter1.call(&greeks_data));
1247 assert!(filter2.call(&greeks_data));
1248 }
1249
1250 #[rstest]
1251 fn test_portfolio_greeks_params_with_filter() {
1252 fn filter_high_delta(data: &GreeksData) -> bool {
1253 data.delta.abs() > 0.1
1254 }
1255
1256 let filter = GreeksFilterCallback::from_fn(filter_high_delta);
1257
1258 let params = PortfolioGreeksParamsBuilder::default()
1259 .greeks_filter(Some(filter))
1260 .flat_interest_rate(0.05)
1261 .build()
1262 .expect("Failed to build PortfolioGreeksParams");
1263
1264 assert!(params.greeks_filter.is_some());
1265 assert_eq!(params.flat_interest_rate, 0.05);
1266
1267 let greeks_data = GreeksData::from_delta(
1269 InstrumentId::from("TEST.NASDAQ"),
1270 0.5,
1271 1.0,
1272 UnixNanos::default(),
1273 );
1274
1275 let filter_ref = params.greeks_filter.as_ref().unwrap();
1276 assert!(filter_ref.call(&greeks_data));
1277 }
1278
1279 #[rstest]
1280 fn test_portfolio_greeks_params_with_closure_filter() {
1281 let min_gamma = 0.01;
1282 let filter =
1283 GreeksFilterCallback::from_closure(move |data: &GreeksData| data.gamma > min_gamma);
1284
1285 let params = PortfolioGreeksParamsBuilder::default()
1286 .greeks_filter(Some(filter))
1287 .build()
1288 .expect("Failed to build PortfolioGreeksParams");
1289
1290 assert!(params.greeks_filter.is_some());
1291
1292 let debug_str = format!("{params:?}");
1294 assert!(debug_str.contains("greeks_filter"));
1295 }
1296
1297 #[rstest]
1298 fn test_greeks_filter_to_greeks_filter_conversion() {
1299 fn filter_fn(data: &GreeksData) -> bool {
1300 data.delta > 0.0
1301 }
1302
1303 let callback = GreeksFilterCallback::from_fn(filter_fn);
1304 let greeks_filter = callback.to_greeks_filter();
1305
1306 let greeks_data = GreeksData::from_delta(
1307 InstrumentId::from("TEST.NASDAQ"),
1308 0.5,
1309 1.0,
1310 UnixNanos::default(),
1311 );
1312
1313 assert!(greeks_filter(&greeks_data));
1314 }
1315}