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