1pub mod any;
31pub mod limit;
32pub mod limit_if_touched;
33pub mod list;
34pub mod market;
35pub mod market_if_touched;
36pub mod market_to_limit;
37pub mod stop_limit;
38pub mod stop_market;
39pub mod trailing_stop_limit;
40pub mod trailing_stop_market;
41
42#[cfg(any(test, feature = "stubs"))]
43pub mod builder;
44#[cfg(any(test, feature = "stubs"))]
45pub mod stubs;
46
47use ahash::AHashSet;
49use enum_dispatch::enum_dispatch;
50use indexmap::IndexMap;
51use nautilus_core::{
52 UUID4, UnixNanos,
53 correctness::{CorrectnessError, check_predicate_false},
54};
55use rust_decimal::Decimal;
56use serde::{Deserialize, Serialize};
57use ustr::Ustr;
58
59#[cfg(any(test, feature = "stubs"))]
60pub use crate::orders::builder::OrderTestBuilder;
61pub use crate::orders::{
62 any::{LimitOrderAny, OrderAny, OrderReplayError, PassiveOrderAny, StopOrderAny},
63 limit::LimitOrder,
64 limit_if_touched::LimitIfTouchedOrder,
65 list::{OrderList, OrderListValidationError},
66 market::MarketOrder,
67 market_if_touched::MarketIfTouchedOrder,
68 market_to_limit::MarketToLimitOrder,
69 stop_limit::StopLimitOrder,
70 stop_market::StopMarketOrder,
71 trailing_stop_limit::TrailingStopLimitOrder,
72 trailing_stop_market::TrailingStopMarketOrder,
73};
74use crate::{
75 enums::{
76 ContingencyType, LiquiditySide, OrderSide, OrderSideSpecified, OrderStatus, OrderType,
77 PositionSide, TimeInForce, TrailingOffsetType, TriggerType,
78 },
79 events::{
80 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
81 OrderEventAny, OrderExpired, OrderFillVoided, OrderFilled, OrderInitialized,
82 OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased,
83 OrderSubmitted, OrderTriggered, OrderUpdated,
84 },
85 identifiers::{
86 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
87 StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
88 },
89 orderbook::OwnBookOrder,
90 reports::OrderStatusReport,
91 types::{
92 Currency, Money, Price, Quantity,
93 quantity::{QUANTITY_RAW_MAX, QuantityRaw},
94 },
95};
96
97pub const STOP_ORDER_TYPES: &[OrderType] = &[
99 OrderType::StopMarket,
100 OrderType::StopLimit,
101 OrderType::MarketIfTouched,
102 OrderType::LimitIfTouched,
103];
104
105pub const LIMIT_ORDER_TYPES: &[OrderType] = &[
107 OrderType::Limit,
108 OrderType::StopLimit,
109 OrderType::LimitIfTouched,
110 OrderType::TrailingStopLimit,
111];
112
113pub const TRIGGERABLE_ORDER_TYPES: &[OrderType] = &[
118 OrderType::StopLimit,
119 OrderType::TrailingStopLimit,
120 OrderType::LimitIfTouched,
121];
122
123pub const LOCAL_ACTIVE_ORDER_STATUSES: &[OrderStatus] = &[
125 OrderStatus::Initialized,
126 OrderStatus::Emulated,
127 OrderStatus::Released,
128];
129
130#[derive(thiserror::Error, Debug)]
131pub enum OrderError {
132 #[error("Order not found: {0}")]
133 NotFound(ClientOrderId),
134 #[error("Order invariant failed: must have a side for this operation")]
135 NoOrderSide,
136 #[error("Invalid event for order type")]
137 InvalidOrderEvent,
138 #[error("Invalid order state transition")]
139 InvalidStateTransition,
140 #[error("Order was already initialized")]
141 AlreadyInitialized,
142 #[error("Order had no previous state")]
143 NoPreviousState,
144 #[error("Duplicate fill: trade_id {0} already applied to order")]
145 DuplicateFill(TradeId),
146 #[error("Duplicate fill void: trade_id {0} already has this cumulative correction")]
147 DuplicateFillVoid(TradeId),
148 #[error("Fill void for trade_id {0} is older than the applied correction")]
149 StaleFillVoid(TradeId),
150 #[error("Fill void quantity exceeds the original fill for trade_id {0}")]
151 OverVoid(TradeId),
152 #[error("Fill void commission is missing or conflicts with trade_id {0}")]
153 InvalidFillVoidCommission(TradeId),
154 #[error("{0}")]
155 Invariant(#[from] CorrectnessError),
156}
157
158#[must_use]
160pub fn ustr_indexmap_to_str(h: IndexMap<Ustr, Ustr>) -> IndexMap<String, String> {
161 h.into_iter()
162 .map(|(k, v)| (k.to_string(), v.to_string()))
163 .collect()
164}
165
166#[must_use]
168pub fn str_indexmap_to_ustr(h: IndexMap<String, String>) -> IndexMap<Ustr, Ustr> {
169 h.into_iter()
170 .map(|(k, v)| (Ustr::from(&k), Ustr::from(&v)))
171 .collect()
172}
173
174#[inline]
175pub(crate) fn check_display_qty(
176 display_qty: Option<Quantity>,
177 quantity: Quantity,
178) -> Result<(), OrderError> {
179 if let Some(q) = display_qty {
180 check_predicate_false(q > quantity, "`display_qty` may not exceed `quantity`")?;
181 }
182 Ok(())
183}
184
185#[inline]
186pub(crate) fn check_time_in_force(
187 time_in_force: TimeInForce,
188 expire_time: Option<UnixNanos>,
189) -> Result<(), OrderError> {
190 check_predicate_false(
191 time_in_force == TimeInForce::Gtd && expire_time.unwrap_or_default() == 0,
192 "`expire_time` is required for `GTD` order",
193 )?;
194 Ok(())
195}
196
197#[inline]
198fn checked_quantity_raw_sum(lhs: QuantityRaw, rhs: QuantityRaw) -> Option<QuantityRaw> {
199 lhs.checked_add(rhs).filter(|raw| *raw <= QUANTITY_RAW_MAX)
200}
201
202#[inline]
203fn quantity_from_domain_raw(raw: QuantityRaw, precision: u8) -> Quantity {
204 Quantity::from_raw(raw.min(QUANTITY_RAW_MAX), precision)
205}
206
207impl OrderStatus {
208 #[rustfmt::skip]
214 pub fn transition(&mut self, event: &OrderEventAny) -> Result<Self, OrderError> {
215 let new_state = match (self, event) {
216 (Self::Initialized, OrderEventAny::Denied(_)) => Self::Denied,
217 (Self::Initialized, OrderEventAny::Emulated(_)) => Self::Emulated, (Self::Initialized, OrderEventAny::Released(_)) => Self::Released, (Self::Initialized, OrderEventAny::Submitted(_)) => Self::Submitted,
220 (Self::Initialized, OrderEventAny::Rejected(_)) => Self::Rejected, (Self::Initialized, OrderEventAny::Accepted(_)) => Self::Accepted, (Self::Initialized, OrderEventAny::Canceled(_)) => Self::Canceled, (Self::Initialized, OrderEventAny::Expired(_)) => Self::Expired, (Self::Initialized, OrderEventAny::Triggered(_)) => Self::Triggered, (Self::Initialized, OrderEventAny::Updated(_)) => Self::Initialized, (Self::Emulated, OrderEventAny::Canceled(_)) => Self::Canceled, (Self::Emulated, OrderEventAny::Expired(_)) => Self::Expired, (Self::Emulated, OrderEventAny::Updated(_)) => Self::Emulated, (Self::Emulated, OrderEventAny::Released(_)) => Self::Released, (Self::Released, OrderEventAny::Submitted(_)) => Self::Submitted, (Self::Released, OrderEventAny::Denied(_)) => Self::Denied, (Self::Released, OrderEventAny::Canceled(_)) => Self::Canceled, (Self::Released, OrderEventAny::Updated(_)) => Self::Released, (Self::Submitted, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
235 (Self::Submitted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
236 (Self::Submitted, OrderEventAny::Rejected(_)) => Self::Rejected,
237 (Self::Submitted, OrderEventAny::Canceled(_)) => Self::Canceled, (Self::Submitted, OrderEventAny::Accepted(_)) => Self::Accepted,
239 (Self::Submitted, OrderEventAny::Updated(_)) => Self::Submitted,
240 (Self::Submitted, OrderEventAny::Filled(_)) => Self::Filled,
241 (Self::Accepted, OrderEventAny::Rejected(_)) => Self::Rejected, (Self::Accepted, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
243 (Self::Accepted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
244 (Self::Accepted, OrderEventAny::Canceled(_)) => Self::Canceled,
245 (Self::Accepted, OrderEventAny::Triggered(_)) => Self::Triggered,
246 (Self::Accepted, OrderEventAny::Updated(_)) => Self::Accepted, (Self::Accepted, OrderEventAny::Expired(_)) => Self::Expired,
248 (Self::Accepted, OrderEventAny::Filled(_)) => Self::Filled,
249 (Self::Accepted, OrderEventAny::FillVoided(_)) => Self::Accepted,
250 (Self::Canceled, OrderEventAny::Filled(_)) => Self::Filled, (Self::Canceled, OrderEventAny::FillVoided(_)) => Self::Canceled,
252 (Self::Canceled, OrderEventAny::Updated(_)) => Self::Canceled,
253 (Self::PendingUpdate, OrderEventAny::Rejected(_)) => Self::Rejected,
254 (Self::PendingUpdate, OrderEventAny::Accepted(_)) => Self::Accepted,
255 (Self::PendingUpdate, OrderEventAny::Canceled(_)) => Self::Canceled,
256 (Self::PendingUpdate, OrderEventAny::Expired(_)) => Self::Expired,
257 (Self::PendingUpdate, OrderEventAny::Triggered(_)) => Self::Triggered,
258 (Self::PendingUpdate, OrderEventAny::Submitted(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
261 (Self::PendingUpdate, OrderEventAny::ModifyRejected(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::Updated(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::Filled(_)) => Self::Filled,
264 (Self::PendingUpdate, OrderEventAny::FillVoided(_)) => Self::PendingUpdate,
265 (Self::PendingCancel, OrderEventAny::Rejected(_)) => Self::Rejected,
266 (Self::PendingCancel, OrderEventAny::PendingCancel(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::CancelRejected(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::Canceled(_)) => Self::Canceled,
269 (Self::PendingCancel, OrderEventAny::Expired(_)) => Self::Expired,
270 (Self::PendingCancel, OrderEventAny::Accepted(_)) => Self::Accepted, (Self::PendingCancel, OrderEventAny::Updated(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::Filled(_)) => Self::Filled,
273 (Self::PendingCancel, OrderEventAny::FillVoided(_)) => Self::PendingCancel,
274 (Self::Triggered, OrderEventAny::Rejected(_)) => Self::Rejected,
275 (Self::Triggered, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
276 (Self::Triggered, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
277 (Self::Triggered, OrderEventAny::Canceled(_)) => Self::Canceled,
278 (Self::Triggered, OrderEventAny::Expired(_)) => Self::Expired,
279 (Self::Triggered, OrderEventAny::Filled(_)) => Self::Filled,
280 (Self::Triggered, OrderEventAny::Updated(_)) => Self::Triggered,
281 (Self::Triggered, OrderEventAny::FillVoided(_)) => Self::Triggered,
282 (Self::PartiallyFilled, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
283 (Self::PartiallyFilled, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
284 (Self::PartiallyFilled, OrderEventAny::Canceled(_)) => Self::Canceled,
285 (Self::PartiallyFilled, OrderEventAny::Expired(_)) => Self::Expired,
286 (Self::PartiallyFilled, OrderEventAny::Filled(_)) => Self::Filled,
287 (Self::PartiallyFilled, OrderEventAny::Accepted(_)) => Self::Accepted,
288 (Self::PartiallyFilled, OrderEventAny::Updated(_)) => Self::PartiallyFilled,
289 (Self::PartiallyFilled, OrderEventAny::FillVoided(_)) => Self::PartiallyFilled,
290 (Self::Filled, OrderEventAny::FillVoided(_)) => Self::Voided,
291 (Self::Filled, OrderEventAny::Updated(_)) => Self::Filled,
292 (Self::Expired, OrderEventAny::FillVoided(_)) => Self::Expired,
293 (Self::Expired, OrderEventAny::Updated(_)) => Self::Expired,
294 (Self::Voided, OrderEventAny::FillVoided(_)) => Self::Voided,
295 _ => return Err(OrderError::InvalidStateTransition),
296 };
297 Ok(new_state)
298 }
299}
300
301#[enum_dispatch]
302pub trait Order: 'static + Send {
303 fn into_any(self) -> OrderAny;
304 fn status(&self) -> OrderStatus;
305 fn trader_id(&self) -> TraderId;
306 fn strategy_id(&self) -> StrategyId;
307 fn instrument_id(&self) -> InstrumentId;
308 fn symbol(&self) -> Symbol;
309 fn venue(&self) -> Venue;
310 fn client_order_id(&self) -> ClientOrderId;
311 fn venue_order_id(&self) -> Option<VenueOrderId>;
312 fn position_id(&self) -> Option<PositionId>;
313 fn account_id(&self) -> Option<AccountId>;
314 fn last_trade_id(&self) -> Option<TradeId>;
315 fn order_side(&self) -> OrderSide;
316 fn order_type(&self) -> OrderType;
317 fn quantity(&self) -> Quantity;
318 fn time_in_force(&self) -> TimeInForce;
319 fn expire_time(&self) -> Option<UnixNanos>;
320 fn price(&self) -> Option<Price>;
321 fn activation_price(&self) -> Option<Price> {
322 None
323 }
324 fn trigger_price(&self) -> Option<Price>;
325 fn trigger_type(&self) -> Option<TriggerType>;
326 fn liquidity_side(&self) -> Option<LiquiditySide>;
327 fn is_post_only(&self) -> bool;
328 fn is_reduce_only(&self) -> bool;
329 fn is_quote_quantity(&self) -> bool;
330 fn display_qty(&self) -> Option<Quantity>;
331 fn limit_offset(&self) -> Option<Decimal>;
332 fn trailing_offset(&self) -> Option<Decimal>;
333 fn trailing_offset_type(&self) -> Option<TrailingOffsetType>;
334 fn emulation_trigger(&self) -> Option<TriggerType>;
335 fn trigger_instrument_id(&self) -> Option<InstrumentId>;
336 fn contingency_type(&self) -> Option<ContingencyType>;
337 fn order_list_id(&self) -> Option<OrderListId>;
338 fn linked_order_ids(&self) -> Option<&[ClientOrderId]>;
339 fn parent_order_id(&self) -> Option<ClientOrderId>;
340 fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId>;
341 fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>>;
342 fn exec_spawn_id(&self) -> Option<ClientOrderId>;
343 fn tags(&self) -> Option<&[Ustr]>;
344 fn filled_qty(&self) -> Quantity;
345 fn voided_qty(&self) -> Quantity;
346 fn leaves_qty(&self) -> Quantity;
347 fn overfill_qty(&self) -> Quantity;
348
349 fn calculate_overfill(&self, fill_qty: Quantity) -> Quantity {
351 let potential_filled = self.filled_qty() + fill_qty;
352 let quantity = self.quantity();
353 if potential_filled > quantity {
354 potential_filled - quantity
355 } else {
356 Quantity::zero(fill_qty.precision)
357 }
358 }
359
360 fn avg_px(&self) -> Option<Decimal>;
361 fn slippage(&self) -> Option<Decimal>;
362 fn init_id(&self) -> UUID4;
363 fn ts_init(&self) -> UnixNanos;
364 fn ts_submitted(&self) -> Option<UnixNanos>;
365 fn ts_accepted(&self) -> Option<UnixNanos>;
366 fn ts_closed(&self) -> Option<UnixNanos>;
367 fn ts_last(&self) -> UnixNanos;
368
369 fn order_side_specified(&self) -> OrderSideSpecified {
370 self.order_side().as_specified()
371 }
372 fn commissions(&self) -> &IndexMap<Currency, Money>;
373
374 fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError>;
380
381 fn update(&mut self, event: &OrderUpdated);
382
383 fn events(&self) -> Vec<&OrderEventAny>;
384
385 fn last_event(&self) -> &OrderEventAny {
386 self.events()
387 .last()
388 .expect("Order invariant violated: no events")
389 }
390
391 fn event_count(&self) -> usize {
392 self.events().len()
393 }
394
395 fn venue_order_ids(&self) -> Vec<&VenueOrderId>;
396
397 fn trade_ids(&self) -> Vec<&TradeId>;
398
399 fn has_price(&self) -> bool;
400
401 fn is_duplicate_fill(&self, fill: &OrderFilled) -> bool {
403 self.events().iter().any(|event| {
404 if let OrderEventAny::Filled(existing) = event {
405 existing.trade_id == fill.trade_id
406 && existing.order_side == fill.order_side
407 && existing.last_qty == fill.last_qty
408 && existing.last_px == fill.last_px
409 } else {
410 false
411 }
412 })
413 }
414
415 fn is_buy(&self) -> bool {
416 self.order_side() == OrderSide::Buy
417 }
418
419 fn is_sell(&self) -> bool {
420 self.order_side() == OrderSide::Sell
421 }
422
423 fn is_passive(&self) -> bool {
424 self.order_type() != OrderType::Market
425 }
426
427 fn is_aggressive(&self) -> bool {
428 self.order_type() == OrderType::Market
429 }
430
431 fn is_emulated(&self) -> bool {
432 self.status() == OrderStatus::Emulated
433 }
434
435 fn is_active_local(&self) -> bool {
436 matches!(
437 self.status(),
438 OrderStatus::Initialized | OrderStatus::Emulated | OrderStatus::Released
439 )
440 }
441
442 fn is_primary(&self) -> bool {
443 self.exec_algorithm_id().is_some()
444 && self
445 .exec_spawn_id()
446 .is_some_and(|spawn_id| self.client_order_id() == spawn_id)
447 }
448
449 fn is_spawned(&self) -> bool {
450 self.exec_algorithm_id().is_some()
451 && self
452 .exec_spawn_id()
453 .is_some_and(|spawn_id| self.client_order_id() != spawn_id)
454 }
455
456 fn is_contingency(&self) -> bool {
457 self.contingency_type().is_some()
458 }
459
460 fn is_parent_order(&self) -> bool {
461 match self.contingency_type() {
462 Some(c) => c == ContingencyType::Oto,
463 None => false,
464 }
465 }
466
467 fn is_child_order(&self) -> bool {
468 self.parent_order_id().is_some()
469 }
470
471 fn is_open(&self) -> bool {
472 if let Some(emulation_trigger) = self.emulation_trigger()
473 && emulation_trigger != TriggerType::NoTrigger
474 {
475 return false;
476 }
477
478 matches!(
479 self.status(),
480 OrderStatus::Accepted
481 | OrderStatus::Triggered
482 | OrderStatus::PendingCancel
483 | OrderStatus::PendingUpdate
484 | OrderStatus::PartiallyFilled
485 )
486 }
487
488 fn is_canceled(&self) -> bool {
489 self.status() == OrderStatus::Canceled
490 }
491
492 fn is_closed(&self) -> bool {
493 matches!(
494 self.status(),
495 OrderStatus::Denied
496 | OrderStatus::Rejected
497 | OrderStatus::Canceled
498 | OrderStatus::Expired
499 | OrderStatus::Filled
500 | OrderStatus::Voided
501 )
502 }
503
504 fn is_inflight(&self) -> bool {
505 if let Some(emulation_trigger) = self.emulation_trigger()
506 && emulation_trigger != TriggerType::NoTrigger
507 {
508 return false;
509 }
510
511 matches!(
512 self.status(),
513 OrderStatus::Submitted | OrderStatus::PendingCancel | OrderStatus::PendingUpdate
514 )
515 }
516
517 fn is_pending_update(&self) -> bool {
518 self.status() == OrderStatus::PendingUpdate
519 }
520
521 fn is_pending_cancel(&self) -> bool {
522 self.status() == OrderStatus::PendingCancel
523 }
524
525 fn to_own_book_order(&self) -> OwnBookOrder {
526 OwnBookOrder::new(
527 self.trader_id(),
528 self.client_order_id(),
529 self.venue_order_id(),
530 self.order_side().as_specified(),
531 self.price().expect("`OwnBookOrder` must have a price"),
532 self.leaves_qty(),
533 self.order_type(),
534 self.time_in_force(),
535 self.status(),
536 self.ts_last(),
537 self.ts_accepted().unwrap_or_default(),
538 self.ts_submitted().unwrap_or_default(),
539 self.ts_init(),
540 )
541 }
542
543 fn to_order_status_report(&self, report_id: Option<UUID4>) -> Option<OrderStatusReport> {
549 let account_id = self.account_id()?;
550 let venue_order_id = self.venue_order_id()?;
551
552 let mut report = OrderStatusReport::new(
553 account_id,
554 self.instrument_id(),
555 Some(self.client_order_id()),
556 venue_order_id,
557 self.order_side(),
558 self.order_type(),
559 self.time_in_force(),
560 self.status(),
561 self.quantity(),
562 self.filled_qty(),
563 self.ts_accepted().unwrap_or_else(|| self.ts_last()),
564 self.ts_last(),
565 self.ts_init(),
566 report_id,
567 )
568 .with_post_only(self.is_post_only())
569 .with_reduce_only(self.is_reduce_only());
570
571 if let Some(price) = self.price() {
572 report = report.with_price(price);
573 }
574
575 if let Some(activation_price) = self.activation_price() {
576 report = report.with_activation_price(activation_price);
577 }
578
579 if let Some(trigger_price) = self.trigger_price() {
580 report = report.with_trigger_price(trigger_price);
581 }
582
583 if let Some(trigger_type) = self.trigger_type() {
584 report = report.with_trigger_type(trigger_type);
585 }
586
587 if let Some(limit_offset) = self.limit_offset() {
588 report = report.with_limit_offset(limit_offset);
589 }
590
591 if let Some(trailing_offset) = self.trailing_offset() {
592 report = report.with_trailing_offset(trailing_offset);
593 }
594
595 if let Some(trailing_offset_type) = self.trailing_offset_type() {
596 report = report.with_trailing_offset_type(trailing_offset_type);
597 }
598
599 if let Some(display_qty) = self.display_qty() {
600 report = report.with_display_qty(display_qty);
601 }
602
603 if let Some(expire_time) = self.expire_time() {
604 report = report.with_expire_time(expire_time);
605 }
606
607 if let Some(contingency_type) = self.contingency_type() {
608 report = report.with_contingency_type(contingency_type);
609 }
610
611 if let Some(order_list_id) = self.order_list_id() {
612 report = report.with_order_list_id(order_list_id);
613 }
614
615 if let Some(linked_order_ids) = self.linked_order_ids() {
616 report = report.with_linked_order_ids(linked_order_ids.iter().copied());
617 }
618
619 if let Some(parent_order_id) = self.parent_order_id() {
620 report = report.with_parent_order_id(parent_order_id);
621 }
622
623 if let Some(position_id) = self.position_id() {
624 report = report.with_venue_position_id(position_id);
625 }
626
627 let mut ts_triggered = None;
630 let mut rejected_reason = None;
631
632 for event in self.events() {
633 match event {
634 OrderEventAny::Triggered(triggered) => ts_triggered = Some(triggered.ts_event),
635 OrderEventAny::Rejected(rejected) => rejected_reason = Some(rejected.reason),
636 _ => {}
637 }
638 }
639
640 if let Some(ts_triggered) = ts_triggered {
641 report = report.with_ts_triggered(ts_triggered);
642 }
643
644 if let Some(reason) = rejected_reason {
647 report = report.with_cancel_reason(reason.to_string());
648 }
649
650 if let Some(avg_px) = self.avg_px() {
651 report = report.with_avg_px(avg_px);
652 }
653
654 Some(report)
655 }
656
657 fn is_triggered(&self) -> Option<bool>; fn set_position_id(&mut self, position_id: Option<PositionId>);
659 fn set_quantity(&mut self, quantity: Quantity);
660 fn set_leaves_qty(&mut self, leaves_qty: Quantity);
661 fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>);
662 fn set_is_quote_quantity(&mut self, is_quote_quantity: bool);
663 fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide);
664 fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool;
665 fn previous_status(&self) -> Option<OrderStatus>;
666}
667
668impl<T> From<&T> for OrderInitialized
669where
670 T: Order,
671{
672 fn from(order: &T) -> Self {
673 Self {
674 trader_id: order.trader_id(),
675 strategy_id: order.strategy_id(),
676 instrument_id: order.instrument_id(),
677 client_order_id: order.client_order_id(),
678 order_side: order.order_side(),
679 order_type: order.order_type(),
680 quantity: order.quantity(),
681 price: order.price(),
682 activation_price: order.activation_price(),
683 trigger_price: order.trigger_price(),
684 trigger_type: order.trigger_type(),
685 time_in_force: order.time_in_force(),
686 expire_time: order.expire_time(),
687 post_only: order.is_post_only(),
688 reduce_only: order.is_reduce_only(),
689 quote_quantity: order.is_quote_quantity(),
690 display_qty: order.display_qty(),
691 limit_offset: order.limit_offset(),
692 trailing_offset: order.trailing_offset(),
693 trailing_offset_type: order.trailing_offset_type(),
694 emulation_trigger: order.emulation_trigger(),
695 trigger_instrument_id: order.trigger_instrument_id(),
696 contingency_type: order.contingency_type(),
697 order_list_id: order.order_list_id(),
698 linked_order_ids: order.linked_order_ids().map(<[ClientOrderId]>::to_vec),
699 parent_order_id: order.parent_order_id(),
700 exec_algorithm_id: order.exec_algorithm_id(),
701 exec_algorithm_params: order.exec_algorithm_params().map(ToOwned::to_owned),
702 exec_spawn_id: order.exec_spawn_id(),
703 tags: order.tags().map(<[Ustr]>::to_vec),
704 event_id: order.init_id(),
705 ts_event: order.ts_init(),
706 ts_init: order.ts_init(),
707 reconciliation: false,
708 causation_id: None,
709 }
710 }
711}
712
713#[derive(Clone, Debug, Serialize, Deserialize)]
714pub struct OrderCore {
715 pub events: Vec<OrderEventAny>,
716 pub commissions: IndexMap<Currency, Money>,
717 pub venue_order_ids: Vec<VenueOrderId>,
718 pub trade_ids: Vec<TradeId>,
719 pub previous_status: Option<OrderStatus>,
720 pub status: OrderStatus,
721 pub trader_id: TraderId,
722 pub strategy_id: StrategyId,
723 pub instrument_id: InstrumentId,
724 pub client_order_id: ClientOrderId,
725 pub venue_order_id: Option<VenueOrderId>,
726 pub position_id: Option<PositionId>,
727 pub account_id: Option<AccountId>,
728 pub last_trade_id: Option<TradeId>,
729 pub side: OrderSide,
730 pub order_type: OrderType,
731 pub quantity: Quantity,
732 pub time_in_force: TimeInForce,
733 pub liquidity_side: Option<LiquiditySide>,
734 pub is_reduce_only: bool,
735 pub is_quote_quantity: bool,
736 pub emulation_trigger: Option<TriggerType>,
737 pub contingency_type: Option<ContingencyType>,
738 pub order_list_id: Option<OrderListId>,
739 pub linked_order_ids: Option<Vec<ClientOrderId>>,
740 pub parent_order_id: Option<ClientOrderId>,
741 pub exec_algorithm_id: Option<ExecAlgorithmId>,
742 pub exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
743 pub exec_spawn_id: Option<ClientOrderId>,
744 pub tags: Option<Vec<Ustr>>,
745 pub filled_qty: Quantity,
746 #[serde(default)]
747 pub voided_qty: Quantity,
748 pub leaves_qty: Quantity,
749 pub overfill_qty: Quantity,
750 pub avg_px: Option<Decimal>,
751 pub slippage: Option<Decimal>,
752 pub init_id: UUID4,
753 pub ts_init: UnixNanos,
754 pub ts_submitted: Option<UnixNanos>,
755 pub ts_accepted: Option<UnixNanos>,
756 pub ts_closed: Option<UnixNanos>,
757 pub ts_last: UnixNanos,
758}
759
760impl OrderCore {
761 #[must_use]
763 pub fn new(init: OrderInitialized) -> Self {
764 let events: Vec<OrderEventAny> = vec![OrderEventAny::Initialized(init.clone())];
765 Self {
766 events,
767 commissions: IndexMap::new(),
768 venue_order_ids: Vec::new(),
769 trade_ids: Vec::new(),
770 previous_status: None,
771 status: OrderStatus::Initialized,
772 trader_id: init.trader_id,
773 strategy_id: init.strategy_id,
774 instrument_id: init.instrument_id,
775 client_order_id: init.client_order_id,
776 venue_order_id: None,
777 position_id: None,
778 account_id: None,
779 last_trade_id: None,
780 side: init.order_side,
781 order_type: init.order_type,
782 quantity: init.quantity,
783 time_in_force: init.time_in_force,
784 liquidity_side: Some(LiquiditySide::NoLiquiditySide),
785 is_reduce_only: init.reduce_only,
786 is_quote_quantity: init.quote_quantity,
787 emulation_trigger: init.emulation_trigger.or(Some(TriggerType::NoTrigger)),
788 contingency_type: init
789 .contingency_type
790 .or(Some(ContingencyType::NoContingency)),
791 order_list_id: init.order_list_id,
792 linked_order_ids: init.linked_order_ids,
793 parent_order_id: init.parent_order_id,
794 exec_algorithm_id: init.exec_algorithm_id,
795 exec_algorithm_params: init.exec_algorithm_params,
796 exec_spawn_id: init.exec_spawn_id,
797 tags: init.tags,
798 filled_qty: Quantity::zero(init.quantity.precision),
799 voided_qty: Quantity::zero(init.quantity.precision),
800 leaves_qty: init.quantity,
801 overfill_qty: Quantity::zero(init.quantity.precision),
802 avg_px: None,
803 slippage: None,
804 init_id: init.event_id,
805 ts_init: init.ts_event,
806 ts_submitted: None,
807 ts_accepted: None,
808 ts_closed: None,
809 ts_last: init.ts_event,
810 }
811 }
812
813 pub fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
820 if self.client_order_id != event.client_order_id() {
821 return Err(CorrectnessError::PredicateViolation {
822 message: format!(
823 "Event client_order_id {} does not match order client_order_id {}",
824 event.client_order_id(),
825 self.client_order_id
826 ),
827 }
828 .into());
829 }
830
831 if self.strategy_id != event.strategy_id() {
832 return Err(CorrectnessError::PredicateViolation {
833 message: format!(
834 "Event strategy_id {} does not match order strategy_id {}",
835 event.strategy_id(),
836 self.strategy_id
837 ),
838 }
839 .into());
840 }
841
842 if let OrderEventAny::FillVoided(event) = &event {
843 self.validate_fill_void(event)?;
844 }
845
846 if let OrderEventAny::Filled(fill) = &event
848 && self.events.iter().any(
849 |event| matches!(event, OrderEventAny::Filled(existing) if existing.trade_id == fill.trade_id),
850 )
851 {
852 return Err(OrderError::DuplicateFill(fill.trade_id));
853 }
854
855 if matches!(event, OrderEventAny::Triggered(_))
856 && !TRIGGERABLE_ORDER_TYPES.contains(&self.order_type)
857 {
858 return Err(OrderError::InvalidOrderEvent);
859 }
860
861 if matches!(event, OrderEventAny::Initialized(_)) {
862 return Err(OrderError::AlreadyInitialized);
863 }
864
865 let new_status = self.status.transition(&event)?;
866 if let OrderEventAny::Filled(fill) = &event
867 && checked_quantity_raw_sum(self.filled_qty.raw, fill.last_qty.raw).is_none()
868 {
869 return Err(CorrectnessError::PredicateViolation {
870 message: format!(
871 "filled quantity overflowed Quantity raw bounds: {} + {}",
872 self.filled_qty, fill.last_qty
873 ),
874 }
875 .into());
876 }
877
878 let rejection_status = if matches!(
879 event,
880 OrderEventAny::ModifyRejected(_) | OrderEventAny::CancelRejected(_)
881 ) {
882 self.previous_status.ok_or(OrderError::NoPreviousState)?
883 } else {
884 self.status
885 };
886
887 if let OrderEventAny::Updated(update) = &event {
891 let invalid_fields = match self.order_type {
892 OrderType::Market => update.price.is_some() || update.trigger_price.is_some(),
893 OrderType::StopMarket
894 | OrderType::MarketIfTouched
895 | OrderType::TrailingStopMarket => update.price.is_some(),
896 OrderType::Limit | OrderType::MarketToLimit => update.trigger_price.is_some(),
897 OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit => {
898 false
899 }
900 };
901
902 if invalid_fields {
903 return Err(OrderError::InvalidOrderEvent);
904 }
905 }
906
907 let source_status = self.status;
908
909 if !matches!(
914 event,
915 OrderEventAny::Initialized(_)
916 | OrderEventAny::ModifyRejected(_)
917 | OrderEventAny::CancelRejected(_)
918 ) && !matches!(
919 self.status,
920 OrderStatus::PendingUpdate | OrderStatus::PendingCancel
921 ) {
922 self.previous_status = Some(self.status);
923 }
924
925 self.status = new_status;
926
927 match &event {
928 OrderEventAny::Initialized(_) => return Err(OrderError::AlreadyInitialized),
930 OrderEventAny::Denied(event) => self.denied(event),
931 OrderEventAny::Emulated(event) => self.emulated(event),
932 OrderEventAny::Released(event) => self.released(event),
933 OrderEventAny::Submitted(event) => self.submitted(event),
934 OrderEventAny::Rejected(event) => self.rejected(event),
935 OrderEventAny::Accepted(event) => self.accepted(event),
936 OrderEventAny::PendingUpdate(event) => self.pending_update(event),
937 OrderEventAny::PendingCancel(event) => self.pending_cancel(event),
938 OrderEventAny::ModifyRejected(event) => self.modify_rejected(event, rejection_status),
939 OrderEventAny::CancelRejected(event) => self.cancel_rejected(event, rejection_status),
940 OrderEventAny::Updated(event) => self.updated(event),
941 OrderEventAny::Triggered(event) => self.triggered(event),
942 OrderEventAny::Canceled(event) => self.canceled(event),
943 OrderEventAny::Expired(event) => self.expired(event),
944 OrderEventAny::Filled(event) => self.filled(event),
945 OrderEventAny::FillVoided(event) => self.fill_voided(event, source_status),
946 }
947
948 self.ts_last = event.ts_event();
949 self.events.push(event);
950 Ok(())
951 }
952
953 fn denied(&mut self, event: &OrderDenied) {
954 self.ts_closed = Some(event.ts_event);
955 }
956
957 fn emulated(&self, _event: &OrderEmulated) {
958 }
960
961 fn released(&mut self, _event: &OrderReleased) {
962 self.emulation_trigger = None;
963 }
964
965 fn submitted(&mut self, event: &OrderSubmitted) {
966 self.account_id = Some(event.account_id);
967 self.ts_submitted = Some(event.ts_event);
968 }
969
970 fn accepted(&mut self, event: &OrderAccepted) {
971 self.account_id = Some(event.account_id);
972 self.venue_order_id = Some(event.venue_order_id);
973 self.venue_order_ids.push(event.venue_order_id);
974 self.ts_accepted = Some(event.ts_event);
975 }
976
977 fn rejected(&mut self, event: &OrderRejected) {
978 self.ts_closed = Some(event.ts_event);
979 }
980
981 fn pending_update(&self, _event: &OrderPendingUpdate) {
982 }
984
985 fn pending_cancel(&self, _event: &OrderPendingCancel) {
986 }
988
989 fn modify_rejected(&mut self, _event: &OrderModifyRejected, previous_status: OrderStatus) {
990 self.status = previous_status;
991 }
992
993 fn cancel_rejected(&mut self, _event: &OrderCancelRejected, previous_status: OrderStatus) {
994 self.status = previous_status;
995 }
996
997 fn triggered(&self, _event: &OrderTriggered) {}
998
999 fn canceled(&mut self, event: &OrderCanceled) {
1000 self.ts_closed = Some(event.ts_event);
1001 }
1002
1003 fn expired(&mut self, event: &OrderExpired) {
1004 self.ts_closed = Some(event.ts_event);
1005 }
1006
1007 fn validate_fill_void(&self, event: &OrderFillVoided) -> Result<(), OrderError> {
1008 let fill = self.events.iter().find_map(|candidate| match candidate {
1009 OrderEventAny::Filled(fill) if fill.trade_id == event.trade_id => Some(fill),
1010 _ => None,
1011 });
1012
1013 if event.voided_qty.is_zero() {
1014 return Err(OrderError::OverVoid(event.trade_id));
1015 }
1016
1017 let previous = self.latest_fill_void(event.trade_id);
1018 if let Some(previous) = previous {
1019 if event.voided_qty < previous.voided_qty {
1020 return Err(OrderError::StaleFillVoid(event.trade_id));
1021 }
1022
1023 if event.voided_qty == previous.voided_qty
1024 && event.commission_voided == previous.commission_voided
1025 && event.is_reopened == previous.is_reopened
1026 {
1027 return Err(OrderError::DuplicateFillVoid(event.trade_id));
1028 }
1029 }
1030
1031 let Some(fill) = fill else {
1032 if event.is_reopened {
1033 return Err(OrderError::InvalidOrderEvent);
1034 }
1035
1036 if !self.matches_fill_void_identity(event) {
1037 return Err(OrderError::InvalidOrderEvent);
1038 }
1039
1040 if event.voided_qty > self.quantity
1041 || event
1042 .commission_voided
1043 .is_some_and(|commission| !commission.is_zero())
1044 {
1045 return Err(OrderError::OverVoid(event.trade_id));
1046 }
1047 return Ok(());
1048 };
1049
1050 if fill.venue_order_id != event.venue_order_id
1051 || fill.account_id != event.account_id
1052 || fill.order_side != event.order_side
1053 || fill.order_type != event.order_type
1054 || fill.last_px != event.last_px
1055 || fill.currency != event.currency
1056 || fill.liquidity_side != event.liquidity_side
1057 || fill.position_id != event.position_id
1058 {
1059 return Err(OrderError::InvalidOrderEvent);
1060 }
1061
1062 if event.voided_qty > fill.last_qty {
1063 return Err(OrderError::OverVoid(event.trade_id));
1064 }
1065
1066 self.validate_fill_void_commission(fill, event, previous)
1067 }
1068
1069 fn matches_fill_void_identity(&self, event: &OrderFillVoided) -> bool {
1070 event.trader_id == self.trader_id
1071 && event.instrument_id == self.instrument_id
1072 && event.order_side == self.side
1073 && event.order_type == self.order_type
1074 && (self.venue_order_id == Some(event.venue_order_id)
1075 || self.venue_order_ids.contains(&event.venue_order_id))
1076 && self.account_id == Some(event.account_id)
1077 }
1078
1079 fn validate_fill_void_commission(
1080 &self,
1081 fill: &OrderFilled,
1082 event: &OrderFillVoided,
1083 previous: Option<&OrderFillVoided>,
1084 ) -> Result<(), OrderError> {
1085 match (fill.commission, event.commission_voided) {
1086 (None, None) => Ok(()),
1087 (None, Some(commission)) if commission.is_zero() => Ok(()),
1088 (Some(_), None) if previous.and_then(|prior| prior.commission_voided).is_none() => {
1089 Ok(())
1090 }
1091 (Some(commission), Some(voided))
1092 if commission.currency == voided.currency
1093 && commission.raw.signum() == voided.raw.signum()
1094 && voided.raw.abs() <= commission.raw.abs()
1095 && previous
1096 .and_then(|prior| prior.commission_voided)
1097 .is_none_or(|prior| voided.raw.abs() >= prior.raw.abs()) =>
1098 {
1099 Ok(())
1100 }
1101 _ => Err(OrderError::InvalidFillVoidCommission(event.trade_id)),
1102 }
1103 }
1104
1105 fn latest_fill_void(&self, trade_id: TradeId) -> Option<&OrderFillVoided> {
1106 self.events
1107 .iter()
1108 .rev()
1109 .find_map(|candidate| match candidate {
1110 OrderEventAny::FillVoided(event) if event.trade_id == trade_id => Some(event),
1111 _ => None,
1112 })
1113 }
1114
1115 fn fill_voided(&mut self, event: &OrderFillVoided, source_status: OrderStatus) {
1116 let has_local_fill = self.events.iter().any(
1117 |candidate| matches!(candidate, OrderEventAny::Filled(fill) if fill.trade_id == event.trade_id),
1118 );
1119 let non_reopened_voided_qty = self.recompute_fill_state(Some(event));
1120 let unfilled_qty = self.quantity.saturating_sub(self.filled_qty);
1121 let working_leaves = unfilled_qty.saturating_sub(non_reopened_voided_qty);
1122
1123 match source_status {
1124 OrderStatus::Voided => {
1125 self.status = OrderStatus::Voided;
1126 self.leaves_qty = Quantity::zero(self.quantity.precision);
1127 }
1128 _ if !has_local_fill && !event.is_reopened => {
1129 self.status = OrderStatus::Voided;
1130 self.leaves_qty = Quantity::zero(self.quantity.precision);
1131 }
1132 OrderStatus::Canceled | OrderStatus::Expired => {
1133 self.status = source_status;
1134 self.leaves_qty = unfilled_qty;
1135 }
1136 _ if working_leaves.is_zero() => {
1137 self.status = OrderStatus::Voided;
1138 self.leaves_qty = working_leaves;
1139 }
1140 OrderStatus::PendingUpdate | OrderStatus::PendingCancel | OrderStatus::Triggered => {
1141 self.status = source_status;
1142 self.leaves_qty = working_leaves;
1143 }
1144 _ => {
1145 self.status = if self.filled_qty.is_zero() {
1146 OrderStatus::Accepted
1147 } else {
1148 OrderStatus::PartiallyFilled
1149 };
1150 self.leaves_qty = working_leaves;
1151 }
1152 }
1153
1154 if self.status.is_open() {
1155 self.ts_closed = None;
1156 } else if self.status.is_closed() {
1157 self.ts_closed = self.ts_closed.or(Some(event.ts_event));
1158 }
1159 }
1160
1161 fn recompute_fill_state(&mut self, additional: Option<&OrderFillVoided>) -> Quantity {
1162 let corrections = self.fill_corrections(additional);
1163
1164 let mut filled_raw = Quantity::zero(self.quantity.precision).raw;
1165 let mut voided_raw = Quantity::zero(self.quantity.precision).raw;
1166 let mut non_reopened_voided_raw = Quantity::zero(self.quantity.precision).raw;
1167 let mut commissions = IndexMap::<Currency, Money>::new();
1168 let mut trade_ids = Vec::new();
1169 let mut last_trade_id = None;
1170 let mut position_id = None;
1171 let mut liquidity_side = Some(LiquiditySide::NoLiquiditySide);
1172 let mut matched_corrections = AHashSet::new();
1173
1174 for candidate in &self.events {
1175 let OrderEventAny::Filled(fill) = candidate else {
1176 continue;
1177 };
1178 let correction = corrections.get(&fill.trade_id).copied();
1179 if correction.is_some() {
1180 matched_corrections.insert(fill.trade_id);
1181 }
1182 let removed = Self::removed_fill_qty(fill, correction);
1183 let effective = fill.last_qty - removed;
1184 voided_raw = voided_raw.saturating_add(removed.raw);
1185 if correction.is_some_and(|event| !event.is_reopened) {
1186 non_reopened_voided_raw = non_reopened_voided_raw.saturating_add(removed.raw);
1187 }
1188
1189 if !effective.is_zero() {
1190 filled_raw = filled_raw.saturating_add(effective.raw);
1191 trade_ids.push(fill.trade_id);
1192 last_trade_id = Some(fill.trade_id);
1193 position_id = fill.position_id;
1194 liquidity_side = Some(fill.liquidity_side);
1195 }
1196
1197 if let Some(commission) = fill.commission {
1198 let surviving = correction
1199 .and_then(|event| event.commission_voided)
1200 .filter(|voided| !voided.is_zero())
1201 .map_or(commission, |voided| commission - voided);
1202
1203 if !surviving.is_zero() {
1204 commissions
1205 .entry(surviving.currency)
1206 .and_modify(|total| *total = *total + surviving)
1207 .or_insert(surviving);
1208 }
1209 }
1210 }
1211
1212 for (trade_id, correction) in corrections {
1213 if !matched_corrections.contains(&trade_id) {
1214 voided_raw = voided_raw.saturating_add(correction.voided_qty.raw);
1215 }
1216 }
1217
1218 self.filled_qty = quantity_from_domain_raw(filled_raw, self.quantity.precision);
1219 self.voided_qty = quantity_from_domain_raw(voided_raw, self.quantity.precision);
1220 self.overfill_qty = self.filled_qty.saturating_sub(self.quantity);
1221 self.avg_px = self.avg_px_from_fills(additional, None);
1222 self.commissions = commissions;
1223 self.trade_ids = trade_ids;
1224 self.last_trade_id = last_trade_id;
1225 self.position_id = position_id;
1226 self.liquidity_side = liquidity_side;
1227
1228 quantity_from_domain_raw(non_reopened_voided_raw, self.quantity.precision)
1229 }
1230
1231 fn updated(&mut self, event: &OrderUpdated) {
1232 if matches!(
1233 self.status,
1234 OrderStatus::PendingUpdate | OrderStatus::PendingCancel,
1235 ) && let Some(previous) = self.previous_status
1236 {
1237 self.status = previous;
1238 }
1239
1240 if event.reconciliation && !self.filled_qty.is_zero() && event.quantity == self.filled_qty {
1241 self.status = OrderStatus::Filled;
1242 self.ts_closed = Some(event.ts_event);
1243 }
1244
1245 if let Some(venue_order_id) = &event.venue_order_id
1246 && (self.venue_order_id.is_none()
1247 || venue_order_id != self.venue_order_id.as_ref().unwrap())
1248 {
1249 self.venue_order_id = Some(*venue_order_id);
1250 self.venue_order_ids.push(*venue_order_id);
1251 }
1252
1253 self.is_quote_quantity = event.is_quote_quantity;
1254 }
1255
1256 fn filled(&mut self, event: &OrderFilled) {
1257 let raw = checked_quantity_raw_sum(self.filled_qty.raw, event.last_qty.raw)
1258 .expect("fill raw bounds pre-checked");
1259 let new_filled_qty = Quantity::from_raw(raw, self.filled_qty.precision);
1260
1261 if new_filled_qty > self.quantity {
1263 let overfill_raw = new_filled_qty.raw - self.quantity.raw;
1264 self.overfill_qty = quantity_from_domain_raw(
1265 self.overfill_qty.raw.saturating_add(overfill_raw),
1266 self.filled_qty.precision,
1267 );
1268 }
1269
1270 let new_leaves_qty = self.leaves_qty.saturating_sub(event.last_qty);
1271
1272 if new_filled_qty >= self.quantity {
1273 self.status = OrderStatus::Filled;
1274 self.ts_closed = Some(event.ts_event);
1275 } else if new_leaves_qty.is_zero() && !self.voided_qty.is_zero() {
1276 self.status = OrderStatus::Voided;
1277 self.ts_closed = Some(event.ts_event);
1278 } else {
1279 self.status = OrderStatus::PartiallyFilled;
1280 }
1281
1282 self.venue_order_id = Some(event.venue_order_id);
1283 self.position_id = event.position_id;
1284 self.trade_ids.push(event.trade_id);
1285 self.last_trade_id = Some(event.trade_id);
1286 self.liquidity_side = Some(event.liquidity_side);
1287 self.filled_qty = new_filled_qty;
1288 self.leaves_qty = new_leaves_qty;
1289 self.ts_last = event.ts_event;
1290
1291 if let Some(commission) = event.commission {
1292 let commission_currency = commission.currency;
1293 if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) {
1294 *existing_commission = *existing_commission + commission;
1295 } else {
1296 self.commissions.insert(commission_currency, commission);
1297 }
1298 }
1299
1300 if self.ts_accepted.is_none() {
1301 self.ts_accepted = Some(event.ts_event);
1303 }
1304
1305 self.avg_px = self.avg_px_from_fills(None, Some(event));
1306
1307 debug_assert!(
1308 matches!(
1309 self.status,
1310 OrderStatus::PartiallyFilled | OrderStatus::Filled | OrderStatus::Voided
1311 ),
1312 "Invariant: status must be PartiallyFilled, Filled, or Voided after fill handler (status={:?})",
1313 self.status
1314 );
1315 debug_assert!(
1316 self.venue_order_id.is_some()
1317 && self.last_trade_id.is_some()
1318 && !self.trade_ids.is_empty(),
1319 "Invariant: venue_order_id, last_trade_id, and trade_ids must be set after fill"
1320 );
1321 debug_assert!(
1322 self.filled_qty
1323 .raw
1324 .saturating_add(self.voided_qty.raw)
1325 .saturating_add(self.leaves_qty.raw)
1326 >= self.quantity.raw,
1327 "Invariant: filled_qty + voided_qty + leaves_qty >= quantity (filled={}, voided={}, leaves={}, quantity={})",
1328 self.filled_qty,
1329 self.voided_qty,
1330 self.leaves_qty,
1331 self.quantity
1332 );
1333 }
1334
1335 fn avg_px_from_fills(
1338 &self,
1339 additional: Option<&OrderFillVoided>,
1340 pending: Option<&OrderFilled>,
1341 ) -> Option<Decimal> {
1342 let corrections = self.fill_corrections(additional);
1343 let mut notional = Decimal::ZERO;
1344 let mut quantity = Decimal::ZERO;
1345
1346 for candidate in &self.events {
1347 let OrderEventAny::Filled(fill) = candidate else {
1348 continue;
1349 };
1350 let removed = Self::removed_fill_qty(fill, corrections.get(&fill.trade_id).copied());
1351 let effective = (fill.last_qty - removed).as_decimal();
1352 if effective.is_zero() {
1353 continue;
1354 }
1355
1356 notional = notional.saturating_add(effective.saturating_mul(fill.last_px.as_decimal()));
1357 quantity = quantity.saturating_add(effective);
1358 }
1359
1360 if let Some(fill) = pending {
1361 let last_qty = fill.last_qty.as_decimal();
1362 notional = notional.saturating_add(last_qty.saturating_mul(fill.last_px.as_decimal()));
1363 quantity = quantity.saturating_add(last_qty);
1364 }
1365
1366 notional.checked_div(quantity)
1367 }
1368
1369 fn fill_corrections<'a>(
1370 &'a self,
1371 additional: Option<&'a OrderFillVoided>,
1372 ) -> IndexMap<TradeId, &'a OrderFillVoided> {
1373 let mut corrections = IndexMap::new();
1374
1375 for candidate in &self.events {
1376 if let OrderEventAny::FillVoided(event) = candidate {
1377 corrections.insert(event.trade_id, event);
1378 }
1379 }
1380
1381 if let Some(event) = additional {
1382 corrections.insert(event.trade_id, event);
1383 }
1384
1385 corrections
1386 }
1387
1388 fn removed_fill_qty(fill: &OrderFilled, correction: Option<&OrderFillVoided>) -> Quantity {
1389 correction.map_or_else(
1390 || Quantity::zero(fill.last_qty.precision),
1391 |event| event.voided_qty.min(fill.last_qty),
1392 )
1393 }
1394
1395 pub fn set_slippage(&mut self, price: Price) {
1396 self.slippage = self.avg_px.and_then(|avg_px| {
1397 let current_price = price.as_decimal();
1398 match self.side {
1399 OrderSide::Buy if avg_px > current_price => avg_px.checked_sub(current_price),
1400 OrderSide::Sell if avg_px < current_price => current_price.checked_sub(avg_px),
1401 _ => None,
1402 }
1403 });
1404 }
1405
1406 #[must_use]
1408 pub fn opposite_side(side: OrderSide) -> OrderSide {
1409 match side {
1410 OrderSide::Buy => OrderSide::Sell,
1411 OrderSide::Sell => OrderSide::Buy,
1412 OrderSide::NoOrderSide => OrderSide::NoOrderSide,
1413 }
1414 }
1415
1416 #[must_use]
1418 pub fn closing_side(side: PositionSide) -> OrderSide {
1419 match side {
1420 PositionSide::Long => OrderSide::Sell,
1421 PositionSide::Short => OrderSide::Buy,
1422 PositionSide::Flat => OrderSide::NoOrderSide,
1423 PositionSide::NoPositionSide => OrderSide::NoOrderSide,
1424 }
1425 }
1426
1427 #[must_use]
1431 pub fn signed_decimal_qty(&self) -> Decimal {
1432 match self.side {
1433 OrderSide::Buy => self.quantity.as_decimal(),
1434 OrderSide::Sell => -self.quantity.as_decimal(),
1435 OrderSide::NoOrderSide => panic!("Invalid order side"),
1436 }
1437 }
1438
1439 #[must_use]
1440 pub fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
1441 if side == PositionSide::Flat {
1442 return false;
1443 }
1444
1445 match (self.side, side) {
1446 (OrderSide::Buy, PositionSide::Long) => false,
1447 (OrderSide::Buy, PositionSide::Short) => self.leaves_qty <= position_qty,
1448 (OrderSide::Sell, PositionSide::Short) => false,
1449 (OrderSide::Sell, PositionSide::Long) => self.leaves_qty <= position_qty,
1450 _ => true,
1451 }
1452 }
1453
1454 #[must_use]
1455 pub fn commission(&self, currency: &Currency) -> Option<Money> {
1456 self.commissions.get(currency).copied()
1457 }
1458
1459 #[must_use]
1460 pub fn commissions(&self) -> IndexMap<Currency, Money> {
1461 self.commissions.clone()
1462 }
1463
1464 #[must_use]
1465 pub fn commissions_vec(&self) -> Vec<Money> {
1466 self.commissions.values().copied().collect()
1467 }
1468
1469 #[must_use]
1470 pub fn init_event(&self) -> Option<OrderEventAny> {
1471 self.events.first().cloned()
1472 }
1473}
1474
1475#[cfg(test)]
1476mod tests {
1477 use std::str::FromStr;
1478
1479 use rstest::rstest;
1480 use rust_decimal_macros::dec;
1481
1482 use super::*;
1483 use crate::{
1484 enums::{LiquiditySide, OrderSide, OrderStatus, PositionSide, TriggerType},
1485 events::order::spec::{
1486 OrderAcceptedSpec, OrderCancelRejectedSpec, OrderCanceledSpec, OrderDeniedSpec,
1487 OrderExpiredSpec, OrderFillVoidedSpec, OrderFilledSpec, OrderInitializedSpec,
1488 OrderModifyRejectedSpec, OrderPendingCancelSpec, OrderPendingUpdateSpec,
1489 OrderRejectedSpec, OrderSubmittedSpec, OrderTriggeredSpec, OrderUpdatedSpec,
1490 },
1491 identifiers::InstrumentId,
1492 instruments::{CurrencyPair, Instrument, InstrumentAny, stubs::audusd_sim},
1493 orders::{MarketOrder, builder::OrderTestBuilder, stubs::TestOrderStubs},
1494 types::{
1495 Price, Quantity,
1496 quantity::{QUANTITY_RAW_MAX, QuantityRaw},
1497 },
1498 };
1499
1500 #[rstest]
1511 #[case(OrderSide::Buy, OrderSide::Sell)]
1512 #[case(OrderSide::Sell, OrderSide::Buy)]
1513 #[case(OrderSide::NoOrderSide, OrderSide::NoOrderSide)]
1514 fn test_order_opposite_side(#[case] order_side: OrderSide, #[case] expected_side: OrderSide) {
1515 let result = OrderCore::opposite_side(order_side);
1516 assert_eq!(result, expected_side);
1517 }
1518
1519 #[rstest]
1520 #[case(PositionSide::Long, OrderSide::Sell)]
1521 #[case(PositionSide::Short, OrderSide::Buy)]
1522 #[case(PositionSide::NoPositionSide, OrderSide::NoOrderSide)]
1523 fn test_closing_side(#[case] position_side: PositionSide, #[case] expected_side: OrderSide) {
1524 let result = OrderCore::closing_side(position_side);
1525 assert_eq!(result, expected_side);
1526 }
1527
1528 #[rstest]
1529 #[case(OrderSide::Buy, dec!(10_000))]
1530 #[case(OrderSide::Sell, dec!(-10_000))]
1531 fn test_signed_decimal_qty(#[case] order_side: OrderSide, #[case] expected: Decimal) {
1532 let order: MarketOrder = OrderInitializedSpec::builder()
1533 .order_side(order_side)
1534 .quantity(Quantity::from(10_000))
1535 .build()
1536 .try_into()
1537 .unwrap();
1538
1539 let result = order.signed_decimal_qty();
1540 assert_eq!(result, expected);
1541 }
1542
1543 #[rustfmt::skip]
1544 #[rstest]
1545 #[case(OrderSide::Buy, Quantity::from(100), PositionSide::Long, Quantity::from(50), false)]
1546 #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(50), true)]
1547 #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(100), true)]
1548 #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)]
1549 #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)]
1550 #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(50), true)]
1551 #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(100), true)]
1552 #[case(OrderSide::Sell, Quantity::from(100), PositionSide::Short, Quantity::from(50), false)]
1553 fn test_would_reduce_only(
1554 #[case] order_side: OrderSide,
1555 #[case] order_qty: Quantity,
1556 #[case] position_side: PositionSide,
1557 #[case] position_qty: Quantity,
1558 #[case] expected: bool,
1559 ) {
1560 let order: MarketOrder = OrderInitializedSpec::builder()
1561 .order_side(order_side)
1562 .quantity(order_qty)
1563 .build()
1564 .try_into()
1565 .unwrap();
1566
1567 assert_eq!(
1568 order.would_reduce_only(position_side, position_qty),
1569 expected
1570 );
1571 }
1572
1573 #[rstest]
1574 fn test_order_state_transition_denied() {
1575 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
1576 let denied = OrderDeniedSpec::builder().build();
1577 let event = OrderEventAny::Denied(denied);
1578
1579 order.apply(event.clone()).unwrap();
1580
1581 assert_eq!(order.status, OrderStatus::Denied);
1582 assert!(order.is_closed());
1583 assert!(!order.is_open());
1584 assert_eq!(order.event_count(), 2);
1585 assert_eq!(order.last_event(), &event);
1586 }
1587
1588 #[rstest]
1589 fn test_order_life_cycle_to_filled() {
1590 let init = OrderInitializedSpec::builder().build();
1591 let submitted = OrderSubmittedSpec::builder().build();
1592 let accepted = OrderAcceptedSpec::builder().build();
1593 let filled = OrderFilledSpec::builder().build();
1594
1595 let mut order: MarketOrder = init.clone().try_into().unwrap();
1596 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1597 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1598 order.apply(OrderEventAny::Filled(filled)).unwrap();
1599
1600 assert_eq!(order.client_order_id, init.client_order_id);
1601 assert_eq!(order.status(), OrderStatus::Filled);
1602 assert_eq!(order.filled_qty(), Quantity::from(100_000));
1603 assert_eq!(order.leaves_qty(), Quantity::from(0));
1604 assert_eq!(order.avg_px(), Some(dec!(1.0)));
1605 assert!(!order.is_open());
1606 assert!(order.is_closed());
1607 assert_eq!(order.commission(&Currency::USD()), None);
1608 assert_eq!(order.commissions(), &IndexMap::new());
1609 }
1610
1611 #[rstest]
1612 fn test_order_life_cycle_fills_with_negative_prices() {
1613 let init = OrderInitializedSpec::builder()
1617 .quantity(Quantity::from(100_000))
1618 .build();
1619 let submitted = OrderSubmittedSpec::builder().build();
1620 let accepted = OrderAcceptedSpec::builder().build();
1621 let fill1 = OrderFilledSpec::builder()
1622 .last_qty(Quantity::from(50_000))
1623 .last_px(Price::from("-5.00000"))
1624 .trade_id(TradeId::from("TRADE-1"))
1625 .build();
1626 let fill2 = OrderFilledSpec::builder()
1627 .last_qty(Quantity::from(50_000))
1628 .last_px(Price::from("-7.00000"))
1629 .trade_id(TradeId::from("TRADE-2"))
1630 .build();
1631
1632 let mut order: MarketOrder = init.try_into().unwrap();
1633 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1634 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1635 order.apply(OrderEventAny::Filled(fill1)).unwrap();
1636 order.apply(OrderEventAny::Filled(fill2)).unwrap();
1637
1638 assert_eq!(order.status(), OrderStatus::Filled);
1639 assert_eq!(order.filled_qty(), Quantity::from(100_000));
1640 assert_eq!(order.leaves_qty(), Quantity::from(0));
1641 assert_eq!(order.avg_px(), Some(dec!(-6.0)));
1643 }
1644
1645 fn fill(trade_id: &str, last_qty: &str, last_px: &str) -> (TradeId, Quantity, Price) {
1646 (
1647 TradeId::from(trade_id),
1648 Quantity::from(last_qty),
1649 Price::from(last_px),
1650 )
1651 }
1652
1653 fn market_order_with_fills(
1654 order_side: OrderSide,
1655 quantity: Quantity,
1656 fills: &[(TradeId, Quantity, Price)],
1657 ) -> MarketOrder {
1658 let mut order: MarketOrder = OrderInitializedSpec::builder()
1659 .order_side(order_side)
1660 .quantity(quantity)
1661 .build()
1662 .try_into()
1663 .unwrap();
1664 order
1665 .apply(OrderEventAny::Accepted(
1666 OrderAcceptedSpec::builder().build(),
1667 ))
1668 .unwrap();
1669
1670 for (trade_id, last_qty, last_px) in fills {
1671 order
1672 .apply(OrderEventAny::Filled(
1673 OrderFilledSpec::builder()
1674 .order_side(order_side)
1675 .trade_id(*trade_id)
1676 .last_qty(*last_qty)
1677 .last_px(*last_px)
1678 .build(),
1679 ))
1680 .unwrap();
1681 }
1682
1683 order
1684 }
1685
1686 #[rstest]
1687 #[case(OrderSide::Buy)]
1688 #[case(OrderSide::Sell)]
1689 fn test_avg_px_weighted_average_is_exact(#[case] order_side: OrderSide) {
1690 let order = market_order_with_fills(
1693 order_side,
1694 Quantity::from(6),
1695 &[
1696 fill("TRADE-1", "1", "0.07"),
1697 fill("TRADE-2", "2", "0.13"),
1698 fill("TRADE-3", "3", "0.29"),
1699 ],
1700 );
1701
1702 assert_eq!(order.status(), OrderStatus::Filled);
1703 assert_eq!(order.filled_qty(), Quantity::from(6));
1704 assert_eq!(order.leaves_qty(), Quantity::from(0));
1705 assert_eq!(order.voided_qty(), Quantity::from(0));
1706 assert_eq!(order.avg_px(), Some(dec!(0.2)));
1707 }
1708
1709 #[rstest]
1710 fn test_avg_px_keeps_a_quotient_no_f64_can_hold() {
1711 let order = market_order_with_fills(
1714 OrderSide::Buy,
1715 Quantity::from(3),
1716 &[fill("TRADE-1", "1", "1.00"), fill("TRADE-2", "2", "2.00")],
1717 );
1718
1719 assert_eq!(order.status(), OrderStatus::Filled);
1720 assert_eq!(order.filled_qty(), Quantity::from(3));
1721 assert_eq!(order.avg_px(), Some(dec!(1.6666666666666666666666666667)));
1722 assert_ne!(
1723 order.avg_px(),
1724 Some(Decimal::from_f64_retain(5.0_f64 / 3.0).unwrap())
1725 );
1726 }
1727
1728 #[rstest]
1729 #[case(OrderSide::Buy, "1.07", "1.00", Some(dec!(0.07)))]
1730 #[case(OrderSide::Sell, "0.93", "1.00", Some(dec!(0.07)))]
1731 #[case(OrderSide::Buy, "0.93", "1.00", None)]
1732 #[case(OrderSide::Sell, "1.07", "1.00", None)]
1733 fn test_set_slippage_by_side(
1734 #[case] order_side: OrderSide,
1735 #[case] fill_px: &str,
1736 #[case] reference_px: &str,
1737 #[case] expected: Option<Decimal>,
1738 ) {
1739 let mut order =
1742 market_order_with_fills(order_side, Quantity::from(1), &[fill("T-1", "1", fill_px)]);
1743
1744 order.set_slippage(Price::from(reference_px));
1745
1746 assert_eq!(order.avg_px(), Some(Decimal::from_str(fill_px).unwrap()));
1747 assert_eq!(order.slippage(), expected);
1748 }
1749
1750 #[rstest]
1751 fn test_avg_px_invariant_to_fill_arrival_order() {
1752 let mut fills = [
1756 fill("TRADE-1", "50000", "1.00001"),
1757 fill("TRADE-2", "30000", "1.00002"),
1758 fill("TRADE-3", "20000", "1.00003"),
1759 ];
1760
1761 let ascending = market_order_with_fills(OrderSide::Buy, Quantity::from(100_000), &fills);
1762 fills.reverse();
1763 let descending = market_order_with_fills(OrderSide::Buy, Quantity::from(100_000), &fills);
1764
1765 assert_eq!(ascending.avg_px(), Some(dec!(1.000_017)));
1766 assert_eq!(descending.avg_px(), ascending.avg_px());
1767 assert_eq!(descending.filled_qty(), ascending.filled_qty());
1768 }
1769
1770 #[rstest]
1771 fn test_avg_px_after_fill_void_matches_order_without_voided_fill() {
1772 let surviving = [fill("TRADE-1", "1", "0.05"), fill("TRADE-2", "3", "0.15")];
1773 let voided = fill("TRADE-VOIDED", "2", "0.13");
1774
1775 let expected = market_order_with_fills(OrderSide::Buy, Quantity::from(6), &surviving);
1776 let mut corrected = market_order_with_fills(
1777 OrderSide::Buy,
1778 Quantity::from(6),
1779 &[surviving[0], surviving[1], voided],
1780 );
1781 corrected
1782 .apply(OrderEventAny::FillVoided(
1783 OrderFillVoidedSpec::builder()
1784 .trade_id(voided.0)
1785 .voided_qty(voided.1)
1786 .last_px(voided.2)
1787 .build(),
1788 ))
1789 .unwrap();
1790
1791 let replayed =
1792 OrderAny::from_events(corrected.events().into_iter().cloned().collect()).unwrap();
1793
1794 assert_eq!(expected.avg_px(), Some(dec!(0.125)));
1798 assert_eq!(corrected.avg_px(), expected.avg_px());
1799 assert_eq!(replayed.avg_px(), expected.avg_px());
1800 assert_eq!(corrected.status(), OrderStatus::Voided);
1801 assert_eq!(corrected.filled_qty(), Quantity::from(4));
1802 assert_eq!(corrected.voided_qty(), Quantity::from(2));
1803 assert_eq!(corrected.leaves_qty(), Quantity::from(0));
1804 assert_eq!(replayed.filled_qty(), corrected.filled_qty());
1805 assert_eq!(replayed.voided_qty(), corrected.voided_qty());
1806 }
1807
1808 #[rstest]
1809 fn test_avg_px_recomputed_from_partially_voided_fill() {
1810 let mut order = market_order_with_fills(
1811 OrderSide::Buy,
1812 Quantity::from(6),
1813 &[fill("TRADE-1", "4", "1.00"), fill("TRADE-2", "2", "1.14")],
1814 );
1815 order
1816 .apply(OrderEventAny::FillVoided(
1817 OrderFillVoidedSpec::builder()
1818 .trade_id(TradeId::from("TRADE-1"))
1819 .voided_qty(Quantity::from(2))
1820 .last_px(Price::from("1.00"))
1821 .build(),
1822 ))
1823 .unwrap();
1824
1825 assert_eq!(order.status(), OrderStatus::Voided);
1828 assert_eq!(order.filled_qty(), Quantity::from(4));
1829 assert_eq!(order.voided_qty(), Quantity::from(2));
1830 assert_eq!(order.leaves_qty(), Quantity::from(0));
1831 assert_eq!(order.avg_px(), Some(dec!(1.07)));
1832 }
1833
1834 #[rstest]
1835 fn test_avg_px_over_reopened_fill_void_and_replacement_fill() {
1836 let mut order = market_order_with_fills(
1837 OrderSide::Buy,
1838 Quantity::from(6),
1839 &[fill("TRADE-1", "4", "1.00")],
1840 );
1841 order
1842 .apply(OrderEventAny::FillVoided(
1843 OrderFillVoidedSpec::builder()
1844 .trade_id(TradeId::from("TRADE-1"))
1845 .voided_qty(Quantity::from(2))
1846 .last_px(Price::from("1.00"))
1847 .is_reopened(true)
1848 .build(),
1849 ))
1850 .unwrap();
1851 order
1852 .apply(OrderEventAny::Filled(
1853 OrderFilledSpec::builder()
1854 .trade_id(TradeId::from("TRADE-2"))
1855 .last_qty(Quantity::from(2))
1856 .last_px(Price::from("1.14"))
1857 .build(),
1858 ))
1859 .unwrap();
1860 order.set_slippage(Price::from("1.00"));
1861
1862 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
1866 assert_eq!(order.filled_qty(), Quantity::from(4));
1867 assert_eq!(order.voided_qty(), Quantity::from(2));
1868 assert_eq!(order.leaves_qty(), Quantity::from(2));
1869 assert_eq!(order.avg_px(), Some(dec!(1.07)));
1870 assert_eq!(order.slippage(), Some(dec!(0.07)));
1871 }
1872
1873 #[rstest]
1874 fn test_avg_px_cleared_when_every_fill_is_voided() {
1875 let mut order = market_order_with_fills(
1876 OrderSide::Buy,
1877 Quantity::from(6),
1878 &[fill("TRADE-1", "4", "1.50")],
1879 );
1880 order
1881 .apply(OrderEventAny::FillVoided(
1882 OrderFillVoidedSpec::builder()
1883 .trade_id(TradeId::from("TRADE-1"))
1884 .voided_qty(Quantity::from(4))
1885 .last_px(Price::from("1.50"))
1886 .build(),
1887 ))
1888 .unwrap();
1889
1890 assert_eq!(order.status(), OrderStatus::Accepted);
1891 assert_eq!(order.filled_qty(), Quantity::from(0));
1892 assert_eq!(order.voided_qty(), Quantity::from(4));
1893 assert_eq!(order.leaves_qty(), Quantity::from(2));
1894 assert_eq!(order.avg_px(), None);
1895 }
1896
1897 #[cfg(feature = "high-precision")]
1898 #[rstest]
1899 fn test_avg_px_exact_over_high_precision_quantities() {
1900 let order = market_order_with_fills(
1903 OrderSide::Buy,
1904 Quantity::from("0.000000000010"),
1905 &[
1906 fill("TRADE-1", "0.000000000005", "1.00001"),
1907 fill("TRADE-2", "0.000000000003", "1.00002"),
1908 fill("TRADE-3", "0.000000000002", "1.00003"),
1909 ],
1910 );
1911
1912 assert_eq!(order.status(), OrderStatus::Filled);
1913 assert_eq!(order.filled_qty(), Quantity::from("0.000000000010"));
1914 assert_eq!(order.avg_px(), Some(dec!(1.000_017)));
1915 }
1916
1917 #[cfg(feature = "high-precision")]
1918 #[rstest]
1919 fn test_avg_px_exact_at_the_widest_representable_fill_scale() {
1920 let order = market_order_with_fills(
1923 OrderSide::Buy,
1924 Quantity::from("0.0000000000000008"),
1925 &[
1926 fill("TRADE-1", "0.0000000000000002", "0.000000000001"),
1927 fill("TRADE-2", "0.0000000000000006", "0.000000000003"),
1928 ],
1929 );
1930
1931 assert_eq!(order.status(), OrderStatus::Filled);
1933 assert_eq!(order.filled_qty(), Quantity::from("0.0000000000000008"));
1934 assert_eq!(order.avg_px(), Some(dec!(2.5e-12)));
1935 }
1936
1937 #[rstest]
1938 fn test_order_life_cycle_accumulates_fill_commissions() {
1939 let init = OrderInitializedSpec::builder()
1940 .quantity(Quantity::from(100_000))
1941 .build();
1942 let submitted = OrderSubmittedSpec::builder().build();
1943 let accepted = OrderAcceptedSpec::builder().build();
1944 let fill1 = OrderFilledSpec::builder()
1945 .last_qty(Quantity::from(50_000))
1946 .trade_id(TradeId::from("TRADE-1"))
1947 .commission(Money::from("1.25 USD"))
1948 .build();
1949 let fill2 = OrderFilledSpec::builder()
1950 .last_qty(Quantity::from(50_000))
1951 .trade_id(TradeId::from("TRADE-2"))
1952 .commission(Money::from("1.35 USD"))
1953 .build();
1954
1955 let mut order: MarketOrder = init.try_into().unwrap();
1956 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1957 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1958 order.apply(OrderEventAny::Filled(fill1)).unwrap();
1959 order.apply(OrderEventAny::Filled(fill2)).unwrap();
1960
1961 assert_eq!(order.status(), OrderStatus::Filled);
1962 assert_eq!(
1963 order.commission(&Currency::USD()),
1964 Some(Money::from("2.60 USD"))
1965 );
1966 assert_eq!(order.commissions_vec(), vec![Money::from("2.60 USD")]);
1967 }
1968
1969 #[rstest]
1970 fn test_fill_void_reopens_only_when_explicitly_marked() {
1971 let init = OrderInitializedSpec::builder()
1972 .quantity(Quantity::from(100_000))
1973 .build();
1974 let filled = OrderFilledSpec::builder()
1975 .trade_id(TradeId::from("TRADE-VOID"))
1976 .last_qty(Quantity::from(100_000))
1977 .commission(Money::from("1.00 USD"))
1978 .build();
1979 let fill_voided = OrderFillVoidedSpec::builder()
1980 .trade_id(filled.trade_id)
1981 .voided_qty(Quantity::from(40_000))
1982 .commission_voided(Money::from("0.40 USD"))
1983 .build();
1984 let later_fill = OrderFilledSpec::builder()
1985 .trade_id(TradeId::from("TRADE-AFTER-REOPEN"))
1986 .last_qty(Quantity::from(40_000))
1987 .build();
1988 let mut order: MarketOrder = init.try_into().unwrap();
1989 order
1990 .apply(OrderEventAny::Accepted(
1991 OrderAcceptedSpec::builder().build(),
1992 ))
1993 .unwrap();
1994 order.apply(OrderEventAny::Filled(filled)).unwrap();
1995
1996 let mut terminal = order.clone();
1997 terminal
1998 .apply(OrderEventAny::FillVoided(fill_voided.clone()))
1999 .unwrap();
2000 let fill_while_terminal = terminal.apply(OrderEventAny::Filled(later_fill.clone()));
2001
2002 order
2003 .apply(OrderEventAny::FillVoided(OrderFillVoided {
2004 is_reopened: true,
2005 ..fill_voided
2006 }))
2007 .unwrap();
2008 let status_after_correction = order.status();
2009 order.apply(OrderEventAny::Filled(later_fill)).unwrap();
2010
2011 assert!(matches!(
2012 fill_while_terminal,
2013 Err(OrderError::InvalidStateTransition)
2014 ));
2015 assert_eq!(terminal.status(), OrderStatus::Voided);
2016 assert_eq!(status_after_correction, OrderStatus::PartiallyFilled);
2017 assert_eq!(order.status(), OrderStatus::Filled);
2018 assert_eq!(order.filled_qty(), Quantity::from(100_000));
2019 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2020 assert_eq!(order.leaves_qty(), Quantity::from(0));
2021 assert_eq!(
2022 order.commission(&Currency::USD()),
2023 Some(Money::from("0.60 USD"))
2024 );
2025 }
2026
2027 #[rstest]
2028 fn test_fill_void_preserves_existing_working_remainder() {
2029 let init = OrderInitializedSpec::builder()
2030 .quantity(Quantity::from(100_000))
2031 .build();
2032 let filled = OrderFilledSpec::builder()
2033 .trade_id(TradeId::from("TRADE-VOID"))
2034 .last_qty(Quantity::from(40_000))
2035 .build();
2036 let later_fill = OrderFilledSpec::builder()
2037 .trade_id(TradeId::from("TRADE-REMAINDER"))
2038 .last_qty(Quantity::from(60_000))
2039 .build();
2040 let mut order: MarketOrder = init.try_into().unwrap();
2041 order
2042 .apply(OrderEventAny::Accepted(
2043 OrderAcceptedSpec::builder().build(),
2044 ))
2045 .unwrap();
2046 order.apply(OrderEventAny::Filled(filled.clone())).unwrap();
2047
2048 order
2049 .apply(OrderEventAny::FillVoided(
2050 OrderFillVoidedSpec::builder()
2051 .trade_id(filled.trade_id)
2052 .voided_qty(filled.last_qty)
2053 .build(),
2054 ))
2055 .unwrap();
2056
2057 assert_eq!(order.status(), OrderStatus::Accepted);
2058 assert_eq!(order.filled_qty(), Quantity::from(0));
2059 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2060 assert_eq!(order.leaves_qty(), Quantity::from(60_000));
2061 assert!(order.is_open());
2062
2063 order.apply(OrderEventAny::Filled(later_fill)).unwrap();
2064
2065 assert_eq!(order.status(), OrderStatus::Voided);
2066 assert_eq!(order.filled_qty(), Quantity::from(60_000));
2067 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2068 assert_eq!(order.leaves_qty(), Quantity::from(0));
2069 assert!(order.is_closed());
2070 }
2071
2072 #[rstest]
2073 fn test_reopened_fill_void_requires_existing_fill() {
2074 let trade_id = TradeId::from("TRADE-OUT-OF-ORDER");
2075 let init = OrderInitializedSpec::builder()
2076 .quantity(Quantity::from(100_000))
2077 .build();
2078 let early_void = OrderFillVoidedSpec::builder()
2079 .trade_id(trade_id)
2080 .voided_qty(Quantity::from(10_000))
2081 .is_reopened(true)
2082 .build();
2083 let late_fill = OrderFilledSpec::builder()
2084 .trade_id(trade_id)
2085 .last_qty(Quantity::from(40_000))
2086 .build();
2087 let mut order: MarketOrder = init.try_into().unwrap();
2088 order
2089 .apply(OrderEventAny::Accepted(
2090 OrderAcceptedSpec::builder().build(),
2091 ))
2092 .unwrap();
2093
2094 let result = order.apply(OrderEventAny::FillVoided(early_void));
2095 order.apply(OrderEventAny::Filled(late_fill)).unwrap();
2096
2097 assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
2098 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2099 assert_eq!(order.filled_qty(), Quantity::from(40_000));
2100 assert_eq!(order.voided_qty(), Quantity::from(0));
2101 assert_eq!(order.leaves_qty(), Quantity::from(60_000));
2102 }
2103
2104 #[rstest]
2105 #[case(OrderStatus::Accepted)]
2106 #[case(OrderStatus::Canceled)]
2107 #[case(OrderStatus::Expired)]
2108 fn test_terminal_fill_void_rejects_later_order_events(#[case] status_before_void: OrderStatus) {
2109 let trade_id = TradeId::from("TRADE-TERMINAL-VOID");
2110 let init = OrderInitializedSpec::builder()
2111 .quantity(Quantity::from(100_000))
2112 .build();
2113 let terminal_void = OrderFillVoidedSpec::builder()
2114 .trade_id(trade_id)
2115 .voided_qty(Quantity::from(40_000))
2116 .commission_voided(Money::from("0 USD"))
2117 .build();
2118 let late_fill = OrderFilledSpec::builder()
2119 .trade_id(trade_id)
2120 .last_qty(Quantity::from(40_000))
2121 .build();
2122 let mut order: MarketOrder = init.try_into().unwrap();
2123 order
2124 .apply(OrderEventAny::Accepted(
2125 OrderAcceptedSpec::builder().build(),
2126 ))
2127 .unwrap();
2128
2129 match status_before_void {
2130 OrderStatus::Accepted => {}
2131 OrderStatus::Canceled => order
2132 .apply(OrderEventAny::Canceled(
2133 OrderCanceledSpec::builder().build(),
2134 ))
2135 .unwrap(),
2136 OrderStatus::Expired => order
2137 .apply(OrderEventAny::Expired(OrderExpiredSpec::builder().build()))
2138 .unwrap(),
2139 _ => unreachable!(),
2140 }
2141
2142 order
2143 .apply(OrderEventAny::FillVoided(terminal_void))
2144 .unwrap();
2145 let update_result = order.apply(OrderEventAny::Updated(
2146 OrderUpdatedSpec::builder()
2147 .quantity(Quantity::from(120_000))
2148 .build(),
2149 ));
2150 let cancel_result = order.apply(OrderEventAny::Canceled(
2151 OrderCanceledSpec::builder().build(),
2152 ));
2153 let fill_result = order.apply(OrderEventAny::Filled(late_fill));
2154
2155 assert!(matches!(
2156 update_result,
2157 Err(OrderError::InvalidStateTransition)
2158 ));
2159 assert!(matches!(
2160 cancel_result,
2161 Err(OrderError::InvalidStateTransition)
2162 ));
2163 assert!(matches!(
2164 fill_result,
2165 Err(OrderError::InvalidStateTransition)
2166 ));
2167 assert_eq!(order.status(), OrderStatus::Voided);
2168 assert_eq!(order.filled_qty(), Quantity::from(0));
2169 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2170 assert_eq!(order.leaves_qty(), Quantity::from(0));
2171 }
2172
2173 #[rstest]
2174 #[case(Quantity::from(100_001), None)]
2175 #[case(Quantity::from(100_000), Some(Money::from("1 USD")))]
2176 fn test_terminal_fill_void_rejects_invalid_economic_values(
2177 #[case] voided_qty: Quantity,
2178 #[case] commission_voided: Option<Money>,
2179 ) {
2180 let trade_id = TradeId::from("TRADE-TERMINAL-VOID");
2181 let init = OrderInitializedSpec::builder()
2182 .quantity(Quantity::from(100_000))
2183 .build();
2184 let mut order: MarketOrder = init.try_into().unwrap();
2185 order
2186 .apply(OrderEventAny::Accepted(
2187 OrderAcceptedSpec::builder().build(),
2188 ))
2189 .unwrap();
2190 let mut invalid = OrderFillVoidedSpec::builder()
2191 .trade_id(trade_id)
2192 .voided_qty(voided_qty)
2193 .build();
2194 invalid.commission_voided = commission_voided;
2195
2196 let result = order.apply(OrderEventAny::FillVoided(invalid));
2197
2198 assert!(matches!(
2199 result,
2200 Err(OrderError::OverVoid(error_trade_id)) if error_trade_id == trade_id
2201 ));
2202 assert_eq!(order.status(), OrderStatus::Accepted);
2203 assert_eq!(order.voided_qty(), Quantity::from(0));
2204 }
2205
2206 #[rstest]
2207 fn test_terminal_fill_void_accepts_current_venue_order_id_after_fill_before_accept() {
2208 let init = OrderInitializedSpec::builder()
2209 .quantity(Quantity::from(100_000))
2210 .build();
2211 let submitted = OrderSubmittedSpec::builder().build();
2212 let filled = OrderFilledSpec::builder()
2213 .account_id(submitted.account_id)
2214 .trade_id(TradeId::from("TRADE-APPLIED"))
2215 .last_qty(Quantity::from(40_000))
2216 .build();
2217 let terminal_void = OrderFillVoidedSpec::builder()
2218 .venue_order_id(filled.venue_order_id)
2219 .account_id(filled.account_id)
2220 .trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
2221 .voided_qty(Quantity::from(10_000))
2222 .build();
2223 let mut order: MarketOrder = init.try_into().unwrap();
2224 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2225 order.apply(OrderEventAny::Filled(filled)).unwrap();
2226
2227 order
2228 .apply(OrderEventAny::FillVoided(terminal_void))
2229 .unwrap();
2230
2231 assert_eq!(order.status(), OrderStatus::Voided);
2232 assert_eq!(order.filled_qty(), Quantity::from(40_000));
2233 assert_eq!(order.voided_qty(), Quantity::from(10_000));
2234 assert_eq!(order.leaves_qty(), Quantity::from(0));
2235 }
2236
2237 #[rstest]
2238 fn test_terminal_fill_void_requires_matching_order_identity() {
2239 let init = OrderInitializedSpec::builder()
2240 .quantity(Quantity::from(100_000))
2241 .build();
2242 let mut order: MarketOrder = init.try_into().unwrap();
2243 order
2244 .apply(OrderEventAny::Accepted(
2245 OrderAcceptedSpec::builder().build(),
2246 ))
2247 .unwrap();
2248 let mismatched = OrderFillVoidedSpec::builder()
2249 .instrument_id(InstrumentId::from("OTHER.SIM"))
2250 .trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
2251 .voided_qty(Quantity::from(100_000))
2252 .build();
2253
2254 let result = order.apply(OrderEventAny::FillVoided(mismatched));
2255
2256 assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
2257 assert_eq!(order.status(), OrderStatus::Accepted);
2258 assert_eq!(order.voided_qty(), Quantity::from(0));
2259 }
2260
2261 #[rstest]
2262 #[case(OrderStatus::Canceled)]
2263 #[case(OrderStatus::Expired)]
2264 fn test_fill_void_does_not_reopen_canceled_or_expired_order(
2265 #[case] terminal_status: OrderStatus,
2266 ) {
2267 let init = OrderInitializedSpec::builder()
2268 .quantity(Quantity::from(100_000))
2269 .build();
2270 let filled = OrderFilledSpec::builder()
2271 .trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
2272 .last_qty(Quantity::from(60_000))
2273 .build();
2274 let mut order: MarketOrder = init.try_into().unwrap();
2275 order
2276 .apply(OrderEventAny::Accepted(
2277 OrderAcceptedSpec::builder().build(),
2278 ))
2279 .unwrap();
2280 order.apply(OrderEventAny::Filled(filled.clone())).unwrap();
2281 match terminal_status {
2282 OrderStatus::Canceled => order
2283 .apply(OrderEventAny::Canceled(
2284 OrderCanceledSpec::builder().build(),
2285 ))
2286 .unwrap(),
2287 OrderStatus::Expired => order
2288 .apply(OrderEventAny::Expired(OrderExpiredSpec::builder().build()))
2289 .unwrap(),
2290 _ => unreachable!(),
2291 }
2292 let voided = OrderFillVoidedSpec::builder()
2293 .trade_id(filled.trade_id)
2294 .voided_qty(Quantity::from(20_000))
2295 .is_reopened(true)
2296 .build();
2297
2298 order.apply(OrderEventAny::FillVoided(voided)).unwrap();
2299
2300 assert_eq!(order.status(), terminal_status);
2301 assert_eq!(order.filled_qty(), Quantity::from(40_000));
2302 assert_eq!(order.voided_qty(), Quantity::from(20_000));
2303 assert!(order.is_closed());
2304 assert!(order.ts_closed().is_some());
2305 }
2306
2307 #[rstest]
2308 fn test_fill_void_rejects_duplicate_stale_and_over_void() {
2309 let init = OrderInitializedSpec::builder()
2310 .quantity(Quantity::from(100_000))
2311 .build();
2312 let filled = OrderFilledSpec::builder()
2313 .trade_id(TradeId::from("TRADE-VOID"))
2314 .last_qty(Quantity::from(100_000))
2315 .build();
2316 let mut order: MarketOrder = init.try_into().unwrap();
2317 order
2318 .apply(OrderEventAny::Accepted(
2319 OrderAcceptedSpec::builder().build(),
2320 ))
2321 .unwrap();
2322 order.apply(OrderEventAny::Filled(filled)).unwrap();
2323 order
2324 .apply(OrderEventAny::FillVoided(
2325 OrderFillVoidedSpec::builder()
2326 .trade_id(TradeId::from("TRADE-VOID"))
2327 .voided_qty(Quantity::from(40_000))
2328 .build(),
2329 ))
2330 .unwrap();
2331
2332 let duplicate = order.apply(OrderEventAny::FillVoided(
2333 OrderFillVoidedSpec::builder()
2334 .trade_id(TradeId::from("TRADE-VOID"))
2335 .voided_qty(Quantity::from(40_000))
2336 .build(),
2337 ));
2338 let stale = order.apply(OrderEventAny::FillVoided(
2339 OrderFillVoidedSpec::builder()
2340 .trade_id(TradeId::from("TRADE-VOID"))
2341 .voided_qty(Quantity::from(30_000))
2342 .build(),
2343 ));
2344 let over_void = order.apply(OrderEventAny::FillVoided(
2345 OrderFillVoidedSpec::builder()
2346 .trade_id(TradeId::from("TRADE-VOID"))
2347 .voided_qty(Quantity::from(100_001))
2348 .build(),
2349 ));
2350 assert!(matches!(
2351 duplicate,
2352 Err(OrderError::DuplicateFillVoid(trade_id))
2353 if trade_id == TradeId::from("TRADE-VOID")
2354 ));
2355 assert!(matches!(
2356 stale,
2357 Err(OrderError::StaleFillVoid(trade_id))
2358 if trade_id == TradeId::from("TRADE-VOID")
2359 ));
2360 assert!(matches!(
2361 over_void,
2362 Err(OrderError::OverVoid(trade_id))
2363 if trade_id == TradeId::from("TRADE-VOID")
2364 ));
2365 assert_eq!(order.filled_qty(), Quantity::from(60_000));
2366 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2367 }
2368
2369 #[rstest]
2370 fn test_unknown_fill_void_does_not_reverse_surviving_fill() {
2371 let init = OrderInitializedSpec::builder()
2372 .quantity(Quantity::from(100_000))
2373 .build();
2374 let filled = OrderFilledSpec::builder()
2375 .trade_id(TradeId::from("TRADE-APPLIED"))
2376 .last_qty(Quantity::from(60_000))
2377 .build();
2378 let voided = OrderFillVoidedSpec::builder()
2379 .trade_id(TradeId::from("TRADE-NEVER-APPLIED"))
2380 .voided_qty(Quantity::from(40_000))
2381 .build();
2382 let mut order: MarketOrder = init.try_into().unwrap();
2383 order
2384 .apply(OrderEventAny::Accepted(
2385 OrderAcceptedSpec::builder().build(),
2386 ))
2387 .unwrap();
2388 order.apply(OrderEventAny::Filled(filled)).unwrap();
2389
2390 order.apply(OrderEventAny::FillVoided(voided)).unwrap();
2391 order
2392 .apply(OrderEventAny::FillVoided(
2393 OrderFillVoidedSpec::builder()
2394 .trade_id(TradeId::from("TRADE-APPLIED"))
2395 .voided_qty(Quantity::from(10_000))
2396 .is_reopened(true)
2397 .build(),
2398 ))
2399 .unwrap();
2400 let late_fill = order.apply(OrderEventAny::Filled(
2401 OrderFilledSpec::builder()
2402 .trade_id(TradeId::from("TRADE-LATE"))
2403 .last_qty(Quantity::from(10_000))
2404 .build(),
2405 ));
2406
2407 assert_eq!(order.status(), OrderStatus::Voided);
2408 assert_eq!(order.filled_qty(), Quantity::from(50_000));
2409 assert_eq!(order.voided_qty(), Quantity::from(50_000));
2410 assert_eq!(order.leaves_qty(), Quantity::zero(0));
2411 assert!(order.is_closed());
2412 assert!(matches!(late_fill, Err(OrderError::InvalidStateTransition)));
2413 }
2414
2415 #[rstest]
2416 fn test_order_state_transition_to_canceled() {
2417 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
2418 let submitted = OrderSubmittedSpec::builder().build();
2419 let canceled = OrderCanceledSpec::builder().build();
2420
2421 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2422 order.apply(OrderEventAny::Canceled(canceled)).unwrap();
2423
2424 assert_eq!(order.status(), OrderStatus::Canceled);
2425 assert!(order.is_closed());
2426 assert!(!order.is_open());
2427 }
2428
2429 #[rstest]
2430 fn test_order_life_cycle_to_partially_filled() {
2431 let init = OrderInitializedSpec::builder().build();
2432 let submitted = OrderSubmittedSpec::builder().build();
2433 let accepted = OrderAcceptedSpec::builder().build();
2434 let filled = OrderFilledSpec::builder()
2435 .last_qty(Quantity::from(50_000))
2436 .build();
2437
2438 let mut order: MarketOrder = init.clone().try_into().unwrap();
2439 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2440 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2441 order.apply(OrderEventAny::Filled(filled)).unwrap();
2442
2443 assert_eq!(order.client_order_id, init.client_order_id);
2444 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2445 assert_eq!(order.filled_qty(), Quantity::from(50_000));
2446 assert_eq!(order.leaves_qty(), Quantity::from(50_000));
2447 assert!(order.is_open());
2448 assert!(!order.is_closed());
2449 }
2450
2451 #[rstest]
2452 fn test_order_commission_calculation() {
2453 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
2454 order
2455 .commissions
2456 .insert(Currency::USD(), Money::new(10.0, Currency::USD()));
2457
2458 assert_eq!(
2459 order.commission(&Currency::USD()),
2460 Some(Money::new(10.0, Currency::USD()))
2461 );
2462 assert_eq!(
2463 order.commissions_vec(),
2464 vec![Money::new(10.0, Currency::USD())]
2465 );
2466 }
2467
2468 #[rstest]
2469 fn test_order_is_primary() {
2470 let order: MarketOrder = OrderInitializedSpec::builder()
2471 .exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
2472 .exec_spawn_id(ClientOrderId::from("O-001"))
2473 .client_order_id(ClientOrderId::from("O-001"))
2474 .build()
2475 .try_into()
2476 .unwrap();
2477
2478 assert!(order.is_primary());
2479 assert!(!order.is_spawned());
2480 }
2481
2482 #[rstest]
2483 fn test_order_is_spawned() {
2484 let order: MarketOrder = OrderInitializedSpec::builder()
2485 .exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
2486 .exec_spawn_id(ClientOrderId::from("O-002"))
2487 .client_order_id(ClientOrderId::from("O-001"))
2488 .build()
2489 .try_into()
2490 .unwrap();
2491
2492 assert!(!order.is_primary());
2493 assert!(order.is_spawned());
2494 }
2495
2496 #[rstest]
2497 fn test_order_is_contingency() {
2498 let order: MarketOrder = OrderInitializedSpec::builder()
2499 .contingency_type(ContingencyType::Oto)
2500 .linked_order_ids(vec![ClientOrderId::from("O-LINKED")])
2501 .build()
2502 .try_into()
2503 .unwrap();
2504
2505 assert!(order.is_contingency());
2506 assert!(order.is_parent_order());
2507 assert!(!order.is_child_order());
2508 }
2509
2510 #[rstest]
2511 fn test_order_is_child_order() {
2512 let order: MarketOrder = OrderInitializedSpec::builder()
2513 .parent_order_id(ClientOrderId::from("PARENT-001"))
2514 .build()
2515 .try_into()
2516 .unwrap();
2517
2518 assert!(order.is_child_order());
2519 assert!(!order.is_parent_order());
2520 }
2521
2522 #[rstest]
2523 fn test_to_own_book_order_timestamp_ordering() {
2524 use crate::orders::limit::LimitOrder;
2525
2526 let init = OrderInitializedSpec::builder()
2528 .price(Price::from("100.00"))
2529 .build();
2530 let submitted = OrderSubmittedSpec::builder()
2531 .ts_event(UnixNanos::from(1_000_000))
2532 .build();
2533 let accepted = OrderAcceptedSpec::builder()
2534 .ts_event(UnixNanos::from(2_000_000))
2535 .build();
2536
2537 let mut order: LimitOrder = init.try_into().unwrap();
2538 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2539 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2540
2541 let own_book_order = order.to_own_book_order();
2542
2543 assert_eq!(own_book_order.ts_submitted, UnixNanos::from(1_000_000));
2545 assert_eq!(own_book_order.ts_accepted, UnixNanos::from(2_000_000));
2546 assert_eq!(own_book_order.ts_last, UnixNanos::from(2_000_000));
2547 }
2548
2549 #[rstest]
2550 fn test_to_own_book_order_partial_fill_uses_leaves_qty() {
2551 use crate::orders::limit::LimitOrder;
2552
2553 let init = OrderInitializedSpec::builder()
2554 .price(Price::from("100.00"))
2555 .quantity(Quantity::from(100_000))
2556 .build();
2557 let submitted = OrderSubmittedSpec::builder().build();
2558 let accepted = OrderAcceptedSpec::builder().build();
2559 let filled = OrderFilledSpec::builder()
2560 .order_type(OrderType::Limit)
2561 .last_qty(Quantity::from(40_000))
2562 .last_px(Price::from("100.00"))
2563 .build();
2564
2565 let mut order: LimitOrder = init.try_into().unwrap();
2566 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2567 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2568 order.apply(OrderEventAny::Filled(filled)).unwrap();
2569
2570 let own_book_order = order.to_own_book_order();
2571
2572 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2573 assert_eq!(order.leaves_qty(), Quantity::from(60_000));
2574 assert_eq!(own_book_order.size, Quantity::from(60_000));
2575 }
2576
2577 #[rstest]
2578 fn test_order_accepted_without_submitted_sets_account_id() {
2579 let init = OrderInitializedSpec::builder().build();
2581 let accepted = OrderAcceptedSpec::builder()
2582 .account_id(AccountId::from("EXTERNAL-001"))
2583 .build();
2584
2585 let mut order: MarketOrder = init.try_into().unwrap();
2586
2587 assert_eq!(order.account_id(), None);
2589
2590 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2592
2593 assert_eq!(order.account_id(), Some(AccountId::from("EXTERNAL-001")));
2595 assert_eq!(order.status(), OrderStatus::Accepted);
2596 }
2597
2598 #[rstest]
2599 fn test_order_accepted_after_submitted_preserves_account_id() {
2600 let init = OrderInitializedSpec::builder().build();
2602 let submitted = OrderSubmittedSpec::builder()
2603 .account_id(AccountId::from("SUBMITTED-001"))
2604 .build();
2605 let accepted = OrderAcceptedSpec::builder()
2606 .account_id(AccountId::from("ACCEPTED-001"))
2607 .build();
2608
2609 let mut order: MarketOrder = init.try_into().unwrap();
2610 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2611
2612 assert_eq!(order.account_id(), Some(AccountId::from("SUBMITTED-001")));
2614
2615 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2617
2618 assert_eq!(order.account_id(), Some(AccountId::from("ACCEPTED-001")));
2620 assert_eq!(order.status(), OrderStatus::Accepted);
2621 }
2622
2623 #[rstest]
2624 fn test_overfill_tracks_overfill_qty() {
2625 let init = OrderInitializedSpec::builder()
2627 .quantity(Quantity::from(100_000))
2628 .build();
2629 let submitted = OrderSubmittedSpec::builder().build();
2630 let accepted = OrderAcceptedSpec::builder().build();
2631 let overfill = OrderFilledSpec::builder()
2632 .last_qty(Quantity::from(110_000)) .build();
2634
2635 let mut order: MarketOrder = init.try_into().unwrap();
2636 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2637 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2638 order.apply(OrderEventAny::Filled(overfill)).unwrap();
2639
2640 assert_eq!(order.overfill_qty(), Quantity::from(10_000));
2642 assert_eq!(order.filled_qty(), Quantity::from(110_000));
2643 assert_eq!(order.leaves_qty(), Quantity::from(0));
2644 assert_eq!(order.status(), OrderStatus::Filled);
2645 }
2646
2647 #[rstest]
2648 fn test_partial_fill_then_overfill() {
2649 let init = OrderInitializedSpec::builder()
2651 .quantity(Quantity::from(100_000))
2652 .build();
2653 let submitted = OrderSubmittedSpec::builder().build();
2654 let accepted = OrderAcceptedSpec::builder().build();
2655 let fill1 = OrderFilledSpec::builder()
2656 .last_qty(Quantity::from(80_000))
2657 .trade_id(TradeId::from("TRADE-1"))
2658 .build();
2659 let fill2 = OrderFilledSpec::builder()
2660 .last_qty(Quantity::from(30_000)) .trade_id(TradeId::from("TRADE-2"))
2662 .build();
2663
2664 let mut order: MarketOrder = init.try_into().unwrap();
2665 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2666 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2667 order.apply(OrderEventAny::Filled(fill1)).unwrap();
2668
2669 assert_eq!(order.overfill_qty(), Quantity::from(0));
2671 assert_eq!(order.filled_qty(), Quantity::from(80_000));
2672 assert_eq!(order.leaves_qty(), Quantity::from(20_000));
2673
2674 order.apply(OrderEventAny::Filled(fill2)).unwrap();
2675
2676 assert_eq!(order.overfill_qty(), Quantity::from(10_000));
2678 assert_eq!(order.filled_qty(), Quantity::from(110_000));
2679 assert_eq!(order.leaves_qty(), Quantity::from(0));
2680 assert_eq!(order.status(), OrderStatus::Filled);
2681 }
2682
2683 #[rstest]
2684 fn test_exact_fill_no_overfill() {
2685 let init = OrderInitializedSpec::builder()
2687 .quantity(Quantity::from(100_000))
2688 .build();
2689 let submitted = OrderSubmittedSpec::builder().build();
2690 let accepted = OrderAcceptedSpec::builder().build();
2691 let filled = OrderFilledSpec::builder()
2692 .last_qty(Quantity::from(100_000)) .build();
2694
2695 let mut order: MarketOrder = init.try_into().unwrap();
2696 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2697 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2698 order.apply(OrderEventAny::Filled(filled)).unwrap();
2699
2700 assert_eq!(order.overfill_qty(), Quantity::from(0));
2702 assert_eq!(order.filled_qty(), Quantity::from(100_000));
2703 assert_eq!(order.leaves_qty(), Quantity::from(0));
2704 }
2705
2706 #[rstest]
2707 fn test_partial_fill_then_overfill_with_fractional_quantities() {
2708 let init = OrderInitializedSpec::builder()
2712 .quantity(Quantity::from("2450.5"))
2713 .build();
2714 let submitted = OrderSubmittedSpec::builder().build();
2715 let accepted = OrderAcceptedSpec::builder().build();
2716 let fill1 = OrderFilledSpec::builder()
2717 .last_qty(Quantity::from("1202.5"))
2718 .trade_id(TradeId::from("TRADE-1"))
2719 .build();
2720 let fill2 = OrderFilledSpec::builder()
2721 .last_qty(Quantity::from("1285.5")) .trade_id(TradeId::from("TRADE-2"))
2723 .build();
2724
2725 let mut order: MarketOrder = init.try_into().unwrap();
2726 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2727 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2728 order.apply(OrderEventAny::Filled(fill1)).unwrap();
2729
2730 assert_eq!(order.overfill_qty(), Quantity::from(0));
2732 assert_eq!(order.filled_qty(), Quantity::from("1202.5"));
2733 assert_eq!(order.leaves_qty(), Quantity::from("1248.0"));
2734 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2735
2736 order.apply(OrderEventAny::Filled(fill2)).unwrap();
2737
2738 assert_eq!(order.overfill_qty(), Quantity::from("37.5"));
2740 assert_eq!(order.filled_qty(), Quantity::from("2488.0"));
2741 assert_eq!(order.leaves_qty(), Quantity::from(0));
2742 assert_eq!(order.status(), OrderStatus::Filled);
2743 }
2744
2745 #[rstest]
2746 fn test_calculate_overfill_returns_zero_when_no_overfill() {
2747 let order: MarketOrder = OrderInitializedSpec::builder()
2748 .quantity(Quantity::from(100_000))
2749 .build()
2750 .try_into()
2751 .unwrap();
2752
2753 let overfill = order.calculate_overfill(Quantity::from(50_000));
2755 assert_eq!(overfill, Quantity::from(0));
2756
2757 let overfill = order.calculate_overfill(Quantity::from(100_000));
2759 assert_eq!(overfill, Quantity::from(0));
2760 }
2761
2762 #[rstest]
2763 fn test_calculate_overfill_returns_overfill_amount() {
2764 let order: MarketOrder = OrderInitializedSpec::builder()
2765 .quantity(Quantity::from(100_000))
2766 .build()
2767 .try_into()
2768 .unwrap();
2769
2770 let overfill = order.calculate_overfill(Quantity::from(110_000));
2772 assert_eq!(overfill, Quantity::from(10_000));
2773 }
2774
2775 #[rstest]
2776 fn test_calculate_overfill_accounts_for_existing_fills() {
2777 let init = OrderInitializedSpec::builder()
2778 .quantity(Quantity::from(100_000))
2779 .build();
2780 let submitted = OrderSubmittedSpec::builder().build();
2781 let accepted = OrderAcceptedSpec::builder().build();
2782 let partial_fill = OrderFilledSpec::builder()
2783 .last_qty(Quantity::from(60_000))
2784 .build();
2785
2786 let mut order: MarketOrder = init.try_into().unwrap();
2787 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2788 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2789 order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
2790
2791 let overfill = order.calculate_overfill(Quantity::from(50_000));
2794 assert_eq!(overfill, Quantity::from(10_000));
2795
2796 let overfill = order.calculate_overfill(Quantity::from(40_000));
2798 assert_eq!(overfill, Quantity::from(0));
2799 }
2800
2801 #[rstest]
2802 fn test_calculate_overfill_with_fractional_quantities() {
2803 let order: MarketOrder = OrderInitializedSpec::builder()
2804 .quantity(Quantity::from("2450.5"))
2805 .build()
2806 .try_into()
2807 .unwrap();
2808
2809 let overfill = order.calculate_overfill(Quantity::from("2488.0"));
2812 assert_eq!(overfill, Quantity::from("37.5"));
2813 }
2814
2815 #[rstest]
2816 fn test_calculate_overfill_zero_after_fractional_partial_fill() {
2817 let init = OrderInitializedSpec::builder()
2818 .quantity(Quantity::from("1.000"))
2819 .build();
2820 let submitted = OrderSubmittedSpec::builder().build();
2821 let accepted = OrderAcceptedSpec::builder().build();
2822 let partial_fill = OrderFilledSpec::builder()
2823 .last_qty(Quantity::from("0.072"))
2824 .build();
2825
2826 let mut order: MarketOrder = init.try_into().unwrap();
2827 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2828 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2829 order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
2830
2831 let overfill = order.calculate_overfill(Quantity::from("0.072"));
2833 assert_eq!(overfill, Quantity::from("0.000"));
2834 }
2835
2836 #[rstest]
2837 fn test_duplicate_fill_rejected() {
2838 let init = OrderInitializedSpec::builder()
2839 .quantity(Quantity::from(100_000))
2840 .build();
2841 let submitted = OrderSubmittedSpec::builder().build();
2842 let accepted = OrderAcceptedSpec::builder().build();
2843 let fill1 = OrderFilledSpec::builder()
2844 .last_qty(Quantity::from(50_000))
2845 .trade_id(TradeId::from("TRADE-001"))
2846 .build();
2847 let fill2_duplicate = OrderFilledSpec::builder()
2848 .last_qty(Quantity::from(50_000))
2849 .trade_id(TradeId::from("TRADE-001")) .build();
2851
2852 let mut order: MarketOrder = init.try_into().unwrap();
2853 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2854 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2855 order.apply(OrderEventAny::Filled(fill1)).unwrap();
2856
2857 assert_eq!(order.filled_qty(), Quantity::from(50_000));
2859 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2860
2861 let result = order.apply(OrderEventAny::Filled(fill2_duplicate));
2863 assert!(result.is_err());
2864 match result.unwrap_err() {
2865 OrderError::DuplicateFill(trade_id) => {
2866 assert_eq!(trade_id, TradeId::from("TRADE-001"));
2867 }
2868 e => panic!("Expected DuplicateFill error, was: {e:?}"),
2869 }
2870
2871 assert_eq!(order.filled_qty(), Quantity::from(50_000));
2873 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2874 }
2875
2876 #[rstest]
2877 fn test_fill_raw_overflow_rejected_without_mutation() {
2878 let unit = Quantity::from(1);
2879 let almost_max = Quantity::from_raw(QUANTITY_RAW_MAX - unit.raw, 0);
2880 let max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
2881 let init = OrderInitializedSpec::builder().quantity(max).build();
2882 let accepted = OrderAcceptedSpec::builder().build();
2883 let fill1 = OrderFilledSpec::builder()
2884 .last_qty(almost_max)
2885 .trade_id(TradeId::from("TRADE-001"))
2886 .build();
2887 let fill2 = OrderFilledSpec::builder()
2888 .last_qty(Quantity::from(2))
2889 .trade_id(TradeId::from("TRADE-002"))
2890 .build();
2891
2892 let mut order: MarketOrder = init.try_into().unwrap();
2893 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2894 order.apply(OrderEventAny::Filled(fill1)).unwrap();
2895
2896 assert_eq!(order.filled_qty(), almost_max);
2897 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2898
2899 let result = order.apply(OrderEventAny::Filled(fill2));
2900 let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
2901 else {
2902 panic!("Expected typed invariant error, was: {result:?}");
2903 };
2904 assert_eq!(
2905 message,
2906 format!("filled quantity overflowed Quantity raw bounds: {almost_max} + 2")
2907 );
2908 assert_eq!(order.filled_qty(), almost_max);
2909 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2910 assert_eq!(order.event_count(), 3);
2911 }
2912
2913 #[rstest]
2914 fn test_late_fill_on_filled_order_is_invalid_transition_not_overflow() {
2915 let max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
2916 let init = OrderInitializedSpec::builder().quantity(max).build();
2917 let accepted = OrderAcceptedSpec::builder().build();
2918 let fill1 = OrderFilledSpec::builder()
2919 .last_qty(max)
2920 .trade_id(TradeId::from("TRADE-001"))
2921 .build();
2922 let fill2 = OrderFilledSpec::builder()
2923 .last_qty(Quantity::from(1))
2924 .trade_id(TradeId::from("TRADE-002"))
2925 .build();
2926
2927 let mut order: MarketOrder = init.try_into().unwrap();
2928 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2929 order.apply(OrderEventAny::Filled(fill1)).unwrap();
2930
2931 let result = order.apply(OrderEventAny::Filled(fill2));
2932 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
2933 assert_eq!(order.filled_qty(), max);
2934 assert_eq!(order.status(), OrderStatus::Filled);
2935 }
2936
2937 #[rstest]
2938 fn test_quantity_from_domain_raw_clamps_undef_sentinel() {
2939 let qty = quantity_from_domain_raw(QuantityRaw::MAX, 0);
2940
2941 assert_eq!(qty, Quantity::from_raw(QUANTITY_RAW_MAX, 0));
2942 assert!(!qty.is_undefined());
2943 }
2944
2945 #[rstest]
2946 fn test_check_display_qty_returns_typed_invariant_with_stable_display() {
2947 let error = check_display_qty(Some(Quantity::from(2)), Quantity::from(1)).unwrap_err();
2948
2949 match error {
2950 OrderError::Invariant(CorrectnessError::PredicateViolation { ref message }) => {
2951 assert_eq!(message, "`display_qty` may not exceed `quantity`");
2952 }
2953 other => panic!("Expected typed invariant error, was: {other:?}"),
2954 }
2955
2956 assert_eq!(error.to_string(), "`display_qty` may not exceed `quantity`");
2957 }
2958
2959 #[rstest]
2960 fn test_check_time_in_force_returns_typed_invariant_with_stable_display() {
2961 let error = check_time_in_force(TimeInForce::Gtd, None).unwrap_err();
2962
2963 match error {
2964 OrderError::Invariant(CorrectnessError::PredicateViolation { ref message }) => {
2965 assert_eq!(message, "`expire_time` is required for `GTD` order");
2966 }
2967 other => panic!("Expected typed invariant error, was: {other:?}"),
2968 }
2969
2970 assert_eq!(
2971 error.to_string(),
2972 "`expire_time` is required for `GTD` order"
2973 );
2974 }
2975
2976 #[rstest]
2977 fn test_different_trade_ids_allowed() {
2978 let init = OrderInitializedSpec::builder()
2979 .quantity(Quantity::from(100_000))
2980 .build();
2981 let submitted = OrderSubmittedSpec::builder().build();
2982 let accepted = OrderAcceptedSpec::builder().build();
2983 let fill1 = OrderFilledSpec::builder()
2984 .last_qty(Quantity::from(50_000))
2985 .trade_id(TradeId::from("TRADE-001"))
2986 .build();
2987 let fill2 = OrderFilledSpec::builder()
2988 .last_qty(Quantity::from(50_000))
2989 .trade_id(TradeId::from("TRADE-002")) .build();
2991
2992 let mut order: MarketOrder = init.try_into().unwrap();
2993 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2994 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2995 order.apply(OrderEventAny::Filled(fill1)).unwrap();
2996 order.apply(OrderEventAny::Filled(fill2)).unwrap();
2997
2998 assert_eq!(order.filled_qty(), Quantity::from(100_000));
3000 assert_eq!(order.status(), OrderStatus::Filled);
3001 assert_eq!(order.trade_ids.len(), 2);
3002 }
3003
3004 #[rstest]
3005 fn test_pending_update_order_restores_status_on_updated() {
3006 let init = OrderInitializedSpec::builder()
3007 .quantity(Quantity::from(100_000))
3008 .build();
3009 let submitted = OrderSubmittedSpec::builder().build();
3010 let accepted = OrderAcceptedSpec::builder().build();
3011 let pending_update = OrderPendingUpdateSpec::builder().build();
3012 let updated = OrderUpdatedSpec::builder()
3013 .quantity(Quantity::from(50_000))
3014 .build();
3015
3016 let mut order: MarketOrder = init.try_into().unwrap();
3017 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3018 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3019
3020 assert_eq!(order.status(), OrderStatus::Accepted);
3021
3022 order
3023 .apply(OrderEventAny::PendingUpdate(pending_update))
3024 .unwrap();
3025 assert_eq!(order.status(), OrderStatus::PendingUpdate);
3026
3027 order.apply(OrderEventAny::Updated(updated)).unwrap();
3028
3029 assert_eq!(order.status(), OrderStatus::Accepted);
3030 assert_eq!(order.quantity(), Quantity::from(50_000));
3031 }
3032
3033 #[rstest]
3034 fn test_pending_update_accepts_delayed_submitted() {
3035 let init = OrderInitializedSpec::builder()
3036 .quantity(Quantity::from(100_000))
3037 .build();
3038 let submitted = OrderSubmittedSpec::builder()
3039 .ts_event(UnixNanos::from(1_000))
3040 .build();
3041 let accepted = OrderAcceptedSpec::builder().build();
3042 let pending_update = OrderPendingUpdateSpec::builder().build();
3043
3044 let delayed_submitted = OrderSubmittedSpec::builder()
3048 .account_id(AccountId::from("SIM-002"))
3049 .ts_event(UnixNanos::from(3_000))
3050 .build();
3051 let updated = OrderUpdatedSpec::builder()
3052 .quantity(Quantity::from(50_000))
3053 .build();
3054
3055 let mut order: MarketOrder = init.try_into().unwrap();
3056 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3057 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3058 order
3059 .apply(OrderEventAny::PendingUpdate(pending_update))
3060 .unwrap();
3061
3062 assert_eq!(order.status(), OrderStatus::PendingUpdate);
3063 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
3064 assert_eq!(order.account_id(), Some(AccountId::from("SIM-001")));
3065 assert_eq!(order.ts_submitted(), Some(UnixNanos::from(1_000)));
3066
3067 order
3068 .apply(OrderEventAny::Submitted(delayed_submitted))
3069 .unwrap();
3070
3071 assert_eq!(order.status(), OrderStatus::PendingUpdate);
3074 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
3075
3076 assert_eq!(order.account_id(), Some(AccountId::from("SIM-002")));
3077 assert_eq!(order.ts_submitted(), Some(UnixNanos::from(3_000)));
3078
3079 order.apply(OrderEventAny::Updated(updated)).unwrap();
3080
3081 assert_eq!(order.status(), OrderStatus::Accepted);
3082 assert_eq!(order.quantity(), Quantity::from(50_000));
3083 }
3084
3085 #[rstest]
3086 fn test_rejected_event_preserves_status_restoration() {
3087 let init = OrderInitializedSpec::builder().build();
3088 let submitted = OrderSubmittedSpec::builder().build();
3089 let accepted = OrderAcceptedSpec::builder().build();
3090 let invalid = OrderSubmittedSpec::builder().build();
3091 let pending_update = OrderPendingUpdateSpec::builder().build();
3092 let modify_rejected = OrderModifyRejectedSpec::builder().build();
3093 let mut order: MarketOrder = init.try_into().unwrap();
3094 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3095 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3096 let previous_status = order.previous_status();
3097
3098 let result = order.apply(OrderEventAny::Submitted(invalid));
3099
3100 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
3101 assert_eq!(order.previous_status(), previous_status);
3102
3103 order
3104 .apply(OrderEventAny::PendingUpdate(pending_update))
3105 .unwrap();
3106 order
3107 .apply(OrderEventAny::ModifyRejected(modify_rejected))
3108 .unwrap();
3109 assert_eq!(order.status(), OrderStatus::Accepted);
3110 }
3111
3112 #[rstest]
3113 fn test_rejected_event_preserves_cancel_rejected_restoration() {
3114 let init = OrderInitializedSpec::builder().build();
3115 let submitted = OrderSubmittedSpec::builder().build();
3116 let accepted = OrderAcceptedSpec::builder().build();
3117 let invalid = OrderSubmittedSpec::builder().build();
3118 let pending_cancel = OrderPendingCancelSpec::builder().build();
3119 let cancel_rejected = OrderCancelRejectedSpec::builder().build();
3120 let mut order: MarketOrder = init.try_into().unwrap();
3121 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3122 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3123 let previous_status = order.previous_status();
3124
3125 let result = order.apply(OrderEventAny::Submitted(invalid));
3126
3127 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
3128 assert_eq!(order.previous_status(), previous_status);
3129
3130 order
3131 .apply(OrderEventAny::PendingCancel(pending_cancel))
3132 .unwrap();
3133 order
3134 .apply(OrderEventAny::CancelRejected(cancel_rejected))
3135 .unwrap();
3136 assert_eq!(order.status(), OrderStatus::Accepted);
3137 }
3138
3139 #[rstest]
3140 #[case(true, false, "client_order_id")]
3141 #[case(false, true, "strategy_id")]
3142 fn test_update_identity_mismatch_precedes_field_validation(
3143 #[case] wrong_client: bool,
3144 #[case] wrong_strategy: bool,
3145 #[case] expected_field: &str,
3146 ) {
3147 let init = OrderInitializedSpec::builder().build();
3148 let submitted = OrderSubmittedSpec::builder().build();
3149 let accepted = OrderAcceptedSpec::builder().build();
3150 let mut order: MarketOrder = init.try_into().unwrap();
3151 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3152 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3153 let state = (
3154 order.status(),
3155 order.previous_status(),
3156 order.ts_last(),
3157 order.events().len(),
3158 );
3159 let client_order_id = if wrong_client {
3160 ClientOrderId::from("O-INVALID")
3161 } else {
3162 order.client_order_id()
3163 };
3164 let strategy_id = if wrong_strategy {
3165 StrategyId::from("OTHER-001")
3166 } else {
3167 order.strategy_id()
3168 };
3169 let updated = OrderUpdatedSpec::builder()
3172 .client_order_id(client_order_id)
3173 .strategy_id(strategy_id)
3174 .price(Price::new(95.0, 2))
3175 .build();
3176
3177 let result = order.apply(OrderEventAny::Updated(updated));
3178
3179 let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
3181 else {
3182 panic!("expected an identity predicate violation, was {result:?}");
3183 };
3184 assert!(
3185 message.contains(expected_field),
3186 "message did not name {expected_field}: {message}"
3187 );
3188 assert_eq!(order.status(), state.0);
3189 assert_eq!(order.previous_status(), state.1);
3190 assert_eq!(order.ts_last(), state.2);
3191 assert_eq!(order.events().len(), state.3);
3192 }
3193
3194 #[rstest]
3195 fn test_update_invalid_transition_precedes_field_validation() {
3196 let init = OrderInitializedSpec::builder().build();
3197 let denied = OrderDeniedSpec::builder().build();
3198 let mut order: MarketOrder = init.try_into().unwrap();
3199 order.apply(OrderEventAny::Denied(denied)).unwrap();
3200 let state = (
3201 order.status(),
3202 order.previous_status(),
3203 order.ts_last(),
3204 order.events().len(),
3205 );
3206 let updated = OrderUpdatedSpec::builder()
3209 .price(Price::new(95.0, 2))
3210 .build();
3211
3212 let result = order.apply(OrderEventAny::Updated(updated));
3213
3214 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
3215 assert_eq!(order.status(), state.0);
3216 assert_eq!(order.previous_status(), state.1);
3217 assert_eq!(order.ts_last(), state.2);
3218 assert_eq!(order.events().len(), state.3);
3219 }
3220
3221 #[rstest]
3222 fn test_direct_update_panics_on_invalid_field_before_mutation() {
3223 let init = OrderInitializedSpec::builder()
3224 .quantity(Quantity::from(100_000))
3225 .build();
3226 let mut order: MarketOrder = init.try_into().unwrap();
3227 let original_quantity = order.quantity();
3228 let updated = OrderUpdatedSpec::builder()
3233 .quantity(Quantity::from(50_000))
3234 .price(Price::new(95.0, 2))
3235 .build();
3236
3237 let result =
3238 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| order.update(&updated)));
3239
3240 assert!(result.is_err());
3241 assert_eq!(order.quantity(), original_quantity);
3242 }
3243
3244 #[rstest]
3245 fn test_partially_filled_order_can_be_updated() {
3246 let init = OrderInitializedSpec::builder()
3249 .quantity(Quantity::from(100_000))
3250 .build();
3251 let submitted = OrderSubmittedSpec::builder().build();
3252 let accepted = OrderAcceptedSpec::builder().build();
3253 let partial_fill = OrderFilledSpec::builder()
3254 .last_qty(Quantity::from(40_000))
3255 .build();
3256 let updated = OrderUpdatedSpec::builder()
3257 .quantity(Quantity::from(80_000)) .build();
3259
3260 let mut order: MarketOrder = init.try_into().unwrap();
3261 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3262 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3263 order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
3264
3265 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3266 assert_eq!(order.filled_qty(), Quantity::from(40_000));
3267
3268 order.apply(OrderEventAny::Updated(updated)).unwrap();
3269
3270 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3271 assert_eq!(order.quantity(), Quantity::from(80_000));
3272 assert_eq!(order.leaves_qty(), Quantity::from(40_000)); }
3274
3275 #[rstest]
3276 fn test_triggered_order_can_be_updated() {
3277 let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
3280 let submitted = OrderSubmittedSpec::builder().build();
3281 let accepted = OrderAcceptedSpec::builder().build();
3282 let triggered = OrderTriggeredSpec::builder().build();
3283 let updated = OrderUpdatedSpec::builder()
3284 .quantity(Quantity::from(80_000))
3285 .build();
3286
3287 let mut order = OrderTestBuilder::new(OrderType::StopLimit)
3288 .instrument_id(instrument_id)
3289 .quantity(Quantity::from(100_000))
3290 .price(Price::from("0.99500"))
3291 .trigger_price(Price::from("1.00000"))
3292 .trigger_type(TriggerType::LastPrice)
3293 .build();
3294 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3295 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3296 order.apply(OrderEventAny::Triggered(triggered)).unwrap();
3297
3298 assert_eq!(order.status(), OrderStatus::Triggered);
3299
3300 order.apply(OrderEventAny::Updated(updated)).unwrap();
3301
3302 assert_eq!(order.status(), OrderStatus::Triggered);
3303 assert_eq!(order.quantity(), Quantity::from(80_000));
3304 }
3305
3306 #[rstest]
3307 fn test_order_updated_with_is_quote_quantity_clears_flag() {
3308 let init = OrderInitializedSpec::builder()
3309 .quantity(Quantity::new(10.0, 6))
3310 .quote_quantity(true)
3311 .build();
3312 let submitted = OrderSubmittedSpec::builder().build();
3313 let accepted = OrderAcceptedSpec::builder().build();
3314 let updated = OrderUpdatedSpec::builder()
3315 .quantity(Quantity::new(47.393_365, 6))
3316 .is_quote_quantity(false)
3317 .build();
3318
3319 let mut order: MarketOrder = init.try_into().unwrap();
3320 assert!(order.is_quote_quantity());
3321
3322 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3323 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3324 order.apply(OrderEventAny::Updated(updated)).unwrap();
3325
3326 assert!(!order.is_quote_quantity());
3327 assert_eq!(order.quantity(), Quantity::new(47.393_365, 6));
3328 assert_eq!(order.leaves_qty(), Quantity::new(47.393_365, 6));
3329 }
3330
3331 #[rstest]
3332 fn test_order_updated_default_is_quote_quantity_clears_flag() {
3333 let init = OrderInitializedSpec::builder()
3334 .quantity(Quantity::new(10.0, 6))
3335 .quote_quantity(true)
3336 .build();
3337 let submitted = OrderSubmittedSpec::builder().build();
3338 let accepted = OrderAcceptedSpec::builder().build();
3339 let updated = OrderUpdatedSpec::builder()
3341 .quantity(Quantity::new(8.0, 6))
3342 .build();
3343
3344 let mut order: MarketOrder = init.try_into().unwrap();
3345 assert!(order.is_quote_quantity());
3346
3347 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3348 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3349 order.apply(OrderEventAny::Updated(updated)).unwrap();
3350
3351 assert!(!order.is_quote_quantity());
3352 assert_eq!(order.quantity(), Quantity::new(8.0, 6));
3353 }
3354
3355 #[rstest]
3356 fn test_canceled_then_partial_fill_then_canceled() {
3357 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
3358 let submitted = OrderSubmittedSpec::builder().build();
3359 let accepted = OrderAcceptedSpec::builder().build();
3360 let canceled1 = OrderCanceledSpec::builder().build();
3361 let fill = OrderFilledSpec::builder()
3362 .last_qty(Quantity::from(50_000))
3363 .trade_id(TradeId::from("FILL-1"))
3364 .build();
3365 let canceled2 = OrderCanceledSpec::builder().build();
3366
3367 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3368 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3369 order.apply(OrderEventAny::Canceled(canceled1)).unwrap();
3370 assert_eq!(order.status(), OrderStatus::Canceled);
3371 assert!(order.is_closed());
3372
3373 order.apply(OrderEventAny::Filled(fill)).unwrap();
3375 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3376 assert_eq!(order.filled_qty(), Quantity::from(50_000));
3377 assert!(order.is_open());
3378
3379 order.apply(OrderEventAny::Canceled(canceled2)).unwrap();
3381 assert_eq!(order.status(), OrderStatus::Canceled);
3382 assert!(order.is_closed());
3383 }
3384
3385 #[rstest]
3386 fn test_updated_restores_status_before_pending_cancel() {
3387 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
3388 let submitted = OrderSubmittedSpec::builder().build();
3389 let accepted = OrderAcceptedSpec::builder().build();
3390 let pending_cancel = OrderPendingCancelSpec::builder().build();
3391 let updated = OrderUpdatedSpec::builder()
3392 .quantity(Quantity::from(150_000))
3393 .build();
3394
3395 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3396 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3397 let ts_accepted = order.ts_accepted();
3398 let venue_order_ids: Vec<VenueOrderId> =
3399 order.venue_order_ids().into_iter().copied().collect();
3400 order
3401 .apply(OrderEventAny::PendingCancel(pending_cancel))
3402 .unwrap();
3403 order.apply(OrderEventAny::Updated(updated)).unwrap();
3404
3405 assert_eq!(order.status(), OrderStatus::Accepted);
3406 assert_eq!(order.quantity(), Quantity::from(150_000));
3407 assert_eq!(order.ts_accepted(), ts_accepted);
3408 assert_eq!(
3409 order
3410 .venue_order_ids()
3411 .into_iter()
3412 .copied()
3413 .collect::<Vec<_>>(),
3414 venue_order_ids,
3415 );
3416 }
3417
3418 #[rstest]
3419 fn test_apply_triggered_to_stop_market_order_returns_error() {
3420 let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
3421 let submitted = OrderSubmittedSpec::builder().build();
3422 let accepted = OrderAcceptedSpec::builder().build();
3423 let triggered = OrderTriggeredSpec::builder().build();
3424
3425 let mut order = OrderTestBuilder::new(OrderType::StopMarket)
3426 .instrument_id(instrument_id)
3427 .quantity(Quantity::from(1))
3428 .trigger_price(Price::from("1.00000"))
3429 .trigger_type(TriggerType::LastPrice)
3430 .build();
3431 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3432 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3433
3434 let result = order.apply(OrderEventAny::Triggered(triggered));
3435 assert!(result.is_err());
3436 assert_eq!(order.status(), OrderStatus::Accepted);
3437 }
3438
3439 #[rstest]
3440 fn test_apply_triggered_to_stop_limit_order_succeeds() {
3441 let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
3442 let submitted = OrderSubmittedSpec::builder().build();
3443 let accepted = OrderAcceptedSpec::builder().build();
3444 let triggered = OrderTriggeredSpec::builder().build();
3445
3446 let mut order = OrderTestBuilder::new(OrderType::StopLimit)
3447 .instrument_id(instrument_id)
3448 .quantity(Quantity::from(1))
3449 .price(Price::from("0.99500"))
3450 .trigger_price(Price::from("1.00000"))
3451 .trigger_type(TriggerType::LastPrice)
3452 .build();
3453 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3454 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3455 order.apply(OrderEventAny::Triggered(triggered)).unwrap();
3456
3457 assert_eq!(order.status(), OrderStatus::Triggered);
3458 }
3459
3460 #[rstest]
3461 fn test_to_order_status_report_for_accepted_limit_order() {
3462 let order = OrderTestBuilder::new(OrderType::Limit)
3463 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
3464 .side(OrderSide::Buy)
3465 .quantity(Quantity::from(100_000))
3466 .price(Price::from("1.00000"))
3467 .build();
3468 let accepted = TestOrderStubs::make_accepted_order(&order);
3469
3470 let report = accepted.to_order_status_report(None).unwrap();
3471
3472 assert_eq!(report.account_id, AccountId::from("SIM-001"));
3473 assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
3474 assert_eq!(report.client_order_id, Some(accepted.client_order_id()));
3475 assert_eq!(report.venue_order_id, VenueOrderId::from("V-001"));
3476 assert_eq!(report.order_side, OrderSide::Buy);
3477 assert_eq!(report.order_type, OrderType::Limit);
3478 assert_eq!(report.time_in_force, accepted.time_in_force());
3479 assert_eq!(report.order_status, OrderStatus::Accepted);
3480 assert_eq!(report.quantity, Quantity::from(100_000));
3481 assert_eq!(report.filled_qty, Quantity::from(0));
3482 assert_eq!(report.price, Some(Price::from("1.00000")));
3483 assert_eq!(report.avg_px, None);
3484 assert_eq!(report.ts_accepted, accepted.ts_accepted().unwrap());
3485 assert_eq!(report.ts_last, accepted.ts_last());
3486 assert_eq!(report.ts_init, accepted.ts_init());
3487 }
3488
3489 #[rstest]
3490 fn test_to_order_status_report_for_filled_market_order(audusd_sim: CurrencyPair) {
3491 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
3492 let order = OrderTestBuilder::new(OrderType::Market)
3493 .instrument_id(audusd_sim.id())
3494 .side(OrderSide::Buy)
3495 .quantity(Quantity::from(100_000))
3496 .build();
3497 let filled = TestOrderStubs::make_filled_order(&order, &audusd_sim, LiquiditySide::Maker);
3498 let report_id = UUID4::new();
3499
3500 let report = filled.to_order_status_report(Some(report_id)).unwrap();
3501
3502 assert_eq!(report.report_id, report_id);
3503 assert_eq!(report.order_status, OrderStatus::Filled);
3504 assert_eq!(report.quantity, Quantity::from(100_000));
3505 assert_eq!(report.filled_qty, Quantity::from(100_000));
3506 assert_eq!(report.price, None);
3507 assert_eq!(report.avg_px, Some(dec!(1)));
3508 assert_eq!(report.venue_position_id, Some(PositionId::from("1")));
3509 assert_eq!(report.ts_accepted, filled.ts_accepted().unwrap());
3510 assert_eq!(report.ts_last, filled.ts_last());
3511 }
3512
3513 #[rstest]
3514 fn test_to_order_status_report_maps_optional_fields() {
3515 let order = OrderTestBuilder::new(OrderType::StopLimit)
3516 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
3517 .side(OrderSide::Sell)
3518 .quantity(Quantity::from(100_000))
3519 .price(Price::from("0.99500"))
3520 .trigger_price(Price::from("1.00000"))
3521 .trigger_type(TriggerType::LastPrice)
3522 .time_in_force(TimeInForce::Gtd)
3523 .expire_time(UnixNanos::from(5_000_000_000))
3524 .display_qty(Quantity::from(10_000))
3525 .post_only(true)
3526 .reduce_only(true)
3527 .contingency_type(ContingencyType::Oto)
3528 .order_list_id(OrderListId::from("OL-001"))
3529 .linked_order_ids(vec![ClientOrderId::from("O-CHILD")])
3530 .parent_order_id(ClientOrderId::from("O-PARENT"))
3531 .build();
3532 let accepted = TestOrderStubs::make_accepted_order(&order);
3533
3534 let report = accepted.to_order_status_report(None).unwrap();
3535
3536 assert_eq!(report.price, Some(Price::from("0.99500")));
3537 assert_eq!(report.trigger_price, Some(Price::from("1.00000")));
3538 assert_eq!(report.trigger_type, Some(TriggerType::LastPrice));
3539 assert_eq!(report.expire_time, Some(UnixNanos::from(5_000_000_000)));
3540 assert_eq!(report.display_qty, Some(Quantity::from(10_000)));
3541 assert!(report.post_only);
3542 assert!(report.reduce_only);
3543 assert_eq!(report.contingency_type, ContingencyType::Oto);
3544 assert_eq!(report.order_list_id, Some(OrderListId::from("OL-001")));
3545 assert_eq!(
3546 report.linked_order_ids,
3547 Some(vec![ClientOrderId::from("O-CHILD")])
3548 );
3549 assert_eq!(
3550 report.parent_order_id,
3551 Some(ClientOrderId::from("O-PARENT"))
3552 );
3553 }
3554
3555 #[rstest]
3556 fn test_to_order_status_report_maps_ts_triggered() {
3557 let order = OrderTestBuilder::new(OrderType::StopLimit)
3558 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
3559 .side(OrderSide::Buy)
3560 .quantity(Quantity::from(100_000))
3561 .price(Price::from("0.99500"))
3562 .trigger_price(Price::from("1.00000"))
3563 .trigger_type(TriggerType::LastPrice)
3564 .build();
3565 let mut order = TestOrderStubs::make_accepted_order(&order);
3566 let triggered = OrderTriggeredSpec::builder()
3567 .ts_event(UnixNanos::from(1_500_000_000))
3568 .build();
3569 order.apply(OrderEventAny::Triggered(triggered)).unwrap();
3570
3571 let report = order.to_order_status_report(None).unwrap();
3572
3573 assert_eq!(report.order_status, OrderStatus::Triggered);
3574 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
3575 }
3576
3577 #[rstest]
3578 fn test_to_order_status_report_maps_rejection_reason() {
3579 let order = OrderTestBuilder::new(OrderType::Limit)
3580 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
3581 .side(OrderSide::Buy)
3582 .quantity(Quantity::from(100_000))
3583 .price(Price::from("1.00000"))
3584 .build();
3585 let mut order = TestOrderStubs::make_accepted_order(&order);
3586 let rejected = OrderRejectedSpec::builder()
3587 .reason(Ustr::from("INSUFFICIENT_MARGIN"))
3588 .build();
3589 order.apply(OrderEventAny::Rejected(rejected)).unwrap();
3590
3591 let report = order.to_order_status_report(None).unwrap();
3592
3593 assert_eq!(report.order_status, OrderStatus::Rejected);
3594 assert_eq!(
3595 report.cancel_reason,
3596 Some("INSUFFICIENT_MARGIN".to_string())
3597 );
3598 }
3599
3600 #[rstest]
3601 fn test_to_order_status_report_maps_distinct_timestamps() {
3602 let mut order = OrderTestBuilder::new(OrderType::Limit)
3603 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
3604 .side(OrderSide::Buy)
3605 .quantity(Quantity::from(100_000))
3606 .price(Price::from("1.00000"))
3607 .ts_init(UnixNanos::from(1_000))
3608 .build();
3609 let submitted = OrderSubmittedSpec::builder()
3610 .ts_event(UnixNanos::from(2_000))
3611 .build();
3612 let accepted = OrderAcceptedSpec::builder()
3613 .ts_event(UnixNanos::from(3_000))
3614 .build();
3615 let filled = OrderFilledSpec::builder()
3616 .last_qty(Quantity::from(50_000))
3617 .ts_event(UnixNanos::from(4_000))
3618 .build();
3619 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3620 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3621 order.apply(OrderEventAny::Filled(filled)).unwrap();
3622
3623 let report = order.to_order_status_report(None).unwrap();
3624
3625 assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
3626 assert_eq!(report.filled_qty, Quantity::from(50_000));
3627 assert_eq!(report.ts_accepted, UnixNanos::from(3_000));
3628 assert_eq!(report.ts_last, UnixNanos::from(4_000));
3629 assert_eq!(report.ts_init, UnixNanos::from(1_000));
3630 }
3631
3632 #[rstest]
3633 fn test_to_order_status_report_ts_accepted_falls_back_to_ts_last() {
3634 let mut order = OrderTestBuilder::new(OrderType::Limit)
3636 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
3637 .side(OrderSide::Buy)
3638 .quantity(Quantity::from(100_000))
3639 .price(Price::from("1.00000"))
3640 .build();
3641 let submitted = OrderSubmittedSpec::builder()
3642 .ts_event(UnixNanos::from(2_000))
3643 .build();
3644 let updated = OrderUpdatedSpec::builder()
3645 .venue_order_id(VenueOrderId::from("V-001"))
3646 .ts_event(UnixNanos::from(3_000))
3647 .build();
3648 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3649 order.apply(OrderEventAny::Updated(updated)).unwrap();
3650
3651 let report = order.to_order_status_report(None).unwrap();
3652
3653 assert_eq!(report.order_status, OrderStatus::Submitted);
3654 assert_eq!(report.venue_order_id, VenueOrderId::from("V-001"));
3655 assert_eq!(report.ts_accepted, UnixNanos::from(3_000));
3656 assert_eq!(report.ts_last, UnixNanos::from(3_000));
3657 }
3658
3659 #[rstest]
3660 fn test_to_order_status_report_maps_trailing_offsets() {
3661 let order = OrderTestBuilder::new(OrderType::TrailingStopLimit)
3662 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
3663 .side(OrderSide::Sell)
3664 .quantity(Quantity::from(100_000))
3665 .price(Price::from("0.99500"))
3666 .activation_price(Price::from("1.00500"))
3667 .trigger_price(Price::from("1.00000"))
3668 .trigger_type(TriggerType::LastPrice)
3669 .limit_offset(dec!(0.0001))
3670 .trailing_offset(dec!(0.0002))
3671 .trailing_offset_type(TrailingOffsetType::Price)
3672 .build();
3673 let accepted = TestOrderStubs::make_accepted_order(&order);
3674
3675 let report = accepted.to_order_status_report(None).unwrap();
3676
3677 assert_eq!(report.activation_price, Some(Price::from("1.00500")));
3678 assert_eq!(report.limit_offset, Some(dec!(0.0001)));
3679 assert_eq!(report.trailing_offset, Some(dec!(0.0002)));
3680 assert_eq!(report.trailing_offset_type, TrailingOffsetType::Price);
3681 }
3682
3683 #[rstest]
3684 fn test_to_order_status_report_returns_none_before_venue_ack() {
3685 let order = OrderTestBuilder::new(OrderType::Limit)
3686 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
3687 .side(OrderSide::Buy)
3688 .quantity(Quantity::from(100_000))
3689 .price(Price::from("1.00000"))
3690 .build();
3691
3692 assert!(order.to_order_status_report(None).is_none());
3693 }
3694
3695 #[rstest]
3696 fn test_to_order_status_report_returns_none_for_submitted_order() {
3697 let mut order = OrderTestBuilder::new(OrderType::Limit)
3699 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
3700 .side(OrderSide::Buy)
3701 .quantity(Quantity::from(100_000))
3702 .price(Price::from("1.00000"))
3703 .build();
3704 let submitted = OrderSubmittedSpec::builder().build();
3705 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3706
3707 assert!(order.account_id().is_some());
3708 assert!(order.to_order_status_report(None).is_none());
3709 }
3710}