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