nautilus_data/option_chains/
atm_tracker.rs1use nautilus_core::correctness::CorrectnessResult;
22use nautilus_model::{data::option_chain::OptionGreeks, types::Price};
23
24#[derive(Debug)]
29pub struct AtmTracker {
30 atm_price: Option<Price>,
31 forward_precision: u8,
33}
34
35impl AtmTracker {
36 pub fn new() -> Self {
38 Self {
39 atm_price: None,
40 forward_precision: 2,
41 }
42 }
43
44 pub fn set_forward_precision(&mut self, precision: u8) {
46 self.forward_precision = precision;
47 }
48
49 #[must_use]
51 pub fn atm_price(&self) -> Option<Price> {
52 self.atm_price
53 }
54
55 pub fn set_initial_price(&mut self, price: Price) {
60 self.atm_price = Some(price);
61 }
62
63 pub fn update_from_option_greeks(&mut self, greeks: &OptionGreeks) -> bool {
68 self.try_update_from_option_greeks(greeks).unwrap_or(false)
69 }
70
71 pub fn try_update_from_option_greeks(
78 &mut self,
79 greeks: &OptionGreeks,
80 ) -> CorrectnessResult<bool> {
81 if let Some(fwd) = greeks.underlying_price {
82 self.atm_price = Some(Price::new_checked(fwd, self.forward_precision)?);
83 return Ok(true);
84 }
85 Ok(false)
86 }
87}
88
89impl Default for AtmTracker {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use nautilus_model::{
98 data::option_chain::OptionGreeks, identifiers::InstrumentId, types::Price,
99 };
100 use rstest::*;
101
102 use super::*;
103
104 #[rstest]
105 fn test_atm_tracker_initial_none() {
106 let tracker = AtmTracker::new();
107 assert!(tracker.atm_price().is_none());
108 }
109
110 #[rstest]
111 fn test_atm_tracker_update_from_option_greeks() {
112 let mut tracker = AtmTracker::new();
113 let greeks = OptionGreeks {
114 instrument_id: InstrumentId::from("BTC-20240101-50000-C.DERIBIT"),
115 underlying_price: Some(50500.0),
116 ..Default::default()
117 };
118 assert!(tracker.update_from_option_greeks(&greeks));
119 assert_eq!(tracker.atm_price().unwrap(), Price::from("50500.00"));
120 }
121
122 #[rstest]
123 fn test_atm_tracker_try_update_from_option_greeks_rejects_invalid_forward_price() {
124 let mut tracker = AtmTracker::new();
125 tracker.set_initial_price(Price::from("50000.00"));
126 let greeks = OptionGreeks {
127 instrument_id: InstrumentId::from("BTC-20240101-50000-C.DERIBIT"),
128 underlying_price: Some(f64::NAN),
129 ..Default::default()
130 };
131
132 let error = tracker.try_update_from_option_greeks(&greeks).unwrap_err();
133
134 assert_eq!(error.to_string(), "invalid f64 for 'value', was NaN");
135 assert_eq!(tracker.atm_price().unwrap(), Price::from("50000.00"));
136 }
137
138 #[rstest]
139 fn test_atm_tracker_forward_ignores_none_underlying() {
140 let mut tracker = AtmTracker::new();
141 let greeks = OptionGreeks {
142 instrument_id: InstrumentId::from("BTC-20240101-50000-C.DERIBIT"),
143 underlying_price: None,
144 ..Default::default()
145 };
146 assert!(!tracker.update_from_option_greeks(&greeks));
147 assert!(tracker.atm_price().is_none());
148 }
149
150 #[rstest]
151 fn test_atm_tracker_set_initial_price() {
152 let mut tracker = AtmTracker::new();
153 tracker.set_initial_price(Price::from("50000.00"));
154 assert_eq!(tracker.atm_price().unwrap(), Price::from("50000.00"));
155 }
156
157 #[rstest]
158 fn test_atm_tracker_set_forward_precision() {
159 let mut tracker = AtmTracker::new();
160 tracker.set_forward_precision(4);
161 let greeks = OptionGreeks {
162 instrument_id: InstrumentId::from("BTC-20240101-50000-C.DERIBIT"),
163 underlying_price: Some(50500.1234),
164 ..Default::default()
165 };
166 assert!(tracker.update_from_option_greeks(&greeks));
167 assert_eq!(tracker.atm_price().unwrap(), Price::from("50500.1234"));
168 }
169}