1use indexmap::IndexMap;
17use nautilus_core::{UUID4, UnixNanos};
18use rust_decimal::Decimal;
19use ustr::Ustr;
20
21use crate::{
22 enums::{
23 ContingencyType, LiquiditySide, OrderSide, OrderType, TimeInForce, TrailingOffsetType,
24 TriggerType,
25 },
26 identifiers::{
27 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
28 StrategyId, TradeId, TraderId, VenueOrderId,
29 },
30 types::{Currency, Money, Price, Quantity},
31};
32
33pub mod accepted;
34pub mod accepted_batch;
35pub mod any;
36pub mod cancel_rejected;
37pub mod canceled;
38pub mod canceled_batch;
39pub mod denied;
40pub mod denied_reason;
41pub mod emulated;
42pub mod expired;
43pub mod fill_voided;
44pub mod filled;
45pub mod initialized;
46pub mod modify_rejected;
47pub mod pending_cancel;
48pub mod pending_update;
49pub mod rejected;
50pub mod released;
51pub mod snapshot;
52pub mod submitted;
53pub mod submitted_batch;
54pub mod triggered;
55pub mod updated;
56
57#[cfg(any(test, feature = "test-support"))]
58pub mod spec;
59#[cfg(any(test, feature = "test-support"))]
60pub mod stubs;
61
62#[derive(Debug, PartialEq, Eq)]
64pub enum OrderEventType {
65 Initialized,
66 Denied,
67 Emulated,
68 Released,
69 Submitted,
70 Accepted,
71 Rejected,
72 Canceled,
73 Expired,
74 Triggered,
75 PendingUpdate,
76 PendingCancel,
77 ModifyRejected,
78 CancelRejected,
79 Updated,
80 PartiallyFilled,
81 Filled,
82 FillVoided,
83}
84
85pub trait OrderEvent: 'static + Send {
86 fn id(&self) -> UUID4;
87 fn type_name(&self) -> &'static str;
88 fn order_type(&self) -> Option<OrderType>;
89 fn order_side(&self) -> Option<OrderSide>;
90 fn trader_id(&self) -> TraderId;
91 fn strategy_id(&self) -> StrategyId;
92 fn instrument_id(&self) -> InstrumentId;
93 fn trade_id(&self) -> Option<TradeId>;
94 fn currency(&self) -> Option<Currency>;
95 fn client_order_id(&self) -> ClientOrderId;
96 fn reason(&self) -> Option<Ustr>;
97 fn quantity(&self) -> Option<Quantity>;
98 fn time_in_force(&self) -> Option<TimeInForce>;
99 fn liquidity_side(&self) -> Option<LiquiditySide>;
100 fn post_only(&self) -> Option<bool>;
101 fn reduce_only(&self) -> Option<bool>;
102 fn quote_quantity(&self) -> Option<bool>;
103 fn reconciliation(&self) -> bool;
104 fn price(&self) -> Option<Price>;
105 fn last_px(&self) -> Option<Price>;
106 fn last_qty(&self) -> Option<Quantity>;
107 fn activation_price(&self) -> Option<Price>;
108 fn trigger_price(&self) -> Option<Price>;
109 fn trigger_type(&self) -> Option<TriggerType>;
110 fn limit_offset(&self) -> Option<Decimal>;
111 fn trailing_offset(&self) -> Option<Decimal>;
112 fn trailing_offset_type(&self) -> Option<TrailingOffsetType>;
113 fn expire_time(&self) -> Option<UnixNanos>;
114 fn display_qty(&self) -> Option<Quantity>;
115 fn emulation_trigger(&self) -> Option<TriggerType>;
116 fn trigger_instrument_id(&self) -> Option<InstrumentId>;
117 fn contingency_type(&self) -> Option<ContingencyType>;
118 fn order_list_id(&self) -> Option<OrderListId>;
119 fn linked_order_ids(&self) -> Option<Vec<ClientOrderId>>;
120 fn parent_order_id(&self) -> Option<ClientOrderId>;
121 fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId>;
122 fn exec_algorithm_params(&self) -> Option<IndexMap<Ustr, Ustr>> {
123 None
124 }
125 fn exec_spawn_id(&self) -> Option<ClientOrderId>;
126 fn tags(&self) -> Option<Vec<Ustr>> {
127 None
128 }
129 fn venue_order_id(&self) -> Option<VenueOrderId>;
130 fn account_id(&self) -> Option<AccountId>;
131 fn position_id(&self) -> Option<PositionId>;
132 fn commission(&self) -> Option<Money>;
133 fn ts_event(&self) -> UnixNanos;
134 fn ts_init(&self) -> UnixNanos;
135 fn causation_id(&self) -> Option<UUID4> {
136 None
137 }
138 fn released_price(&self) -> Option<Price> {
139 None
140 }
141 fn protection_price(&self) -> Option<Price> {
142 None
143 }
144 fn due_post_only(&self) -> bool {
145 false
146 }
147 fn correction_id(&self) -> Option<Ustr> {
148 None
149 }
150 fn is_reopened(&self) -> bool {
151 false
152 }
153 fn info(&self) -> Option<IndexMap<Ustr, Ustr>> {
154 None
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use indexmap::IndexMap;
161 use rstest::rstest;
162 use rust_decimal_macros::dec;
163
164 use super::*;
165 use crate::events::order::{any::OrderEventAny, spec::*};
166
167 fn optional_fields_present(event: &dyn OrderEvent) -> Vec<&'static str> {
169 let mut present: Vec<&'static str> = [
170 ("activation_price", event.activation_price().is_some()),
171 ("account_id", event.account_id().is_some()),
172 ("causation_id", event.causation_id().is_some()),
173 ("commission", event.commission().is_some()),
174 ("contingency_type", event.contingency_type().is_some()),
175 ("correction_id", event.correction_id().is_some()),
176 ("currency", event.currency().is_some()),
177 ("display_qty", event.display_qty().is_some()),
178 ("emulation_trigger", event.emulation_trigger().is_some()),
179 ("exec_algorithm_id", event.exec_algorithm_id().is_some()),
180 (
181 "exec_algorithm_params",
182 event.exec_algorithm_params().is_some(),
183 ),
184 ("exec_spawn_id", event.exec_spawn_id().is_some()),
185 ("expire_time", event.expire_time().is_some()),
186 ("info", event.info().is_some()),
187 ("last_px", event.last_px().is_some()),
188 ("last_qty", event.last_qty().is_some()),
189 ("limit_offset", event.limit_offset().is_some()),
190 ("linked_order_ids", event.linked_order_ids().is_some()),
191 ("liquidity_side", event.liquidity_side().is_some()),
192 ("order_list_id", event.order_list_id().is_some()),
193 ("order_side", event.order_side().is_some()),
194 ("order_type", event.order_type().is_some()),
195 ("parent_order_id", event.parent_order_id().is_some()),
196 ("position_id", event.position_id().is_some()),
197 ("post_only", event.post_only().is_some()),
198 ("price", event.price().is_some()),
199 ("protection_price", event.protection_price().is_some()),
200 ("quantity", event.quantity().is_some()),
201 ("quote_quantity", event.quote_quantity().is_some()),
202 ("reason", event.reason().is_some()),
203 ("reduce_only", event.reduce_only().is_some()),
204 ("released_price", event.released_price().is_some()),
205 ("tags", event.tags().is_some()),
206 ("time_in_force", event.time_in_force().is_some()),
207 ("trade_id", event.trade_id().is_some()),
208 ("trailing_offset", event.trailing_offset().is_some()),
209 (
210 "trailing_offset_type",
211 event.trailing_offset_type().is_some(),
212 ),
213 (
214 "trigger_instrument_id",
215 event.trigger_instrument_id().is_some(),
216 ),
217 ("trigger_price", event.trigger_price().is_some()),
218 ("trigger_type", event.trigger_type().is_some()),
219 ("venue_order_id", event.venue_order_id().is_some()),
220 ]
221 .into_iter()
222 .filter(|(_, present)| *present)
223 .map(|(name, _)| name)
224 .collect();
225 present.sort_unstable();
226 present
227 }
228
229 fn params() -> IndexMap<Ustr, Ustr> {
230 IndexMap::from([(Ustr::from("k"), Ustr::from("v"))])
231 }
232
233 fn fully_populated_events() -> Vec<(OrderEventAny, Vec<&'static str>)> {
234 let venue_order_id = VenueOrderId::from("V-1");
235 let account_id = AccountId::from("SIM-001");
236 let reason = Ustr::from("REASON");
237 let px = Price::from("1.00");
238
239 vec![
240 (
241 OrderEventAny::Initialized(
242 OrderInitializedSpec::builder()
243 .price(px)
244 .activation_price(px)
245 .trigger_price(px)
246 .trigger_type(TriggerType::LastPrice)
247 .limit_offset(dec!(0.5))
248 .trailing_offset(dec!(0.5))
249 .trailing_offset_type(TrailingOffsetType::Price)
250 .expire_time(UnixNanos::from(9))
251 .display_qty(Quantity::from(1))
252 .emulation_trigger(TriggerType::BidAsk)
253 .trigger_instrument_id(InstrumentId::from("AUD/USD.SIM"))
254 .contingency_type(ContingencyType::Oto)
255 .order_list_id(OrderListId::from("OL-1"))
256 .linked_order_ids(vec![ClientOrderId::from("O-2")])
257 .parent_order_id(ClientOrderId::from("O-3"))
258 .exec_algorithm_id(ExecAlgorithmId::from("TWAP"))
259 .exec_algorithm_params(params())
260 .exec_spawn_id(ClientOrderId::from("O-4"))
261 .tags(vec![Ustr::from("TAG")])
262 .build(),
263 ),
264 vec![
265 "activation_price",
266 "contingency_type",
267 "display_qty",
268 "emulation_trigger",
269 "exec_algorithm_id",
270 "exec_algorithm_params",
271 "exec_spawn_id",
272 "expire_time",
273 "limit_offset",
274 "linked_order_ids",
275 "order_list_id",
276 "order_side",
277 "order_type",
278 "parent_order_id",
279 "post_only",
280 "price",
281 "quantity",
282 "quote_quantity",
283 "reduce_only",
284 "tags",
285 "time_in_force",
286 "trailing_offset",
287 "trailing_offset_type",
288 "trigger_instrument_id",
289 "trigger_price",
290 "trigger_type",
291 ],
292 ),
293 (
294 OrderEventAny::Denied(OrderDeniedSpec::builder().build()),
295 vec!["reason"],
296 ),
297 (
298 OrderEventAny::Emulated(OrderEmulatedSpec::builder().build()),
299 vec![],
300 ),
301 (
302 OrderEventAny::Released(OrderReleasedSpec::builder().build()),
303 vec!["released_price"],
304 ),
305 (
306 OrderEventAny::Submitted(OrderSubmittedSpec::builder().build()),
307 vec!["account_id"],
308 ),
309 (
310 OrderEventAny::Accepted(OrderAcceptedSpec::builder().build()),
311 vec!["account_id", "venue_order_id"],
312 ),
313 (
314 OrderEventAny::Rejected(OrderRejectedSpec::builder().build()),
315 vec!["account_id", "reason"],
316 ),
317 (
318 OrderEventAny::Canceled(
319 OrderCanceledSpec::builder()
320 .venue_order_id(venue_order_id)
321 .account_id(account_id)
322 .reason(reason)
323 .build(),
324 ),
325 vec!["account_id", "reason", "venue_order_id"],
326 ),
327 (
328 OrderEventAny::Expired(
329 OrderExpiredSpec::builder()
330 .venue_order_id(venue_order_id)
331 .account_id(account_id)
332 .build(),
333 ),
334 vec!["account_id", "venue_order_id"],
335 ),
336 (
337 OrderEventAny::Triggered(
338 OrderTriggeredSpec::builder()
339 .venue_order_id(venue_order_id)
340 .account_id(account_id)
341 .build(),
342 ),
343 vec!["account_id", "venue_order_id"],
344 ),
345 (
346 OrderEventAny::PendingUpdate(
347 OrderPendingUpdateSpec::builder()
348 .venue_order_id(venue_order_id)
349 .account_id(account_id)
350 .build(),
351 ),
352 vec!["account_id", "venue_order_id"],
353 ),
354 (
355 OrderEventAny::PendingCancel(
356 OrderPendingCancelSpec::builder()
357 .venue_order_id(venue_order_id)
358 .account_id(account_id)
359 .build(),
360 ),
361 vec!["account_id", "venue_order_id"],
362 ),
363 (
364 OrderEventAny::ModifyRejected(
365 OrderModifyRejectedSpec::builder()
366 .venue_order_id(venue_order_id)
367 .account_id(account_id)
368 .build(),
369 ),
370 vec!["account_id", "reason", "venue_order_id"],
371 ),
372 (
373 OrderEventAny::CancelRejected(
374 OrderCancelRejectedSpec::builder()
375 .venue_order_id(venue_order_id)
376 .account_id(account_id)
377 .build(),
378 ),
379 vec!["account_id", "reason", "venue_order_id"],
380 ),
381 (
382 OrderEventAny::Updated(
383 OrderUpdatedSpec::builder()
384 .venue_order_id(venue_order_id)
385 .account_id(account_id)
386 .price(px)
387 .trigger_price(px)
388 .protection_price(px)
389 .build(),
390 ),
391 vec![
392 "account_id",
393 "price",
394 "protection_price",
395 "quantity",
396 "quote_quantity",
397 "trigger_price",
398 "venue_order_id",
399 ],
400 ),
401 (
402 OrderEventAny::Filled(
403 OrderFilledSpec::builder()
404 .position_id(PositionId::from("P-1"))
405 .commission(Money::from("1.00 USD"))
406 .info(params())
407 .build(),
408 ),
409 vec![
410 "account_id",
411 "commission",
412 "currency",
413 "info",
414 "last_px",
415 "last_qty",
416 "liquidity_side",
417 "order_side",
418 "order_type",
419 "position_id",
420 "quantity",
421 "trade_id",
422 "venue_order_id",
423 ],
424 ),
425 (
426 OrderEventAny::FillVoided(
427 OrderFillVoidedSpec::builder()
428 .commission_voided(Money::from("1.00 USD"))
429 .position_id(PositionId::from("P-1"))
430 .reason(reason)
431 .info(params())
432 .build(),
433 ),
434 vec![
435 "account_id",
436 "commission",
437 "correction_id",
438 "currency",
439 "info",
440 "last_px",
441 "last_qty",
442 "liquidity_side",
443 "order_side",
444 "order_type",
445 "position_id",
446 "quantity",
447 "reason",
448 "trade_id",
449 "venue_order_id",
450 ],
451 ),
452 ]
453 }
454
455 #[rstest]
456 fn test_optional_field_surface_matches_event_payload() {
457 for (event, expected) in fully_populated_events() {
458 let event_type = event.event_type();
459 let boxed = event.into_boxed();
460
461 assert_eq!(
462 optional_fields_present(boxed.as_ref()),
463 expected,
464 "optional accessor surface changed for {event_type:?}"
465 );
466 }
467 }
468
469 #[rstest]
470 fn test_initialized_accessors_map_distinct_fields() {
471 let event = OrderInitializedSpec::builder()
472 .quantity(Quantity::from(100))
473 .price(Price::from("1.00"))
474 .activation_price(Price::from("2.00"))
475 .trigger_price(Price::from("3.00"))
476 .trigger_type(TriggerType::LastPrice)
477 .emulation_trigger(TriggerType::BidAsk)
478 .limit_offset(dec!(0.5))
479 .trailing_offset(dec!(1.5))
480 .display_qty(Quantity::from(10))
481 .parent_order_id(ClientOrderId::from("O-PARENT"))
482 .exec_spawn_id(ClientOrderId::from("O-SPAWN"))
483 .ts_event(UnixNanos::from(7))
484 .ts_init(UnixNanos::from(8))
485 .build();
486
487 assert_eq!(event.price(), Some(Price::from("1.00")));
488 assert_eq!(event.activation_price(), Some(Price::from("2.00")));
489 assert_eq!(event.trigger_price(), Some(Price::from("3.00")));
490 assert_eq!(event.trigger_type(), Some(TriggerType::LastPrice));
491 assert_eq!(event.emulation_trigger(), Some(TriggerType::BidAsk));
492 assert_eq!(event.limit_offset(), Some(dec!(0.5)));
493 assert_eq!(event.trailing_offset(), Some(dec!(1.5)));
494 assert_eq!(event.quantity(), Some(Quantity::from(100)));
495 assert_eq!(event.display_qty(), Some(Quantity::from(10)));
496 assert_eq!(
497 event.parent_order_id(),
498 Some(ClientOrderId::from("O-PARENT"))
499 );
500 assert_eq!(event.exec_spawn_id(), Some(ClientOrderId::from("O-SPAWN")));
501 assert_eq!(event.ts_event(), UnixNanos::from(7));
502 assert_eq!(event.ts_init(), UnixNanos::from(8));
503 }
504
505 #[rstest]
506 fn test_filled_accessors_map_distinct_fields() {
507 let event = OrderFilledSpec::builder()
508 .last_qty(Quantity::from(25))
509 .last_px(Price::from("4.00"))
510 .commission(Money::from("2.00 USD"))
511 .ts_event(UnixNanos::from(7))
512 .ts_init(UnixNanos::from(8))
513 .build();
514
515 assert_eq!(event.last_px(), Some(Price::from("4.00")));
516 assert_eq!(event.price(), None);
517 assert_eq!(event.last_qty(), Some(Quantity::from(25)));
518 assert_eq!(event.quantity(), Some(Quantity::from(25)));
519 assert_eq!(event.commission(), Some(Money::from("2.00 USD")));
520 assert_eq!(event.ts_event(), UnixNanos::from(7));
521 assert_eq!(event.ts_init(), UnixNanos::from(8));
522 }
523
524 #[rstest]
525 fn test_updated_accessors_map_distinct_prices() {
526 let event = OrderUpdatedSpec::builder()
527 .quantity(Quantity::from(50))
528 .price(Price::from("1.00"))
529 .trigger_price(Price::from("2.00"))
530 .protection_price(Price::from("3.00"))
531 .ts_event(UnixNanos::from(7))
532 .ts_init(UnixNanos::from(8))
533 .build();
534
535 assert_eq!(event.price(), Some(Price::from("1.00")));
536 assert_eq!(event.trigger_price(), Some(Price::from("2.00")));
537 assert_eq!(event.protection_price(), Some(Price::from("3.00")));
538 assert_eq!(event.quantity(), Some(Quantity::from(50)));
539 assert_eq!(event.ts_event(), UnixNanos::from(7));
540 assert_eq!(event.ts_init(), UnixNanos::from(8));
541 }
542
543 #[rstest]
544 fn test_type_name_matches_variant() {
545 let expected = [
546 "OrderInitialized",
547 "OrderDenied",
548 "OrderEmulated",
549 "OrderReleased",
550 "OrderSubmitted",
551 "OrderAccepted",
552 "OrderRejected",
553 "OrderCanceled",
554 "OrderExpired",
555 "OrderTriggered",
556 "OrderPendingUpdate",
557 "OrderPendingCancel",
558 "OrderModifyRejected",
559 "OrderCancelRejected",
560 "OrderUpdated",
561 "OrderFilled",
562 "OrderFillVoided",
563 ];
564
565 let names: Vec<&'static str> = fully_populated_events()
566 .into_iter()
567 .map(|(event, _)| event.into_boxed().type_name())
568 .collect();
569
570 assert_eq!(names, expected);
571 }
572}