nautilus_model/python/data/
depth.rs

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