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