Skip to main content

nautilus_model/python/data/
option_chain.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
16use std::collections::BTreeMap;
17
18use nautilus_core::UnixNanos;
19use pyo3::prelude::*;
20
21use crate::{
22    data::{
23        QuoteTick,
24        greeks::OptionGreekValues,
25        option_chain::{OptionChainSlice, OptionGreeks, OptionStrikeData, StrikeRange},
26    },
27    enums::GreeksConvention,
28    identifiers::{InstrumentId, OptionSeriesId},
29    types::Price,
30};
31
32/// Python wrapper for `StrikeRange` (complex enum).
33#[pyclass(
34    name = "StrikeRange",
35    module = "nautilus_trader.core.nautilus_pyo3.model",
36    from_py_object
37)]
38#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")]
39#[derive(Clone, Debug)]
40pub struct PyStrikeRange {
41    pub inner: StrikeRange,
42}
43
44#[pymethods]
45#[pyo3_stub_gen::derive::gen_stub_pymethods]
46impl PyStrikeRange {
47    /// Creates a `StrikeRange::Fixed` variant.
48    #[staticmethod]
49    #[pyo3(name = "fixed")]
50    fn py_fixed(strikes: Vec<Price>) -> Self {
51        Self {
52            inner: StrikeRange::Fixed(strikes),
53        }
54    }
55
56    /// Creates a `StrikeRange::AtmRelative` variant.
57    #[staticmethod]
58    #[pyo3(name = "atm_relative")]
59    fn py_atm_relative(strikes_above: usize, strikes_below: usize) -> Self {
60        Self {
61            inner: StrikeRange::AtmRelative {
62                strikes_above,
63                strikes_below,
64            },
65        }
66    }
67
68    /// Creates a `StrikeRange::AtmPercent` variant.
69    #[staticmethod]
70    #[pyo3(name = "atm_percent")]
71    fn py_atm_percent(pct: f64) -> Self {
72        Self {
73            inner: StrikeRange::AtmPercent { pct },
74        }
75    }
76
77    /// Returns the variant name (`Fixed`, `AtmRelative`, or `AtmPercent`).
78    #[getter]
79    #[pyo3(name = "kind")]
80    fn py_kind(&self) -> &'static str {
81        match self.inner {
82            StrikeRange::Fixed(_) => "Fixed",
83            StrikeRange::AtmRelative { .. } => "AtmRelative",
84            StrikeRange::AtmPercent { .. } => "AtmPercent",
85        }
86    }
87
88    fn __repr__(&self) -> String {
89        format!("{:?}", self.inner)
90    }
91
92    fn __str__(&self) -> String {
93        format!("{:?}", self.inner)
94    }
95}
96
97#[pymethods]
98#[pyo3_stub_gen::derive::gen_stub_pymethods]
99impl OptionGreeks {
100    /// Exchange-provided option Greeks and implied volatility for a single instrument.
101    #[new]
102    #[pyo3(signature = (instrument_id, delta, gamma, vega, theta, rho=0.0, mark_iv=None, bid_iv=None, ask_iv=None, underlying_price=None, open_interest=None, ts_event=0, ts_init=0, convention=None))]
103    #[expect(clippy::too_many_arguments)]
104    fn py_new(
105        instrument_id: InstrumentId,
106        delta: f64,
107        gamma: f64,
108        vega: f64,
109        theta: f64,
110        rho: f64,
111        mark_iv: Option<f64>,
112        bid_iv: Option<f64>,
113        ask_iv: Option<f64>,
114        underlying_price: Option<f64>,
115        open_interest: Option<f64>,
116        ts_event: u64,
117        ts_init: u64,
118        convention: Option<GreeksConvention>,
119    ) -> Self {
120        Self {
121            instrument_id,
122            convention: convention.unwrap_or_default(),
123            greeks: OptionGreekValues {
124                delta,
125                gamma,
126                vega,
127                theta,
128                rho,
129            },
130            mark_iv,
131            bid_iv,
132            ask_iv,
133            underlying_price,
134            open_interest,
135            ts_event: UnixNanos::from(ts_event),
136            ts_init: UnixNanos::from(ts_init),
137        }
138    }
139
140    #[getter]
141    #[pyo3(name = "convention")]
142    fn py_convention(&self) -> GreeksConvention {
143        self.convention
144    }
145
146    #[getter]
147    #[pyo3(name = "instrument_id")]
148    fn py_instrument_id(&self) -> InstrumentId {
149        self.instrument_id
150    }
151
152    #[getter]
153    #[pyo3(name = "delta")]
154    fn py_delta(&self) -> f64 {
155        self.greeks.delta
156    }
157
158    #[getter]
159    #[pyo3(name = "gamma")]
160    fn py_gamma(&self) -> f64 {
161        self.greeks.gamma
162    }
163
164    #[getter]
165    #[pyo3(name = "vega")]
166    fn py_vega(&self) -> f64 {
167        self.greeks.vega
168    }
169
170    #[getter]
171    #[pyo3(name = "theta")]
172    fn py_theta(&self) -> f64 {
173        self.greeks.theta
174    }
175
176    #[getter]
177    #[pyo3(name = "rho")]
178    fn py_rho(&self) -> f64 {
179        self.greeks.rho
180    }
181
182    #[getter]
183    #[pyo3(name = "mark_iv")]
184    fn py_mark_iv(&self) -> Option<f64> {
185        self.mark_iv
186    }
187
188    #[getter]
189    #[pyo3(name = "bid_iv")]
190    fn py_bid_iv(&self) -> Option<f64> {
191        self.bid_iv
192    }
193
194    #[getter]
195    #[pyo3(name = "ask_iv")]
196    fn py_ask_iv(&self) -> Option<f64> {
197        self.ask_iv
198    }
199
200    #[getter]
201    #[pyo3(name = "underlying_price")]
202    fn py_underlying_price(&self) -> Option<f64> {
203        self.underlying_price
204    }
205
206    #[getter]
207    #[pyo3(name = "open_interest")]
208    fn py_open_interest(&self) -> Option<f64> {
209        self.open_interest
210    }
211
212    #[getter]
213    #[pyo3(name = "ts_event")]
214    fn py_ts_event(&self) -> u64 {
215        self.ts_event.as_u64()
216    }
217
218    #[getter]
219    #[pyo3(name = "ts_init")]
220    fn py_ts_init(&self) -> u64 {
221        self.ts_init.as_u64()
222    }
223
224    fn __repr__(&self) -> String {
225        format!("{self}")
226    }
227
228    fn __str__(&self) -> String {
229        format!("{self}")
230    }
231}
232
233impl OptionGreeks {
234    /// Creates an `OptionGreeks` from a Python object.
235    ///
236    /// # Errors
237    ///
238    /// Returns an error if the Python object is missing required attributes.
239    pub fn from_pyobject(obj: &Bound<'_, PyAny>) -> PyResult<Self> {
240        let instrument_id = obj.getattr("instrument_id")?.extract::<InstrumentId>()?;
241        let delta = obj.getattr("delta")?.extract::<f64>()?;
242        let gamma = obj.getattr("gamma")?.extract::<f64>()?;
243        let vega = obj.getattr("vega")?.extract::<f64>()?;
244        let theta = obj.getattr("theta")?.extract::<f64>()?;
245        let rho = obj.getattr("rho")?.extract::<f64>()?;
246        let mark_iv = obj.getattr("mark_iv")?.extract::<Option<f64>>()?;
247        let bid_iv = obj.getattr("bid_iv")?.extract::<Option<f64>>()?;
248        let ask_iv = obj.getattr("ask_iv")?.extract::<Option<f64>>()?;
249        let underlying_price = obj.getattr("underlying_price")?.extract::<Option<f64>>()?;
250        let open_interest = obj.getattr("open_interest")?.extract::<Option<f64>>()?;
251        let ts_event = obj.getattr("ts_event")?.extract::<u64>()?;
252        let ts_init = obj.getattr("ts_init")?.extract::<u64>()?;
253        let convention = obj
254            .getattr("convention")
255            .ok()
256            .and_then(|v| v.extract::<GreeksConvention>().ok())
257            .unwrap_or_default();
258
259        Ok(Self {
260            instrument_id,
261            convention,
262            greeks: OptionGreekValues {
263                delta,
264                gamma,
265                vega,
266                theta,
267                rho,
268            },
269            mark_iv,
270            bid_iv,
271            ask_iv,
272            underlying_price,
273            open_interest,
274            ts_event: UnixNanos::from(ts_event),
275            ts_init: UnixNanos::from(ts_init),
276        })
277    }
278}
279
280#[pymethods]
281#[pyo3_stub_gen::derive::gen_stub_pymethods]
282impl OptionStrikeData {
283    /// Combined quote and Greeks data for a single strike in an option chain.
284    #[new]
285    #[pyo3(signature = (quote, greeks=None))]
286    fn py_new(quote: QuoteTick, greeks: Option<OptionGreeks>) -> Self {
287        Self { quote, greeks }
288    }
289
290    #[getter]
291    #[pyo3(name = "quote")]
292    fn py_quote(&self) -> QuoteTick {
293        self.quote
294    }
295
296    #[getter]
297    #[pyo3(name = "greeks")]
298    fn py_greeks(&self) -> Option<OptionGreeks> {
299        self.greeks
300    }
301
302    fn __repr__(&self) -> String {
303        format!(
304            "OptionStrikeData(quote={}, greeks={:?})",
305            self.quote, self.greeks
306        )
307    }
308}
309
310#[pymethods]
311#[pyo3_stub_gen::derive::gen_stub_pymethods]
312impl OptionChainSlice {
313    /// A point-in-time snapshot of an option chain for a single series.
314    #[new]
315    #[pyo3(signature = (series_id, atm_strike=None, ts_event=0, ts_init=0))]
316    fn py_new(
317        series_id: OptionSeriesId,
318        atm_strike: Option<Price>,
319        ts_event: u64,
320        ts_init: u64,
321    ) -> Self {
322        Self {
323            series_id,
324            atm_strike,
325            calls: BTreeMap::new(),
326            puts: BTreeMap::new(),
327            ts_event: UnixNanos::from(ts_event),
328            ts_init: UnixNanos::from(ts_init),
329        }
330    }
331
332    #[getter]
333    #[pyo3(name = "series_id")]
334    fn py_series_id(&self) -> OptionSeriesId {
335        self.series_id
336    }
337
338    #[getter]
339    #[pyo3(name = "atm_strike")]
340    fn py_atm_strike(&self) -> Option<Price> {
341        self.atm_strike
342    }
343
344    #[getter]
345    #[pyo3(name = "ts_event")]
346    fn py_ts_event(&self) -> u64 {
347        self.ts_event.as_u64()
348    }
349
350    #[getter]
351    #[pyo3(name = "ts_init")]
352    fn py_ts_init(&self) -> u64 {
353        self.ts_init.as_u64()
354    }
355
356    /// Returns the number of call entries.
357    #[pyo3(name = "call_count")]
358    fn py_call_count(&self) -> usize {
359        self.call_count()
360    }
361
362    /// Returns the number of put entries.
363    #[pyo3(name = "put_count")]
364    fn py_put_count(&self) -> usize {
365        self.put_count()
366    }
367
368    /// Returns the total number of unique strikes.
369    #[pyo3(name = "strike_count")]
370    fn py_strike_count(&self) -> usize {
371        self.strike_count()
372    }
373
374    /// Returns `true` if the chain has no data.
375    #[pyo3(name = "is_empty")]
376    fn py_is_empty(&self) -> bool {
377        self.is_empty()
378    }
379
380    /// Returns all strike prices present in the chain (union of calls and puts).
381    #[pyo3(name = "strikes")]
382    fn py_strikes(&self) -> Vec<Price> {
383        self.strikes()
384    }
385
386    /// Returns the call data for a given strike price.
387    #[pyo3(name = "get_call")]
388    fn py_get_call(&self, strike: Price) -> Option<OptionStrikeData> {
389        self.get_call(&strike).cloned()
390    }
391
392    /// Returns the put data for a given strike price.
393    #[pyo3(name = "get_put")]
394    fn py_get_put(&self, strike: Price) -> Option<OptionStrikeData> {
395        self.get_put(&strike).cloned()
396    }
397
398    /// Returns the call quote for a given strike price.
399    #[pyo3(name = "get_call_quote")]
400    fn py_get_call_quote(&self, strike: Price) -> Option<QuoteTick> {
401        self.get_call_quote(&strike).copied()
402    }
403
404    /// Returns the put quote for a given strike price.
405    #[pyo3(name = "get_put_quote")]
406    fn py_get_put_quote(&self, strike: Price) -> Option<QuoteTick> {
407        self.get_put_quote(&strike).copied()
408    }
409
410    /// Returns the call Greeks for a given strike price.
411    #[pyo3(name = "get_call_greeks")]
412    fn py_get_call_greeks(&self, strike: Price) -> Option<OptionGreeks> {
413        self.get_call_greeks(&strike).copied()
414    }
415
416    /// Returns the put Greeks for a given strike price.
417    #[pyo3(name = "get_put_greeks")]
418    fn py_get_put_greeks(&self, strike: Price) -> Option<OptionGreeks> {
419        self.get_put_greeks(&strike).copied()
420    }
421
422    fn __repr__(&self) -> String {
423        format!("{self}")
424    }
425
426    fn __str__(&self) -> String {
427        format!("{self}")
428    }
429}