Skip to main content

nautilus_data/option_chains/
atm_tracker.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Reactive ATM (at-the-money) price tracker for option chain subscriptions.
17//!
18//! ATM price is always derived from the exchange-provided forward price
19//! embedded in each option greeks/ticker update.
20
21use nautilus_core::correctness::CorrectnessResult;
22use nautilus_model::{data::option_chain::OptionGreeks, types::Price};
23
24/// Tracks the raw ATM price reactively from the forward price in option greeks.
25///
26/// Does not interact with cache — receives updates via handler callbacks.
27/// Closest-strike resolution is delegated to `StrikeRange::resolve()`.
28#[derive(Debug)]
29pub struct AtmTracker {
30    atm_price: Option<Price>,
31    /// Precision used when converting forward prices from f64 to Price.
32    forward_precision: u8,
33}
34
35impl AtmTracker {
36    /// Creates a new [`AtmTracker`].
37    pub fn new() -> Self {
38        Self {
39            atm_price: None,
40            forward_precision: 2,
41        }
42    }
43
44    /// Sets the precision used when converting forward prices from f64 to Price.
45    pub fn set_forward_precision(&mut self, precision: u8) {
46        self.forward_precision = precision;
47    }
48
49    /// Returns the current raw ATM price (if available).
50    #[must_use]
51    pub fn atm_price(&self) -> Option<Price> {
52        self.atm_price
53    }
54
55    /// Sets the initial ATM price (e.g. from a forward price fetched via HTTP).
56    ///
57    /// This allows instant bootstrap without waiting for the first WebSocket tick.
58    /// Subsequent live updates will overwrite this value normally.
59    pub fn set_initial_price(&mut self, price: Price) {
60        self.atm_price = Some(price);
61    }
62
63    /// Updates from an option greeks event.
64    ///
65    /// Extracts `underlying_price` from the greeks — the exchange-provided
66    /// forward price for this expiry. Returns `true` if the ATM price was updated.
67    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    /// Updates from an option greeks event with correctness checking.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if the exchange-provided forward price is outside the
76    /// representable [`Price`] range.
77    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}