Skip to main content

nautilus_model/python/orderbook/
mod.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
16//! Provides a generic L1/L2/L3 order book the trading domain model.
17
18pub mod book;
19pub mod level;
20pub mod own;
21
22#[cfg(test)]
23mod tests {
24    use std::sync::Once;
25
26    use pyo3::{exceptions::PyValueError, prelude::*, types::PyDict};
27    use rstest::rstest;
28    use rust_decimal::Decimal;
29
30    use crate::{
31        enums::BookType,
32        identifiers::InstrumentId,
33        orderbook::{OrderBook, own::OwnOrderBook},
34    };
35
36    fn ensure_python_initialized() {
37        static INIT: Once = Once::new();
38        INIT.call_once(Python::initialize);
39    }
40
41    #[rstest]
42    #[case::own_bids_to_dict("bids_to_dict", false, false)]
43    #[case::own_asks_to_dict("asks_to_dict", false, false)]
44    #[case::own_bid_quantity("bid_quantity", false, false)]
45    #[case::own_ask_quantity("ask_quantity", false, false)]
46    #[case::book_bids_filtered_to_dict("bids_filtered_to_dict", true, false)]
47    #[case::book_asks_filtered_to_dict("asks_filtered_to_dict", true, false)]
48    #[case::book_group_bids_filtered("group_bids_filtered", true, true)]
49    #[case::book_group_asks_filtered("group_asks_filtered", true, true)]
50    #[case::book_filtered_view("filtered_view", true, false)]
51    fn test_python_acceptance_filter_requires_ts_now(
52        #[case] method_name: &str,
53        #[case] filtered_book_method: bool,
54        #[case] requires_group_size: bool,
55    ) {
56        ensure_python_initialized();
57
58        Python::attach(|py| {
59            let instrument_id = InstrumentId::from("AAPL.XNAS");
60            let own_book = Py::new(py, OwnOrderBook::new(instrument_id)).unwrap();
61            let kwargs = PyDict::new(py);
62            kwargs.set_item("accepted_buffer_ns", 1).unwrap();
63
64            let py_err = if filtered_book_method {
65                kwargs.set_item("own_book", own_book.bind(py)).unwrap();
66                let book = Py::new(py, OrderBook::new(instrument_id, BookType::L2_MBP)).unwrap();
67                let result = if requires_group_size {
68                    book.bind(py)
69                        .call_method(method_name, (Decimal::ONE,), Some(&kwargs))
70                } else {
71                    book.bind(py).call_method(method_name, (), Some(&kwargs))
72                };
73                result.unwrap_err()
74            } else {
75                own_book
76                    .bind(py)
77                    .call_method(method_name, (), Some(&kwargs))
78                    .unwrap_err()
79            };
80
81            assert!(
82                py_err.is_instance_of::<PyValueError>(py),
83                "expected PyValueError, received {}",
84                py_err.get_type(py).name().unwrap().to_str().unwrap()
85            );
86            assert_eq!(
87                py_err.value(py).to_string(),
88                "ts_now must be provided when accepted_buffer_ns > 0"
89            );
90        });
91    }
92
93    /// The filtered-book methods reject the invalid pair even with no own book.
94    ///
95    /// Without `own_book` the underlying Rust method never filters own orders, so
96    /// it never reaches the assertion and could not panic. The Python boundary is
97    /// deliberately stricter, treating the arguments as an invalid pair in their
98    /// own right; this pins that decision, which the cases above cannot because
99    /// they all supply an own book.
100    #[rstest]
101    #[case::bids_filtered_to_dict("bids_filtered_to_dict", false)]
102    #[case::asks_filtered_to_dict("asks_filtered_to_dict", false)]
103    #[case::group_bids_filtered("group_bids_filtered", true)]
104    #[case::group_asks_filtered("group_asks_filtered", true)]
105    #[case::filtered_view("filtered_view", false)]
106    fn test_python_filtered_book_requires_ts_now_without_own_book(
107        #[case] method_name: &str,
108        #[case] requires_group_size: bool,
109    ) {
110        ensure_python_initialized();
111
112        Python::attach(|py| {
113            let instrument_id = InstrumentId::from("AAPL.XNAS");
114            let book = Py::new(py, OrderBook::new(instrument_id, BookType::L2_MBP)).unwrap();
115            let kwargs = PyDict::new(py);
116            kwargs.set_item("accepted_buffer_ns", 1).unwrap();
117
118            let result = if requires_group_size {
119                book.bind(py)
120                    .call_method(method_name, (Decimal::ONE,), Some(&kwargs))
121            } else {
122                book.bind(py).call_method(method_name, (), Some(&kwargs))
123            };
124
125            let py_err = result.unwrap_err();
126
127            assert!(
128                py_err.is_instance_of::<PyValueError>(py),
129                "expected PyValueError, received {}",
130                py_err.get_type(py).name().unwrap().to_str().unwrap()
131            );
132            assert_eq!(
133                py_err.value(py).to_string(),
134                "ts_now must be provided when accepted_buffer_ns > 0"
135            );
136        });
137    }
138}