Skip to main content

nautilus_model/python/orderbook/
own.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::{HashSet, hash_map::DefaultHasher},
18    hash::{Hash, Hasher},
19};
20
21use ahash::AHashSet;
22use indexmap::IndexMap;
23use nautilus_core::python::{IntoPyObjectNautilusExt, to_pyruntime_err, to_pyvalue_err};
24use pyo3::{Python, prelude::*, pyclass::CompareOp};
25use rust_decimal::Decimal;
26
27use crate::{
28    enums::{OrderSide, OrderStatus, OrderType, TimeInForce},
29    identifiers::{ClientOrderId, InstrumentId, TraderId, VenueOrderId},
30    orderbook::{OwnBookOrder, own::OwnOrderBook},
31    types::{Price, Quantity},
32};
33
34#[pymethods]
35#[pyo3_stub_gen::derive::gen_stub_pymethods]
36impl OwnBookOrder {
37    /// Represents an own/user order for a book.
38    ///
39    /// This struct models an order that may be in-flight to the trading venue or actively working,
40    /// depending on the value of the `status` field.
41    #[pyo3(signature = (trader_id, client_order_id, side, price, size, order_type, time_in_force, status, ts_last, ts_accepted, ts_submitted, ts_init, venue_order_id=None))]
42    #[new]
43    #[expect(clippy::too_many_arguments)]
44    fn py_new(
45        trader_id: TraderId,
46        client_order_id: ClientOrderId,
47        side: OrderSide,
48        price: Price,
49        size: Quantity,
50        order_type: OrderType,
51        time_in_force: TimeInForce,
52        status: OrderStatus,
53        ts_last: u64,
54        ts_accepted: u64,
55        ts_submitted: u64,
56        ts_init: u64,
57        venue_order_id: Option<VenueOrderId>,
58    ) -> Self {
59        Self::new(
60            trader_id,
61            client_order_id,
62            venue_order_id,
63            side.as_specified(),
64            price,
65            size,
66            order_type,
67            time_in_force,
68            status,
69            ts_last.into(),
70            ts_accepted.into(),
71            ts_submitted.into(),
72            ts_init.into(),
73        )
74    }
75
76    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
77        match op {
78            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
79            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
80            _ => py.NotImplemented(),
81        }
82    }
83
84    fn __hash__(&self) -> isize {
85        let mut hasher = DefaultHasher::new();
86        self.hash(&mut hasher);
87        hasher.finish() as isize
88    }
89
90    fn __repr__(&self) -> String {
91        format!("{self:?}")
92    }
93
94    fn __str__(&self) -> String {
95        self.to_string()
96    }
97
98    #[getter]
99    #[pyo3(name = "client_order_id")]
100    fn py_client_order_id(&self) -> ClientOrderId {
101        self.client_order_id
102    }
103
104    #[getter]
105    #[pyo3(name = "side")]
106    fn py_side(&self) -> OrderSide {
107        self.side.as_order_side()
108    }
109
110    #[getter]
111    #[pyo3(name = "price")]
112    fn py_price(&self) -> Price {
113        self.price
114    }
115
116    #[getter]
117    #[pyo3(name = "size")]
118    fn py_size(&self) -> Quantity {
119        self.size
120    }
121
122    #[getter]
123    #[pyo3(name = "order_type")]
124    fn py_order_type(&self) -> OrderType {
125        self.order_type
126    }
127
128    #[getter]
129    #[pyo3(name = "time_in_force")]
130    fn py_time_in_force(&self) -> TimeInForce {
131        self.time_in_force
132    }
133
134    #[getter]
135    #[pyo3(name = "status")]
136    fn py_status(&self) -> OrderStatus {
137        self.status
138    }
139
140    #[getter]
141    #[pyo3(name = "ts_last")]
142    fn py_ts_last(&self) -> u64 {
143        self.ts_last.into()
144    }
145
146    #[getter]
147    #[pyo3(name = "ts_init")]
148    fn py_ts_init(&self) -> u64 {
149        self.ts_init.into()
150    }
151
152    /// Returns the order exposure as an `f64`.
153    #[pyo3(name = "exposure")]
154    fn py_exposure(&self) -> f64 {
155        self.exposure()
156    }
157
158    /// Returns the signed order exposure as an `f64`.
159    #[pyo3(name = "signed_size")]
160    fn py_signed_size(&self) -> f64 {
161        self.signed_size()
162    }
163}
164
165#[pymethods]
166#[pyo3_stub_gen::derive::gen_stub_pymethods]
167impl OwnOrderBook {
168    /// Creates a new `OwnOrderBook` instance.
169    #[new]
170    fn py_new(instrument_id: InstrumentId) -> Self {
171        Self::new(instrument_id)
172    }
173
174    fn __repr__(&self) -> String {
175        format!("{self:?}")
176    }
177
178    fn __str__(&self) -> String {
179        self.to_string()
180    }
181
182    #[getter]
183    #[pyo3(name = "instrument_id")]
184    fn py_instrument_id(&self) -> InstrumentId {
185        self.instrument_id
186    }
187
188    #[getter]
189    #[pyo3(name = "ts_last")]
190    fn py_ts_last(&self) -> u64 {
191        self.ts_last.as_u64()
192    }
193
194    #[getter]
195    #[pyo3(name = "update_count")]
196    fn py_update_count(&self) -> u64 {
197        self.update_count
198    }
199
200    /// Resets the order book to its initial empty state.
201    #[pyo3(name = "reset")]
202    fn py_reset(&mut self) {
203        self.reset();
204    }
205
206    /// Adds an own order to the book.
207    #[pyo3(name = "add")]
208    fn py_add(&mut self, order: OwnBookOrder) {
209        self.add(order);
210    }
211
212    /// Updates an existing own order in the book.
213    ///
214    /// # Errors
215    ///
216    /// Returns an error if the order is not found.
217    #[pyo3(name = "update")]
218    fn py_update(&mut self, order: OwnBookOrder) -> PyResult<()> {
219        self.update(order).map_err(to_pyruntime_err)
220    }
221
222    /// Deletes an own order from the book.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error if the order is not found.
227    #[pyo3(name = "delete")]
228    fn py_delete(&mut self, order: OwnBookOrder) -> PyResult<()> {
229        self.delete(order).map_err(to_pyruntime_err)
230    }
231
232    /// Clears all orders from both sides of the book.
233    #[pyo3(name = "clear")]
234    fn py_clear(&mut self) {
235        self.clear();
236    }
237
238    /// Returns the client order IDs currently on the bid side.
239    #[pyo3(name = "bid_client_order_ids")]
240    #[must_use]
241    pub fn py_bid_client_order_ids(&self) -> Vec<ClientOrderId> {
242        self.bid_client_order_ids()
243    }
244
245    /// Returns the client order IDs currently on the ask side.
246    #[pyo3(name = "ask_client_order_ids")]
247    #[must_use]
248    pub fn py_ask_client_order_ids(&self) -> Vec<ClientOrderId> {
249        self.ask_client_order_ids()
250    }
251
252    /// Return whether the given client order ID is in the own book.
253    #[pyo3(name = "is_order_in_book")]
254    #[must_use]
255    pub fn py_is_order_in_book(&self, client_order_id: &ClientOrderId) -> bool {
256        self.is_order_in_book(client_order_id)
257    }
258
259    #[pyo3(name = "orders_to_list")]
260    fn py_orders_to_list(&self) -> Vec<OwnBookOrder> {
261        let total_orders = self.bids.cache.len() + self.asks.cache.len();
262        let mut all_orders = Vec::with_capacity(total_orders);
263
264        all_orders.extend(
265            self.bids()
266                .flat_map(|level| level.orders.values().copied())
267                .chain(self.asks().flat_map(|level| level.orders.values().copied())),
268        );
269
270        all_orders
271    }
272
273    #[pyo3(name = "bids_to_list")]
274    fn py_bids_to_list(&self) -> Vec<OwnBookOrder> {
275        self.bids()
276            .flat_map(|level| level.orders.values().copied())
277            .collect()
278    }
279
280    #[pyo3(name = "asks_to_list")]
281    fn py_asks_to_list(&self) -> Vec<OwnBookOrder> {
282        self.asks()
283            .flat_map(|level| level.orders.values().copied())
284            .collect()
285    }
286
287    #[pyo3(name = "bids_to_dict")]
288    #[pyo3(signature = (status=None, accepted_buffer_ns=None, ts_now=None))]
289    fn py_bids_to_dict(
290        &self,
291        status: Option<HashSet<OrderStatus>>,
292        accepted_buffer_ns: Option<u64>,
293        ts_now: Option<u64>,
294    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
295        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
296        self.bids_as_map(status_set.as_ref(), accepted_buffer_ns, ts_now)
297    }
298
299    #[pyo3(name = "asks_to_dict")]
300    #[pyo3(signature = (status=None, accepted_buffer_ns=None, ts_now=None))]
301    fn py_asks_to_dict(
302        &self,
303        status: Option<HashSet<OrderStatus>>,
304        accepted_buffer_ns: Option<u64>,
305        ts_now: Option<u64>,
306    ) -> IndexMap<Decimal, Vec<OwnBookOrder>> {
307        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
308        self.asks_as_map(status_set.as_ref(), accepted_buffer_ns, ts_now)
309    }
310
311    /// Aggregates own bid quantities per price level, omitting zero-quantity levels.
312    ///
313    /// Filters by `status` if provided, including only matching orders. With `accepted_buffer_ns`,
314    /// only includes orders accepted at least that many nanoseconds before `ts_now` (defaults to now).
315    ///
316    /// If `group_size` is provided, groups quantities into price buckets.
317    /// If `depth` is provided, limits the number of price levels returned.
318    #[pyo3(name = "bid_quantity")]
319    #[pyo3(signature = (status=None, depth=None, group_size=None, accepted_buffer_ns=None, ts_now=None))]
320    fn py_bid_quantity(
321        &self,
322        status: Option<HashSet<OrderStatus>>,
323        depth: Option<usize>,
324        group_size: Option<Decimal>,
325        accepted_buffer_ns: Option<u64>,
326        ts_now: Option<u64>,
327    ) -> IndexMap<Decimal, Decimal> {
328        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
329        self.bid_quantity(
330            status_set.as_ref(),
331            depth,
332            group_size,
333            accepted_buffer_ns,
334            ts_now,
335        )
336    }
337
338    /// Aggregates own ask quantities per price level, omitting zero-quantity levels.
339    ///
340    /// Filters by `status` if provided, including only matching orders. With `accepted_buffer_ns`,
341    /// only includes orders accepted at least that many nanoseconds before `ts_now` (defaults to now).
342    ///
343    /// If `group_size` is provided, groups quantities into price buckets.
344    /// If `depth` is provided, limits the number of price levels returned.
345    #[pyo3(name = "ask_quantity")]
346    #[pyo3(signature = (status=None, depth=None, group_size=None, accepted_buffer_ns=None, ts_now=None))]
347    fn py_ask_quantity(
348        &self,
349        status: Option<HashSet<OrderStatus>>,
350        depth: Option<usize>,
351        group_size: Option<Decimal>,
352        accepted_buffer_ns: Option<u64>,
353        ts_now: Option<u64>,
354    ) -> IndexMap<Decimal, Decimal> {
355        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
356        self.ask_quantity(
357            status_set.as_ref(),
358            depth,
359            group_size,
360            accepted_buffer_ns,
361            ts_now,
362        )
363    }
364
365    /// Returns a new own book containing this books orders plus parity-transformed opposite orders.
366    ///
367    /// Opposite asks are transformed into bids with price `1 - price`.
368    /// Opposite bids are transformed into asks with price `1 - price`.
369    ///
370    /// # Errors
371    ///
372    /// Returns `BookViewError.OppositeInstrumentMatch` if `self` and `opposite` have the
373    /// same instrument ID.
374    #[pyo3(name = "combined_with_opposite")]
375    fn py_combined_with_opposite(&self, opposite: &Self) -> PyResult<Self> {
376        self.combined_with_opposite(opposite)
377            .map_err(to_pyvalue_err)
378    }
379
380    #[pyo3(name = "audit_open_orders")]
381    fn py_audit_open_orders(&mut self, open_order_ids: HashSet<ClientOrderId>) {
382        self.audit_open_orders(&open_order_ids.into_iter().collect());
383    }
384
385    /// Return a formatted string representation of the order book.
386    #[pyo3(name = "pprint")]
387    #[pyo3(signature = (num_levels=3, group_size=None))]
388    fn py_pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
389        self.pprint(num_levels, group_size)
390    }
391}