Skip to main content

nautilus_model/events/order/
snapshot.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 indexmap::IndexMap;
17use nautilus_core::{UUID4, UnixNanos};
18use rust_decimal::Decimal;
19use serde::{Deserialize, Serialize};
20use ustr::Ustr;
21
22use crate::{
23    enums::{
24        ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce,
25        TrailingOffsetType, TriggerType,
26    },
27    identifiers::{
28        AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
29        StrategyId, TradeId, TraderId, VenueOrderId,
30    },
31    orders::{Order, OrderAny},
32    types::{Money, Price, Quantity},
33};
34
35/// Represents an order state snapshot as a certain instant.
36#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
37#[cfg_attr(
38    feature = "python",
39    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
40)]
41#[cfg_attr(
42    feature = "python",
43    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
44)]
45pub struct OrderSnapshot {
46    /// The trader ID associated with the order.
47    pub trader_id: TraderId,
48    /// The strategy ID associated with the order.
49    pub strategy_id: StrategyId,
50    /// The order instrument ID.
51    pub instrument_id: InstrumentId,
52    /// The client order ID.
53    pub client_order_id: ClientOrderId,
54    /// The venue assigned order ID.
55    pub venue_order_id: Option<VenueOrderId>,
56    /// The position ID associated with the order.
57    pub position_id: Option<PositionId>,
58    /// The account ID associated with the order.
59    pub account_id: Option<AccountId>,
60    /// The orders last trade match ID.
61    pub last_trade_id: Option<TradeId>,
62    /// The order type.
63    pub order_type: OrderType,
64    /// The order side.
65    pub order_side: OrderSide,
66    /// The order quantity.
67    pub quantity: Quantity,
68    /// The order price (LIMIT).
69    pub price: Option<Price>,
70    /// The order activation price for trailing-stop orders.
71    pub activation_price: Option<Price>,
72    /// The order trigger price (STOP).
73    pub trigger_price: Option<Price>,
74    /// The trigger type for the order.
75    #[serde(default, with = "crate::enums::serde_option_trigger_type")]
76    pub trigger_type: Option<TriggerType>,
77    /// The trailing offset for the orders limit price.
78    pub limit_offset: Option<Decimal>,
79    /// The trailing offset for the orders trigger price (STOP).
80    pub trailing_offset: Option<Decimal>,
81    /// The trailing offset type.
82    #[serde(default, with = "crate::enums::serde_option_trailing_offset_type")]
83    pub trailing_offset_type: Option<TrailingOffsetType>,
84    /// The order time in force.
85    pub time_in_force: TimeInForce,
86    /// The order expiration (UNIX epoch nanoseconds), zero for no expiration.
87    pub expire_time: Option<UnixNanos>,
88    /// The order total filled quantity.
89    pub filled_qty: Quantity,
90    /// The order liquidity side.
91    pub liquidity_side: Option<LiquiditySide>,
92    /// The order average fill price.
93    pub avg_px: Option<Decimal>,
94    /// The order total price slippage.
95    pub slippage: Option<Decimal>,
96    /// The commissions for the order.
97    pub commissions: Vec<Money>,
98    /// The order status.
99    pub status: OrderStatus,
100    /// If the order will only provide liquidity (make a market).
101    pub is_post_only: bool,
102    /// If the order carries the 'reduce-only' execution instruction.
103    pub is_reduce_only: bool,
104    /// If the order quantity is denominated in the quote currency.
105    pub is_quote_quantity: bool,
106    /// The quantity of the `LIMIT` order to display on the public book (iceberg).
107    pub display_qty: Option<Quantity>,
108    /// The order emulation trigger type.
109    #[serde(default, with = "crate::enums::serde_option_trigger_type")]
110    pub emulation_trigger: Option<TriggerType>,
111    /// The order emulation trigger instrument ID (will be `instrument_id` if `None`).
112    pub trigger_instrument_id: Option<InstrumentId>,
113    /// The orders contingency type.
114    #[serde(default, with = "crate::enums::serde_option_contingency_type")]
115    pub contingency_type: Option<ContingencyType>,
116    /// The order list ID associated with the order.
117    pub order_list_id: Option<OrderListId>,
118    /// The orders linked client order ID(s).
119    pub linked_order_ids: Option<Vec<ClientOrderId>>,
120    /// The parent client order ID.
121    pub parent_order_id: Option<ClientOrderId>,
122    /// The execution algorithm ID for the order.
123    pub exec_algorithm_id: Option<ExecAlgorithmId>,
124    /// The execution algorithm parameters for the order.
125    pub exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
126    /// The execution algorithm spawning client order ID.
127    pub exec_spawn_id: Option<ClientOrderId>,
128    /// The order custom user tags.
129    pub tags: Option<Vec<Ustr>>,
130    /// The event ID of the `OrderInitialized` event.
131    pub init_id: UUID4,
132    /// UNIX timestamp (nanoseconds) when the object was initialized.
133    pub ts_init: UnixNanos,
134    /// UNIX timestamp (nanoseconds) when the last event occurred.
135    pub ts_last: UnixNanos,
136    /// The causation ID associated with the snapshot.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub causation_id: Option<UUID4>,
139}
140
141impl From<OrderAny> for OrderSnapshot {
142    fn from(order: OrderAny) -> Self {
143        Self {
144            trader_id: order.trader_id(),
145            strategy_id: order.strategy_id(),
146            instrument_id: order.instrument_id(),
147            client_order_id: order.client_order_id(),
148            venue_order_id: order.venue_order_id(),
149            position_id: order.position_id(),
150            account_id: order.account_id(),
151            last_trade_id: order.last_trade_id(),
152            order_type: order.order_type(),
153            order_side: order.order_side(),
154            quantity: order.quantity(),
155            price: order.price(),
156            activation_price: order.activation_price(),
157            trigger_price: order.trigger_price(),
158            trigger_type: order.trigger_type(),
159            limit_offset: order.limit_offset(),
160            trailing_offset: order.trailing_offset(),
161            trailing_offset_type: order.trailing_offset_type(),
162            time_in_force: order.time_in_force(),
163            expire_time: order.expire_time(),
164            filled_qty: order.filled_qty(),
165            liquidity_side: order.liquidity_side(),
166            avg_px: order.avg_px(),
167            slippage: order.slippage(),
168            commissions: order.commissions().values().copied().collect(),
169            status: order.status(),
170            is_post_only: order.is_post_only(),
171            is_reduce_only: order.is_reduce_only(),
172            is_quote_quantity: order.is_quote_quantity(),
173            display_qty: order.display_qty(),
174            emulation_trigger: order.emulation_trigger(),
175            trigger_instrument_id: order.trigger_instrument_id(),
176            contingency_type: order.contingency_type(),
177            order_list_id: order.order_list_id(),
178            linked_order_ids: order.linked_order_ids().map(Vec::from),
179            parent_order_id: order.parent_order_id(),
180            exec_algorithm_id: order.exec_algorithm_id(),
181            exec_algorithm_params: order.exec_algorithm_params().cloned(),
182            exec_spawn_id: order.exec_spawn_id(),
183            tags: order.tags().map(Vec::from),
184            init_id: order.init_id(),
185            ts_init: order.ts_init(),
186            ts_last: order.ts_last(),
187            causation_id: None,
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use std::str::FromStr;
195
196    use rstest::rstest;
197
198    use super::*;
199    use crate::orders::OrderTestBuilder;
200
201    #[rstest]
202    fn test_snapshot_from_market_order() {
203        let order = OrderTestBuilder::new(OrderType::Market)
204            .instrument_id(InstrumentId::from("EURUSD.SIM"))
205            .side(OrderSide::Buy)
206            .quantity(Quantity::from(100))
207            .build();
208
209        let snapshot = OrderSnapshot::from(order.clone());
210
211        assert_eq!(snapshot.trader_id, order.trader_id());
212        assert_eq!(snapshot.strategy_id, order.strategy_id());
213        assert_eq!(snapshot.instrument_id, order.instrument_id());
214        assert_eq!(snapshot.client_order_id, order.client_order_id());
215        assert_eq!(snapshot.venue_order_id, order.venue_order_id());
216        assert_eq!(snapshot.order_side, order.order_side());
217        assert_eq!(snapshot.order_type, order.order_type());
218        assert_eq!(snapshot.quantity, order.quantity());
219        assert_eq!(snapshot.status, order.status());
220        assert_eq!(snapshot.ts_init, order.ts_init());
221        assert_eq!(snapshot.ts_last, order.ts_last());
222        assert_eq!(snapshot.filled_qty, order.filled_qty());
223        assert!(!snapshot.is_post_only);
224        assert!(!snapshot.is_quote_quantity);
225    }
226
227    #[rstest]
228    fn test_snapshot_serde_round_trip_keeps_avg_px_and_slippage_exact() {
229        let order = OrderTestBuilder::new(OrderType::Market)
230            .instrument_id(InstrumentId::from("EURUSD.SIM"))
231            .side(OrderSide::Buy)
232            .quantity(Quantity::from(100))
233            .build();
234        let mut snapshot = OrderSnapshot::from(order);
235        snapshot.avg_px = Some(Decimal::from_str("1.6666666666666666666666666667").unwrap());
236        snapshot.slippage = Some(Decimal::from_str("0.0000000000000000000000000001").unwrap());
237
238        let json = serde_json::to_value(&snapshot).unwrap();
239        let decoded: OrderSnapshot = serde_json::from_value(json.clone()).unwrap();
240
241        // Serialized as strings, so the payload itself cannot round through a float
242        assert_eq!(json["avg_px"], "1.6666666666666666666666666667");
243        assert_eq!(json["slippage"], "0.0000000000000000000000000001");
244        assert_eq!(decoded, snapshot);
245    }
246
247    #[rstest]
248    fn test_snapshot_deserializes_legacy_float_avg_px_and_slippage() {
249        // Legacy payloads carry `avg_px` and `slippage` as JSON floats rather than decimal
250        // strings, so `from_dict` must keep accepting both forms.
251        let order = OrderTestBuilder::new(OrderType::Market)
252            .instrument_id(InstrumentId::from("EURUSD.SIM"))
253            .side(OrderSide::Buy)
254            .quantity(Quantity::from(100))
255            .build();
256        let mut snapshot = OrderSnapshot::from(order);
257        snapshot.avg_px = Some(Decimal::from_str("1.07").unwrap());
258        snapshot.slippage = Some(Decimal::from_str("0.07").unwrap());
259
260        let mut json = serde_json::to_value(&snapshot).unwrap();
261        json["avg_px"] = serde_json::json!(1.07);
262        json["slippage"] = serde_json::json!(0.07);
263        let decoded: OrderSnapshot = serde_json::from_value(json).unwrap();
264
265        assert_eq!(decoded, snapshot);
266    }
267
268    #[rstest]
269    fn test_snapshot_from_limit_order() {
270        let order = OrderTestBuilder::new(OrderType::Limit)
271            .instrument_id(InstrumentId::from("BTCUSDT.BINANCE"))
272            .side(OrderSide::Sell)
273            .quantity(Quantity::from("0.5"))
274            .price(Price::from("50000"))
275            .build();
276
277        let snapshot = OrderSnapshot::from(order);
278
279        assert_eq!(snapshot.order_type, OrderType::Limit);
280        assert_eq!(snapshot.order_side, OrderSide::Sell);
281        assert_eq!(snapshot.price, Some(Price::from("50000")));
282        assert_eq!(
283            snapshot.instrument_id,
284            InstrumentId::from("BTCUSDT.BINANCE")
285        );
286        assert_eq!(snapshot.quantity, Quantity::from("0.5"));
287    }
288}