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_model::{data::option_chain::OptionGreeks, types::Price};
22
23/// Tracks the raw ATM price reactively from the forward price in option greeks.
24///
25/// Does not interact with cache — receives updates via handler callbacks.
26/// Closest-strike resolution is delegated to `StrikeRange::resolve()`.
27#[derive(Debug)]
28pub struct AtmTracker {
29    atm_price: Option<Price>,
30    /// Precision used when converting forward prices from f64 to Price.
31    forward_precision: u8,
32}
33
34impl AtmTracker {
35    /// Creates a new [`AtmTracker`].
36    pub fn new() -> Self {
37        Self {
38            atm_price: None,
39            forward_precision: 2,
40        }
41    }
42
43    /// Sets the precision used when converting forward prices from f64 to Price.
44    pub fn set_forward_precision(&mut self, precision: u8) {
45        self.forward_precision = precision;
46    }
47
48    /// Returns the current raw ATM price (if available).
49    #[must_use]
50    pub fn atm_price(&self) -> Option<Price> {
51        self.atm_price
52    }
53
54    /// Sets the initial ATM price (e.g. from a forward price fetched via HTTP).
55    ///
56    /// This allows instant bootstrap without waiting for the first WebSocket tick.
57    /// Subsequent live updates will overwrite this value normally.
58    pub fn set_initial_price(&mut self, price: Price) {
59        self.atm_price = Some(price);
60    }
61
62    /// Updates from an option greeks event.
63    ///
64    /// Extracts `underlying_price` from the greeks — the exchange-provided
65    /// forward price for this expiry. Returns `true` if the ATM price was updated.
66    pub fn update_from_option_greeks(&mut self, greeks: &OptionGreeks) -> bool {
67        if let Some(fwd) = greeks.underlying_price {
68            self.atm_price = Some(Price::new(fwd, self.forward_precision));
69            return true;
70        }
71        false
72    }
73}
74
75impl Default for AtmTracker {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use nautilus_model::{
84        data::option_chain::OptionGreeks, identifiers::InstrumentId, types::Price,
85    };
86    use rstest::*;
87
88    use super::*;
89
90    #[rstest]
91    fn test_atm_tracker_initial_none() {
92        let tracker = AtmTracker::new();
93        assert!(tracker.atm_price().is_none());
94    }
95
96    #[rstest]
97    fn test_atm_tracker_update_from_option_greeks() {
98        let mut tracker = AtmTracker::new();
99        let greeks = OptionGreeks {
100            instrument_id: InstrumentId::from("BTC-20240101-50000-C.DERIBIT"),
101            underlying_price: Some(50500.0),
102            ..Default::default()
103        };
104        assert!(tracker.update_from_option_greeks(&greeks));
105        assert_eq!(tracker.atm_price().unwrap(), Price::from("50500.00"));
106    }
107
108    #[rstest]
109    fn test_atm_tracker_forward_ignores_none_underlying() {
110        let mut tracker = AtmTracker::new();
111        let greeks = OptionGreeks {
112            instrument_id: InstrumentId::from("BTC-20240101-50000-C.DERIBIT"),
113            underlying_price: None,
114            ..Default::default()
115        };
116        assert!(!tracker.update_from_option_greeks(&greeks));
117        assert!(tracker.atm_price().is_none());
118    }
119
120    #[rstest]
121    fn test_atm_tracker_set_initial_price() {
122        let mut tracker = AtmTracker::new();
123        tracker.set_initial_price(Price::from("50000.00"));
124        assert_eq!(tracker.atm_price().unwrap(), Price::from("50000.00"));
125    }
126
127    #[rstest]
128    fn test_atm_tracker_set_forward_precision() {
129        let mut tracker = AtmTracker::new();
130        tracker.set_forward_precision(4);
131        let greeks = OptionGreeks {
132            instrument_id: InstrumentId::from("BTC-20240101-50000-C.DERIBIT"),
133            underlying_price: Some(50500.1234),
134            ..Default::default()
135        };
136        assert!(tracker.update_from_option_greeks(&greeks));
137        assert_eq!(tracker.atm_price().unwrap(), Price::from("50500.1234"));
138    }
139}