Skip to main content

nautilus_model/data/
order.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//! A `BookOrder` for use with the `OrderBook` and `OrderBookDelta` data type.
17
18use std::{
19    fmt::{Debug, Display},
20    hash::{Hash, Hasher},
21};
22
23use nautilus_core::serialization::Serializable;
24use serde::{Deserialize, Serialize};
25
26use crate::{
27    enums::OrderSide,
28    orderbook::{BookIntegrityError, BookPrice},
29    types::{Price, Quantity},
30};
31
32pub type OrderId = u64;
33
34/// Represents a NULL book order (used with the `Clear` action or where an order is not specified).
35pub const NULL_ORDER: BookOrder = BookOrder {
36    side: None,
37    price: Price {
38        raw: 0,
39        precision: 0,
40    },
41    size: Quantity {
42        raw: 0,
43        precision: 0,
44    },
45    order_id: 0,
46};
47
48/// Represents an order in a book.
49#[derive(Clone, Copy, Eq, Serialize, Deserialize)]
50#[cfg_attr(
51    feature = "python",
52    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
53)]
54#[cfg_attr(
55    feature = "python",
56    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
57)]
58pub struct BookOrder {
59    /// The order side.
60    #[serde(with = "crate::enums::serde_option_order_side")]
61    pub side: Option<OrderSide>,
62    /// The order price.
63    pub price: Price,
64    /// The order size.
65    pub size: Quantity,
66    /// The order ID.
67    pub order_id: OrderId,
68}
69
70impl BookOrder {
71    /// Creates a new [`BookOrder`] instance.
72    #[must_use]
73    pub fn new(
74        side: impl Into<Option<OrderSide>>,
75        price: Price,
76        size: Quantity,
77        order_id: OrderId,
78    ) -> Self {
79        Self {
80            side: side.into(),
81            price,
82            size,
83            order_id,
84        }
85    }
86
87    /// Returns a [`BookPrice`] from this order.
88    ///
89    /// # Panics
90    ///
91    /// Panics if `self.side` is `None`.
92    #[must_use]
93    pub fn to_book_price(&self) -> BookPrice {
94        BookPrice::new(
95            self.price,
96            self.side.expect("BookOrder side must be Buy or Sell"),
97        )
98    }
99
100    /// Returns the order exposure as an `f64`.
101    #[must_use]
102    pub fn exposure(&self) -> f64 {
103        self.price.as_f64() * self.size.as_f64()
104    }
105
106    /// Returns the signed order size as `f64`, positive for buys, negative for sells.
107    ///
108    /// # Panics
109    ///
110    /// Panics if `self.side` is `None`.
111    #[must_use]
112    pub fn signed_size(&self) -> f64 {
113        match self.side {
114            Some(OrderSide::Buy) => self.size.as_f64(),
115            Some(OrderSide::Sell) => -(self.size.as_f64()),
116            None => panic!("{}", BookIntegrityError::NoOrderSide),
117        }
118    }
119}
120
121impl Default for BookOrder {
122    /// Creates a NULL [`BookOrder`] instance.
123    fn default() -> Self {
124        NULL_ORDER
125    }
126}
127
128impl PartialEq for BookOrder {
129    fn eq(&self, other: &Self) -> bool {
130        self.order_id == other.order_id
131    }
132}
133
134impl Hash for BookOrder {
135    fn hash<H: Hasher>(&self, state: &mut H) {
136        self.order_id.hash(state);
137    }
138}
139
140impl Debug for BookOrder {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        let side = self.side.as_ref().map_or("NO_ORDER_SIDE", AsRef::as_ref);
143        write!(
144            f,
145            "{}(side={}, price={}, size={}, order_id={})",
146            stringify!(BookOrder),
147            side,
148            self.price,
149            self.size,
150            self.order_id,
151        )
152    }
153}
154
155impl Display for BookOrder {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        let side = self.side.as_ref().map_or("NO_ORDER_SIDE", AsRef::as_ref);
158        write!(f, "{},{},{},{}", side, self.price, self.size, self.order_id)
159    }
160}
161
162impl Serializable for BookOrder {}
163
164#[cfg(test)]
165mod tests {
166    use rstest::rstest;
167
168    use super::*;
169    use crate::enums::OrderSide;
170
171    #[rstest]
172    fn test_new() {
173        let price = Price::from("100.00");
174        let size = Quantity::from("10");
175        let side = OrderSide::Buy;
176        let order_id = 123_456;
177
178        let order = BookOrder::new(side, price, size, order_id);
179
180        assert_eq!(order.price, price);
181        assert_eq!(order.size, size);
182        assert_eq!(order.side, side.into());
183        assert_eq!(order.order_id, order_id);
184    }
185
186    #[rstest]
187    fn test_to_book_price() {
188        let price = Price::from("100.00");
189        let size = Quantity::from("10");
190        let side = OrderSide::Buy;
191        let order_id = 123_456;
192
193        let order = BookOrder::new(side, price, size, order_id);
194        let book_price = order.to_book_price();
195
196        assert_eq!(book_price.value, price);
197        assert_eq!(book_price.side, side);
198    }
199
200    #[rstest]
201    fn test_exposure() {
202        let price = Price::from("100.00");
203        let size = Quantity::from("10");
204        let side = OrderSide::Buy;
205        let order_id = 123_456;
206
207        let order = BookOrder::new(side, price, size, order_id);
208        let exposure = order.exposure();
209
210        assert_eq!(exposure, 100.00 * 10.0);
211    }
212
213    #[rstest]
214    fn test_signed_size() {
215        let price = Price::from("100.00");
216        let size = Quantity::from("10");
217        let order_id = 123_456;
218
219        let order_buy = BookOrder::new(OrderSide::Buy, price, size, order_id);
220        let signed_size_buy = order_buy.signed_size();
221        assert_eq!(signed_size_buy, 10.0);
222
223        let order_sell = BookOrder::new(OrderSide::Sell, price, size, order_id);
224        let signed_size_sell = order_sell.signed_size();
225        assert_eq!(signed_size_sell, -10.0);
226    }
227
228    #[rstest]
229    fn test_debug() {
230        let price = Price::from("100.00");
231        let size = Quantity::from(10);
232        let side = OrderSide::Buy;
233        let order_id = 123_456;
234        let order = BookOrder::new(side, price, size, order_id);
235        let result = format!("{order:?}");
236        let expected = "BookOrder(side=BUY, price=100.00, size=10, order_id=123456)";
237        assert_eq!(result, expected);
238    }
239
240    #[rstest]
241    fn test_display() {
242        let price = Price::from("100.00");
243        let size = Quantity::from(10);
244        let side = OrderSide::Buy;
245        let order_id = 123_456;
246        let order = BookOrder::new(side, price, size, order_id);
247        let result = format!("{order}");
248        let expected = "BUY,100.00,10,123456";
249        assert_eq!(result, expected);
250    }
251}