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