Skip to main content

nautilus_model/python/data/
depth.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::{
17    collections::{HashMap, hash_map::DefaultHasher},
18    hash::{Hash, Hasher},
19};
20
21use nautilus_core::{
22    python::{
23        IntoPyObjectNautilusExt,
24        serialization::{from_dict_pyo3, to_dict_pyo3},
25        to_pyvalue_err,
26    },
27    serialization::{
28        Serializable,
29        msgpack::{FromMsgPack, ToMsgPack},
30    },
31};
32use pyo3::{prelude::*, pyclass::CompareOp, types::PyDict};
33
34use super::data_to_pycapsule;
35use crate::{
36    data::{
37        Data,
38        depth::{DEPTH10_LEN, OrderBookDepth10},
39        order::BookOrder,
40    },
41    enums::OrderSide,
42    identifiers::InstrumentId,
43    python::common::PY_MODULE_MODEL,
44    types::{Price, Quantity},
45};
46
47#[pymethods]
48#[pyo3_stub_gen::derive::gen_stub_pymethods]
49impl OrderBookDepth10 {
50    /// Represents an aggregated order book update with a fixed depth of 10 levels per side.
51    ///
52    /// This structure is specifically designed for scenarios where a snapshot of the top 10 bid and
53    /// ask levels in an order book is needed. It differs from `OrderBookDelta` or `OrderBookDeltas`
54    /// in its fixed-depth nature and is optimized for cases where a full depth representation is not
55    /// required or practical.
56    ///
57    /// Note: This type is not compatible with `OrderBookDelta` or `OrderBookDeltas` due to
58    /// its specialized structure and limited depth use case.
59    #[expect(clippy::too_many_arguments)]
60    #[new]
61    fn py_new(
62        instrument_id: InstrumentId,
63        bids: [BookOrder; DEPTH10_LEN],
64        asks: [BookOrder; DEPTH10_LEN],
65        bid_counts: [u32; DEPTH10_LEN],
66        ask_counts: [u32; DEPTH10_LEN],
67        flags: u8,
68        sequence: u64,
69        ts_event: u64,
70        ts_init: u64,
71    ) -> Self {
72        Self::new(
73            instrument_id,
74            bids,
75            asks,
76            bid_counts,
77            ask_counts,
78            flags,
79            sequence,
80            ts_event.into(),
81            ts_init.into(),
82        )
83    }
84
85    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
86        match op {
87            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
88            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
89            _ => py.NotImplemented(),
90        }
91    }
92
93    fn __hash__(&self) -> isize {
94        let mut h = DefaultHasher::new();
95        self.hash(&mut h);
96        h.finish() as isize
97    }
98
99    fn __repr__(&self) -> String {
100        format!("{self:?}")
101    }
102
103    fn __str__(&self) -> String {
104        self.to_string()
105    }
106
107    #[getter]
108    #[pyo3(name = "instrument_id")]
109    fn py_instrument_id(&self) -> InstrumentId {
110        self.instrument_id
111    }
112
113    #[getter]
114    #[pyo3(name = "bids")]
115    fn py_bids(&self) -> [BookOrder; DEPTH10_LEN] {
116        self.bids
117    }
118
119    #[getter]
120    #[pyo3(name = "asks")]
121    fn py_asks(&self) -> [BookOrder; DEPTH10_LEN] {
122        self.asks
123    }
124
125    #[getter]
126    #[pyo3(name = "bid_counts")]
127    fn py_bid_counts(&self) -> [u32; DEPTH10_LEN] {
128        self.bid_counts
129    }
130
131    #[getter]
132    #[pyo3(name = "ask_counts")]
133    fn py_ask_counts(&self) -> [u32; DEPTH10_LEN] {
134        self.ask_counts
135    }
136
137    #[getter]
138    #[pyo3(name = "flags")]
139    fn py_flags(&self) -> u8 {
140        self.flags
141    }
142
143    #[getter]
144    #[pyo3(name = "sequence")]
145    fn py_sequence(&self) -> u64 {
146        self.sequence
147    }
148
149    #[getter]
150    #[pyo3(name = "ts_event")]
151    fn py_ts_event(&self) -> u64 {
152        self.ts_event.as_u64()
153    }
154
155    #[getter]
156    #[pyo3(name = "ts_init")]
157    fn py_ts_init(&self) -> u64 {
158        self.ts_init.as_u64()
159    }
160
161    #[staticmethod]
162    #[pyo3(name = "fully_qualified_name")]
163    fn py_fully_qualified_name() -> String {
164        format!("{}:{}", PY_MODULE_MODEL, stringify!(OrderBookDepth10))
165    }
166
167    /// Returns the metadata for the type, for use with serialization formats.
168    #[staticmethod]
169    #[pyo3(name = "get_metadata")]
170    fn py_get_metadata(
171        instrument_id: &InstrumentId,
172        price_precision: u8,
173        size_precision: u8,
174    ) -> HashMap<String, String> {
175        Self::get_metadata(instrument_id, price_precision, size_precision)
176    }
177
178    /// Returns the field map for the type, for use with Arrow schemas.
179    #[staticmethod]
180    #[pyo3(name = "get_fields")]
181    fn py_get_fields(py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
182        let py_dict = PyDict::new(py);
183        for (k, v) in Self::get_fields() {
184            py_dict.set_item(k, v)?;
185        }
186
187        Ok(py_dict)
188    }
189
190    // TODO: Expose this properly from a test stub provider
191    #[staticmethod]
192    #[pyo3(name = "get_stub")]
193    fn py_get_stub() -> Self {
194        let instrument_id = InstrumentId::from("AAPL.XNAS");
195        let flags = 0;
196        let sequence = 0;
197        let ts_event = 1;
198        let ts_init = 2;
199
200        let mut bids: [BookOrder; DEPTH10_LEN] = [BookOrder::default(); DEPTH10_LEN];
201        let mut asks: [BookOrder; DEPTH10_LEN] = [BookOrder::default(); DEPTH10_LEN];
202
203        // Create bids
204        let mut price = 99.00;
205        let mut quantity = 100.0;
206
207        for (i, order) in bids.iter_mut().take(DEPTH10_LEN).enumerate() {
208            *order = BookOrder::new(
209                OrderSide::Buy,
210                Price::new(price, 2),
211                Quantity::new(quantity, 0),
212                (i + 1) as u64,
213            );
214
215            price -= 1.0;
216            quantity += 100.0;
217        }
218
219        // Create asks
220        let mut price = 100.00;
221        let mut quantity = 100.0;
222
223        for (i, order) in asks.iter_mut().take(DEPTH10_LEN).enumerate() {
224            *order = BookOrder::new(
225                OrderSide::Sell,
226                Price::new(price, 2),
227                Quantity::new(quantity, 0),
228                (i + 11) as u64,
229            );
230
231            price += 1.0;
232            quantity += 100.0;
233        }
234
235        let bid_counts: [u32; 10] = [1; 10];
236        let ask_counts: [u32; 10] = [1; 10];
237
238        Self::new(
239            instrument_id,
240            bids,
241            asks,
242            bid_counts,
243            ask_counts,
244            flags,
245            sequence,
246            ts_event.into(),
247            ts_init.into(),
248        )
249    }
250
251    /// Returns a new object from the given dictionary representation.
252    #[staticmethod]
253    #[pyo3(name = "from_dict")]
254    fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
255        from_dict_pyo3(py, values)
256    }
257
258    /// Creates a `PyCapsule` containing a raw pointer to a `Data::Depth10` object.
259    ///
260    /// This function takes the current object (assumed to be of a type that can be represented as
261    /// `Data::Depth10`), and encapsulates a raw pointer to it within a `PyCapsule`.
262    ///
263    /// # Safety
264    ///
265    /// This function is safe as long as the following conditions are met:
266    /// - The `Data::Depth10` object pointed to by the capsule must remain valid for the lifetime of the capsule.
267    /// - The consumer of the capsule must ensure proper handling to avoid dereferencing a dangling pointer.
268    ///
269    /// # Panics
270    ///
271    /// The function will panic if the `PyCapsule` creation fails, which can occur if the
272    /// `Data::Depth10` object cannot be converted into a raw pointer.
273    #[pyo3(name = "as_pycapsule")]
274    fn py_as_pycapsule(&self, py: Python<'_>) -> Py<PyAny> {
275        data_to_pycapsule(py, Data::from(*self))
276    }
277
278    /// Return a dictionary representation of the object.
279    #[pyo3(name = "to_dict")]
280    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
281        to_dict_pyo3(py, self)
282    }
283
284    /// Return JSON encoded bytes representation of the object.
285    #[pyo3(name = "to_json_bytes")]
286    fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
287        self.to_json_bytes().unwrap().into_py_any_unwrap(py)
288    }
289
290    /// Return `MsgPack` encoded bytes representation of the object.
291    #[pyo3(name = "to_msgpack_bytes")]
292    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
293        self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
294    }
295}
296
297#[pymethods]
298impl OrderBookDepth10 {
299    #[staticmethod]
300    #[pyo3(name = "from_json")]
301    fn py_from_json(data: &[u8]) -> PyResult<Self> {
302        Self::from_json_bytes(data).map_err(to_pyvalue_err)
303    }
304
305    #[staticmethod]
306    #[pyo3(name = "from_msgpack")]
307    fn py_from_msgpack(data: &[u8]) -> PyResult<Self> {
308        Self::from_msgpack_bytes(data).map_err(to_pyvalue_err)
309    }
310}