nautilus_model/events/order/
snapshot.rs1use 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#[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 pub trader_id: TraderId,
48 pub strategy_id: StrategyId,
50 pub instrument_id: InstrumentId,
52 pub client_order_id: ClientOrderId,
54 pub venue_order_id: Option<VenueOrderId>,
56 pub position_id: Option<PositionId>,
58 pub account_id: Option<AccountId>,
60 pub last_trade_id: Option<TradeId>,
62 pub order_type: OrderType,
64 pub order_side: OrderSide,
66 pub quantity: Quantity,
68 pub price: Option<Price>,
70 pub activation_price: Option<Price>,
72 pub trigger_price: Option<Price>,
74 #[serde(default, with = "crate::enums::serde_option_trigger_type")]
76 pub trigger_type: Option<TriggerType>,
77 pub limit_offset: Option<Decimal>,
79 pub trailing_offset: Option<Decimal>,
81 #[serde(default, with = "crate::enums::serde_option_trailing_offset_type")]
83 pub trailing_offset_type: Option<TrailingOffsetType>,
84 pub time_in_force: TimeInForce,
86 pub expire_time: Option<UnixNanos>,
88 pub filled_qty: Quantity,
90 pub liquidity_side: Option<LiquiditySide>,
92 pub avg_px: Option<Decimal>,
94 pub slippage: Option<Decimal>,
96 pub commissions: Vec<Money>,
98 pub status: OrderStatus,
100 pub is_post_only: bool,
102 pub is_reduce_only: bool,
104 pub is_quote_quantity: bool,
106 pub display_qty: Option<Quantity>,
108 #[serde(default, with = "crate::enums::serde_option_trigger_type")]
110 pub emulation_trigger: Option<TriggerType>,
111 pub trigger_instrument_id: Option<InstrumentId>,
113 #[serde(default, with = "crate::enums::serde_option_contingency_type")]
115 pub contingency_type: Option<ContingencyType>,
116 pub order_list_id: Option<OrderListId>,
118 pub linked_order_ids: Option<Vec<ClientOrderId>>,
120 pub parent_order_id: Option<ClientOrderId>,
122 pub exec_algorithm_id: Option<ExecAlgorithmId>,
124 pub exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
126 pub exec_spawn_id: Option<ClientOrderId>,
128 pub tags: Option<Vec<Ustr>>,
130 pub init_id: UUID4,
132 pub ts_init: UnixNanos,
134 pub ts_last: UnixNanos,
136 #[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 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 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}