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