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