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, msgbus::TypedHandler};
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 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 1.0,
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 = (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);
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 self.clock.borrow().timestamp_ns()
408 } else {
409 ts_event
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 );
462 let (delta, gamma, vega) = self.modify_greeks(
463 greeks.delta,
464 greeks.gamma,
465 underlying_instrument_id,
466 underlying_price,
467 underlying_price,
468 percent_greeks,
469 index_instrument_id,
470 beta_weights.as_ref(),
471 greeks.vega,
472 greeks.vol,
473 expiry_in_days,
474 vega_time_weight_base,
475 );
476 greeks_data = Some(GreeksData::new(
477 utc_now_ns,
478 utc_now_ns,
479 instrument_id,
480 is_call,
481 strike,
482 expiry_int,
483 expiry_in_days,
484 expiry_in_years,
485 multiplier.as_f64(),
486 1.0,
487 underlying_price,
488 interest_rate,
489 cost_of_carry,
490 greeks.vol,
491 0.0,
492 greeks.price,
493 delta,
494 gamma,
495 vega,
496 greeks.theta,
497 greeks.itm_prob,
498 ));
499
500 if cache_greeks {
502 let mut cache = self.cache.borrow_mut();
503 cache
504 .add_greeks(greeks_data.clone().unwrap())
505 .unwrap_or_default();
506 }
507
508 if publish_greeks {
510 let topic = format!(
511 "data.GreeksData.instrument_id={}",
512 instrument_id.symbol.as_str()
513 )
514 .into();
515 msgbus::publish_greeks(topic, &greeks_data.clone().unwrap());
516 }
517 }
518
519 let mut greeks_data = greeks_data.unwrap();
520
521 if spot_shock != 0.0 || vol_shock != 0.0 || time_to_expiry_shock != 0.0 {
522 let underlying_price = greeks_data.underlying_price;
523 let shocked_underlying_price = underlying_price + spot_shock;
524 let shocked_vol = greeks_data.vol + vol_shock;
525 let shocked_time_to_expiry = greeks_data.expiry_in_years - time_to_expiry_shock;
526 let shocked_expiry_in_days = (shocked_time_to_expiry * 365.25) as i32;
527
528 let greeks = black_scholes_greeks(
529 shocked_underlying_price,
530 greeks_data.interest_rate,
531 greeks_data.cost_of_carry,
532 shocked_vol,
533 greeks_data.is_call,
534 greeks_data.strike,
535 shocked_time_to_expiry,
536 );
537 let (delta, gamma, vega) = self.modify_greeks(
538 greeks.delta,
539 greeks.gamma,
540 underlying_instrument_id,
541 shocked_underlying_price,
542 underlying_price,
543 percent_greeks,
544 index_instrument_id,
545 beta_weights.as_ref(),
546 greeks.vega,
547 shocked_vol,
548 shocked_expiry_in_days,
549 vega_time_weight_base,
550 );
551 greeks_data = GreeksData::new(
552 greeks_data.ts_event,
553 greeks_data.ts_event,
554 greeks_data.instrument_id,
555 greeks_data.is_call,
556 greeks_data.strike,
557 greeks_data.expiry,
558 shocked_expiry_in_days,
559 shocked_time_to_expiry,
560 greeks_data.multiplier,
561 greeks_data.quantity,
562 shocked_underlying_price,
563 greeks_data.interest_rate,
564 greeks_data.cost_of_carry,
565 shocked_vol,
566 0.0,
567 greeks.price,
568 delta,
569 gamma,
570 vega,
571 greeks.theta,
572 greeks.itm_prob,
573 );
574 }
575
576 if let Some(pos) = position {
577 greeks_data.pnl = greeks_data.price - greeks_data.multiplier * pos.avg_px_open;
578 }
579
580 Ok(greeks_data)
581 }
582
583 #[allow(clippy::too_many_arguments)]
602 pub fn modify_greeks(
603 &self,
604 delta_input: f64,
605 gamma_input: f64,
606 underlying_instrument_id: InstrumentId,
607 underlying_price: f64,
608 unshocked_underlying_price: f64,
609 percent_greeks: bool,
610 index_instrument_id: Option<InstrumentId>,
611 beta_weights: Option<&HashMap<InstrumentId, f64>>,
612 vega_input: f64,
613 vol: f64,
614 expiry_in_days: i32,
615 vega_time_weight_base: Option<i32>,
616 ) -> (f64, f64, f64) {
617 let mut delta = delta_input;
618 let mut gamma = gamma_input;
619 let mut vega = vega_input;
620
621 let mut index_price = None;
622
623 if let Some(index_id) = index_instrument_id {
624 let cache = self.cache.borrow();
625 index_price = Some(
626 cache
627 .price(&index_id, PriceType::Last)
628 .unwrap_or_default()
629 .as_f64(),
630 );
631
632 let mut beta = 1.0;
633 if let Some(weights) = beta_weights
634 && let Some(&weight) = weights.get(&underlying_instrument_id)
635 {
636 beta = weight;
637 }
638
639 if let Some(ref mut idx_price) = index_price {
640 if underlying_price != unshocked_underlying_price {
641 *idx_price += 1.0 / beta
642 * (*idx_price / unshocked_underlying_price)
643 * (underlying_price - unshocked_underlying_price);
644 }
645
646 let delta_multiplier = beta * underlying_price / *idx_price;
647 delta *= delta_multiplier;
648 gamma *= delta_multiplier.powi(2);
649 }
650 }
651
652 if percent_greeks {
653 if let Some(idx_price) = index_price {
654 delta *= idx_price / 100.0;
655 gamma *= (idx_price / 100.0).powi(2);
656 } else {
657 delta *= underlying_price / 100.0;
658 gamma *= (underlying_price / 100.0).powi(2);
659 }
660
661 vega *= vol / 100.0;
663 }
664
665 if let Some(time_base) = vega_time_weight_base
667 && expiry_in_days > 0
668 {
669 let time_weight = (time_base as f64 / expiry_in_days as f64).sqrt();
670 vega *= time_weight;
671 }
672
673 (delta, gamma, vega)
674 }
675
676 #[allow(clippy::too_many_arguments)]
693 pub fn portfolio_greeks(
694 &self,
695 underlyings: Option<Vec<String>>,
696 venue: Option<Venue>,
697 instrument_id: Option<InstrumentId>,
698 strategy_id: Option<StrategyId>,
699 side: Option<PositionSide>,
700 flat_interest_rate: Option<f64>,
701 flat_dividend_yield: Option<f64>,
702 spot_shock: Option<f64>,
703 vol_shock: Option<f64>,
704 time_to_expiry_shock: Option<f64>,
705 use_cached_greeks: Option<bool>,
706 cache_greeks: Option<bool>,
707 publish_greeks: Option<bool>,
708 percent_greeks: Option<bool>,
709 index_instrument_id: Option<InstrumentId>,
710 beta_weights: Option<HashMap<InstrumentId, f64>>,
711 greeks_filter: Option<GreeksFilter>,
712 vega_time_weight_base: Option<i32>,
713 ) -> anyhow::Result<PortfolioGreeks> {
714 let ts_event = self.clock.borrow().timestamp_ns();
715 let mut portfolio_greeks =
716 PortfolioGreeks::new(ts_event, ts_event, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
717
718 let flat_interest_rate = flat_interest_rate.unwrap_or(0.0425);
720 let spot_shock = spot_shock.unwrap_or(0.0);
721 let vol_shock = vol_shock.unwrap_or(0.0);
722 let time_to_expiry_shock = time_to_expiry_shock.unwrap_or(0.0);
723 let use_cached_greeks = use_cached_greeks.unwrap_or(false);
724 let cache_greeks = cache_greeks.unwrap_or(false);
725 let publish_greeks = publish_greeks.unwrap_or(false);
726 let percent_greeks = percent_greeks.unwrap_or(false);
727 let side = side.unwrap_or(PositionSide::NoPositionSide);
728
729 let cache = self.cache.borrow();
730 let open_positions = cache.positions(
731 venue.as_ref(),
732 instrument_id.as_ref(),
733 strategy_id.as_ref(),
734 None, Some(side),
736 );
737 let open_positions: Vec<Position> = open_positions.iter().map(|&p| p.clone()).collect();
738
739 for position in open_positions {
740 let position_instrument_id = position.instrument_id;
741
742 if let Some(ref underlyings_list) = underlyings {
743 let mut skip_position = true;
744
745 for underlying in underlyings_list {
746 if position_instrument_id
747 .symbol
748 .as_str()
749 .starts_with(underlying)
750 {
751 skip_position = false;
752 break;
753 }
754 }
755
756 if skip_position {
757 continue;
758 }
759 }
760
761 let quantity = position.signed_qty;
762 let instrument_greeks = self.instrument_greeks(
763 position_instrument_id,
764 Some(flat_interest_rate),
765 flat_dividend_yield,
766 Some(spot_shock),
767 Some(vol_shock),
768 Some(time_to_expiry_shock),
769 Some(use_cached_greeks),
770 Some(cache_greeks),
771 Some(publish_greeks),
772 Some(ts_event),
773 Some(position),
774 Some(percent_greeks),
775 index_instrument_id,
776 beta_weights.clone(),
777 vega_time_weight_base,
778 )?;
779 let position_greeks = (quantity * &instrument_greeks).into();
780
781 if greeks_filter.is_none() || greeks_filter.as_ref().unwrap()(&instrument_greeks) {
783 portfolio_greeks = portfolio_greeks + position_greeks;
784 }
785 }
786
787 Ok(portfolio_greeks)
788 }
789
790 pub fn subscribe_greeks<F>(&self, underlying: &str, handler: Option<F>)
794 where
795 F: Fn(&GreeksData) + 'static,
796 {
797 let pattern = format!("data.GreeksData.instrument_id={underlying}*").into();
798
799 if let Some(custom_handler) = handler {
800 let typed_handler = TypedHandler::from(custom_handler);
801 msgbus::subscribe_greeks(pattern, typed_handler, None);
802 } else {
803 let cache_ref = self.cache.clone();
804 let typed_handler = TypedHandler::from(move |greeks: &GreeksData| {
805 let mut cache = cache_ref.borrow_mut();
806 cache.add_greeks(greeks.clone()).unwrap_or_default();
807 });
808 msgbus::subscribe_greeks(pattern, typed_handler, None);
809 }
810 }
811}
812
813#[cfg(test)]
814mod tests {
815 use std::{cell::RefCell, collections::HashMap, rc::Rc};
816
817 use nautilus_model::{
818 enums::PositionSide,
819 identifiers::{InstrumentId, StrategyId, Venue},
820 };
821 use rstest::rstest;
822
823 use super::*;
824 use crate::{cache::Cache, clock::TestClock};
825
826 fn create_test_calculator() -> GreeksCalculator {
827 let cache = Rc::new(RefCell::new(Cache::new(None, None)));
828 let clock = Rc::new(RefCell::new(TestClock::new()));
829 GreeksCalculator::new(cache, clock)
830 }
831
832 #[rstest]
833 fn test_greeks_calculator_creation() {
834 let calculator = create_test_calculator();
835 assert!(format!("{calculator:?}").contains("GreeksCalculator"));
837 }
838
839 #[rstest]
840 fn test_greeks_calculator_debug() {
841 let calculator = create_test_calculator();
842 let debug_str = format!("{calculator:?}");
844 assert!(debug_str.contains("GreeksCalculator"));
845 }
846
847 #[rstest]
848 fn test_greeks_calculator_has_python_bindings() {
849 let calculator = create_test_calculator();
852 assert!(format!("{calculator:?}").contains("GreeksCalculator"));
855 }
856
857 #[rstest]
858 fn test_instrument_greeks_params_builder_default() {
859 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
860
861 let params = InstrumentGreeksParamsBuilder::default()
862 .instrument_id(instrument_id)
863 .build()
864 .expect("Failed to build InstrumentGreeksParams");
865
866 assert_eq!(params.instrument_id, instrument_id);
867 assert_eq!(params.flat_interest_rate, 0.0425);
868 assert_eq!(params.flat_dividend_yield, None);
869 assert_eq!(params.spot_shock, 0.0);
870 assert_eq!(params.vol_shock, 0.0);
871 assert_eq!(params.time_to_expiry_shock, 0.0);
872 assert!(!params.use_cached_greeks);
873 assert!(!params.cache_greeks);
874 assert!(!params.publish_greeks);
875 assert_eq!(params.ts_event, None);
876 assert_eq!(params.position, None);
877 assert!(!params.percent_greeks);
878 assert_eq!(params.index_instrument_id, None);
879 assert_eq!(params.beta_weights, None);
880 }
881
882 #[rstest]
883 fn test_instrument_greeks_params_builder_custom_values() {
884 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
885 let index_id = InstrumentId::from("SPY.NASDAQ");
886 let mut beta_weights = HashMap::new();
887 beta_weights.insert(instrument_id, 1.2);
888
889 let params = InstrumentGreeksParamsBuilder::default()
890 .instrument_id(instrument_id)
891 .flat_interest_rate(0.05)
892 .flat_dividend_yield(Some(0.02))
893 .spot_shock(0.01)
894 .vol_shock(0.05)
895 .time_to_expiry_shock(0.1)
896 .use_cached_greeks(true)
897 .cache_greeks(true)
898 .publish_greeks(true)
899 .percent_greeks(true)
900 .index_instrument_id(Some(index_id))
901 .beta_weights(Some(beta_weights.clone()))
902 .build()
903 .expect("Failed to build InstrumentGreeksParams");
904
905 assert_eq!(params.instrument_id, instrument_id);
906 assert_eq!(params.flat_interest_rate, 0.05);
907 assert_eq!(params.flat_dividend_yield, Some(0.02));
908 assert_eq!(params.spot_shock, 0.01);
909 assert_eq!(params.vol_shock, 0.05);
910 assert_eq!(params.time_to_expiry_shock, 0.1);
911 assert!(params.use_cached_greeks);
912 assert!(params.cache_greeks);
913 assert!(params.publish_greeks);
914 assert!(params.percent_greeks);
915 assert_eq!(params.index_instrument_id, Some(index_id));
916 assert_eq!(params.beta_weights, Some(beta_weights));
917 }
918
919 #[rstest]
920 fn test_instrument_greeks_params_debug() {
921 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
922
923 let params = InstrumentGreeksParamsBuilder::default()
924 .instrument_id(instrument_id)
925 .build()
926 .expect("Failed to build InstrumentGreeksParams");
927
928 let debug_str = format!("{params:?}");
929 assert!(debug_str.contains("InstrumentGreeksParams"));
930 assert!(debug_str.contains("AAPL.NASDAQ"));
931 }
932
933 #[rstest]
934 fn test_portfolio_greeks_params_builder_default() {
935 let params = PortfolioGreeksParamsBuilder::default()
936 .build()
937 .expect("Failed to build PortfolioGreeksParams");
938
939 assert_eq!(params.underlyings, None);
940 assert_eq!(params.venue, None);
941 assert_eq!(params.instrument_id, None);
942 assert_eq!(params.strategy_id, None);
943 assert_eq!(params.side, None);
944 assert_eq!(params.flat_interest_rate, 0.0425);
945 assert_eq!(params.flat_dividend_yield, None);
946 assert_eq!(params.spot_shock, 0.0);
947 assert_eq!(params.vol_shock, 0.0);
948 assert_eq!(params.time_to_expiry_shock, 0.0);
949 assert!(!params.use_cached_greeks);
950 assert!(!params.cache_greeks);
951 assert!(!params.publish_greeks);
952 assert!(!params.percent_greeks);
953 assert_eq!(params.index_instrument_id, None);
954 assert_eq!(params.beta_weights, None);
955 }
956
957 #[rstest]
958 fn test_portfolio_greeks_params_builder_custom_values() {
959 let venue = Venue::from("NASDAQ");
960 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
961 let strategy_id = StrategyId::from("test-strategy");
962 let index_id = InstrumentId::from("SPY.NASDAQ");
963 let underlyings = vec!["AAPL".to_string(), "MSFT".to_string()];
964 let mut beta_weights = HashMap::new();
965 beta_weights.insert(instrument_id, 1.2);
966
967 let params = PortfolioGreeksParamsBuilder::default()
968 .underlyings(Some(underlyings.clone()))
969 .venue(Some(venue))
970 .instrument_id(Some(instrument_id))
971 .strategy_id(Some(strategy_id))
972 .side(Some(PositionSide::Long))
973 .flat_interest_rate(0.05)
974 .flat_dividend_yield(Some(0.02))
975 .spot_shock(0.01)
976 .vol_shock(0.05)
977 .time_to_expiry_shock(0.1)
978 .use_cached_greeks(true)
979 .cache_greeks(true)
980 .publish_greeks(true)
981 .percent_greeks(true)
982 .index_instrument_id(Some(index_id))
983 .beta_weights(Some(beta_weights.clone()))
984 .build()
985 .expect("Failed to build PortfolioGreeksParams");
986
987 assert_eq!(params.underlyings, Some(underlyings));
988 assert_eq!(params.venue, Some(venue));
989 assert_eq!(params.instrument_id, Some(instrument_id));
990 assert_eq!(params.strategy_id, Some(strategy_id));
991 assert_eq!(params.side, Some(PositionSide::Long));
992 assert_eq!(params.flat_interest_rate, 0.05);
993 assert_eq!(params.flat_dividend_yield, Some(0.02));
994 assert_eq!(params.spot_shock, 0.01);
995 assert_eq!(params.vol_shock, 0.05);
996 assert_eq!(params.time_to_expiry_shock, 0.1);
997 assert!(params.use_cached_greeks);
998 assert!(params.cache_greeks);
999 assert!(params.publish_greeks);
1000 assert!(params.percent_greeks);
1001 assert_eq!(params.index_instrument_id, Some(index_id));
1002 assert_eq!(params.beta_weights, Some(beta_weights));
1003 }
1004
1005 #[rstest]
1006 fn test_portfolio_greeks_params_debug() {
1007 let venue = Venue::from("NASDAQ");
1008
1009 let params = PortfolioGreeksParamsBuilder::default()
1010 .venue(Some(venue))
1011 .build()
1012 .expect("Failed to build PortfolioGreeksParams");
1013
1014 let debug_str = format!("{params:?}");
1015 assert!(debug_str.contains("PortfolioGreeksParams"));
1016 assert!(debug_str.contains("NASDAQ"));
1017 }
1018
1019 #[rstest]
1020 fn test_instrument_greeks_params_builder_missing_required_field() {
1021 let result = InstrumentGreeksParamsBuilder::default().build();
1023 assert!(result.is_err());
1024 }
1025
1026 #[rstest]
1027 fn test_portfolio_greeks_params_builder_fluent_api() {
1028 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
1029
1030 let params = PortfolioGreeksParamsBuilder::default()
1031 .instrument_id(Some(instrument_id))
1032 .flat_interest_rate(0.05)
1033 .spot_shock(0.01)
1034 .percent_greeks(true)
1035 .build()
1036 .expect("Failed to build PortfolioGreeksParams");
1037
1038 assert_eq!(params.instrument_id, Some(instrument_id));
1039 assert_eq!(params.flat_interest_rate, 0.05);
1040 assert_eq!(params.spot_shock, 0.01);
1041 assert!(params.percent_greeks);
1042 }
1043
1044 #[rstest]
1045 fn test_instrument_greeks_params_builder_fluent_chaining() {
1046 let instrument_id = InstrumentId::from("TSLA.NASDAQ");
1047
1048 let params = InstrumentGreeksParamsBuilder::default()
1050 .instrument_id(instrument_id)
1051 .flat_interest_rate(0.03)
1052 .spot_shock(0.02)
1053 .vol_shock(0.1)
1054 .use_cached_greeks(true)
1055 .percent_greeks(true)
1056 .build()
1057 .expect("Failed to build InstrumentGreeksParams");
1058
1059 assert_eq!(params.instrument_id, instrument_id);
1060 assert_eq!(params.flat_interest_rate, 0.03);
1061 assert_eq!(params.spot_shock, 0.02);
1062 assert_eq!(params.vol_shock, 0.1);
1063 assert!(params.use_cached_greeks);
1064 assert!(params.percent_greeks);
1065 }
1066
1067 #[rstest]
1068 fn test_portfolio_greeks_params_builder_with_underlyings() {
1069 let underlyings = vec!["AAPL".to_string(), "MSFT".to_string(), "GOOGL".to_string()];
1070
1071 let params = PortfolioGreeksParamsBuilder::default()
1072 .underlyings(Some(underlyings.clone()))
1073 .flat_interest_rate(0.04)
1074 .build()
1075 .expect("Failed to build PortfolioGreeksParams");
1076
1077 assert_eq!(params.underlyings, Some(underlyings));
1078 assert_eq!(params.flat_interest_rate, 0.04);
1079 }
1080
1081 #[rstest]
1082 fn test_builders_with_empty_beta_weights() {
1083 let instrument_id = InstrumentId::from("NVDA.NASDAQ");
1084 let empty_beta_weights = HashMap::new();
1085
1086 let instrument_params = InstrumentGreeksParamsBuilder::default()
1087 .instrument_id(instrument_id)
1088 .beta_weights(Some(empty_beta_weights.clone()))
1089 .build()
1090 .expect("Failed to build InstrumentGreeksParams");
1091
1092 let portfolio_params = PortfolioGreeksParamsBuilder::default()
1093 .beta_weights(Some(empty_beta_weights.clone()))
1094 .build()
1095 .expect("Failed to build PortfolioGreeksParams");
1096
1097 assert_eq!(
1098 instrument_params.beta_weights,
1099 Some(empty_beta_weights.clone())
1100 );
1101 assert_eq!(portfolio_params.beta_weights, Some(empty_beta_weights));
1102 }
1103
1104 #[rstest]
1105 fn test_builders_with_all_shocks() {
1106 let instrument_id = InstrumentId::from("AMD.NASDAQ");
1107
1108 let instrument_params = InstrumentGreeksParamsBuilder::default()
1109 .instrument_id(instrument_id)
1110 .spot_shock(0.05)
1111 .vol_shock(0.1)
1112 .time_to_expiry_shock(0.01)
1113 .build()
1114 .expect("Failed to build InstrumentGreeksParams");
1115
1116 let portfolio_params = PortfolioGreeksParamsBuilder::default()
1117 .spot_shock(0.05)
1118 .vol_shock(0.1)
1119 .time_to_expiry_shock(0.01)
1120 .build()
1121 .expect("Failed to build PortfolioGreeksParams");
1122
1123 assert_eq!(instrument_params.spot_shock, 0.05);
1124 assert_eq!(instrument_params.vol_shock, 0.1);
1125 assert_eq!(instrument_params.time_to_expiry_shock, 0.01);
1126
1127 assert_eq!(portfolio_params.spot_shock, 0.05);
1128 assert_eq!(portfolio_params.vol_shock, 0.1);
1129 assert_eq!(portfolio_params.time_to_expiry_shock, 0.01);
1130 }
1131
1132 #[rstest]
1133 fn test_builders_with_all_boolean_flags() {
1134 let instrument_id = InstrumentId::from("META.NASDAQ");
1135
1136 let instrument_params = InstrumentGreeksParamsBuilder::default()
1137 .instrument_id(instrument_id)
1138 .use_cached_greeks(true)
1139 .cache_greeks(true)
1140 .publish_greeks(true)
1141 .percent_greeks(true)
1142 .build()
1143 .expect("Failed to build InstrumentGreeksParams");
1144
1145 let portfolio_params = PortfolioGreeksParamsBuilder::default()
1146 .use_cached_greeks(true)
1147 .cache_greeks(true)
1148 .publish_greeks(true)
1149 .percent_greeks(true)
1150 .build()
1151 .expect("Failed to build PortfolioGreeksParams");
1152
1153 assert!(instrument_params.use_cached_greeks);
1154 assert!(instrument_params.cache_greeks);
1155 assert!(instrument_params.publish_greeks);
1156 assert!(instrument_params.percent_greeks);
1157
1158 assert!(portfolio_params.use_cached_greeks);
1159 assert!(portfolio_params.cache_greeks);
1160 assert!(portfolio_params.publish_greeks);
1161 assert!(portfolio_params.percent_greeks);
1162 }
1163
1164 #[rstest]
1165 fn test_greeks_filter_callback_function() {
1166 fn filter_positive_delta(data: &GreeksData) -> bool {
1168 data.delta > 0.0
1169 }
1170
1171 let filter = GreeksFilterCallback::from_fn(filter_positive_delta);
1172
1173 let greeks_data = GreeksData::from_delta(
1175 InstrumentId::from("TEST.NASDAQ"),
1176 0.5,
1177 1.0,
1178 UnixNanos::default(),
1179 );
1180
1181 assert!(filter.call(&greeks_data));
1182
1183 let debug_str = format!("{filter:?}");
1185 assert!(debug_str.contains("GreeksFilterCallback::Function"));
1186 }
1187
1188 #[rstest]
1189 fn test_greeks_filter_callback_closure() {
1190 let min_delta = 0.3;
1192 let filter =
1193 GreeksFilterCallback::from_closure(move |data: &GreeksData| data.delta > min_delta);
1194
1195 let greeks_data = GreeksData::from_delta(
1197 InstrumentId::from("TEST.NASDAQ"),
1198 0.5,
1199 1.0,
1200 UnixNanos::default(),
1201 );
1202
1203 assert!(filter.call(&greeks_data));
1204
1205 let debug_str = format!("{filter:?}");
1207 assert!(debug_str.contains("GreeksFilterCallback::Closure"));
1208 }
1209
1210 #[rstest]
1211 fn test_greeks_filter_callback_clone() {
1212 fn filter_fn(data: &GreeksData) -> bool {
1213 data.delta > 0.0
1214 }
1215
1216 let filter1 = GreeksFilterCallback::from_fn(filter_fn);
1217 let filter2 = filter1.clone();
1218
1219 let greeks_data = GreeksData::from_delta(
1220 InstrumentId::from("TEST.NASDAQ"),
1221 0.5,
1222 1.0,
1223 UnixNanos::default(),
1224 );
1225
1226 assert!(filter1.call(&greeks_data));
1227 assert!(filter2.call(&greeks_data));
1228 }
1229
1230 #[rstest]
1231 fn test_portfolio_greeks_params_with_filter() {
1232 fn filter_high_delta(data: &GreeksData) -> bool {
1233 data.delta.abs() > 0.1
1234 }
1235
1236 let filter = GreeksFilterCallback::from_fn(filter_high_delta);
1237
1238 let params = PortfolioGreeksParamsBuilder::default()
1239 .greeks_filter(Some(filter))
1240 .flat_interest_rate(0.05)
1241 .build()
1242 .expect("Failed to build PortfolioGreeksParams");
1243
1244 assert!(params.greeks_filter.is_some());
1245 assert_eq!(params.flat_interest_rate, 0.05);
1246
1247 let greeks_data = GreeksData::from_delta(
1249 InstrumentId::from("TEST.NASDAQ"),
1250 0.5,
1251 1.0,
1252 UnixNanos::default(),
1253 );
1254
1255 let filter_ref = params.greeks_filter.as_ref().unwrap();
1256 assert!(filter_ref.call(&greeks_data));
1257 }
1258
1259 #[rstest]
1260 fn test_portfolio_greeks_params_with_closure_filter() {
1261 let min_gamma = 0.01;
1262 let filter =
1263 GreeksFilterCallback::from_closure(move |data: &GreeksData| data.gamma > min_gamma);
1264
1265 let params = PortfolioGreeksParamsBuilder::default()
1266 .greeks_filter(Some(filter))
1267 .build()
1268 .expect("Failed to build PortfolioGreeksParams");
1269
1270 assert!(params.greeks_filter.is_some());
1271
1272 let debug_str = format!("{params:?}");
1274 assert!(debug_str.contains("greeks_filter"));
1275 }
1276
1277 #[rstest]
1278 fn test_greeks_filter_to_greeks_filter_conversion() {
1279 fn filter_fn(data: &GreeksData) -> bool {
1280 data.delta > 0.0
1281 }
1282
1283 let callback = GreeksFilterCallback::from_fn(filter_fn);
1284 let greeks_filter = callback.to_greeks_filter();
1285
1286 let greeks_data = GreeksData::from_delta(
1287 InstrumentId::from("TEST.NASDAQ"),
1288 0.5,
1289 1.0,
1290 UnixNanos::default(),
1291 );
1292
1293 assert!(greeks_filter(&greeks_data));
1294 }
1295}