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    #[allow(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        let mut order_id = 1;
207
208        for order in bids.iter_mut().take(DEPTH10_LEN) {
209            *order = BookOrder::new(
210                OrderSide::Buy,
211                Price::new(price, 2),
212                Quantity::new(quantity, 0),
213                order_id,
214            );
215
216            price -= 1.0;
217            quantity += 100.0;
218            order_id += 1;
219        }
220
221        // Create asks
222        let mut price = 100.00;
223        let mut quantity = 100.0;
224        let mut order_id = 11;
225
226        for order in asks.iter_mut().take(DEPTH10_LEN) {
227            *order = BookOrder::new(
228                OrderSide::Sell,
229                Price::new(price, 2),
230                Quantity::new(quantity, 0),
231                order_id,
232            );
233
234            price += 1.0;
235            quantity += 100.0;
236            order_id += 1;
237        }
238
239        let bid_counts: [u32; 10] = [1; 10];
240        let ask_counts: [u32; 10] = [1; 10];
241
242        Self::new(
243            instrument_id,
244            bids,
245            asks,
246            bid_counts,
247            ask_counts,
248            flags,
249            sequence,
250            ts_event.into(),
251            ts_init.into(),
252        )
253    }
254
255    /// Returns a new object from the given dictionary representation.
256    #[staticmethod]
257    #[pyo3(name = "from_dict")]
258    fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
259        from_dict_pyo3(py, values)
260    }
261
262    /// Creates a `PyCapsule` containing a raw pointer to a `Data::Depth10` object.
263    ///
264    /// This function takes the current object (assumed to be of a type that can be represented as
265    /// `Data::Depth10`), and encapsulates a raw pointer to it within a `PyCapsule`.
266    ///
267    /// # Safety
268    ///
269    /// This function is safe as long as the following conditions are met:
270    /// - The `Data::Depth10` object pointed to by the capsule must remain valid for the lifetime of the capsule.
271    /// - The consumer of the capsule must ensure proper handling to avoid dereferencing a dangling pointer.
272    ///
273    /// # Panics
274    ///
275    /// The function will panic if the `PyCapsule` creation fails, which can occur if the
276    /// `Data::Depth10` object cannot be converted into a raw pointer.
277    #[pyo3(name = "as_pycapsule")]
278    fn py_as_pycapsule(&self, py: Python<'_>) -> Py<PyAny> {
279        data_to_pycapsule(py, Data::from(*self))
280    }
281
282    /// Return a dictionary representation of the object.
283    #[pyo3(name = "to_dict")]
284    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
285        to_dict_pyo3(py, self)
286    }
287
288    /// Return JSON encoded bytes representation of the object.
289    #[pyo3(name = "to_json_bytes")]
290    fn py_to_json_bytes(&self, py: Python<'_>) -> Py<PyAny> {
291        self.to_json_bytes().unwrap().into_py_any_unwrap(py)
292    }
293
294    /// Return MsgPack encoded bytes representation of the object.
295    #[pyo3(name = "to_msgpack_bytes")]
296    fn py_to_msgpack_bytes(&self, py: Python<'_>) -> Py<PyAny> {
297        self.to_msgpack_bytes().unwrap().into_py_any_unwrap(py)
298    }
299}
300
301#[pymethods]
302impl OrderBookDepth10 {
303    #[staticmethod]
304    #[pyo3(name = "from_json")]
305    fn py_from_json(data: &[u8]) -> PyResult<Self> {
306        Self::from_json_bytes(data).map_err(to_pyvalue_err)
307    }
308
309    #[staticmethod]
310    #[pyo3(name = "from_msgpack")]
311    fn py_from_msgpack(data: &[u8]) -> PyResult<Self> {
312        Self::from_msgpack_bytes(data).map_err(to_pyvalue_err)
313    }
314}