1pub mod any;
32pub mod limit;
33pub mod limit_if_touched;
34pub mod list;
35pub mod market;
36pub mod market_if_touched;
37pub mod market_to_limit;
38pub mod stop_limit;
39pub mod stop_market;
40pub mod trailing_stop_limit;
41pub mod trailing_stop_market;
42
43#[cfg(any(test, feature = "test-support"))]
44pub mod builder;
45
46#[cfg(any(test, feature = "test-support"))]
47pub mod stubs;
48
49use ahash::AHashSet;
51use enum_dispatch::enum_dispatch;
52use indexmap::IndexMap;
53use nautilus_core::{
54 UUID4, UnixNanos,
55 correctness::{CorrectnessError, check_predicate_false},
56};
57use rust_decimal::Decimal;
58use serde::{Deserialize, Serialize};
59use ustr::Ustr;
60
61#[cfg(any(test, feature = "test-support"))]
62pub use crate::orders::builder::OrderTestBuilder;
63pub use crate::orders::{
64 any::{LimitOrderAny, OrderAny, OrderReplayError, PassiveOrderAny, StopOrderAny},
65 limit::LimitOrder,
66 limit_if_touched::LimitIfTouchedOrder,
67 list::{OrderList, OrderListValidationError},
68 market::MarketOrder,
69 market_if_touched::MarketIfTouchedOrder,
70 market_to_limit::MarketToLimitOrder,
71 stop_limit::StopLimitOrder,
72 stop_market::StopMarketOrder,
73 trailing_stop_limit::TrailingStopLimitOrder,
74 trailing_stop_market::TrailingStopMarketOrder,
75};
76use crate::{
77 enums::{
78 ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide,
79 TimeInForce, TrailingOffsetType, TriggerType,
80 },
81 events::{
82 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
83 OrderEventAny, OrderExpired, OrderFillVoided, OrderFilled, OrderInitialized,
84 OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased,
85 OrderSubmitted, OrderTriggered, OrderUpdated,
86 },
87 identifiers::{
88 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
89 StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
90 },
91 orderbook::OwnBookOrder,
92 reports::OrderStatusReport,
93 types::{Currency, Money, Price, Quantity},
94};
95
96pub const STOP_ORDER_TYPES: &[OrderType] = &[
98 OrderType::StopMarket,
99 OrderType::StopLimit,
100 OrderType::MarketIfTouched,
101 OrderType::LimitIfTouched,
102];
103
104pub const LIMIT_ORDER_TYPES: &[OrderType] = &[
106 OrderType::Limit,
107 OrderType::StopLimit,
108 OrderType::LimitIfTouched,
109 OrderType::TrailingStopLimit,
110];
111
112pub const TRIGGERABLE_ORDER_TYPES: &[OrderType] = &[
117 OrderType::StopLimit,
118 OrderType::TrailingStopLimit,
119 OrderType::LimitIfTouched,
120];
121
122pub const LOCAL_ACTIVE_ORDER_STATUSES: &[OrderStatus] = &[
124 OrderStatus::Initialized,
125 OrderStatus::Emulated,
126 OrderStatus::Released,
127];
128
129#[derive(thiserror::Error, Debug)]
130pub enum OrderError {
131 #[error("Order not found: {0}")]
132 NotFound(ClientOrderId),
133 #[error("Order invariant failed: must have a side for this operation")]
134 NoOrderSide,
135 #[error("Invalid event for order type")]
136 InvalidOrderEvent,
137 #[error("Invalid order state transition")]
138 InvalidStateTransition,
139 #[error("Order was already initialized")]
140 AlreadyInitialized,
141 #[error("Order had no previous state")]
142 NoPreviousState,
143 #[error("Duplicate fill: trade_id {0} already applied to order")]
144 DuplicateFill(TradeId),
145 #[error("Duplicate fill void: trade_id {0} already has this cumulative correction")]
146 DuplicateFillVoid(TradeId),
147 #[error("Fill void for trade_id {0} is older than the applied correction")]
148 StaleFillVoid(TradeId),
149 #[error("Fill void quantity exceeds the original fill for trade_id {0}")]
150 OverVoid(TradeId),
151 #[error("Fill void commission is missing or conflicts with trade_id {0}")]
152 InvalidFillVoidCommission(TradeId),
153 #[error("{0}")]
154 Invariant(#[from] CorrectnessError),
155}
156
157#[must_use]
159pub fn ustr_indexmap_to_str(h: IndexMap<Ustr, Ustr>) -> IndexMap<String, String> {
160 h.into_iter()
161 .map(|(k, v)| (k.to_string(), v.to_string()))
162 .collect()
163}
164
165#[must_use]
167pub fn str_indexmap_to_ustr(h: IndexMap<String, String>) -> IndexMap<Ustr, Ustr> {
168 h.into_iter()
169 .map(|(k, v)| (Ustr::from(&k), Ustr::from(&v)))
170 .collect()
171}
172
173#[inline]
174pub(crate) fn check_display_qty(
175 display_qty: Option<Quantity>,
176 quantity: Quantity,
177) -> Result<(), OrderError> {
178 if let Some(q) = display_qty {
179 check_predicate_false(q > quantity, "`display_qty` may not exceed `quantity`")?;
180 }
181 Ok(())
182}
183
184#[inline]
185pub(crate) fn check_time_in_force(
186 time_in_force: TimeInForce,
187 expire_time: Option<UnixNanos>,
188) -> Result<(), OrderError> {
189 check_predicate_false(
190 time_in_force == TimeInForce::Gtd && expire_time.unwrap_or_default() == 0,
191 "`expire_time` is required for `GTD` order",
192 )?;
193 Ok(())
194}
195
196impl OrderStatus {
197 #[rustfmt::skip]
203 pub fn transition(&mut self, event: &OrderEventAny) -> Result<Self, OrderError> {
204 let new_state = match (self, event) {
205 (Self::Initialized, OrderEventAny::Denied(_)) => Self::Denied,
206 (Self::Initialized, OrderEventAny::Emulated(_)) => Self::Emulated, (Self::Initialized, OrderEventAny::Released(_)) => Self::Released, (Self::Initialized, OrderEventAny::Submitted(_)) => Self::Submitted,
209 (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,
224 (Self::Submitted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
225 (Self::Submitted, OrderEventAny::Rejected(_)) => Self::Rejected,
226 (Self::Submitted, OrderEventAny::Canceled(_)) => Self::Canceled, (Self::Submitted, OrderEventAny::Accepted(_)) => Self::Accepted,
228 (Self::Submitted, OrderEventAny::Updated(_)) => Self::Submitted,
229 (Self::Submitted, OrderEventAny::Filled(_)) => Self::Filled,
230 (Self::Accepted, OrderEventAny::Rejected(_)) => Self::Rejected, (Self::Accepted, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
232 (Self::Accepted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
233 (Self::Accepted, OrderEventAny::CancelRejected(_)) => Self::Accepted, (Self::Accepted, OrderEventAny::Canceled(_)) => Self::Canceled,
235 (Self::Accepted, OrderEventAny::Triggered(_)) => Self::Triggered,
236 (Self::Accepted, OrderEventAny::Updated(_)) => Self::Accepted, (Self::Accepted, OrderEventAny::Expired(_)) => Self::Expired,
238 (Self::Accepted, OrderEventAny::Filled(_)) => Self::Filled,
239 (Self::Accepted, OrderEventAny::FillVoided(_)) => Self::Accepted,
240 (Self::Canceled, OrderEventAny::Filled(_)) => Self::Filled, (Self::Canceled, OrderEventAny::FillVoided(_)) => Self::Canceled,
242 (Self::Canceled, OrderEventAny::Updated(_)) => Self::Canceled,
243 (Self::PendingUpdate, OrderEventAny::Rejected(_)) => Self::Rejected,
244 (Self::PendingUpdate, OrderEventAny::Accepted(_)) => Self::Accepted,
245 (Self::PendingUpdate, OrderEventAny::Canceled(_)) => Self::Canceled,
246 (Self::PendingUpdate, OrderEventAny::Expired(_)) => Self::Expired,
247 (Self::PendingUpdate, OrderEventAny::Triggered(_)) => Self::Triggered,
248 (Self::PendingUpdate, OrderEventAny::Submitted(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
251 (Self::PendingUpdate, OrderEventAny::ModifyRejected(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::Updated(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::Filled(_)) => Self::Filled,
254 (Self::PendingUpdate, OrderEventAny::FillVoided(_)) => Self::PendingUpdate,
255 (Self::PendingCancel, OrderEventAny::Rejected(_)) => Self::Rejected,
256 (Self::PendingCancel, OrderEventAny::PendingCancel(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::ModifyRejected(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::CancelRejected(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::Canceled(_)) => Self::Canceled,
260 (Self::PendingCancel, OrderEventAny::Expired(_)) => Self::Expired,
261 (Self::PendingCancel, OrderEventAny::Accepted(_)) => Self::Accepted, (Self::PendingCancel, OrderEventAny::Updated(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::Filled(_)) => Self::Filled,
264 (Self::PendingCancel, OrderEventAny::FillVoided(_)) => Self::PendingCancel,
265 (Self::Triggered, OrderEventAny::Rejected(_)) => Self::Rejected,
266 (Self::Triggered, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
267 (Self::Triggered, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
268 (Self::Triggered, OrderEventAny::Canceled(_)) => Self::Canceled,
269 (Self::Triggered, OrderEventAny::Expired(_)) => Self::Expired,
270 (Self::Triggered, OrderEventAny::Filled(_)) => Self::Filled,
271 (Self::Triggered, OrderEventAny::Updated(_)) => Self::Triggered,
272 (Self::Triggered, OrderEventAny::FillVoided(_)) => Self::Triggered,
273 (Self::PartiallyFilled, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
274 (Self::PartiallyFilled, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
275 (Self::PartiallyFilled, OrderEventAny::Canceled(_)) => Self::Canceled,
276 (Self::PartiallyFilled, OrderEventAny::Expired(_)) => Self::Expired,
277 (Self::PartiallyFilled, OrderEventAny::Filled(_)) => Self::Filled,
278 (Self::PartiallyFilled, OrderEventAny::Accepted(_)) => Self::Accepted,
279 (Self::PartiallyFilled, OrderEventAny::Updated(_)) => Self::PartiallyFilled,
280 (Self::PartiallyFilled, OrderEventAny::FillVoided(_)) => Self::PartiallyFilled,
281 (Self::Filled, OrderEventAny::FillVoided(_)) => Self::Voided,
282 (Self::Filled, OrderEventAny::Updated(_)) => Self::Filled,
283 (Self::Expired, OrderEventAny::FillVoided(_)) => Self::Expired,
284 (Self::Expired, OrderEventAny::Updated(_)) => Self::Expired,
285 (Self::Voided, OrderEventAny::FillVoided(_)) => Self::Voided,
286 _ => return Err(OrderError::InvalidStateTransition),
287 };
288 Ok(new_state)
289 }
290}
291
292#[enum_dispatch]
293pub trait Order: 'static + Send {
294 fn into_any(self) -> OrderAny;
295 fn status(&self) -> OrderStatus;
296 fn trader_id(&self) -> TraderId;
297 fn strategy_id(&self) -> StrategyId;
298 fn instrument_id(&self) -> InstrumentId;
299 fn symbol(&self) -> Symbol;
300 fn venue(&self) -> Venue;
301 fn client_order_id(&self) -> ClientOrderId;
302 fn venue_order_id(&self) -> Option<VenueOrderId>;
303 fn position_id(&self) -> Option<PositionId>;
304 fn account_id(&self) -> Option<AccountId>;
305 fn last_trade_id(&self) -> Option<TradeId>;
306 fn order_side(&self) -> OrderSide;
307 fn order_type(&self) -> OrderType;
308 fn quantity(&self) -> Quantity;
309 fn time_in_force(&self) -> TimeInForce;
310 fn expire_time(&self) -> Option<UnixNanos>;
311 fn price(&self) -> Option<Price>;
312 fn activation_price(&self) -> Option<Price> {
313 None
314 }
315 fn trigger_price(&self) -> Option<Price>;
316 fn trigger_type(&self) -> Option<TriggerType>;
317 fn liquidity_side(&self) -> Option<LiquiditySide>;
318 fn is_post_only(&self) -> bool;
319 fn is_reduce_only(&self) -> bool;
320 fn is_quote_quantity(&self) -> bool;
321 fn display_qty(&self) -> Option<Quantity>;
322 fn limit_offset(&self) -> Option<Decimal>;
323 fn trailing_offset(&self) -> Option<Decimal>;
324 fn trailing_offset_type(&self) -> Option<TrailingOffsetType>;
325 fn emulation_trigger(&self) -> Option<TriggerType>;
326 fn trigger_instrument_id(&self) -> Option<InstrumentId>;
327 fn contingency_type(&self) -> Option<ContingencyType>;
328 fn order_list_id(&self) -> Option<OrderListId>;
329 fn linked_order_ids(&self) -> Option<&[ClientOrderId]>;
330 fn parent_order_id(&self) -> Option<ClientOrderId>;
331 fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId>;
332 fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>>;
333 fn exec_spawn_id(&self) -> Option<ClientOrderId>;
334 fn tags(&self) -> Option<&[Ustr]>;
335 fn filled_qty(&self) -> Quantity;
336 fn voided_qty(&self) -> Quantity;
337 fn leaves_qty(&self) -> Quantity;
338 fn overfill_qty(&self) -> Quantity;
339
340 fn calculate_overfill(&self, fill_qty: Quantity) -> Quantity {
342 let potential_filled = self.filled_qty() + fill_qty;
343 let quantity = self.quantity();
344 if potential_filled > quantity {
345 potential_filled - quantity
346 } else {
347 Quantity::zero(fill_qty.precision)
348 }
349 }
350
351 fn avg_px(&self) -> Option<Decimal>;
352 fn slippage(&self) -> Option<Decimal>;
353 fn init_id(&self) -> UUID4;
354 fn ts_init(&self) -> UnixNanos;
355 fn ts_submitted(&self) -> Option<UnixNanos>;
356 fn ts_accepted(&self) -> Option<UnixNanos>;
357 fn ts_closed(&self) -> Option<UnixNanos>;
358 fn ts_last(&self) -> UnixNanos;
359
360 fn commissions(&self) -> &IndexMap<Currency, Money>;
361
362 fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError>;
368
369 fn update(&mut self, event: &OrderUpdated);
370
371 fn events(&self) -> Vec<&OrderEventAny>;
372
373 fn last_event(&self) -> &OrderEventAny {
374 self.events()
375 .last()
376 .expect("Order invariant violated: no events")
377 }
378
379 fn event_count(&self) -> usize {
380 self.events().len()
381 }
382
383 fn venue_order_ids(&self) -> Vec<&VenueOrderId>;
384
385 fn trade_ids(&self) -> Vec<&TradeId>;
386
387 fn has_price(&self) -> bool;
388
389 fn is_duplicate_fill(&self, fill: &OrderFilled) -> bool {
391 self.events().iter().any(|event| {
392 if let OrderEventAny::Filled(existing) = event {
393 existing.trade_id == fill.trade_id
394 && existing.order_side == fill.order_side
395 && existing.last_qty == fill.last_qty
396 && existing.last_px == fill.last_px
397 } else {
398 false
399 }
400 })
401 }
402
403 fn is_buy(&self) -> bool {
404 self.order_side() == OrderSide::Buy
405 }
406
407 fn is_sell(&self) -> bool {
408 self.order_side() == OrderSide::Sell
409 }
410
411 fn is_passive(&self) -> bool {
412 self.order_type() != OrderType::Market
413 }
414
415 fn is_aggressive(&self) -> bool {
416 self.order_type() == OrderType::Market
417 }
418
419 fn is_emulated(&self) -> bool {
420 self.status() == OrderStatus::Emulated
421 }
422
423 fn is_active_local(&self) -> bool {
424 matches!(
425 self.status(),
426 OrderStatus::Initialized | OrderStatus::Emulated | OrderStatus::Released
427 )
428 }
429
430 fn is_primary(&self) -> bool {
431 self.exec_algorithm_id().is_some()
432 && self
433 .exec_spawn_id()
434 .is_some_and(|spawn_id| self.client_order_id() == spawn_id)
435 }
436
437 fn is_spawned(&self) -> bool {
438 self.exec_algorithm_id().is_some()
439 && self
440 .exec_spawn_id()
441 .is_some_and(|spawn_id| self.client_order_id() != spawn_id)
442 }
443
444 fn is_contingency(&self) -> bool {
445 self.contingency_type().is_some()
446 }
447
448 fn is_parent_order(&self) -> bool {
449 match self.contingency_type() {
450 Some(c) => c == ContingencyType::Oto,
451 None => false,
452 }
453 }
454
455 fn is_child_order(&self) -> bool {
456 self.parent_order_id().is_some()
457 }
458
459 fn is_open(&self) -> bool {
460 if self.emulation_trigger().is_some() {
461 return false;
462 }
463
464 matches!(
465 self.status(),
466 OrderStatus::Accepted
467 | OrderStatus::Triggered
468 | OrderStatus::PendingCancel
469 | OrderStatus::PendingUpdate
470 | OrderStatus::PartiallyFilled
471 )
472 }
473
474 fn is_canceled(&self) -> bool {
475 self.status() == OrderStatus::Canceled
476 }
477
478 fn is_closed(&self) -> bool {
479 matches!(
480 self.status(),
481 OrderStatus::Denied
482 | OrderStatus::Rejected
483 | OrderStatus::Canceled
484 | OrderStatus::Expired
485 | OrderStatus::Filled
486 | OrderStatus::Voided
487 )
488 }
489
490 fn is_inflight(&self) -> bool {
491 if self.emulation_trigger().is_some() {
492 return false;
493 }
494
495 matches!(
496 self.status(),
497 OrderStatus::Submitted | OrderStatus::PendingCancel | OrderStatus::PendingUpdate
498 )
499 }
500
501 fn is_pending_update(&self) -> bool {
502 self.status() == OrderStatus::PendingUpdate
503 }
504
505 fn is_pending_cancel(&self) -> bool {
506 self.status() == OrderStatus::PendingCancel
507 }
508
509 fn to_own_book_order(&self) -> OwnBookOrder {
510 OwnBookOrder::new(
511 self.trader_id(),
512 self.client_order_id(),
513 self.venue_order_id(),
514 self.order_side(),
515 self.price().expect("`OwnBookOrder` must have a price"),
516 self.leaves_qty(),
517 self.order_type(),
518 self.time_in_force(),
519 self.status(),
520 self.ts_last(),
521 self.ts_accepted().unwrap_or_default(),
522 self.ts_submitted().unwrap_or_default(),
523 self.ts_init(),
524 )
525 }
526
527 fn to_order_status_report(&self, report_id: Option<UUID4>) -> Option<OrderStatusReport> {
533 let account_id = self.account_id()?;
534 let venue_order_id = self.venue_order_id()?;
535
536 let mut report = OrderStatusReport::new(
537 account_id,
538 self.instrument_id(),
539 Some(self.client_order_id()),
540 venue_order_id,
541 self.order_side().into(),
542 self.order_type(),
543 self.time_in_force(),
544 self.status(),
545 self.quantity(),
546 self.filled_qty(),
547 self.ts_accepted().unwrap_or_else(|| self.ts_last()),
548 self.ts_last(),
549 self.ts_init(),
550 report_id,
551 )
552 .with_post_only(self.is_post_only())
553 .with_reduce_only(self.is_reduce_only());
554
555 if let Some(price) = self.price() {
556 report = report.with_price(price);
557 }
558
559 if let Some(activation_price) = self.activation_price() {
560 report = report.with_activation_price(activation_price);
561 }
562
563 if let Some(trigger_price) = self.trigger_price() {
564 report = report.with_trigger_price(trigger_price);
565 }
566
567 if let Some(trigger_type) = self.trigger_type() {
568 report = report.with_trigger_type(trigger_type);
569 }
570
571 if let Some(limit_offset) = self.limit_offset() {
572 report = report.with_limit_offset(limit_offset);
573 }
574
575 if let Some(trailing_offset) = self.trailing_offset() {
576 report = report.with_trailing_offset(trailing_offset);
577 }
578
579 if let Some(trailing_offset_type) = self.trailing_offset_type() {
580 report = report.with_trailing_offset_type(trailing_offset_type);
581 }
582
583 if let Some(display_qty) = self.display_qty() {
584 report = report.with_display_qty(display_qty);
585 }
586
587 if let Some(expire_time) = self.expire_time() {
588 report = report.with_expire_time(expire_time);
589 }
590
591 if let Some(contingency_type) = self.contingency_type() {
592 report = report.with_contingency_type(contingency_type);
593 }
594
595 if let Some(order_list_id) = self.order_list_id() {
596 report = report.with_order_list_id(order_list_id);
597 }
598
599 if let Some(linked_order_ids) = self.linked_order_ids() {
600 report = report.with_linked_order_ids(linked_order_ids.iter().copied());
601 }
602
603 if let Some(parent_order_id) = self.parent_order_id() {
604 report = report.with_parent_order_id(parent_order_id);
605 }
606
607 if let Some(position_id) = self.position_id() {
608 report = report.with_venue_position_id(position_id);
609 }
610
611 let mut ts_triggered = None;
614 let mut rejected_reason = None;
615
616 for event in self.events() {
617 match event {
618 OrderEventAny::Triggered(triggered) => ts_triggered = Some(triggered.ts_event),
619 OrderEventAny::Rejected(rejected) => rejected_reason = Some(rejected.reason),
620 _ => {}
621 }
622 }
623
624 if let Some(ts_triggered) = ts_triggered {
625 report = report.with_ts_triggered(ts_triggered);
626 }
627
628 if let Some(reason) = rejected_reason {
631 report = report.with_cancel_reason(reason.to_string());
632 }
633
634 if let Some(avg_px) = self.avg_px() {
635 report = report.with_avg_px(avg_px);
636 }
637
638 Some(report)
639 }
640
641 fn is_triggered(&self) -> Option<bool>; fn set_position_id(&mut self, position_id: Option<PositionId>);
643 fn set_quantity(&mut self, quantity: Quantity);
644 fn set_leaves_qty(&mut self, leaves_qty: Quantity);
645 fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>);
646 fn set_is_quote_quantity(&mut self, is_quote_quantity: bool);
647 fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide);
648 fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool;
649 fn previous_status(&self) -> Option<OrderStatus>;
650}
651
652impl<T> From<&T> for OrderInitialized
653where
654 T: Order,
655{
656 fn from(order: &T) -> Self {
657 Self {
658 trader_id: order.trader_id(),
659 strategy_id: order.strategy_id(),
660 instrument_id: order.instrument_id(),
661 client_order_id: order.client_order_id(),
662 order_side: order.order_side(),
663 order_type: order.order_type(),
664 quantity: order.quantity(),
665 price: order.price(),
666 activation_price: order.activation_price(),
667 trigger_price: order.trigger_price(),
668 trigger_type: order.trigger_type(),
669 time_in_force: order.time_in_force(),
670 expire_time: order.expire_time(),
671 post_only: order.is_post_only(),
672 reduce_only: order.is_reduce_only(),
673 quote_quantity: order.is_quote_quantity(),
674 display_qty: order.display_qty(),
675 limit_offset: order.limit_offset(),
676 trailing_offset: order.trailing_offset(),
677 trailing_offset_type: order.trailing_offset_type(),
678 emulation_trigger: order.emulation_trigger(),
679 trigger_instrument_id: order.trigger_instrument_id(),
680 contingency_type: order.contingency_type(),
681 order_list_id: order.order_list_id(),
682 linked_order_ids: order.linked_order_ids().map(<[ClientOrderId]>::to_vec),
683 parent_order_id: order.parent_order_id(),
684 exec_algorithm_id: order.exec_algorithm_id(),
685 exec_algorithm_params: order.exec_algorithm_params().map(ToOwned::to_owned),
686 exec_spawn_id: order.exec_spawn_id(),
687 tags: order.tags().map(<[Ustr]>::to_vec),
688 event_id: order.init_id(),
689 ts_event: order.ts_init(),
690 ts_init: order.ts_init(),
691 reconciliation: false,
692 causation_id: None,
693 }
694 }
695}
696
697#[derive(Clone, Debug, Serialize, Deserialize)]
703pub struct OrderCore {
704 events: Vec<OrderEventAny>,
705 #[serde(skip)]
706 fill_totals: Option<FillTotals>,
707 pub commissions: IndexMap<Currency, Money>,
708 pub venue_order_ids: Vec<VenueOrderId>,
709 pub trade_ids: Vec<TradeId>,
710 pub previous_status: Option<OrderStatus>,
711 pub status: OrderStatus,
712 pub trader_id: TraderId,
713 pub strategy_id: StrategyId,
714 pub instrument_id: InstrumentId,
715 pub client_order_id: ClientOrderId,
716 pub venue_order_id: Option<VenueOrderId>,
717 pub position_id: Option<PositionId>,
718 pub account_id: Option<AccountId>,
719 pub last_trade_id: Option<TradeId>,
720 pub side: OrderSide,
721 pub order_type: OrderType,
722 pub quantity: Quantity,
723 pub time_in_force: TimeInForce,
724 pub liquidity_side: Option<LiquiditySide>,
725 pub is_reduce_only: bool,
726 pub is_quote_quantity: bool,
727 #[serde(default, with = "crate::enums::serde_option_trigger_type")]
728 pub emulation_trigger: Option<TriggerType>,
729 #[serde(default, with = "crate::enums::serde_option_contingency_type")]
730 pub contingency_type: Option<ContingencyType>,
731 pub order_list_id: Option<OrderListId>,
732 pub linked_order_ids: Option<Vec<ClientOrderId>>,
733 pub parent_order_id: Option<ClientOrderId>,
734 pub exec_algorithm_id: Option<ExecAlgorithmId>,
735 pub exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
736 pub exec_spawn_id: Option<ClientOrderId>,
737 pub tags: Option<Vec<Ustr>>,
738 pub filled_qty: Quantity,
739 #[serde(default)]
740 pub voided_qty: Quantity,
741 pub leaves_qty: Quantity,
742 pub overfill_qty: Quantity,
743 pub avg_px: Option<Decimal>,
744 pub slippage: Option<Decimal>,
745 pub init_id: UUID4,
746 pub ts_init: UnixNanos,
747 pub ts_submitted: Option<UnixNanos>,
748 pub ts_accepted: Option<UnixNanos>,
749 pub ts_closed: Option<UnixNanos>,
750 pub ts_last: UnixNanos,
751}
752
753impl OrderCore {
754 #[must_use]
756 pub fn new(init: OrderInitialized) -> Self {
757 let events: Vec<OrderEventAny> = vec![OrderEventAny::Initialized(init.clone())];
758 Self {
759 events,
760 fill_totals: Some(FillTotals::default()),
761 commissions: IndexMap::new(),
762 venue_order_ids: Vec::new(),
763 trade_ids: Vec::new(),
764 previous_status: None,
765 status: OrderStatus::Initialized,
766 trader_id: init.trader_id,
767 strategy_id: init.strategy_id,
768 instrument_id: init.instrument_id,
769 client_order_id: init.client_order_id,
770 venue_order_id: None,
771 position_id: None,
772 account_id: None,
773 last_trade_id: None,
774 side: init.order_side,
775 order_type: init.order_type,
776 quantity: init.quantity,
777 time_in_force: init.time_in_force,
778 liquidity_side: Some(LiquiditySide::NoLiquiditySide),
779 is_reduce_only: init.reduce_only,
780 is_quote_quantity: init.quote_quantity,
781 emulation_trigger: init.emulation_trigger,
782 contingency_type: init.contingency_type,
783 order_list_id: init.order_list_id,
784 linked_order_ids: init.linked_order_ids,
785 parent_order_id: init.parent_order_id,
786 exec_algorithm_id: init.exec_algorithm_id,
787 exec_algorithm_params: init.exec_algorithm_params,
788 exec_spawn_id: init.exec_spawn_id,
789 tags: init.tags,
790 filled_qty: Quantity::zero(init.quantity.precision),
791 voided_qty: Quantity::zero(init.quantity.precision),
792 leaves_qty: init.quantity,
793 overfill_qty: Quantity::zero(init.quantity.precision),
794 avg_px: None,
795 slippage: None,
796 init_id: init.event_id,
797 ts_init: init.ts_event,
798 ts_submitted: None,
799 ts_accepted: None,
800 ts_closed: None,
801 ts_last: init.ts_event,
802 }
803 }
804
805 pub fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
812 if self.client_order_id != event.client_order_id() {
813 return Err(CorrectnessError::PredicateViolation {
814 message: format!(
815 "Event client_order_id {} does not match order client_order_id {}",
816 event.client_order_id(),
817 self.client_order_id
818 ),
819 }
820 .into());
821 }
822
823 if self.strategy_id != event.strategy_id() {
824 return Err(CorrectnessError::PredicateViolation {
825 message: format!(
826 "Event strategy_id {} does not match order strategy_id {}",
827 event.strategy_id(),
828 self.strategy_id
829 ),
830 }
831 .into());
832 }
833
834 if let OrderEventAny::FillVoided(event) = &event {
835 self.validate_fill_void(event)?;
836 }
837
838 let mut has_prior_fill_void = false;
839
840 if let OrderEventAny::Filled(fill) = &event {
842 for candidate in &self.events {
843 match candidate {
844 OrderEventAny::Filled(existing) if existing.trade_id == fill.trade_id => {
845 return Err(OrderError::DuplicateFill(fill.trade_id));
846 }
847 OrderEventAny::FillVoided(existing) if existing.trade_id == fill.trade_id => {
848 has_prior_fill_void = true;
849 }
850 _ => {}
851 }
852 }
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 is_reclose_after_fill = self.status == OrderStatus::Canceled
866 && matches!(event, OrderEventAny::Canceled(_))
867 && self
868 .events
869 .iter()
870 .rev()
871 .take_while(|event| !matches!(event, OrderEventAny::Canceled(_)))
872 .any(|event| matches!(event, OrderEventAny::Filled(_)));
873
874 let new_status = if is_reclose_after_fill {
875 OrderStatus::Canceled
876 } else {
877 self.status.transition(&event)?
878 };
879
880 if let OrderEventAny::Filled(fill) = &event
881 && self.filled_qty.checked_add(fill.last_qty).is_none()
882 {
883 return Err(CorrectnessError::PredicateViolation {
884 message: format!(
885 "filled quantity overflowed Quantity raw bounds: {} + {}",
886 self.filled_qty, fill.last_qty
887 ),
888 }
889 .into());
890 }
891
892 let rejection_status = if matches!(
893 (&event, self.status),
894 (OrderEventAny::ModifyRejected(_), _)
895 | (OrderEventAny::CancelRejected(_), OrderStatus::PendingCancel)
896 ) {
897 self.previous_status.ok_or(OrderError::NoPreviousState)?
898 } else {
899 self.status
900 };
901
902 if let OrderEventAny::Updated(update) = &event {
906 let invalid_fields = match self.order_type {
907 OrderType::Market => update.price.is_some() || update.trigger_price.is_some(),
908 OrderType::StopMarket
909 | OrderType::MarketIfTouched
910 | OrderType::TrailingStopMarket => update.price.is_some(),
911 OrderType::Limit | OrderType::MarketToLimit => update.trigger_price.is_some(),
912 OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit => {
913 false
914 }
915 };
916
917 if invalid_fields {
918 return Err(OrderError::InvalidOrderEvent);
919 }
920 }
921
922 let source_status = self.status;
923
924 if !matches!(
929 event,
930 OrderEventAny::Initialized(_)
931 | OrderEventAny::ModifyRejected(_)
932 | OrderEventAny::CancelRejected(_)
933 ) && !matches!(
934 self.status,
935 OrderStatus::PendingUpdate | OrderStatus::PendingCancel
936 ) {
937 self.previous_status = Some(self.status);
938 }
939
940 self.status = new_status;
941
942 match &event {
943 OrderEventAny::Initialized(_) => return Err(OrderError::AlreadyInitialized),
945 OrderEventAny::Denied(event) => self.denied(event),
946 OrderEventAny::Emulated(event) => self.emulated(event),
947 OrderEventAny::Released(event) => self.released(event),
948 OrderEventAny::Submitted(event) => self.submitted(event),
949 OrderEventAny::Rejected(event) => self.rejected(event),
950 OrderEventAny::Accepted(event) => self.accepted(event),
951 OrderEventAny::PendingUpdate(event) => self.pending_update(event),
952 OrderEventAny::PendingCancel(event) => self.pending_cancel(event),
953 OrderEventAny::ModifyRejected(event) => self.modify_rejected(event, rejection_status),
954 OrderEventAny::CancelRejected(event) => self.cancel_rejected(event, rejection_status),
955 OrderEventAny::Updated(event) => self.updated(event),
956 OrderEventAny::Triggered(event) => self.triggered(event),
957 OrderEventAny::Canceled(event) => self.canceled(event),
958 OrderEventAny::Expired(event) => self.expired(event),
959 OrderEventAny::Filled(event) => self.filled(event, source_status),
960 OrderEventAny::FillVoided(event) => self.fill_voided(event, source_status),
961 }
962
963 if has_prior_fill_void {
964 self.fill_totals = None;
967 }
968
969 self.ts_last = event.ts_event();
970 self.events.push(event);
971 Ok(())
972 }
973
974 fn denied(&mut self, event: &OrderDenied) {
975 self.ts_closed = Some(event.ts_event);
976 }
977
978 fn emulated(&self, _event: &OrderEmulated) {
979 }
981
982 fn released(&mut self, _event: &OrderReleased) {
983 self.emulation_trigger = None;
984 }
985
986 fn submitted(&mut self, event: &OrderSubmitted) {
987 self.account_id = Some(event.account_id);
988 self.ts_submitted = Some(event.ts_event);
989 }
990
991 fn accepted(&mut self, event: &OrderAccepted) {
992 self.account_id = Some(event.account_id);
993 self.venue_order_id = Some(event.venue_order_id);
994 self.venue_order_ids.push(event.venue_order_id);
995 self.ts_accepted = Some(event.ts_event);
996 }
997
998 fn rejected(&mut self, event: &OrderRejected) {
999 self.ts_closed = Some(event.ts_event);
1000 }
1001
1002 fn pending_update(&self, _event: &OrderPendingUpdate) {
1003 }
1005
1006 fn pending_cancel(&self, _event: &OrderPendingCancel) {
1007 }
1009
1010 fn modify_rejected(&mut self, _event: &OrderModifyRejected, previous_status: OrderStatus) {
1011 if self.status != OrderStatus::PendingCancel {
1012 self.status = previous_status;
1013 }
1014 }
1015
1016 fn cancel_rejected(&mut self, _event: &OrderCancelRejected, previous_status: OrderStatus) {
1017 self.status = previous_status;
1018 }
1019
1020 fn triggered(&self, _event: &OrderTriggered) {}
1021
1022 fn canceled(&mut self, event: &OrderCanceled) {
1023 self.ts_closed = Some(event.ts_event);
1024 }
1025
1026 fn expired(&mut self, event: &OrderExpired) {
1027 self.ts_closed = Some(event.ts_event);
1028 }
1029
1030 fn validate_fill_void(&self, event: &OrderFillVoided) -> Result<(), OrderError> {
1031 let fill = self.events.iter().find_map(|candidate| match candidate {
1032 OrderEventAny::Filled(fill) if fill.trade_id == event.trade_id => Some(fill),
1033 _ => None,
1034 });
1035
1036 if event.voided_qty.is_zero() {
1037 return Err(OrderError::OverVoid(event.trade_id));
1038 }
1039
1040 let previous = self.latest_fill_void(event.trade_id);
1041 if let Some(previous) = previous {
1042 if event.voided_qty < previous.voided_qty {
1043 return Err(OrderError::StaleFillVoid(event.trade_id));
1044 }
1045
1046 if event.voided_qty == previous.voided_qty
1047 && event.commission_voided == previous.commission_voided
1048 && event.is_reopened == previous.is_reopened
1049 {
1050 return Err(OrderError::DuplicateFillVoid(event.trade_id));
1051 }
1052 }
1053
1054 let Some(fill) = fill else {
1055 if event.is_reopened {
1056 return Err(OrderError::InvalidOrderEvent);
1057 }
1058
1059 if !self.matches_fill_void_identity(event) {
1060 return Err(OrderError::InvalidOrderEvent);
1061 }
1062
1063 if event.voided_qty > self.quantity
1064 || event
1065 .commission_voided
1066 .is_some_and(|commission| !commission.is_zero())
1067 {
1068 return Err(OrderError::OverVoid(event.trade_id));
1069 }
1070 return Ok(());
1071 };
1072
1073 if fill.venue_order_id != event.venue_order_id
1074 || fill.account_id != event.account_id
1075 || fill.order_side != event.order_side
1076 || fill.order_type != event.order_type
1077 || fill.last_px != event.last_px
1078 || fill.currency != event.currency
1079 || fill.liquidity_side != event.liquidity_side
1080 || fill.position_id != event.position_id
1081 {
1082 return Err(OrderError::InvalidOrderEvent);
1083 }
1084
1085 if event.voided_qty > fill.last_qty {
1086 return Err(OrderError::OverVoid(event.trade_id));
1087 }
1088
1089 self.validate_fill_void_commission(fill, event, previous)
1090 }
1091
1092 fn matches_fill_void_identity(&self, event: &OrderFillVoided) -> bool {
1093 event.trader_id == self.trader_id
1094 && event.instrument_id == self.instrument_id
1095 && event.order_side == self.side
1096 && event.order_type == self.order_type
1097 && (self.venue_order_id == Some(event.venue_order_id)
1098 || self.venue_order_ids.contains(&event.venue_order_id))
1099 && self.account_id == Some(event.account_id)
1100 }
1101
1102 fn validate_fill_void_commission(
1103 &self,
1104 fill: &OrderFilled,
1105 event: &OrderFillVoided,
1106 previous: Option<&OrderFillVoided>,
1107 ) -> Result<(), OrderError> {
1108 match (fill.commission, event.commission_voided) {
1109 (None, None) => Ok(()),
1110 (None, Some(commission)) if commission.is_zero() => Ok(()),
1111 (Some(_), None) if previous.and_then(|prior| prior.commission_voided).is_none() => {
1112 Ok(())
1113 }
1114 (Some(commission), Some(voided))
1115 if commission.currency == voided.currency
1116 && commission.is_positive() == voided.is_positive()
1117 && commission.is_negative() == voided.is_negative()
1118 && voided.abs() <= commission.abs()
1119 && previous
1120 .and_then(|prior| prior.commission_voided)
1121 .is_none_or(|prior| voided.abs() >= prior.abs()) =>
1122 {
1123 Ok(())
1124 }
1125 _ => Err(OrderError::InvalidFillVoidCommission(event.trade_id)),
1126 }
1127 }
1128
1129 fn latest_fill_void(&self, trade_id: TradeId) -> Option<&OrderFillVoided> {
1130 self.events
1131 .iter()
1132 .rev()
1133 .find_map(|candidate| match candidate {
1134 OrderEventAny::FillVoided(event) if event.trade_id == trade_id => Some(event),
1135 _ => None,
1136 })
1137 }
1138
1139 fn fill_voided(&mut self, event: &OrderFillVoided, source_status: OrderStatus) {
1140 let has_local_fill = self.events.iter().any(
1141 |candidate| matches!(candidate, OrderEventAny::Filled(fill) if fill.trade_id == event.trade_id),
1142 );
1143 let non_reopened_voided_qty = self.recompute_fill_state(Some(event));
1144 let unfilled_qty = self.quantity.saturating_sub(self.filled_qty);
1145 let working_leaves = unfilled_qty.saturating_sub(non_reopened_voided_qty);
1146
1147 match source_status {
1148 OrderStatus::Voided => {
1149 self.status = OrderStatus::Voided;
1150 self.leaves_qty = Quantity::zero(self.quantity.precision);
1151 }
1152 _ if !has_local_fill && !event.is_reopened => {
1153 self.status = OrderStatus::Voided;
1154 self.leaves_qty = Quantity::zero(self.quantity.precision);
1155 }
1156 OrderStatus::Canceled | OrderStatus::Expired => {
1157 self.status = source_status;
1158 self.leaves_qty = unfilled_qty;
1159 }
1160 _ if working_leaves.is_zero() => {
1161 self.status = OrderStatus::Voided;
1162 self.leaves_qty = working_leaves;
1163 }
1164 OrderStatus::PendingUpdate | OrderStatus::PendingCancel | OrderStatus::Triggered => {
1165 self.status = source_status;
1166 self.leaves_qty = working_leaves;
1167 }
1168 _ => {
1169 self.status = if self.filled_qty.is_zero() {
1170 OrderStatus::Accepted
1171 } else {
1172 OrderStatus::PartiallyFilled
1173 };
1174 self.leaves_qty = working_leaves;
1175 }
1176 }
1177
1178 if self.status.is_open() {
1179 self.ts_closed = None;
1180 } else if self.status.is_closed() {
1181 self.ts_closed = self.ts_closed.or(Some(event.ts_event));
1182 }
1183 }
1184
1185 fn recompute_fill_state(&mut self, additional: Option<&OrderFillVoided>) -> Quantity {
1186 let corrections = self.fill_corrections(additional);
1187
1188 let mut totals = FillTotals::default();
1189 let mut filled = Quantity::zero(self.quantity.precision);
1190 let mut voided = Quantity::zero(self.quantity.precision);
1191 let mut non_reopened_voided = Quantity::zero(self.quantity.precision);
1192
1193 let mut commissions = IndexMap::<Currency, Money>::new();
1194 let mut trade_ids = Vec::new();
1195 let mut last_trade_id = None;
1196 let mut position_id = None;
1197 let mut liquidity_side = Some(LiquiditySide::NoLiquiditySide);
1198 let mut matched_corrections = AHashSet::new();
1199
1200 for candidate in &self.events {
1201 let OrderEventAny::Filled(fill) = candidate else {
1202 continue;
1203 };
1204 let correction = corrections.get(&fill.trade_id).copied();
1205 if correction.is_some() {
1206 matched_corrections.insert(fill.trade_id);
1207 }
1208 let removed = Self::removed_fill_qty(fill, correction);
1209 let effective = fill.last_qty - removed;
1210 voided = voided.saturating_add(removed);
1211 if correction.is_some_and(|event| !event.is_reopened) {
1212 non_reopened_voided = non_reopened_voided.saturating_add(removed);
1213 }
1214
1215 if !effective.is_zero() {
1216 totals.add(effective.as_decimal(), fill.last_px.as_decimal());
1217 filled = filled.saturating_add(effective);
1218 trade_ids.push(fill.trade_id);
1219 last_trade_id = Some(fill.trade_id);
1220 position_id = fill.position_id;
1221 liquidity_side = Some(fill.liquidity_side);
1222 }
1223
1224 if let Some(commission) = fill.commission {
1225 let surviving = correction
1226 .and_then(|event| event.commission_voided)
1227 .filter(|voided| !voided.is_zero())
1228 .map_or(commission, |voided| commission - voided);
1229
1230 if !surviving.is_zero() {
1231 commissions
1232 .entry(surviving.currency)
1233 .and_modify(|total| *total = *total + surviving)
1234 .or_insert(surviving);
1235 }
1236 }
1237 }
1238
1239 for (trade_id, correction) in corrections {
1240 if !matched_corrections.contains(&trade_id) {
1241 voided = voided.saturating_add(correction.voided_qty);
1242 }
1243 }
1244
1245 self.filled_qty = filled;
1246 self.voided_qty = voided;
1247 self.overfill_qty = self.filled_qty.saturating_sub(self.quantity);
1248 self.avg_px = totals.average();
1249 self.fill_totals = Some(totals);
1250 self.commissions = commissions;
1251 self.trade_ids = trade_ids;
1252 self.last_trade_id = last_trade_id;
1253 self.position_id = position_id;
1254 self.liquidity_side = liquidity_side;
1255
1256 non_reopened_voided
1257 }
1258
1259 fn updated(&mut self, event: &OrderUpdated) {
1260 if matches!(
1261 self.status,
1262 OrderStatus::PendingUpdate | OrderStatus::PendingCancel,
1263 ) && let Some(previous) = self.previous_status
1264 {
1265 self.status = previous;
1266 }
1267
1268 if event.reconciliation && !self.filled_qty.is_zero() && event.quantity == self.filled_qty {
1269 self.status = OrderStatus::Filled;
1270 self.ts_closed = Some(event.ts_event);
1271 }
1272
1273 if let Some(venue_order_id) = &event.venue_order_id
1274 && (self.venue_order_id.is_none()
1275 || venue_order_id != self.venue_order_id.as_ref().unwrap())
1276 {
1277 self.venue_order_id = Some(*venue_order_id);
1278 self.venue_order_ids.push(*venue_order_id);
1279 }
1280
1281 self.is_quote_quantity = event.is_quote_quantity;
1282 }
1283
1284 fn filled(&mut self, event: &OrderFilled, source_status: OrderStatus) {
1285 let new_filled_qty = self
1286 .filled_qty
1287 .checked_add(event.last_qty)
1288 .expect("fill quantity bounds pre-checked");
1289
1290 if new_filled_qty > self.quantity {
1292 let overfill = new_filled_qty - self.quantity;
1293 self.overfill_qty = self.overfill_qty.saturating_add(overfill);
1294 }
1295
1296 let new_leaves_qty = self.leaves_qty.saturating_sub(event.last_qty);
1297
1298 if new_filled_qty >= self.quantity {
1299 self.status = OrderStatus::Filled;
1300 self.ts_closed = Some(event.ts_event);
1301 } else if new_leaves_qty.is_zero() && !self.voided_qty.is_zero() {
1302 self.status = OrderStatus::Voided;
1303 self.ts_closed = Some(event.ts_event);
1304 } else if source_status == OrderStatus::Canceled {
1305 self.status = OrderStatus::Canceled;
1306 } else {
1307 self.status = OrderStatus::PartiallyFilled;
1308
1309 if matches!(
1310 source_status,
1311 OrderStatus::PendingUpdate | OrderStatus::PendingCancel,
1312 ) {
1313 self.previous_status = Some(self.status);
1314 self.status = source_status;
1315 }
1316 }
1317
1318 let is_historical_venue_order_id = self.venue_order_id.is_some_and(|current| {
1319 current != event.venue_order_id && self.venue_order_ids.contains(&event.venue_order_id)
1320 });
1321
1322 if !is_historical_venue_order_id {
1323 self.venue_order_id = Some(event.venue_order_id);
1324 }
1325 self.position_id = event.position_id;
1326 self.trade_ids.push(event.trade_id);
1327 self.last_trade_id = Some(event.trade_id);
1328 self.liquidity_side = Some(event.liquidity_side);
1329 self.filled_qty = new_filled_qty;
1330 self.leaves_qty = new_leaves_qty;
1331 self.ts_last = event.ts_event;
1332
1333 if let Some(commission) = event.commission {
1334 let commission_currency = commission.currency;
1335 if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) {
1336 *existing_commission = *existing_commission + commission;
1337 } else {
1338 self.commissions.insert(commission_currency, commission);
1339 }
1340 }
1341
1342 if self.ts_accepted.is_none() {
1343 self.ts_accepted = Some(event.ts_event);
1345 }
1346
1347 if self.fill_totals.is_none() {
1348 self.fill_totals = Some(self.fill_totals_from_events());
1349 }
1350
1351 let totals = self.fill_totals.as_mut().expect("fill totals initialized");
1352 totals.add(event.last_qty.as_decimal(), event.last_px.as_decimal());
1353 self.avg_px = totals.average();
1354
1355 debug_assert!(
1356 matches!(
1357 self.status,
1358 OrderStatus::PartiallyFilled | OrderStatus::Filled | OrderStatus::Voided
1359 ) || (self.status == source_status
1360 && matches!(
1361 source_status,
1362 OrderStatus::Canceled | OrderStatus::PendingUpdate | OrderStatus::PendingCancel
1363 )),
1364 "Invariant: status must reflect the fill or preserve a pending source status after fill handler (status={:?})",
1365 self.status
1366 );
1367 debug_assert!(
1368 self.venue_order_id.is_some()
1369 && self.last_trade_id.is_some()
1370 && !self.trade_ids.is_empty(),
1371 "Invariant: venue_order_id, last_trade_id, and trade_ids must be set after fill"
1372 );
1373 debug_assert!(
1374 self.filled_qty
1375 .saturating_add(self.voided_qty)
1376 .saturating_add(self.leaves_qty)
1377 >= self.quantity,
1378 "Invariant: filled_qty + voided_qty + leaves_qty >= quantity (filled={}, voided={}, leaves={}, quantity={})",
1379 self.filled_qty,
1380 self.voided_qty,
1381 self.leaves_qty,
1382 self.quantity
1383 );
1384 }
1385
1386 fn fill_totals_from_events(&self) -> FillTotals {
1387 let corrections = self.fill_corrections(None);
1388 let mut totals = FillTotals::default();
1389
1390 for candidate in &self.events {
1391 let OrderEventAny::Filled(fill) = candidate else {
1392 continue;
1393 };
1394 let removed = Self::removed_fill_qty(fill, corrections.get(&fill.trade_id).copied());
1395 let effective = (fill.last_qty - removed).as_decimal();
1396 if !effective.is_zero() {
1397 totals.add(effective, fill.last_px.as_decimal());
1398 }
1399 }
1400
1401 totals
1402 }
1403
1404 fn fill_corrections<'a>(
1405 &'a self,
1406 additional: Option<&'a OrderFillVoided>,
1407 ) -> IndexMap<TradeId, &'a OrderFillVoided> {
1408 let mut corrections = IndexMap::new();
1409
1410 for candidate in &self.events {
1411 if let OrderEventAny::FillVoided(event) = candidate {
1412 corrections.insert(event.trade_id, event);
1413 }
1414 }
1415
1416 if let Some(event) = additional {
1417 corrections.insert(event.trade_id, event);
1418 }
1419
1420 corrections
1421 }
1422
1423 fn removed_fill_qty(fill: &OrderFilled, correction: Option<&OrderFillVoided>) -> Quantity {
1424 correction.map_or_else(
1425 || Quantity::zero(fill.last_qty.precision),
1426 |event| event.voided_qty.min(fill.last_qty),
1427 )
1428 }
1429
1430 pub fn set_slippage(&mut self, price: Price) {
1431 self.slippage = self.avg_px.and_then(|avg_px| {
1432 let current_price = price.as_decimal();
1433 match self.side {
1434 OrderSide::Buy if avg_px > current_price => avg_px.checked_sub(current_price),
1435 OrderSide::Sell if avg_px < current_price => current_price.checked_sub(avg_px),
1436 _ => None,
1437 }
1438 });
1439 }
1440
1441 #[must_use]
1443 pub fn opposite_side(side: OrderSide) -> OrderSide {
1444 side.opposite()
1445 }
1446
1447 #[must_use]
1449 pub fn closing_side(side: PositionSide) -> Option<OrderSide> {
1450 match side {
1451 PositionSide::Long => Some(OrderSide::Sell),
1452 PositionSide::Short => Some(OrderSide::Buy),
1453 PositionSide::Flat => None,
1454 }
1455 }
1456
1457 #[must_use]
1458 pub fn signed_decimal_qty(&self) -> Decimal {
1459 match self.side {
1460 OrderSide::Buy => self.quantity.as_decimal(),
1461 OrderSide::Sell => -self.quantity.as_decimal(),
1462 }
1463 }
1464
1465 #[must_use]
1466 pub fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
1467 if side == PositionSide::Flat {
1468 return false;
1469 }
1470
1471 match (self.side, side) {
1472 (OrderSide::Buy, PositionSide::Long) => false,
1473 (OrderSide::Buy, PositionSide::Short) => self.leaves_qty <= position_qty,
1474 (OrderSide::Sell, PositionSide::Short) => false,
1475 (OrderSide::Sell, PositionSide::Long) => self.leaves_qty <= position_qty,
1476 (_, PositionSide::Flat) => false,
1477 }
1478 }
1479
1480 #[must_use]
1481 pub fn commission(&self, currency: &Currency) -> Option<Money> {
1482 self.commissions.get(currency).copied()
1483 }
1484
1485 #[must_use]
1486 pub fn commissions(&self) -> IndexMap<Currency, Money> {
1487 self.commissions.clone()
1488 }
1489
1490 #[must_use]
1491 pub fn commissions_vec(&self) -> Vec<Money> {
1492 self.commissions.values().copied().collect()
1493 }
1494
1495 #[must_use]
1497 pub fn events(&self) -> &[OrderEventAny] {
1498 &self.events
1499 }
1500
1501 pub fn prepend_events(&mut self, events: impl IntoIterator<Item = OrderEventAny>) {
1506 self.fill_totals = None;
1507 self.events.splice(..0, events);
1508 }
1509
1510 #[must_use]
1511 pub fn init_event(&self) -> Option<OrderEventAny> {
1512 self.events.first().cloned()
1513 }
1514}
1515
1516#[derive(Clone, Debug, Default)]
1519struct FillTotals {
1520 notional: Decimal,
1521 quantity: Decimal,
1522}
1523
1524impl FillTotals {
1525 fn add(&mut self, quantity: Decimal, price: Decimal) {
1526 self.notional = self.notional.saturating_add(quantity.saturating_mul(price));
1527 self.quantity = self.quantity.saturating_add(quantity);
1528 }
1529
1530 fn average(&self) -> Option<Decimal> {
1531 self.notional.checked_div(self.quantity)
1532 }
1533}
1534
1535#[cfg(test)]
1536mod tests {
1537 use std::str::FromStr;
1538
1539 use rstest::rstest;
1540 use rust_decimal_macros::dec;
1541
1542 use super::*;
1543 use crate::{
1544 enums::{
1545 LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, TrailingOffsetType,
1546 TriggerType,
1547 },
1548 events::order::spec::{
1549 OrderAcceptedSpec, OrderCancelRejectedSpec, OrderCanceledSpec, OrderDeniedSpec,
1550 OrderExpiredSpec, OrderFillVoidedSpec, OrderFilledSpec, OrderInitializedSpec,
1551 OrderModifyRejectedSpec, OrderPendingCancelSpec, OrderPendingUpdateSpec,
1552 OrderRejectedSpec, OrderSubmittedSpec, OrderTriggeredSpec, OrderUpdatedSpec,
1553 },
1554 identifiers::InstrumentId,
1555 instruments::{CurrencyPair, Instrument, InstrumentAny, stubs::audusd_sim},
1556 orders::{MarketOrder, builder::OrderTestBuilder, stubs::TestOrderStubs},
1557 types::{
1558 Price, Quantity,
1559 quantity::{QUANTITY_RAW_MAX, QuantityRaw},
1560 },
1561 };
1562
1563 #[rstest]
1574 #[case(OrderSide::Buy, OrderSide::Sell)]
1575 #[case(OrderSide::Sell, OrderSide::Buy)]
1576 fn test_order_opposite_side(#[case] order_side: OrderSide, #[case] expected_side: OrderSide) {
1577 let result = OrderCore::opposite_side(order_side);
1578 assert_eq!(result, expected_side);
1579 }
1580
1581 #[rstest]
1582 #[case(PositionSide::Long, Some(OrderSide::Sell))]
1583 #[case(PositionSide::Short, Some(OrderSide::Buy))]
1584 #[case(PositionSide::Flat, None)]
1585 fn test_closing_side(
1586 #[case] position_side: PositionSide,
1587 #[case] expected_side: Option<OrderSide>,
1588 ) {
1589 let result = OrderCore::closing_side(position_side);
1590 assert_eq!(result, expected_side);
1591 }
1592
1593 #[rstest]
1594 #[case(OrderSide::Buy, dec!(10_000))]
1595 #[case(OrderSide::Sell, dec!(-10_000))]
1596 fn test_signed_decimal_qty(#[case] order_side: OrderSide, #[case] expected: Decimal) {
1597 let order: MarketOrder = OrderInitializedSpec::builder()
1598 .order_side(order_side)
1599 .quantity(Quantity::from(10_000))
1600 .build()
1601 .try_into()
1602 .unwrap();
1603
1604 let result = order.signed_decimal_qty();
1605 assert_eq!(result, expected);
1606 }
1607
1608 #[rustfmt::skip]
1609 #[rstest]
1610 #[case(OrderSide::Buy, Quantity::from(100), PositionSide::Long, Quantity::from(50), false)]
1611 #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(50), true)]
1612 #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(100), true)]
1613 #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)]
1614 #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)]
1615 #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(50), true)]
1616 #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(100), true)]
1617 #[case(OrderSide::Sell, Quantity::from(100), PositionSide::Short, Quantity::from(50), false)]
1618 fn test_would_reduce_only(
1619 #[case] order_side: OrderSide,
1620 #[case] order_qty: Quantity,
1621 #[case] position_side: PositionSide,
1622 #[case] position_qty: Quantity,
1623 #[case] expected: bool,
1624 ) {
1625 let order: MarketOrder = OrderInitializedSpec::builder()
1626 .order_side(order_side)
1627 .quantity(order_qty)
1628 .build()
1629 .try_into()
1630 .unwrap();
1631
1632 assert_eq!(
1633 order.would_reduce_only(position_side, position_qty),
1634 expected
1635 );
1636 }
1637
1638 #[rstest]
1639 fn test_order_state_transition_denied() {
1640 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
1641 let denied = OrderDeniedSpec::builder().build();
1642 let event = OrderEventAny::Denied(denied);
1643
1644 order.apply(event.clone()).unwrap();
1645
1646 assert_eq!(order.status, OrderStatus::Denied);
1647 assert!(order.is_closed());
1648 assert!(!order.is_open());
1649 assert_eq!(order.event_count(), 2);
1650 assert_eq!(order.last_event(), &event);
1651 }
1652
1653 #[rstest]
1654 fn test_order_life_cycle_to_filled() {
1655 let init = OrderInitializedSpec::builder().build();
1656 let submitted = OrderSubmittedSpec::builder().build();
1657 let accepted = OrderAcceptedSpec::builder().build();
1658 let filled = OrderFilledSpec::builder().build();
1659
1660 let mut order: MarketOrder = init.clone().try_into().unwrap();
1661 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1662 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1663 order.apply(OrderEventAny::Filled(filled)).unwrap();
1664
1665 assert_eq!(order.client_order_id, init.client_order_id);
1666 assert_eq!(order.status(), OrderStatus::Filled);
1667 assert_eq!(order.filled_qty(), Quantity::from(100_000));
1668 assert_eq!(order.leaves_qty(), Quantity::from(0));
1669 assert_eq!(order.avg_px(), Some(dec!(1.0)));
1670 assert!(!order.is_open());
1671 assert!(order.is_closed());
1672 assert_eq!(order.commission(&Currency::USD()), None);
1673 assert_eq!(order.commissions(), &IndexMap::new());
1674 }
1675
1676 #[rstest]
1677 fn test_order_life_cycle_fills_with_negative_prices() {
1678 let init = OrderInitializedSpec::builder()
1682 .quantity(Quantity::from(100_000))
1683 .build();
1684 let submitted = OrderSubmittedSpec::builder().build();
1685 let accepted = OrderAcceptedSpec::builder().build();
1686 let fill1 = OrderFilledSpec::builder()
1687 .last_qty(Quantity::from(50_000))
1688 .last_px(Price::from("-5.00000"))
1689 .trade_id(TradeId::from("TRADE-1"))
1690 .build();
1691 let fill2 = OrderFilledSpec::builder()
1692 .last_qty(Quantity::from(50_000))
1693 .last_px(Price::from("-7.00000"))
1694 .trade_id(TradeId::from("TRADE-2"))
1695 .build();
1696
1697 let mut order: MarketOrder = init.try_into().unwrap();
1698 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1699 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1700 order.apply(OrderEventAny::Filled(fill1)).unwrap();
1701 order.apply(OrderEventAny::Filled(fill2)).unwrap();
1702
1703 assert_eq!(order.status(), OrderStatus::Filled);
1704 assert_eq!(order.filled_qty(), Quantity::from(100_000));
1705 assert_eq!(order.leaves_qty(), Quantity::from(0));
1706 assert_eq!(order.avg_px(), Some(dec!(-6.0)));
1708 }
1709
1710 fn fill(trade_id: &str, last_qty: &str, last_px: &str) -> (TradeId, Quantity, Price) {
1711 (
1712 TradeId::from(trade_id),
1713 Quantity::from(last_qty),
1714 Price::from(last_px),
1715 )
1716 }
1717
1718 fn market_order_with_fills(
1719 order_side: OrderSide,
1720 quantity: Quantity,
1721 fills: &[(TradeId, Quantity, Price)],
1722 ) -> MarketOrder {
1723 let mut order: MarketOrder = OrderInitializedSpec::builder()
1724 .order_side(order_side)
1725 .quantity(quantity)
1726 .build()
1727 .try_into()
1728 .unwrap();
1729 order
1730 .apply(OrderEventAny::Accepted(
1731 OrderAcceptedSpec::builder().build(),
1732 ))
1733 .unwrap();
1734
1735 for (trade_id, last_qty, last_px) in fills {
1736 order
1737 .apply(OrderEventAny::Filled(
1738 OrderFilledSpec::builder()
1739 .order_side(order_side)
1740 .trade_id(*trade_id)
1741 .last_qty(*last_qty)
1742 .last_px(*last_px)
1743 .build(),
1744 ))
1745 .unwrap();
1746 }
1747
1748 order
1749 }
1750
1751 #[rstest]
1752 #[case::cloned("clone")]
1753 #[case::snapshot("snapshot")]
1754 #[case::replayed("replay")]
1755 fn test_fill_totals_survive_corrections_and_restoration(#[case] restoration: &str) {
1756 let mut order = market_order_with_fills(OrderSide::Buy, Quantity::from(20), &[]);
1757 let first = OrderFilledSpec::builder()
1758 .trade_id(TradeId::from("T-1"))
1759 .last_qty(Quantity::from(4))
1760 .last_px(Price::from("1.00"))
1761 .build();
1762 let second = OrderFilledSpec::builder()
1763 .trade_id(TradeId::from("T-2"))
1764 .last_qty(Quantity::from(2))
1765 .last_px(Price::from("2.00"))
1766 .build();
1767 let third = OrderFilledSpec::builder()
1768 .trade_id(TradeId::from("T-3"))
1769 .last_qty(Quantity::from(1))
1770 .last_px(Price::from("-3.00"))
1771 .build();
1772 let fourth = OrderFilledSpec::builder()
1773 .trade_id(TradeId::from("T-4"))
1774 .last_qty(Quantity::from(2))
1775 .last_px(Price::from("0.15"))
1776 .build();
1777 let steps = [
1778 (
1779 OrderEventAny::Filled(first.clone()),
1780 4,
1781 0,
1782 16,
1783 Some(dec!(1)),
1784 ),
1785 (
1786 OrderEventAny::Filled(second.clone()),
1787 6,
1788 0,
1789 14,
1790 Some(dec!(1.3333333333333333333333333333)),
1791 ),
1792 (
1793 OrderEventAny::FillVoided(matching_fill_void(&first, Quantity::from(1), None)),
1794 5,
1795 1,
1796 15,
1797 Some(dec!(1.4)),
1798 ),
1799 (
1800 OrderEventAny::FillVoided(matching_fill_void(&first, Quantity::from(3), None)),
1801 3,
1802 3,
1803 17,
1804 Some(dec!(1.6666666666666666666666666667)),
1805 ),
1806 (
1807 OrderEventAny::Filled(third.clone()),
1808 4,
1809 3,
1810 16,
1811 Some(dec!(0.5)),
1812 ),
1813 (
1814 OrderEventAny::FillVoided(matching_fill_void(&first, Quantity::from(4), None)),
1815 3,
1816 4,
1817 17,
1818 Some(dec!(0.3333333333333333333333333333)),
1819 ),
1820 (
1821 OrderEventAny::FillVoided(matching_fill_void(&second, Quantity::from(2), None)),
1822 1,
1823 6,
1824 19,
1825 Some(dec!(-3)),
1826 ),
1827 (
1828 OrderEventAny::FillVoided(matching_fill_void(&third, Quantity::from(1), None)),
1829 0,
1830 7,
1831 20,
1832 None,
1833 ),
1834 (OrderEventAny::Filled(fourth), 2, 7, 18, Some(dec!(0.15))),
1835 ];
1836 let mut expected_events = OrderCore::events(&order).to_vec();
1837
1838 for (event, filled, voided, leaves, average) in steps {
1839 let mut restored = match restoration {
1840 "clone" => order.clone(),
1841 "snapshot" => {
1842 let snapshot = serde_json::to_value(&order).unwrap();
1843 assert!(
1844 !snapshot["core"]
1845 .as_object()
1846 .unwrap()
1847 .contains_key("fill_totals")
1848 );
1849 serde_json::from_value(snapshot).unwrap()
1850 }
1851 "replay" => {
1852 let replayed = OrderAny::from_events(expected_events.clone()).unwrap();
1853
1854 let OrderAny::Market(replayed) = replayed else {
1855 panic!("Expected a market order");
1856 };
1857
1858 replayed
1859 }
1860 _ => unreachable!(),
1861 };
1862
1863 order.apply(event.clone()).unwrap();
1864 restored.apply(event.clone()).unwrap();
1865 expected_events.push(event);
1866
1867 assert_eq!(restored.avg_px(), average);
1868 assert_eq!(restored.filled_qty(), Quantity::from(filled));
1869 assert_eq!(restored.voided_qty(), Quantity::from(voided));
1870 assert_eq!(restored.leaves_qty(), Quantity::from(leaves));
1871 assert_eq!(OrderCore::events(&restored), expected_events);
1872 assert_eq!(
1873 serde_json::to_value(&restored).unwrap(),
1874 serde_json::to_value(&order).unwrap()
1875 );
1876 order = restored;
1877 }
1878 }
1879
1880 #[rstest]
1881 fn test_prepended_events_invalidate_fill_totals_without_applying_state() {
1882 let mut order = market_order_with_fills(
1883 OrderSide::Buy,
1884 Quantity::from(10),
1885 &[fill("T-CURRENT", "1", "2.00")],
1886 );
1887 let historical = [
1888 OrderEventAny::Filled(
1889 OrderFilledSpec::builder()
1890 .trade_id(TradeId::from("T-PAST-1"))
1891 .last_qty(Quantity::from(2))
1892 .last_px(Price::from("3.00"))
1893 .build(),
1894 ),
1895 OrderEventAny::Filled(
1896 OrderFilledSpec::builder()
1897 .trade_id(TradeId::from("T-PAST-2"))
1898 .last_qty(Quantity::from(1))
1899 .last_px(Price::from("4.00"))
1900 .build(),
1901 ),
1902 ];
1903 let mut expected_events = historical.to_vec();
1904 expected_events.extend_from_slice(OrderCore::events(&order));
1905 order.prepend_events(historical);
1906 assert_eq!(OrderCore::events(&order), expected_events);
1907 assert_eq!(order.avg_px(), Some(dec!(2)));
1908 assert_eq!(order.filled_qty(), Quantity::from(1));
1909 assert_eq!(order.leaves_qty(), Quantity::from(9));
1910
1911 let next = OrderEventAny::Filled(
1912 OrderFilledSpec::builder()
1913 .trade_id(TradeId::from("T-NEXT"))
1914 .last_qty(Quantity::from(2))
1915 .last_px(Price::from("3.00"))
1916 .build(),
1917 );
1918 expected_events.push(next.clone());
1919 order.apply(next).unwrap();
1920
1921 assert_eq!(OrderCore::events(&order), expected_events);
1922 assert_eq!(order.avg_px(), Some(dec!(3)));
1923 assert_eq!(order.filled_qty(), Quantity::from(3));
1924 assert_eq!(order.leaves_qty(), Quantity::from(7));
1925 assert_eq!(order.voided_qty(), Quantity::from(0));
1926 }
1927
1928 #[rstest]
1929 fn test_restored_fill_totals_use_history_instead_of_snapshot_average() {
1930 let order = market_order_with_fills(
1931 OrderSide::Buy,
1932 Quantity::from(10),
1933 &[fill("T-1", "1", "0.10"), fill("T-2", "2", "0.20")],
1934 );
1935 let mut snapshot = serde_json::to_value(&order).unwrap();
1936 snapshot["core"]["avg_px"] = serde_json::to_value(dec!(99)).unwrap();
1937 let mut restored: MarketOrder = serde_json::from_value(snapshot).unwrap();
1938 restored
1939 .apply(OrderEventAny::Filled(
1940 OrderFilledSpec::builder()
1941 .trade_id(TradeId::from("T-3"))
1942 .last_qty(Quantity::from(2))
1943 .last_px(Price::from("0.35"))
1944 .build(),
1945 ))
1946 .unwrap();
1947
1948 assert_eq!(restored.avg_px(), Some(dec!(0.24)));
1949 assert_eq!(restored.filled_qty(), Quantity::from(5));
1950 assert_eq!(restored.leaves_qty(), Quantity::from(5));
1951 assert_eq!(restored.voided_qty(), Quantity::from(0));
1952 }
1953
1954 #[rstest]
1955 fn test_restored_unknown_fill_void_applies_after_matching_fill() {
1956 let mut order = market_order_with_fills(OrderSide::Buy, Quantity::from(10), &[]);
1957 let first = OrderFilledSpec::builder()
1958 .trade_id(TradeId::from("T-RESTORED"))
1959 .last_qty(Quantity::from(4))
1960 .last_px(Price::from("1.00"))
1961 .build();
1962 let mut correction = matching_fill_void(&first, Quantity::from(2), None);
1963 correction.is_reopened = false;
1964 order.apply(OrderEventAny::FillVoided(correction)).unwrap();
1965 let mut snapshot = serde_json::to_value(&order).unwrap();
1966 snapshot["core"]["status"] = serde_json::to_value(OrderStatus::Accepted).unwrap();
1967 snapshot["core"]["leaves_qty"] = serde_json::to_value(Quantity::from(8)).unwrap();
1968 let mut restored: MarketOrder = serde_json::from_value(snapshot).unwrap();
1969 restored.apply(OrderEventAny::Filled(first)).unwrap();
1970 assert_eq!(restored.avg_px(), Some(dec!(1)));
1971
1972 restored
1973 .apply(OrderEventAny::Filled(
1974 OrderFilledSpec::builder()
1975 .trade_id(TradeId::from("T-NEXT"))
1976 .last_qty(Quantity::from(2))
1977 .last_px(Price::from("3.00"))
1978 .build(),
1979 ))
1980 .unwrap();
1981
1982 assert_eq!(restored.avg_px(), Some(dec!(2)));
1983 assert_eq!(restored.filled_qty(), Quantity::from(6));
1984 assert_eq!(restored.voided_qty(), Quantity::from(2));
1985 assert_eq!(restored.leaves_qty(), Quantity::from(2));
1986 }
1987
1988 #[rstest]
1989 #[case::positive(Decimal::MAX, Decimal::ONE, Decimal::MAX)]
1990 #[case::negative(Decimal::MIN, -Decimal::ONE, Decimal::MIN)]
1991 fn test_fill_totals_preserve_saturation_order(
1992 #[case] price: Decimal,
1993 #[case] increment: Decimal,
1994 #[case] saturated: Decimal,
1995 ) {
1996 let mut totals = FillTotals::default();
1997 totals.add(dec!(2), price);
1998 totals.add(Decimal::ONE, increment);
1999 totals.add(Decimal::ONE, -increment);
2000
2001 assert_eq!(totals.notional, saturated - increment);
2002 assert_eq!(totals.quantity, dec!(4));
2003 assert_eq!(
2004 totals.average(),
2005 (saturated - increment).checked_div(dec!(4))
2006 );
2007 }
2008
2009 #[rstest]
2010 fn test_fill_totals_preserve_quantity_saturation() {
2011 let mut totals = FillTotals::default();
2012 totals.add(Decimal::MAX, Decimal::ZERO);
2013 totals.add(Decimal::ONE, Decimal::ONE);
2014
2015 assert_eq!(totals.notional, Decimal::ONE);
2016 assert_eq!(totals.quantity, Decimal::MAX);
2017 assert_eq!(totals.average(), Decimal::ONE.checked_div(Decimal::MAX));
2018 }
2019
2020 #[rstest]
2021 #[case(OrderSide::Buy)]
2022 #[case(OrderSide::Sell)]
2023 fn test_avg_px_weighted_average_is_exact(#[case] order_side: OrderSide) {
2024 let order = market_order_with_fills(
2027 order_side,
2028 Quantity::from(6),
2029 &[
2030 fill("TRADE-1", "1", "0.07"),
2031 fill("TRADE-2", "2", "0.13"),
2032 fill("TRADE-3", "3", "0.29"),
2033 ],
2034 );
2035
2036 assert_eq!(order.status(), OrderStatus::Filled);
2037 assert_eq!(order.filled_qty(), Quantity::from(6));
2038 assert_eq!(order.leaves_qty(), Quantity::from(0));
2039 assert_eq!(order.voided_qty(), Quantity::from(0));
2040 assert_eq!(order.avg_px(), Some(dec!(0.2)));
2041 }
2042
2043 #[rstest]
2044 fn test_avg_px_keeps_a_quotient_no_f64_can_hold() {
2045 let order = market_order_with_fills(
2048 OrderSide::Buy,
2049 Quantity::from(3),
2050 &[fill("TRADE-1", "1", "1.00"), fill("TRADE-2", "2", "2.00")],
2051 );
2052
2053 assert_eq!(order.status(), OrderStatus::Filled);
2054 assert_eq!(order.filled_qty(), Quantity::from(3));
2055 assert_eq!(order.avg_px(), Some(dec!(1.6666666666666666666666666667)));
2056 assert_ne!(
2057 order.avg_px(),
2058 Some(Decimal::from_f64_retain(5.0_f64 / 3.0).unwrap())
2059 );
2060 }
2061
2062 #[rstest]
2063 #[case(OrderSide::Buy, "1.07", "1.00", Some(dec!(0.07)))]
2064 #[case(OrderSide::Sell, "0.93", "1.00", Some(dec!(0.07)))]
2065 #[case(OrderSide::Buy, "0.93", "1.00", None)]
2066 #[case(OrderSide::Sell, "1.07", "1.00", None)]
2067 fn test_set_slippage_by_side(
2068 #[case] order_side: OrderSide,
2069 #[case] fill_px: &str,
2070 #[case] reference_px: &str,
2071 #[case] expected: Option<Decimal>,
2072 ) {
2073 let mut order =
2076 market_order_with_fills(order_side, Quantity::from(1), &[fill("T-1", "1", fill_px)]);
2077
2078 order.set_slippage(Price::from(reference_px));
2079
2080 assert_eq!(order.avg_px(), Some(Decimal::from_str(fill_px).unwrap()));
2081 assert_eq!(order.slippage(), expected);
2082 }
2083
2084 #[rstest]
2085 #[case::limit(OrderType::Limit)]
2086 #[case::limit_if_touched(OrderType::LimitIfTouched)]
2087 #[case::market_if_touched(OrderType::MarketIfTouched)]
2088 #[case::market_to_limit(OrderType::MarketToLimit)]
2089 #[case::stop_limit(OrderType::StopLimit)]
2090 #[case::stop_market(OrderType::StopMarket)]
2091 #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
2092 #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
2093 fn test_fill_void_recomputes_slippage(#[case] order_type: OrderType) {
2094 let venue_order_id = VenueOrderId::from("V-SLIPPAGE");
2095 let account_id = AccountId::from("SIM-SLIPPAGE");
2096 let mut order = OrderTestBuilder::new(order_type)
2097 .instrument_id(InstrumentId::from("ETHUSDT-LINEAR.BYBIT"))
2098 .client_order_id(ClientOrderId::from("O-SLIPPAGE"))
2099 .side(OrderSide::Buy)
2100 .quantity(Quantity::from(10))
2101 .price(Price::from("100.00"))
2102 .trigger_price(Price::from("100.00"))
2103 .trigger_type(TriggerType::LastPrice)
2104 .limit_offset(dec!(1))
2105 .trailing_offset(dec!(1))
2106 .trailing_offset_type(TrailingOffsetType::Price)
2107 .build();
2108 let accepted = OrderAcceptedSpec::builder()
2109 .trader_id(order.trader_id())
2110 .strategy_id(order.strategy_id())
2111 .instrument_id(order.instrument_id())
2112 .client_order_id(order.client_order_id())
2113 .venue_order_id(venue_order_id)
2114 .account_id(account_id)
2115 .build();
2116 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2117
2118 if order_type == OrderType::MarketToLimit {
2119 order
2120 .apply(OrderEventAny::Updated(
2121 OrderUpdatedSpec::builder()
2122 .trader_id(order.trader_id())
2123 .strategy_id(order.strategy_id())
2124 .instrument_id(order.instrument_id())
2125 .client_order_id(order.client_order_id())
2126 .quantity(order.quantity())
2127 .venue_order_id(venue_order_id)
2128 .account_id(account_id)
2129 .price(Price::from("100.00"))
2130 .build(),
2131 ))
2132 .unwrap();
2133 }
2134
2135 let fill = |trade_id: &str, last_px: &str| {
2136 OrderFilledSpec::builder()
2137 .trader_id(order.trader_id())
2138 .strategy_id(order.strategy_id())
2139 .instrument_id(order.instrument_id())
2140 .client_order_id(order.client_order_id())
2141 .venue_order_id(venue_order_id)
2142 .account_id(account_id)
2143 .trade_id(TradeId::from(trade_id))
2144 .order_side(order.order_side())
2145 .order_type(order.order_type())
2146 .last_qty(Quantity::from(5))
2147 .last_px(Price::from(last_px))
2148 .build()
2149 };
2150 let unfavorable_fill = fill("T-SLIPPAGE-1", "110.00");
2151 let reference_fill = fill("T-SLIPPAGE-2", "100.00");
2152 order
2153 .apply(OrderEventAny::Filled(unfavorable_fill.clone()))
2154 .unwrap();
2155 order.apply(OrderEventAny::Filled(reference_fill)).unwrap();
2156 assert_eq!(order.avg_px(), Some(dec!(105)));
2157 assert_eq!(order.slippage(), Some(dec!(5)));
2158
2159 let correction = matching_fill_void(&unfavorable_fill, unfavorable_fill.last_qty, None);
2160 let event_count = order.event_count();
2161 order.apply(OrderEventAny::FillVoided(correction)).unwrap();
2162 let replayed =
2163 OrderAny::from_events(order.events().into_iter().cloned().collect()).unwrap();
2164
2165 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2166 assert_eq!(order.filled_qty(), Quantity::from(5));
2167 assert_eq!(order.voided_qty(), Quantity::from(5));
2168 assert_eq!(order.leaves_qty(), Quantity::from(5));
2169 assert_eq!(order.avg_px(), Some(dec!(100)));
2170 assert_eq!(order.slippage(), None);
2171 assert_eq!(order.event_count(), event_count + 1);
2172 assert_eq!(replayed.status(), order.status());
2173 assert_eq!(replayed.filled_qty(), order.filled_qty());
2174 assert_eq!(replayed.voided_qty(), order.voided_qty());
2175 assert_eq!(replayed.leaves_qty(), order.leaves_qty());
2176 assert_eq!(replayed.avg_px(), order.avg_px());
2177 assert_eq!(replayed.slippage(), order.slippage());
2178 }
2179
2180 #[rstest]
2181 fn test_avg_px_invariant_to_fill_arrival_order() {
2182 let mut fills = [
2186 fill("TRADE-1", "50000", "1.00001"),
2187 fill("TRADE-2", "30000", "1.00002"),
2188 fill("TRADE-3", "20000", "1.00003"),
2189 ];
2190
2191 let ascending = market_order_with_fills(OrderSide::Buy, Quantity::from(100_000), &fills);
2192 fills.reverse();
2193 let descending = market_order_with_fills(OrderSide::Buy, Quantity::from(100_000), &fills);
2194
2195 assert_eq!(ascending.avg_px(), Some(dec!(1.000_017)));
2196 assert_eq!(descending.avg_px(), ascending.avg_px());
2197 assert_eq!(descending.filled_qty(), ascending.filled_qty());
2198 }
2199
2200 #[rstest]
2201 fn test_avg_px_after_fill_void_matches_order_without_voided_fill() {
2202 let surviving = [fill("TRADE-1", "1", "0.05"), fill("TRADE-2", "3", "0.15")];
2203 let voided = fill("TRADE-VOIDED", "2", "0.13");
2204
2205 let expected = market_order_with_fills(OrderSide::Buy, Quantity::from(6), &surviving);
2206 let mut corrected = market_order_with_fills(
2207 OrderSide::Buy,
2208 Quantity::from(6),
2209 &[surviving[0], surviving[1], voided],
2210 );
2211 corrected
2212 .apply(OrderEventAny::FillVoided(
2213 OrderFillVoidedSpec::builder()
2214 .trade_id(voided.0)
2215 .voided_qty(voided.1)
2216 .last_px(voided.2)
2217 .build(),
2218 ))
2219 .unwrap();
2220
2221 let replayed =
2222 OrderAny::from_events(corrected.events().into_iter().cloned().collect()).unwrap();
2223
2224 assert_eq!(expected.avg_px(), Some(dec!(0.125)));
2228 assert_eq!(corrected.avg_px(), expected.avg_px());
2229 assert_eq!(replayed.avg_px(), expected.avg_px());
2230 assert_eq!(corrected.status(), OrderStatus::Voided);
2231 assert_eq!(corrected.filled_qty(), Quantity::from(4));
2232 assert_eq!(corrected.voided_qty(), Quantity::from(2));
2233 assert_eq!(corrected.leaves_qty(), Quantity::from(0));
2234 assert_eq!(replayed.filled_qty(), corrected.filled_qty());
2235 assert_eq!(replayed.voided_qty(), corrected.voided_qty());
2236 }
2237
2238 #[rstest]
2239 #[case::lower("1")]
2240 #[case::equal("1.000")]
2241 fn test_fractional_fill_correction_preserves_quantity_precision(#[case] quantity: &str) {
2242 let mut order = market_order_with_fills(
2243 OrderSide::Buy,
2244 Quantity::from(quantity),
2245 &[fill("TRADE-1", "0.750", "1.00")],
2246 );
2247 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2248 assert_eq!(order.filled_qty().as_decimal(), dec!(0.750));
2249 assert_eq!(order.leaves_qty().as_decimal(), dec!(0.250));
2250
2251 order
2252 .apply(OrderEventAny::FillVoided(
2253 OrderFillVoidedSpec::builder()
2254 .trade_id(TradeId::from("TRADE-1"))
2255 .voided_qty(Quantity::from("0.125"))
2256 .last_px(Price::from("1.00"))
2257 .build(),
2258 ))
2259 .unwrap();
2260
2261 assert_eq!(order.filled_qty().as_decimal(), dec!(0.625));
2262 assert_eq!(order.voided_qty().as_decimal(), dec!(0.125));
2263 assert_eq!(order.leaves_qty().as_decimal(), dec!(0.250));
2264 assert_eq!(order.overfill_qty().as_decimal(), dec!(0));
2265 }
2266
2267 #[rstest]
2268 #[case::lower("1")]
2269 #[case::equal("1.000")]
2270 fn test_fractional_overfill_preserves_quantity_precision(
2271 #[case] quantity: &str,
2272 #[values(false, true)] partial_fill_first: bool,
2273 ) {
2274 let fills = if partial_fill_first {
2275 vec![
2276 fill("TRADE-1", "0.750", "1.00"),
2277 fill("TRADE-2", "0.500", "1.00"),
2278 ]
2279 } else {
2280 vec![fill("TRADE-1", "1.250", "1.00")]
2281 };
2282 let order = market_order_with_fills(OrderSide::Buy, Quantity::from(quantity), &fills);
2283
2284 assert_eq!(order.status(), OrderStatus::Filled);
2285 assert_eq!(order.filled_qty().as_decimal(), dec!(1.250));
2286 assert_eq!(order.overfill_qty().as_decimal(), dec!(0.250));
2287 assert_eq!(order.leaves_qty().as_decimal(), dec!(0));
2288 }
2289
2290 #[rstest]
2291 fn test_avg_px_recomputed_from_partially_voided_fill() {
2292 let mut order = market_order_with_fills(
2293 OrderSide::Buy,
2294 Quantity::from(6),
2295 &[fill("TRADE-1", "4", "1.00"), fill("TRADE-2", "2", "1.14")],
2296 );
2297 order
2298 .apply(OrderEventAny::FillVoided(
2299 OrderFillVoidedSpec::builder()
2300 .trade_id(TradeId::from("TRADE-1"))
2301 .voided_qty(Quantity::from(2))
2302 .last_px(Price::from("1.00"))
2303 .build(),
2304 ))
2305 .unwrap();
2306
2307 assert_eq!(order.status(), OrderStatus::Voided);
2310 assert_eq!(order.filled_qty(), Quantity::from(4));
2311 assert_eq!(order.voided_qty(), Quantity::from(2));
2312 assert_eq!(order.leaves_qty(), Quantity::from(0));
2313 assert_eq!(order.avg_px(), Some(dec!(1.07)));
2314 }
2315
2316 #[rstest]
2317 fn test_avg_px_over_reopened_fill_void_and_replacement_fill() {
2318 let mut order = market_order_with_fills(
2319 OrderSide::Buy,
2320 Quantity::from(6),
2321 &[fill("TRADE-1", "4", "1.00")],
2322 );
2323 order
2324 .apply(OrderEventAny::FillVoided(
2325 OrderFillVoidedSpec::builder()
2326 .trade_id(TradeId::from("TRADE-1"))
2327 .voided_qty(Quantity::from(2))
2328 .last_px(Price::from("1.00"))
2329 .is_reopened(true)
2330 .build(),
2331 ))
2332 .unwrap();
2333 order
2334 .apply(OrderEventAny::Filled(
2335 OrderFilledSpec::builder()
2336 .trade_id(TradeId::from("TRADE-2"))
2337 .last_qty(Quantity::from(2))
2338 .last_px(Price::from("1.14"))
2339 .build(),
2340 ))
2341 .unwrap();
2342 order.set_slippage(Price::from("1.00"));
2343
2344 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2348 assert_eq!(order.filled_qty(), Quantity::from(4));
2349 assert_eq!(order.voided_qty(), Quantity::from(2));
2350 assert_eq!(order.leaves_qty(), Quantity::from(2));
2351 assert_eq!(order.avg_px(), Some(dec!(1.07)));
2352 assert_eq!(order.slippage(), Some(dec!(0.07)));
2353 }
2354
2355 #[rstest]
2356 fn test_avg_px_cleared_when_every_fill_is_voided() {
2357 let mut order = market_order_with_fills(
2358 OrderSide::Buy,
2359 Quantity::from(6),
2360 &[fill("TRADE-1", "4", "1.50")],
2361 );
2362 order
2363 .apply(OrderEventAny::FillVoided(
2364 OrderFillVoidedSpec::builder()
2365 .trade_id(TradeId::from("TRADE-1"))
2366 .voided_qty(Quantity::from(4))
2367 .last_px(Price::from("1.50"))
2368 .build(),
2369 ))
2370 .unwrap();
2371
2372 assert_eq!(order.status(), OrderStatus::Accepted);
2373 assert_eq!(order.filled_qty(), Quantity::from(0));
2374 assert_eq!(order.voided_qty(), Quantity::from(4));
2375 assert_eq!(order.leaves_qty(), Quantity::from(2));
2376 assert_eq!(order.avg_px(), None);
2377 }
2378
2379 #[cfg(feature = "high-precision")]
2380 #[rstest]
2381 fn test_avg_px_exact_over_high_precision_quantities() {
2382 let order = market_order_with_fills(
2385 OrderSide::Buy,
2386 Quantity::from("0.000000000010"),
2387 &[
2388 fill("TRADE-1", "0.000000000005", "1.00001"),
2389 fill("TRADE-2", "0.000000000003", "1.00002"),
2390 fill("TRADE-3", "0.000000000002", "1.00003"),
2391 ],
2392 );
2393
2394 assert_eq!(order.status(), OrderStatus::Filled);
2395 assert_eq!(order.filled_qty(), Quantity::from("0.000000000010"));
2396 assert_eq!(order.avg_px(), Some(dec!(1.000_017)));
2397 }
2398
2399 #[cfg(feature = "high-precision")]
2400 #[rstest]
2401 fn test_avg_px_exact_at_the_widest_representable_fill_scale() {
2402 let order = market_order_with_fills(
2405 OrderSide::Buy,
2406 Quantity::from("0.0000000000000008"),
2407 &[
2408 fill("TRADE-1", "0.0000000000000002", "0.000000000001"),
2409 fill("TRADE-2", "0.0000000000000006", "0.000000000003"),
2410 ],
2411 );
2412
2413 assert_eq!(order.status(), OrderStatus::Filled);
2415 assert_eq!(order.filled_qty(), Quantity::from("0.0000000000000008"));
2416 assert_eq!(order.avg_px(), Some(dec!(2.5e-12)));
2417 }
2418
2419 #[rstest]
2420 fn test_order_life_cycle_accumulates_fill_commissions() {
2421 let init = OrderInitializedSpec::builder()
2422 .quantity(Quantity::from(100_000))
2423 .build();
2424 let submitted = OrderSubmittedSpec::builder().build();
2425 let accepted = OrderAcceptedSpec::builder().build();
2426 let fill1 = OrderFilledSpec::builder()
2427 .last_qty(Quantity::from(50_000))
2428 .trade_id(TradeId::from("TRADE-1"))
2429 .commission(Money::from("1.25 USD"))
2430 .build();
2431 let fill2 = OrderFilledSpec::builder()
2432 .last_qty(Quantity::from(50_000))
2433 .trade_id(TradeId::from("TRADE-2"))
2434 .commission(Money::from("1.35 USD"))
2435 .build();
2436
2437 let mut order: MarketOrder = init.try_into().unwrap();
2438 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2439 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2440 order.apply(OrderEventAny::Filled(fill1)).unwrap();
2441 order.apply(OrderEventAny::Filled(fill2)).unwrap();
2442
2443 assert_eq!(order.status(), OrderStatus::Filled);
2444 assert_eq!(
2445 order.commission(&Currency::USD()),
2446 Some(Money::from("2.60 USD"))
2447 );
2448 assert_eq!(order.commissions_vec(), vec![Money::from("2.60 USD")]);
2449 }
2450
2451 #[rstest]
2452 fn test_fill_void_reopens_only_when_explicitly_marked() {
2453 let init = OrderInitializedSpec::builder()
2454 .quantity(Quantity::from(100_000))
2455 .build();
2456 let filled = OrderFilledSpec::builder()
2457 .trade_id(TradeId::from("TRADE-VOID"))
2458 .last_qty(Quantity::from(100_000))
2459 .commission(Money::from("1.00 USD"))
2460 .build();
2461 let fill_voided = OrderFillVoidedSpec::builder()
2462 .trade_id(filled.trade_id)
2463 .voided_qty(Quantity::from(40_000))
2464 .commission_voided(Money::from("0.40 USD"))
2465 .build();
2466 let later_fill = OrderFilledSpec::builder()
2467 .trade_id(TradeId::from("TRADE-AFTER-REOPEN"))
2468 .last_qty(Quantity::from(40_000))
2469 .build();
2470 let mut order: MarketOrder = init.try_into().unwrap();
2471 order
2472 .apply(OrderEventAny::Accepted(
2473 OrderAcceptedSpec::builder().build(),
2474 ))
2475 .unwrap();
2476 order.apply(OrderEventAny::Filled(filled)).unwrap();
2477
2478 let mut terminal = order.clone();
2479 terminal
2480 .apply(OrderEventAny::FillVoided(fill_voided.clone()))
2481 .unwrap();
2482 let fill_while_terminal = terminal.apply(OrderEventAny::Filled(later_fill.clone()));
2483
2484 order
2485 .apply(OrderEventAny::FillVoided(OrderFillVoided {
2486 is_reopened: true,
2487 ..fill_voided
2488 }))
2489 .unwrap();
2490 let status_after_correction = order.status();
2491 order.apply(OrderEventAny::Filled(later_fill)).unwrap();
2492
2493 assert!(matches!(
2494 fill_while_terminal,
2495 Err(OrderError::InvalidStateTransition)
2496 ));
2497 assert_eq!(terminal.status(), OrderStatus::Voided);
2498 assert_eq!(status_after_correction, OrderStatus::PartiallyFilled);
2499 assert_eq!(order.status(), OrderStatus::Filled);
2500 assert_eq!(order.filled_qty(), Quantity::from(100_000));
2501 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2502 assert_eq!(order.leaves_qty(), Quantity::from(0));
2503 assert_eq!(
2504 order.commission(&Currency::USD()),
2505 Some(Money::from("0.60 USD"))
2506 );
2507 }
2508
2509 #[rstest]
2510 fn test_fill_void_preserves_existing_working_remainder() {
2511 let init = OrderInitializedSpec::builder()
2512 .quantity(Quantity::from(100_000))
2513 .build();
2514 let filled = OrderFilledSpec::builder()
2515 .trade_id(TradeId::from("TRADE-VOID"))
2516 .last_qty(Quantity::from(40_000))
2517 .build();
2518 let later_fill = OrderFilledSpec::builder()
2519 .trade_id(TradeId::from("TRADE-REMAINDER"))
2520 .last_qty(Quantity::from(60_000))
2521 .build();
2522 let mut order: MarketOrder = init.try_into().unwrap();
2523 order
2524 .apply(OrderEventAny::Accepted(
2525 OrderAcceptedSpec::builder().build(),
2526 ))
2527 .unwrap();
2528 order.apply(OrderEventAny::Filled(filled.clone())).unwrap();
2529
2530 order
2531 .apply(OrderEventAny::FillVoided(
2532 OrderFillVoidedSpec::builder()
2533 .trade_id(filled.trade_id)
2534 .voided_qty(filled.last_qty)
2535 .build(),
2536 ))
2537 .unwrap();
2538
2539 assert_eq!(order.status(), OrderStatus::Accepted);
2540 assert_eq!(order.filled_qty(), Quantity::from(0));
2541 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2542 assert_eq!(order.leaves_qty(), Quantity::from(60_000));
2543 assert!(order.is_open());
2544
2545 order.apply(OrderEventAny::Filled(later_fill)).unwrap();
2546
2547 assert_eq!(order.status(), OrderStatus::Voided);
2548 assert_eq!(order.filled_qty(), Quantity::from(60_000));
2549 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2550 assert_eq!(order.leaves_qty(), Quantity::from(0));
2551 assert!(order.is_closed());
2552 }
2553
2554 #[rstest]
2555 fn test_canceled_order_with_voided_qty_becomes_voided_when_late_fill_exhausts_leaves() {
2556 let initial_trade_id = TradeId::from("TRADE-VOID");
2557 let late_trade_id = TradeId::from("TRADE-LATE");
2558 let init = OrderInitializedSpec::builder()
2559 .quantity(Quantity::from(100_000))
2560 .build();
2561 let accepted = OrderAcceptedSpec::builder()
2562 .ts_event(UnixNanos::from(1_000))
2563 .build();
2564 let filled = OrderFilledSpec::builder()
2565 .trade_id(initial_trade_id)
2566 .last_qty(Quantity::from(60_000))
2567 .ts_event(UnixNanos::from(2_000))
2568 .build();
2569 let fill_voided = OrderFillVoidedSpec::builder()
2570 .trade_id(initial_trade_id)
2571 .voided_qty(Quantity::from(20_000))
2572 .ts_event(UnixNanos::from(3_000))
2573 .is_reopened(false)
2574 .build();
2575 let canceled = OrderCanceledSpec::builder()
2576 .ts_event(UnixNanos::from(4_000))
2577 .build();
2578 let late_fill = OrderFilledSpec::builder()
2579 .trade_id(late_trade_id)
2580 .last_qty(Quantity::from(40_000))
2581 .ts_event(UnixNanos::from(5_000))
2582 .build();
2583 let mut order: MarketOrder = init.try_into().unwrap();
2584
2585 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2586 order.apply(OrderEventAny::Filled(filled)).unwrap();
2587 order.apply(OrderEventAny::FillVoided(fill_voided)).unwrap();
2588 order.apply(OrderEventAny::Canceled(canceled)).unwrap();
2589
2590 assert_eq!(order.status(), OrderStatus::Canceled);
2591 assert_eq!(order.filled_qty(), Quantity::from(40_000));
2592 assert_eq!(order.voided_qty(), Quantity::from(20_000));
2593 assert_eq!(order.leaves_qty(), Quantity::from(40_000));
2594 assert_eq!(order.ts_closed(), Some(UnixNanos::from(4_000)));
2595 assert_eq!(order.last_trade_id(), Some(initial_trade_id));
2596 assert!(order.is_closed());
2597 assert!(!order.is_open());
2598
2599 order.apply(OrderEventAny::Filled(late_fill)).unwrap();
2600
2601 assert_eq!(order.status(), OrderStatus::Voided);
2602 assert_eq!(order.filled_qty(), Quantity::from(80_000));
2603 assert_eq!(order.voided_qty(), Quantity::from(20_000));
2604 assert_eq!(order.leaves_qty(), Quantity::from(0));
2605 assert_eq!(order.overfill_qty(), Quantity::from(0));
2606 assert_eq!(order.ts_closed(), Some(UnixNanos::from(5_000)));
2607 assert_eq!(order.last_trade_id(), Some(late_trade_id));
2608 assert_eq!(order.trade_ids(), vec![&initial_trade_id, &late_trade_id]);
2609 assert!(order.is_closed());
2610 assert!(!order.is_open());
2611 }
2612
2613 #[rstest]
2614 fn test_reopened_fill_void_requires_existing_fill() {
2615 let trade_id = TradeId::from("TRADE-OUT-OF-ORDER");
2616 let init = OrderInitializedSpec::builder()
2617 .quantity(Quantity::from(100_000))
2618 .build();
2619 let early_void = OrderFillVoidedSpec::builder()
2620 .trade_id(trade_id)
2621 .voided_qty(Quantity::from(10_000))
2622 .is_reopened(true)
2623 .build();
2624 let late_fill = OrderFilledSpec::builder()
2625 .trade_id(trade_id)
2626 .last_qty(Quantity::from(40_000))
2627 .build();
2628 let mut order: MarketOrder = init.try_into().unwrap();
2629 order
2630 .apply(OrderEventAny::Accepted(
2631 OrderAcceptedSpec::builder().build(),
2632 ))
2633 .unwrap();
2634
2635 let result = order.apply(OrderEventAny::FillVoided(early_void));
2636 order.apply(OrderEventAny::Filled(late_fill)).unwrap();
2637
2638 assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
2639 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2640 assert_eq!(order.filled_qty(), Quantity::from(40_000));
2641 assert_eq!(order.voided_qty(), Quantity::from(0));
2642 assert_eq!(order.leaves_qty(), Quantity::from(60_000));
2643 }
2644
2645 #[rstest]
2646 #[case(OrderStatus::Accepted)]
2647 #[case(OrderStatus::Canceled)]
2648 #[case(OrderStatus::Expired)]
2649 fn test_terminal_fill_void_rejects_later_order_events(#[case] status_before_void: OrderStatus) {
2650 let trade_id = TradeId::from("TRADE-TERMINAL-VOID");
2651 let init = OrderInitializedSpec::builder()
2652 .quantity(Quantity::from(100_000))
2653 .build();
2654 let terminal_void = OrderFillVoidedSpec::builder()
2655 .trade_id(trade_id)
2656 .voided_qty(Quantity::from(40_000))
2657 .commission_voided(Money::from("0 USD"))
2658 .build();
2659 let late_fill = OrderFilledSpec::builder()
2660 .trade_id(trade_id)
2661 .last_qty(Quantity::from(40_000))
2662 .build();
2663 let mut order: MarketOrder = init.try_into().unwrap();
2664 order
2665 .apply(OrderEventAny::Accepted(
2666 OrderAcceptedSpec::builder().build(),
2667 ))
2668 .unwrap();
2669
2670 match status_before_void {
2671 OrderStatus::Accepted => {}
2672 OrderStatus::Canceled => order
2673 .apply(OrderEventAny::Canceled(
2674 OrderCanceledSpec::builder().build(),
2675 ))
2676 .unwrap(),
2677 OrderStatus::Expired => order
2678 .apply(OrderEventAny::Expired(OrderExpiredSpec::builder().build()))
2679 .unwrap(),
2680 _ => unreachable!(),
2681 }
2682
2683 order
2684 .apply(OrderEventAny::FillVoided(terminal_void))
2685 .unwrap();
2686 let update_result = order.apply(OrderEventAny::Updated(
2687 OrderUpdatedSpec::builder()
2688 .quantity(Quantity::from(120_000))
2689 .build(),
2690 ));
2691 let cancel_result = order.apply(OrderEventAny::Canceled(
2692 OrderCanceledSpec::builder().build(),
2693 ));
2694 let fill_result = order.apply(OrderEventAny::Filled(late_fill));
2695
2696 assert!(matches!(
2697 update_result,
2698 Err(OrderError::InvalidStateTransition)
2699 ));
2700 assert!(matches!(
2701 cancel_result,
2702 Err(OrderError::InvalidStateTransition)
2703 ));
2704 assert!(matches!(
2705 fill_result,
2706 Err(OrderError::InvalidStateTransition)
2707 ));
2708 assert_eq!(order.status(), OrderStatus::Voided);
2709 assert_eq!(order.filled_qty(), Quantity::from(0));
2710 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2711 assert_eq!(order.leaves_qty(), Quantity::from(0));
2712 }
2713
2714 #[rstest]
2715 #[case(Quantity::from(100_001), None)]
2716 #[case(Quantity::from(100_000), Some(Money::from("1 USD")))]
2717 fn test_terminal_fill_void_rejects_invalid_economic_values(
2718 #[case] voided_qty: Quantity,
2719 #[case] commission_voided: Option<Money>,
2720 ) {
2721 let trade_id = TradeId::from("TRADE-TERMINAL-VOID");
2722 let init = OrderInitializedSpec::builder()
2723 .quantity(Quantity::from(100_000))
2724 .build();
2725 let mut order: MarketOrder = init.try_into().unwrap();
2726 order
2727 .apply(OrderEventAny::Accepted(
2728 OrderAcceptedSpec::builder().build(),
2729 ))
2730 .unwrap();
2731 let mut invalid = OrderFillVoidedSpec::builder()
2732 .trade_id(trade_id)
2733 .voided_qty(voided_qty)
2734 .build();
2735 invalid.commission_voided = commission_voided;
2736
2737 let result = order.apply(OrderEventAny::FillVoided(invalid));
2738
2739 assert!(matches!(
2740 result,
2741 Err(OrderError::OverVoid(error_trade_id)) if error_trade_id == trade_id
2742 ));
2743 assert_eq!(order.status(), OrderStatus::Accepted);
2744 assert_eq!(order.voided_qty(), Quantity::from(0));
2745 }
2746
2747 #[rstest]
2748 fn test_terminal_fill_void_accepts_current_venue_order_id_after_fill_before_accept() {
2749 let init = OrderInitializedSpec::builder()
2750 .quantity(Quantity::from(100_000))
2751 .build();
2752 let submitted = OrderSubmittedSpec::builder().build();
2753 let filled = OrderFilledSpec::builder()
2754 .account_id(submitted.account_id)
2755 .trade_id(TradeId::from("TRADE-APPLIED"))
2756 .last_qty(Quantity::from(40_000))
2757 .build();
2758 let terminal_void = OrderFillVoidedSpec::builder()
2759 .venue_order_id(filled.venue_order_id)
2760 .account_id(filled.account_id)
2761 .trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
2762 .voided_qty(Quantity::from(10_000))
2763 .build();
2764 let mut order: MarketOrder = init.try_into().unwrap();
2765 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2766 order.apply(OrderEventAny::Filled(filled)).unwrap();
2767
2768 order
2769 .apply(OrderEventAny::FillVoided(terminal_void))
2770 .unwrap();
2771
2772 assert_eq!(order.status(), OrderStatus::Voided);
2773 assert_eq!(order.filled_qty(), Quantity::from(40_000));
2774 assert_eq!(order.voided_qty(), Quantity::from(10_000));
2775 assert_eq!(order.leaves_qty(), Quantity::from(0));
2776 }
2777
2778 #[rstest]
2779 fn test_terminal_fill_void_requires_matching_order_identity() {
2780 let init = OrderInitializedSpec::builder()
2781 .quantity(Quantity::from(100_000))
2782 .build();
2783 let mut order: MarketOrder = init.try_into().unwrap();
2784 order
2785 .apply(OrderEventAny::Accepted(
2786 OrderAcceptedSpec::builder().build(),
2787 ))
2788 .unwrap();
2789 let mismatched = OrderFillVoidedSpec::builder()
2790 .instrument_id(InstrumentId::from("OTHER.SIM"))
2791 .trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
2792 .voided_qty(Quantity::from(100_000))
2793 .build();
2794
2795 let result = order.apply(OrderEventAny::FillVoided(mismatched));
2796
2797 assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
2798 assert_eq!(order.status(), OrderStatus::Accepted);
2799 assert_eq!(order.voided_qty(), Quantity::from(0));
2800 }
2801
2802 #[rstest]
2803 #[case(OrderStatus::Canceled)]
2804 #[case(OrderStatus::Expired)]
2805 fn test_fill_void_does_not_reopen_canceled_or_expired_order(
2806 #[case] terminal_status: OrderStatus,
2807 ) {
2808 let init = OrderInitializedSpec::builder()
2809 .quantity(Quantity::from(100_000))
2810 .build();
2811 let filled = OrderFilledSpec::builder()
2812 .trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
2813 .last_qty(Quantity::from(60_000))
2814 .build();
2815 let mut order: MarketOrder = init.try_into().unwrap();
2816 order
2817 .apply(OrderEventAny::Accepted(
2818 OrderAcceptedSpec::builder().build(),
2819 ))
2820 .unwrap();
2821 order.apply(OrderEventAny::Filled(filled.clone())).unwrap();
2822 match terminal_status {
2823 OrderStatus::Canceled => order
2824 .apply(OrderEventAny::Canceled(
2825 OrderCanceledSpec::builder().build(),
2826 ))
2827 .unwrap(),
2828 OrderStatus::Expired => order
2829 .apply(OrderEventAny::Expired(OrderExpiredSpec::builder().build()))
2830 .unwrap(),
2831 _ => unreachable!(),
2832 }
2833 let voided = OrderFillVoidedSpec::builder()
2834 .trade_id(filled.trade_id)
2835 .voided_qty(Quantity::from(20_000))
2836 .is_reopened(true)
2837 .build();
2838
2839 order.apply(OrderEventAny::FillVoided(voided)).unwrap();
2840
2841 assert_eq!(order.status(), terminal_status);
2842 assert_eq!(order.filled_qty(), Quantity::from(40_000));
2843 assert_eq!(order.voided_qty(), Quantity::from(20_000));
2844 assert!(order.is_closed());
2845 assert!(order.ts_closed().is_some());
2846 }
2847
2848 #[rstest]
2849 fn test_fill_void_rejects_duplicate_stale_and_over_void() {
2850 let init = OrderInitializedSpec::builder()
2851 .quantity(Quantity::from(100_000))
2852 .build();
2853 let filled = OrderFilledSpec::builder()
2854 .trade_id(TradeId::from("TRADE-VOID"))
2855 .last_qty(Quantity::from(100_000))
2856 .build();
2857 let mut order: MarketOrder = init.try_into().unwrap();
2858 order
2859 .apply(OrderEventAny::Accepted(
2860 OrderAcceptedSpec::builder().build(),
2861 ))
2862 .unwrap();
2863 order.apply(OrderEventAny::Filled(filled)).unwrap();
2864 order
2865 .apply(OrderEventAny::FillVoided(
2866 OrderFillVoidedSpec::builder()
2867 .trade_id(TradeId::from("TRADE-VOID"))
2868 .voided_qty(Quantity::from(40_000))
2869 .build(),
2870 ))
2871 .unwrap();
2872
2873 let duplicate = order.apply(OrderEventAny::FillVoided(
2874 OrderFillVoidedSpec::builder()
2875 .trade_id(TradeId::from("TRADE-VOID"))
2876 .voided_qty(Quantity::from(40_000))
2877 .build(),
2878 ));
2879 let stale = order.apply(OrderEventAny::FillVoided(
2880 OrderFillVoidedSpec::builder()
2881 .trade_id(TradeId::from("TRADE-VOID"))
2882 .voided_qty(Quantity::from(30_000))
2883 .build(),
2884 ));
2885 let over_void = order.apply(OrderEventAny::FillVoided(
2886 OrderFillVoidedSpec::builder()
2887 .trade_id(TradeId::from("TRADE-VOID"))
2888 .voided_qty(Quantity::from(100_001))
2889 .build(),
2890 ));
2891 assert!(matches!(
2892 duplicate,
2893 Err(OrderError::DuplicateFillVoid(trade_id))
2894 if trade_id == TradeId::from("TRADE-VOID")
2895 ));
2896 assert!(matches!(
2897 stale,
2898 Err(OrderError::StaleFillVoid(trade_id))
2899 if trade_id == TradeId::from("TRADE-VOID")
2900 ));
2901 assert!(matches!(
2902 over_void,
2903 Err(OrderError::OverVoid(trade_id))
2904 if trade_id == TradeId::from("TRADE-VOID")
2905 ));
2906 assert_eq!(order.filled_qty(), Quantity::from(60_000));
2907 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2908 }
2909
2910 enum FillVoidIdentityField {
2911 VenueOrderId,
2912 AccountId,
2913 OrderSide,
2914 OrderType,
2915 LastPx,
2916 Currency,
2917 LiquiditySide,
2918 PositionId,
2919 }
2920
2921 #[rstest]
2922 #[case::venue_order_id(FillVoidIdentityField::VenueOrderId)]
2923 #[case::account_id(FillVoidIdentityField::AccountId)]
2924 #[case::order_side(FillVoidIdentityField::OrderSide)]
2925 #[case::order_type(FillVoidIdentityField::OrderType)]
2926 #[case::last_px(FillVoidIdentityField::LastPx)]
2927 #[case::currency(FillVoidIdentityField::Currency)]
2928 #[case::liquidity_side(FillVoidIdentityField::LiquiditySide)]
2929 #[case::position_id(FillVoidIdentityField::PositionId)]
2930 fn test_fill_void_rejects_known_fill_identity_mismatch(#[case] field: FillVoidIdentityField) {
2931 let (mut order, fill) = filled_market_order_for_void();
2932 let events_before: Vec<_> = order.events().into_iter().cloned().collect();
2933 let mut correction =
2934 matching_fill_void(&fill, Quantity::from(40), Some(Money::from("0.40 USD")));
2935
2936 match field {
2937 FillVoidIdentityField::VenueOrderId => {
2938 correction.venue_order_id = VenueOrderId::from("V-OTHER");
2939 }
2940 FillVoidIdentityField::AccountId => {
2941 correction.account_id = AccountId::from("SIM-OTHER");
2942 }
2943 FillVoidIdentityField::OrderSide => correction.order_side = OrderSide::Sell,
2944 FillVoidIdentityField::OrderType => correction.order_type = OrderType::Limit,
2945 FillVoidIdentityField::LastPx => correction.last_px = Price::from("1.50"),
2946 FillVoidIdentityField::Currency => correction.currency = Currency::EUR(),
2947 FillVoidIdentityField::LiquiditySide => {
2948 correction.liquidity_side = LiquiditySide::Maker;
2949 }
2950 FillVoidIdentityField::PositionId => {
2951 correction.position_id = Some(PositionId::from("P-OTHER"));
2952 }
2953 }
2954
2955 let result = order.apply(OrderEventAny::FillVoided(correction));
2956
2957 assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
2958 assert_fill_void_rejected_without_mutation(&order, &events_before);
2959 }
2960
2961 #[rstest]
2962 #[case::currency("0.40 EUR")]
2963 #[case::sign("-0.40 USD")]
2964 #[case::amount("1.01 USD")]
2965 fn test_fill_void_rejects_invalid_commission_without_mutation(#[case] commission: &str) {
2966 let (mut order, fill) = filled_market_order_for_void();
2967 let events_before: Vec<_> = order.events().into_iter().cloned().collect();
2968 let correction =
2969 matching_fill_void(&fill, Quantity::from(40), Some(Money::from(commission)));
2970
2971 let result = order.apply(OrderEventAny::FillVoided(correction));
2972
2973 assert!(matches!(
2974 result,
2975 Err(OrderError::InvalidFillVoidCommission(trade_id)) if trade_id == fill.trade_id
2976 ));
2977 assert_fill_void_rejected_without_mutation(&order, &events_before);
2978 }
2979
2980 #[rstest]
2981 fn test_fill_void_rejects_zero_quantity_without_mutation() {
2982 let (mut order, fill) = filled_market_order_for_void();
2983 let events_before: Vec<_> = order.events().into_iter().cloned().collect();
2984 let correction = matching_fill_void(
2985 &fill,
2986 Quantity::zero(fill.last_qty.precision),
2987 Some(Money::from("0.00 USD")),
2988 );
2989
2990 let result = order.apply(OrderEventAny::FillVoided(correction));
2991
2992 assert!(matches!(
2993 result,
2994 Err(OrderError::OverVoid(trade_id)) if trade_id == fill.trade_id
2995 ));
2996 assert_fill_void_rejected_without_mutation(&order, &events_before);
2997 }
2998
2999 #[rstest]
3000 fn test_fill_void_uses_latest_cumulative_correction() {
3001 let (mut order, fill) = filled_market_order_for_void();
3002 order
3003 .apply(OrderEventAny::FillVoided(matching_fill_void(
3004 &fill,
3005 Quantity::from(40),
3006 Some(Money::from("0.40 USD")),
3007 )))
3008 .unwrap();
3009 let event_count = order.event_count();
3010
3011 let decreasing_commission = order.apply(OrderEventAny::FillVoided(matching_fill_void(
3012 &fill,
3013 Quantity::from(50),
3014 Some(Money::from("0.30 USD")),
3015 )));
3016
3017 assert!(matches!(
3018 decreasing_commission,
3019 Err(OrderError::InvalidFillVoidCommission(trade_id)) if trade_id == fill.trade_id
3020 ));
3021 assert_eq!(order.event_count(), event_count);
3022 assert_eq!(order.filled_qty(), Quantity::from(60));
3023 assert_eq!(order.voided_qty(), Quantity::from(40));
3024 assert_eq!(
3025 order.commission(&Currency::USD()),
3026 Some(Money::from("0.60 USD")),
3027 );
3028
3029 order
3030 .apply(OrderEventAny::FillVoided(matching_fill_void(
3031 &fill,
3032 Quantity::from(60),
3033 Some(Money::from("0.60 USD")),
3034 )))
3035 .unwrap();
3036 let replayed =
3037 OrderAny::from_events(order.events().into_iter().cloned().collect()).unwrap();
3038
3039 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3040 assert_eq!(order.filled_qty(), Quantity::from(40));
3041 assert_eq!(order.voided_qty(), Quantity::from(60));
3042 assert_eq!(order.leaves_qty(), Quantity::from(60));
3043 assert_eq!(order.avg_px(), Some(dec!(1.25)));
3044 assert_eq!(
3045 order.commission(&Currency::USD()),
3046 Some(Money::from("0.40 USD")),
3047 );
3048 assert_eq!(order.trade_ids(), vec![&fill.trade_id]);
3049 assert_eq!(order.last_trade_id(), Some(fill.trade_id));
3050 assert_eq!(order.event_count(), event_count + 1);
3051 assert_eq!(replayed.status(), order.status());
3052 assert_eq!(replayed.filled_qty(), order.filled_qty());
3053 assert_eq!(replayed.voided_qty(), order.voided_qty());
3054 assert_eq!(replayed.leaves_qty(), order.leaves_qty());
3055 assert_eq!(replayed.avg_px(), order.avg_px());
3056 assert_eq!(replayed.commissions(), order.commissions());
3057 assert_eq!(replayed.trade_ids(), order.trade_ids());
3058 assert_eq!(replayed.last_trade_id(), order.last_trade_id());
3059 }
3060
3061 fn filled_market_order_for_void() -> (MarketOrder, OrderFilled) {
3062 let venue_order_id = VenueOrderId::from("V-FILL-VOID");
3063 let account_id = AccountId::from("SIM-FILL-VOID");
3064 let fill = OrderFilledSpec::builder()
3065 .venue_order_id(venue_order_id)
3066 .account_id(account_id)
3067 .trade_id(TradeId::from("T-FILL-VOID"))
3068 .last_qty(Quantity::from(100))
3069 .last_px(Price::from("1.25"))
3070 .position_id(PositionId::from("P-FILL-VOID"))
3071 .commission(Money::from("1.00 USD"))
3072 .build();
3073 let mut order: MarketOrder = OrderInitializedSpec::builder()
3074 .quantity(Quantity::from(100))
3075 .build()
3076 .try_into()
3077 .unwrap();
3078 order
3079 .apply(OrderEventAny::Accepted(
3080 OrderAcceptedSpec::builder()
3081 .venue_order_id(venue_order_id)
3082 .account_id(account_id)
3083 .build(),
3084 ))
3085 .unwrap();
3086 order.apply(OrderEventAny::Filled(fill.clone())).unwrap();
3087
3088 (order, fill)
3089 }
3090
3091 fn matching_fill_void(
3092 fill: &OrderFilled,
3093 voided_qty: Quantity,
3094 commission_voided: Option<Money>,
3095 ) -> OrderFillVoided {
3096 OrderFillVoidedSpec::builder()
3097 .trader_id(fill.trader_id)
3098 .strategy_id(fill.strategy_id)
3099 .instrument_id(fill.instrument_id)
3100 .client_order_id(fill.client_order_id)
3101 .venue_order_id(fill.venue_order_id)
3102 .account_id(fill.account_id)
3103 .trade_id(fill.trade_id)
3104 .voided_qty(voided_qty)
3105 .order_side(fill.order_side)
3106 .order_type(fill.order_type)
3107 .last_px(fill.last_px)
3108 .currency(fill.currency)
3109 .liquidity_side(fill.liquidity_side)
3110 .maybe_position_id(fill.position_id)
3111 .maybe_commission_voided(commission_voided)
3112 .is_reopened(true)
3113 .build()
3114 }
3115
3116 fn assert_fill_void_rejected_without_mutation(
3117 order: &MarketOrder,
3118 events_before: &[OrderEventAny],
3119 ) {
3120 assert_eq!(order.status(), OrderStatus::Filled);
3121 assert_eq!(order.filled_qty(), Quantity::from(100));
3122 assert_eq!(order.voided_qty(), Quantity::from(0));
3123 assert_eq!(order.leaves_qty(), Quantity::from(0));
3124 assert_eq!(order.avg_px(), Some(dec!(1.25)));
3125 assert_eq!(
3126 order.commission(&Currency::USD()),
3127 Some(Money::from("1.00 USD")),
3128 );
3129 assert_eq!(
3130 order.events().into_iter().cloned().collect::<Vec<_>>(),
3131 events_before,
3132 );
3133 }
3134
3135 #[rstest]
3136 fn test_unknown_fill_void_does_not_reverse_surviving_fill() {
3137 let init = OrderInitializedSpec::builder()
3138 .quantity(Quantity::from(100_000))
3139 .build();
3140 let filled = OrderFilledSpec::builder()
3141 .trade_id(TradeId::from("TRADE-APPLIED"))
3142 .last_qty(Quantity::from(60_000))
3143 .build();
3144 let voided = OrderFillVoidedSpec::builder()
3145 .trade_id(TradeId::from("TRADE-NEVER-APPLIED"))
3146 .voided_qty(Quantity::from(40_000))
3147 .build();
3148 let mut order: MarketOrder = init.try_into().unwrap();
3149 order
3150 .apply(OrderEventAny::Accepted(
3151 OrderAcceptedSpec::builder().build(),
3152 ))
3153 .unwrap();
3154 order.apply(OrderEventAny::Filled(filled)).unwrap();
3155
3156 order.apply(OrderEventAny::FillVoided(voided)).unwrap();
3157 order
3158 .apply(OrderEventAny::FillVoided(
3159 OrderFillVoidedSpec::builder()
3160 .trade_id(TradeId::from("TRADE-APPLIED"))
3161 .voided_qty(Quantity::from(10_000))
3162 .is_reopened(true)
3163 .build(),
3164 ))
3165 .unwrap();
3166 let late_fill = order.apply(OrderEventAny::Filled(
3167 OrderFilledSpec::builder()
3168 .trade_id(TradeId::from("TRADE-LATE"))
3169 .last_qty(Quantity::from(10_000))
3170 .build(),
3171 ));
3172
3173 assert_eq!(order.status(), OrderStatus::Voided);
3174 assert_eq!(order.filled_qty(), Quantity::from(50_000));
3175 assert_eq!(order.voided_qty(), Quantity::from(50_000));
3176 assert_eq!(order.leaves_qty(), Quantity::zero(0));
3177 assert!(order.is_closed());
3178 assert!(matches!(late_fill, Err(OrderError::InvalidStateTransition)));
3179 }
3180
3181 #[rstest]
3182 fn test_order_state_transition_to_canceled() {
3183 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
3184 let submitted = OrderSubmittedSpec::builder().build();
3185 let canceled = OrderCanceledSpec::builder().build();
3186
3187 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3188 order.apply(OrderEventAny::Canceled(canceled)).unwrap();
3189
3190 assert_eq!(order.status(), OrderStatus::Canceled);
3191 assert!(order.is_closed());
3192 assert!(!order.is_open());
3193 }
3194
3195 #[rstest]
3196 fn test_order_life_cycle_to_partially_filled() {
3197 let init = OrderInitializedSpec::builder().build();
3198 let submitted = OrderSubmittedSpec::builder().build();
3199 let accepted = OrderAcceptedSpec::builder().build();
3200 let filled = OrderFilledSpec::builder()
3201 .last_qty(Quantity::from(50_000))
3202 .build();
3203
3204 let mut order: MarketOrder = init.clone().try_into().unwrap();
3205 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3206 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3207 order.apply(OrderEventAny::Filled(filled)).unwrap();
3208
3209 assert_eq!(order.client_order_id, init.client_order_id);
3210 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3211 assert_eq!(order.filled_qty(), Quantity::from(50_000));
3212 assert_eq!(order.leaves_qty(), Quantity::from(50_000));
3213 assert!(order.is_open());
3214 assert!(!order.is_closed());
3215 }
3216
3217 #[rstest]
3218 fn test_order_commission_calculation() {
3219 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
3220 order
3221 .commissions
3222 .insert(Currency::USD(), Money::new(10.0, Currency::USD()));
3223
3224 assert_eq!(
3225 order.commission(&Currency::USD()),
3226 Some(Money::new(10.0, Currency::USD()))
3227 );
3228 assert_eq!(
3229 order.commissions_vec(),
3230 vec![Money::new(10.0, Currency::USD())]
3231 );
3232 }
3233
3234 #[rstest]
3235 fn test_order_is_primary() {
3236 let order: MarketOrder = OrderInitializedSpec::builder()
3237 .exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
3238 .exec_spawn_id(ClientOrderId::from("O-001"))
3239 .client_order_id(ClientOrderId::from("O-001"))
3240 .build()
3241 .try_into()
3242 .unwrap();
3243
3244 assert!(order.is_primary());
3245 assert!(!order.is_spawned());
3246 }
3247
3248 #[rstest]
3249 fn test_order_is_spawned() {
3250 let order: MarketOrder = OrderInitializedSpec::builder()
3251 .exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
3252 .exec_spawn_id(ClientOrderId::from("O-002"))
3253 .client_order_id(ClientOrderId::from("O-001"))
3254 .build()
3255 .try_into()
3256 .unwrap();
3257
3258 assert!(!order.is_primary());
3259 assert!(order.is_spawned());
3260 }
3261
3262 #[rstest]
3263 fn test_order_is_contingency() {
3264 let order: MarketOrder = OrderInitializedSpec::builder()
3265 .contingency_type(ContingencyType::Oto)
3266 .linked_order_ids(vec![ClientOrderId::from("O-LINKED")])
3267 .build()
3268 .try_into()
3269 .unwrap();
3270
3271 assert!(order.is_contingency());
3272 assert!(order.is_parent_order());
3273 assert!(!order.is_child_order());
3274 }
3275
3276 #[rstest]
3277 fn test_order_is_child_order() {
3278 let order: MarketOrder = OrderInitializedSpec::builder()
3279 .parent_order_id(ClientOrderId::from("PARENT-001"))
3280 .build()
3281 .try_into()
3282 .unwrap();
3283
3284 assert!(order.is_child_order());
3285 assert!(!order.is_parent_order());
3286 }
3287
3288 #[rstest]
3289 fn test_to_own_book_order_timestamp_ordering() {
3290 use crate::orders::limit::LimitOrder;
3291
3292 let init = OrderInitializedSpec::builder()
3294 .price(Price::from("100.00"))
3295 .build();
3296 let submitted = OrderSubmittedSpec::builder()
3297 .ts_event(UnixNanos::from(1_000_000))
3298 .build();
3299 let accepted = OrderAcceptedSpec::builder()
3300 .ts_event(UnixNanos::from(2_000_000))
3301 .build();
3302
3303 let mut order: LimitOrder = init.try_into().unwrap();
3304 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3305 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3306
3307 let own_book_order = order.to_own_book_order();
3308
3309 assert_eq!(own_book_order.ts_submitted, UnixNanos::from(1_000_000));
3311 assert_eq!(own_book_order.ts_accepted, UnixNanos::from(2_000_000));
3312 assert_eq!(own_book_order.ts_last, UnixNanos::from(2_000_000));
3313 }
3314
3315 #[rstest]
3316 fn test_to_own_book_order_partial_fill_uses_leaves_qty() {
3317 use crate::orders::limit::LimitOrder;
3318
3319 let init = OrderInitializedSpec::builder()
3320 .price(Price::from("100.00"))
3321 .quantity(Quantity::from(100_000))
3322 .build();
3323 let submitted = OrderSubmittedSpec::builder().build();
3324 let accepted = OrderAcceptedSpec::builder().build();
3325 let filled = OrderFilledSpec::builder()
3326 .order_type(OrderType::Limit)
3327 .last_qty(Quantity::from(40_000))
3328 .last_px(Price::from("100.00"))
3329 .build();
3330
3331 let mut order: LimitOrder = init.try_into().unwrap();
3332 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3333 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3334 order.apply(OrderEventAny::Filled(filled)).unwrap();
3335
3336 let own_book_order = order.to_own_book_order();
3337
3338 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3339 assert_eq!(order.leaves_qty(), Quantity::from(60_000));
3340 assert_eq!(own_book_order.size, Quantity::from(60_000));
3341 }
3342
3343 #[rstest]
3344 fn test_order_accepted_without_submitted_sets_account_id() {
3345 let init = OrderInitializedSpec::builder().build();
3347 let accepted = OrderAcceptedSpec::builder()
3348 .account_id(AccountId::from("EXTERNAL-001"))
3349 .build();
3350
3351 let mut order: MarketOrder = init.try_into().unwrap();
3352
3353 assert_eq!(order.account_id(), None);
3355
3356 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3358
3359 assert_eq!(order.account_id(), Some(AccountId::from("EXTERNAL-001")));
3361 assert_eq!(order.status(), OrderStatus::Accepted);
3362 }
3363
3364 #[rstest]
3365 fn test_order_accepted_after_submitted_preserves_account_id() {
3366 let init = OrderInitializedSpec::builder().build();
3368 let submitted = OrderSubmittedSpec::builder()
3369 .account_id(AccountId::from("SUBMITTED-001"))
3370 .build();
3371 let accepted = OrderAcceptedSpec::builder()
3372 .account_id(AccountId::from("ACCEPTED-001"))
3373 .build();
3374
3375 let mut order: MarketOrder = init.try_into().unwrap();
3376 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3377
3378 assert_eq!(order.account_id(), Some(AccountId::from("SUBMITTED-001")));
3380
3381 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3383
3384 assert_eq!(order.account_id(), Some(AccountId::from("ACCEPTED-001")));
3386 assert_eq!(order.status(), OrderStatus::Accepted);
3387 }
3388
3389 #[rstest]
3390 fn test_overfill_tracks_overfill_qty() {
3391 let init = OrderInitializedSpec::builder()
3393 .quantity(Quantity::from(100_000))
3394 .build();
3395 let submitted = OrderSubmittedSpec::builder().build();
3396 let accepted = OrderAcceptedSpec::builder().build();
3397 let overfill = OrderFilledSpec::builder()
3398 .last_qty(Quantity::from(110_000)) .build();
3400
3401 let mut order: MarketOrder = init.try_into().unwrap();
3402 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3403 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3404 order.apply(OrderEventAny::Filled(overfill)).unwrap();
3405
3406 assert_eq!(order.overfill_qty(), Quantity::from(10_000));
3408 assert_eq!(order.filled_qty(), Quantity::from(110_000));
3409 assert_eq!(order.leaves_qty(), Quantity::from(0));
3410 assert_eq!(order.status(), OrderStatus::Filled);
3411 }
3412
3413 #[rstest]
3414 fn test_partial_fill_then_overfill() {
3415 let init = OrderInitializedSpec::builder()
3417 .quantity(Quantity::from(100_000))
3418 .build();
3419 let submitted = OrderSubmittedSpec::builder().build();
3420 let accepted = OrderAcceptedSpec::builder().build();
3421 let fill1 = OrderFilledSpec::builder()
3422 .last_qty(Quantity::from(80_000))
3423 .trade_id(TradeId::from("TRADE-1"))
3424 .build();
3425 let fill2 = OrderFilledSpec::builder()
3426 .last_qty(Quantity::from(30_000)) .trade_id(TradeId::from("TRADE-2"))
3428 .build();
3429
3430 let mut order: MarketOrder = init.try_into().unwrap();
3431 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3432 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3433 order.apply(OrderEventAny::Filled(fill1)).unwrap();
3434
3435 assert_eq!(order.overfill_qty(), Quantity::from(0));
3437 assert_eq!(order.filled_qty(), Quantity::from(80_000));
3438 assert_eq!(order.leaves_qty(), Quantity::from(20_000));
3439
3440 order.apply(OrderEventAny::Filled(fill2)).unwrap();
3441
3442 assert_eq!(order.overfill_qty(), Quantity::from(10_000));
3444 assert_eq!(order.filled_qty(), Quantity::from(110_000));
3445 assert_eq!(order.leaves_qty(), Quantity::from(0));
3446 assert_eq!(order.status(), OrderStatus::Filled);
3447 }
3448
3449 #[rstest]
3450 fn test_exact_fill_no_overfill() {
3451 let init = OrderInitializedSpec::builder()
3453 .quantity(Quantity::from(100_000))
3454 .build();
3455 let submitted = OrderSubmittedSpec::builder().build();
3456 let accepted = OrderAcceptedSpec::builder().build();
3457 let filled = OrderFilledSpec::builder()
3458 .last_qty(Quantity::from(100_000)) .build();
3460
3461 let mut order: MarketOrder = init.try_into().unwrap();
3462 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3463 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3464 order.apply(OrderEventAny::Filled(filled)).unwrap();
3465
3466 assert_eq!(order.overfill_qty(), Quantity::from(0));
3468 assert_eq!(order.filled_qty(), Quantity::from(100_000));
3469 assert_eq!(order.leaves_qty(), Quantity::from(0));
3470 }
3471
3472 #[rstest]
3473 fn test_partial_fill_then_overfill_with_fractional_quantities() {
3474 let init = OrderInitializedSpec::builder()
3478 .quantity(Quantity::from("2450.5"))
3479 .build();
3480 let submitted = OrderSubmittedSpec::builder().build();
3481 let accepted = OrderAcceptedSpec::builder().build();
3482 let fill1 = OrderFilledSpec::builder()
3483 .last_qty(Quantity::from("1202.5"))
3484 .trade_id(TradeId::from("TRADE-1"))
3485 .build();
3486 let fill2 = OrderFilledSpec::builder()
3487 .last_qty(Quantity::from("1285.5")) .trade_id(TradeId::from("TRADE-2"))
3489 .build();
3490
3491 let mut order: MarketOrder = init.try_into().unwrap();
3492 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3493 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3494 order.apply(OrderEventAny::Filled(fill1)).unwrap();
3495
3496 assert_eq!(order.overfill_qty(), Quantity::from(0));
3498 assert_eq!(order.filled_qty(), Quantity::from("1202.5"));
3499 assert_eq!(order.leaves_qty(), Quantity::from("1248.0"));
3500 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3501
3502 order.apply(OrderEventAny::Filled(fill2)).unwrap();
3503
3504 assert_eq!(order.overfill_qty(), Quantity::from("37.5"));
3506 assert_eq!(order.filled_qty(), Quantity::from("2488.0"));
3507 assert_eq!(order.leaves_qty(), Quantity::from(0));
3508 assert_eq!(order.status(), OrderStatus::Filled);
3509 }
3510
3511 #[rstest]
3512 fn test_calculate_overfill_returns_zero_when_no_overfill() {
3513 let order: MarketOrder = OrderInitializedSpec::builder()
3514 .quantity(Quantity::from(100_000))
3515 .build()
3516 .try_into()
3517 .unwrap();
3518
3519 let overfill = order.calculate_overfill(Quantity::from(50_000));
3521 assert_eq!(overfill, Quantity::from(0));
3522
3523 let overfill = order.calculate_overfill(Quantity::from(100_000));
3525 assert_eq!(overfill, Quantity::from(0));
3526 }
3527
3528 #[rstest]
3529 fn test_calculate_overfill_returns_overfill_amount() {
3530 let order: MarketOrder = OrderInitializedSpec::builder()
3531 .quantity(Quantity::from(100_000))
3532 .build()
3533 .try_into()
3534 .unwrap();
3535
3536 let overfill = order.calculate_overfill(Quantity::from(110_000));
3538 assert_eq!(overfill, Quantity::from(10_000));
3539 }
3540
3541 #[rstest]
3542 fn test_calculate_overfill_accounts_for_existing_fills() {
3543 let init = OrderInitializedSpec::builder()
3544 .quantity(Quantity::from(100_000))
3545 .build();
3546 let submitted = OrderSubmittedSpec::builder().build();
3547 let accepted = OrderAcceptedSpec::builder().build();
3548 let partial_fill = OrderFilledSpec::builder()
3549 .last_qty(Quantity::from(60_000))
3550 .build();
3551
3552 let mut order: MarketOrder = init.try_into().unwrap();
3553 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3554 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3555 order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
3556
3557 let overfill = order.calculate_overfill(Quantity::from(50_000));
3560 assert_eq!(overfill, Quantity::from(10_000));
3561
3562 let overfill = order.calculate_overfill(Quantity::from(40_000));
3564 assert_eq!(overfill, Quantity::from(0));
3565 }
3566
3567 #[rstest]
3568 fn test_calculate_overfill_with_fractional_quantities() {
3569 let order: MarketOrder = OrderInitializedSpec::builder()
3570 .quantity(Quantity::from("2450.5"))
3571 .build()
3572 .try_into()
3573 .unwrap();
3574
3575 let overfill = order.calculate_overfill(Quantity::from("2488.0"));
3578 assert_eq!(overfill, Quantity::from("37.5"));
3579 }
3580
3581 #[rstest]
3582 fn test_calculate_overfill_zero_after_fractional_partial_fill() {
3583 let init = OrderInitializedSpec::builder()
3584 .quantity(Quantity::from("1.000"))
3585 .build();
3586 let submitted = OrderSubmittedSpec::builder().build();
3587 let accepted = OrderAcceptedSpec::builder().build();
3588 let partial_fill = OrderFilledSpec::builder()
3589 .last_qty(Quantity::from("0.072"))
3590 .build();
3591
3592 let mut order: MarketOrder = init.try_into().unwrap();
3593 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3594 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3595 order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
3596
3597 let overfill = order.calculate_overfill(Quantity::from("0.072"));
3599 assert_eq!(overfill, Quantity::from("0.000"));
3600 }
3601
3602 #[rstest]
3603 fn test_duplicate_fill_rejected() {
3604 let init = OrderInitializedSpec::builder()
3605 .quantity(Quantity::from(100_000))
3606 .build();
3607 let submitted = OrderSubmittedSpec::builder().build();
3608 let accepted = OrderAcceptedSpec::builder().build();
3609 let fill1 = OrderFilledSpec::builder()
3610 .last_qty(Quantity::from(50_000))
3611 .trade_id(TradeId::from("TRADE-001"))
3612 .build();
3613 let fill2_duplicate = OrderFilledSpec::builder()
3614 .last_qty(Quantity::from(50_000))
3615 .trade_id(TradeId::from("TRADE-001")) .build();
3617
3618 let mut order: MarketOrder = init.try_into().unwrap();
3619 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3620 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3621 order.apply(OrderEventAny::Filled(fill1)).unwrap();
3622
3623 assert_eq!(order.filled_qty(), Quantity::from(50_000));
3625 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3626
3627 let result = order.apply(OrderEventAny::Filled(fill2_duplicate));
3629 assert!(result.is_err());
3630 match result.unwrap_err() {
3631 OrderError::DuplicateFill(trade_id) => {
3632 assert_eq!(trade_id, TradeId::from("TRADE-001"));
3633 }
3634 e => panic!("Expected DuplicateFill error, was: {e:?}"),
3635 }
3636
3637 assert_eq!(order.filled_qty(), Quantity::from(50_000));
3639 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3640 }
3641
3642 #[rstest]
3643 fn test_fill_raw_overflow_rejected_without_mutation() {
3644 let unit = Quantity::from(1);
3645 let almost_max = Quantity::from_raw(QUANTITY_RAW_MAX - unit.raw(), 0);
3646 let max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
3647 let init = OrderInitializedSpec::builder().quantity(max).build();
3648 let accepted = OrderAcceptedSpec::builder().build();
3649 let fill1 = OrderFilledSpec::builder()
3650 .last_qty(almost_max)
3651 .trade_id(TradeId::from("TRADE-001"))
3652 .build();
3653 let fill2 = OrderFilledSpec::builder()
3654 .last_qty(Quantity::from(2))
3655 .trade_id(TradeId::from("TRADE-002"))
3656 .build();
3657
3658 let mut order: MarketOrder = init.try_into().unwrap();
3659 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3660 order.apply(OrderEventAny::Filled(fill1)).unwrap();
3661
3662 assert_eq!(order.filled_qty(), almost_max);
3663 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3664
3665 let result = order.apply(OrderEventAny::Filled(fill2));
3666 let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
3667 else {
3668 panic!("Expected typed invariant error, was: {result:?}");
3669 };
3670 assert_eq!(
3671 message,
3672 format!("filled quantity overflowed Quantity raw bounds: {almost_max} + 2")
3673 );
3674 assert_eq!(order.filled_qty(), almost_max);
3675 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3676 assert_eq!(order.event_count(), 3);
3677 }
3678
3679 #[rstest]
3680 fn test_late_fill_on_filled_order_is_invalid_transition_not_overflow() {
3681 let max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
3682 let init = OrderInitializedSpec::builder().quantity(max).build();
3683 let accepted = OrderAcceptedSpec::builder().build();
3684 let fill1 = OrderFilledSpec::builder()
3685 .last_qty(max)
3686 .trade_id(TradeId::from("TRADE-001"))
3687 .build();
3688 let fill2 = OrderFilledSpec::builder()
3689 .last_qty(Quantity::from(1))
3690 .trade_id(TradeId::from("TRADE-002"))
3691 .build();
3692
3693 let mut order: MarketOrder = init.try_into().unwrap();
3694 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3695 order.apply(OrderEventAny::Filled(fill1)).unwrap();
3696
3697 let result = order.apply(OrderEventAny::Filled(fill2));
3698 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
3699 assert_eq!(order.filled_qty(), max);
3700 assert_eq!(order.status(), OrderStatus::Filled);
3701 }
3702
3703 #[rstest]
3704 fn test_quantity_saturating_add_clamps_undef_sentinel() {
3705 let qty = Quantity::from_raw(QuantityRaw::MAX, 0).saturating_add(Quantity::zero(0));
3706
3707 assert_eq!(qty, Quantity::from_raw(QUANTITY_RAW_MAX, 0));
3708 assert!(!qty.is_undefined());
3709 }
3710
3711 #[rstest]
3712 fn test_check_display_qty_returns_typed_invariant_with_stable_display() {
3713 let error = check_display_qty(Some(Quantity::from(2)), Quantity::from(1)).unwrap_err();
3714
3715 match error {
3716 OrderError::Invariant(CorrectnessError::PredicateViolation { ref message }) => {
3717 assert_eq!(message, "`display_qty` may not exceed `quantity`");
3718 }
3719 other => panic!("Expected typed invariant error, was: {other:?}"),
3720 }
3721
3722 assert_eq!(error.to_string(), "`display_qty` may not exceed `quantity`");
3723 }
3724
3725 #[rstest]
3726 fn test_check_time_in_force_returns_typed_invariant_with_stable_display() {
3727 let error = check_time_in_force(TimeInForce::Gtd, None).unwrap_err();
3728
3729 match error {
3730 OrderError::Invariant(CorrectnessError::PredicateViolation { ref message }) => {
3731 assert_eq!(message, "`expire_time` is required for `GTD` order");
3732 }
3733 other => panic!("Expected typed invariant error, was: {other:?}"),
3734 }
3735
3736 assert_eq!(
3737 error.to_string(),
3738 "`expire_time` is required for `GTD` order"
3739 );
3740 }
3741
3742 #[rstest]
3743 fn test_different_trade_ids_allowed() {
3744 let init = OrderInitializedSpec::builder()
3745 .quantity(Quantity::from(100_000))
3746 .build();
3747 let submitted = OrderSubmittedSpec::builder().build();
3748 let accepted = OrderAcceptedSpec::builder().build();
3749 let fill1 = OrderFilledSpec::builder()
3750 .last_qty(Quantity::from(50_000))
3751 .trade_id(TradeId::from("TRADE-001"))
3752 .build();
3753 let fill2 = OrderFilledSpec::builder()
3754 .last_qty(Quantity::from(50_000))
3755 .trade_id(TradeId::from("TRADE-002")) .build();
3757
3758 let mut order: MarketOrder = init.try_into().unwrap();
3759 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3760 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3761 order.apply(OrderEventAny::Filled(fill1)).unwrap();
3762 order.apply(OrderEventAny::Filled(fill2)).unwrap();
3763
3764 assert_eq!(order.filled_qty(), Quantity::from(100_000));
3766 assert_eq!(order.status(), OrderStatus::Filled);
3767 assert_eq!(order.trade_ids.len(), 2);
3768 }
3769
3770 #[rstest]
3771 fn test_pending_update_order_restores_status_on_updated() {
3772 let init = OrderInitializedSpec::builder()
3773 .quantity(Quantity::from(100_000))
3774 .build();
3775 let submitted = OrderSubmittedSpec::builder().build();
3776 let accepted = OrderAcceptedSpec::builder().build();
3777 let pending_update = OrderPendingUpdateSpec::builder().build();
3778 let fill = OrderFilledSpec::builder()
3779 .last_qty(Quantity::from(10_000))
3780 .build();
3781 let updated = OrderUpdatedSpec::builder()
3782 .quantity(Quantity::from(50_000))
3783 .build();
3784
3785 let mut order: MarketOrder = init.try_into().unwrap();
3786 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3787 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3788
3789 assert_eq!(order.status(), OrderStatus::Accepted);
3790
3791 order
3792 .apply(OrderEventAny::PendingUpdate(pending_update))
3793 .unwrap();
3794 order.apply(OrderEventAny::Filled(fill)).unwrap();
3795 assert_eq!(order.status(), OrderStatus::PendingUpdate);
3796 assert_eq!(order.previous_status(), Some(OrderStatus::PartiallyFilled));
3797 assert_eq!(order.filled_qty(), Quantity::from(10_000));
3798
3799 order.apply(OrderEventAny::Updated(updated)).unwrap();
3800
3801 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3802 assert_eq!(order.quantity(), Quantity::from(50_000));
3803 assert_eq!(order.leaves_qty(), Quantity::from(40_000));
3804 }
3805
3806 #[rstest]
3807 fn test_pending_cancel_supersedes_pending_update_without_losing_previous_status() {
3808 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
3809 order
3810 .apply(OrderEventAny::Submitted(
3811 OrderSubmittedSpec::builder().build(),
3812 ))
3813 .unwrap();
3814 order
3815 .apply(OrderEventAny::Accepted(
3816 OrderAcceptedSpec::builder().build(),
3817 ))
3818 .unwrap();
3819 order
3820 .apply(OrderEventAny::PendingUpdate(
3821 OrderPendingUpdateSpec::builder().build(),
3822 ))
3823 .unwrap();
3824
3825 assert_eq!(order.status(), OrderStatus::PendingUpdate);
3826 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
3827
3828 order
3829 .apply(OrderEventAny::PendingCancel(
3830 OrderPendingCancelSpec::builder().build(),
3831 ))
3832 .unwrap();
3833
3834 assert_eq!(order.status(), OrderStatus::PendingCancel);
3835 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
3836
3837 let repeated = OrderEventAny::PendingCancel(OrderPendingCancelSpec::builder().build());
3838 order.apply(repeated.clone()).unwrap();
3839
3840 assert_eq!(order.status(), OrderStatus::PendingCancel);
3841 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
3842 assert_eq!(order.event_count(), 6);
3843 assert_eq!(order.last_event(), &repeated);
3844 }
3845
3846 #[rstest]
3847 fn test_late_fill_for_historical_venue_order_keeps_current_venue_order_id() {
3848 let old_venue_order_id = VenueOrderId::from("V-OLD");
3849 let new_venue_order_id = VenueOrderId::from("V-NEW");
3850 let init = OrderInitializedSpec::builder()
3851 .quantity(Quantity::from(10))
3852 .build();
3853 let accepted = OrderAcceptedSpec::builder()
3854 .venue_order_id(old_venue_order_id)
3855 .build();
3856 let updated = OrderUpdatedSpec::builder()
3857 .quantity(Quantity::from(10))
3858 .venue_order_id(new_venue_order_id)
3859 .build();
3860 let late_fill = OrderFilledSpec::builder()
3861 .venue_order_id(old_venue_order_id)
3862 .last_qty(Quantity::from(2))
3863 .trade_id(TradeId::from("T-LATE"))
3864 .build();
3865
3866 let mut order: MarketOrder = init.try_into().unwrap();
3867 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3868 order.apply(OrderEventAny::Updated(updated)).unwrap();
3869 order.apply(OrderEventAny::Filled(late_fill)).unwrap();
3870
3871 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3872 assert_eq!(order.quantity(), Quantity::from(10));
3873 assert_eq!(order.filled_qty(), Quantity::from(2));
3874 assert_eq!(order.leaves_qty(), Quantity::from(8));
3875 assert_eq!(order.venue_order_id(), Some(new_venue_order_id));
3876 assert_eq!(
3877 order
3878 .venue_order_ids()
3879 .into_iter()
3880 .copied()
3881 .collect::<Vec<_>>(),
3882 vec![old_venue_order_id, new_venue_order_id],
3883 );
3884 let OrderEventAny::Filled(event) = order.last_event() else {
3885 panic!("last event was not the late fill");
3886 };
3887 assert_eq!(event.venue_order_id, old_venue_order_id);
3888 }
3889
3890 #[rstest]
3891 fn test_pending_update_accepts_delayed_submitted() {
3892 let init = OrderInitializedSpec::builder()
3893 .quantity(Quantity::from(100_000))
3894 .build();
3895 let submitted = OrderSubmittedSpec::builder()
3896 .ts_event(UnixNanos::from(1_000))
3897 .build();
3898 let accepted = OrderAcceptedSpec::builder().build();
3899 let pending_update = OrderPendingUpdateSpec::builder().build();
3900
3901 let delayed_submitted = OrderSubmittedSpec::builder()
3905 .account_id(AccountId::from("SIM-002"))
3906 .ts_event(UnixNanos::from(3_000))
3907 .build();
3908 let updated = OrderUpdatedSpec::builder()
3909 .quantity(Quantity::from(50_000))
3910 .build();
3911
3912 let mut order: MarketOrder = init.try_into().unwrap();
3913 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3914 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3915 order
3916 .apply(OrderEventAny::PendingUpdate(pending_update))
3917 .unwrap();
3918
3919 assert_eq!(order.status(), OrderStatus::PendingUpdate);
3920 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
3921 assert_eq!(order.account_id(), Some(AccountId::from("SIM-001")));
3922 assert_eq!(order.ts_submitted(), Some(UnixNanos::from(1_000)));
3923
3924 order
3925 .apply(OrderEventAny::Submitted(delayed_submitted))
3926 .unwrap();
3927
3928 assert_eq!(order.status(), OrderStatus::PendingUpdate);
3931 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
3932
3933 assert_eq!(order.account_id(), Some(AccountId::from("SIM-002")));
3934 assert_eq!(order.ts_submitted(), Some(UnixNanos::from(3_000)));
3935
3936 order.apply(OrderEventAny::Updated(updated)).unwrap();
3937
3938 assert_eq!(order.status(), OrderStatus::Accepted);
3939 assert_eq!(order.quantity(), Quantity::from(50_000));
3940 }
3941
3942 #[rstest]
3943 fn test_rejected_event_preserves_status_restoration() {
3944 let init = OrderInitializedSpec::builder().build();
3945 let submitted = OrderSubmittedSpec::builder().build();
3946 let accepted = OrderAcceptedSpec::builder().build();
3947 let invalid = OrderSubmittedSpec::builder().build();
3948 let pending_update = OrderPendingUpdateSpec::builder().build();
3949 let modify_rejected = OrderModifyRejectedSpec::builder().build();
3950 let mut order: MarketOrder = init.try_into().unwrap();
3951 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3952 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3953 let previous_status = order.previous_status();
3954
3955 let result = order.apply(OrderEventAny::Submitted(invalid));
3956
3957 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
3958 assert_eq!(order.previous_status(), previous_status);
3959
3960 order
3961 .apply(OrderEventAny::PendingUpdate(pending_update))
3962 .unwrap();
3963 order
3964 .apply(OrderEventAny::ModifyRejected(modify_rejected))
3965 .unwrap();
3966 assert_eq!(order.status(), OrderStatus::Accepted);
3967 }
3968
3969 #[rstest]
3970 fn test_modify_rejected_preserves_pending_cancel() {
3971 let init = OrderInitializedSpec::builder().build();
3972 let submitted = OrderSubmittedSpec::builder().build();
3973 let accepted = OrderAcceptedSpec::builder().build();
3974 let pending_update = OrderPendingUpdateSpec::builder().build();
3975 let pending_cancel = OrderPendingCancelSpec::builder().build();
3976 let modify_rejected = OrderModifyRejectedSpec::builder().build();
3977 let cancel_rejected = OrderCancelRejectedSpec::builder().build();
3978 let mut order: MarketOrder = init.try_into().unwrap();
3979
3980 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3981 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3982 order
3983 .apply(OrderEventAny::PendingUpdate(pending_update))
3984 .unwrap();
3985 order
3986 .apply(OrderEventAny::PendingCancel(pending_cancel))
3987 .unwrap();
3988 order
3989 .apply(OrderEventAny::ModifyRejected(modify_rejected))
3990 .unwrap();
3991
3992 assert_eq!(order.status(), OrderStatus::PendingCancel);
3993 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
3994
3995 order
3996 .apply(OrderEventAny::CancelRejected(cancel_rejected))
3997 .unwrap();
3998
3999 assert_eq!(order.status(), OrderStatus::Accepted);
4000 }
4001
4002 #[rstest]
4003 #[case::submitted(false)]
4004 #[case::accepted(true)]
4005 fn test_cancel_rejected_after_acceptance_preserves_accepted_status(
4006 #[case] accepted_before_cancel: bool,
4007 ) {
4008 let init = OrderInitializedSpec::builder().build();
4009 let mut order: MarketOrder = init.try_into().unwrap();
4010 order
4011 .apply(OrderEventAny::Submitted(
4012 OrderSubmittedSpec::builder().build(),
4013 ))
4014 .unwrap();
4015
4016 if accepted_before_cancel {
4017 order
4018 .apply(OrderEventAny::Accepted(
4019 OrderAcceptedSpec::builder().build(),
4020 ))
4021 .unwrap();
4022 }
4023 order
4024 .apply(OrderEventAny::PendingCancel(
4025 OrderPendingCancelSpec::builder().build(),
4026 ))
4027 .unwrap();
4028 order
4029 .apply(OrderEventAny::Accepted(
4030 OrderAcceptedSpec::builder().build(),
4031 ))
4032 .unwrap();
4033 let previous_status = if accepted_before_cancel {
4034 OrderStatus::Accepted
4035 } else {
4036 OrderStatus::Submitted
4037 };
4038 let cancel_rejected =
4039 OrderEventAny::CancelRejected(OrderCancelRejectedSpec::builder().build());
4040
4041 order.apply(cancel_rejected.clone()).unwrap();
4042
4043 assert_eq!(order.status(), OrderStatus::Accepted);
4044 assert_eq!(order.previous_status(), Some(previous_status));
4045 assert_eq!(order.last_event(), &cancel_rejected);
4046 }
4047
4048 #[rstest]
4049 fn test_rejected_event_preserves_cancel_rejected_restoration() {
4050 let init = OrderInitializedSpec::builder().build();
4051 let submitted = OrderSubmittedSpec::builder().build();
4052 let accepted = OrderAcceptedSpec::builder().build();
4053 let invalid = OrderSubmittedSpec::builder().build();
4054 let pending_cancel = OrderPendingCancelSpec::builder().build();
4055 let cancel_rejected = OrderCancelRejectedSpec::builder().build();
4056 let mut order: MarketOrder = init.try_into().unwrap();
4057 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4058 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4059 let previous_status = order.previous_status();
4060
4061 let result = order.apply(OrderEventAny::Submitted(invalid));
4062
4063 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
4064 assert_eq!(order.previous_status(), previous_status);
4065
4066 order
4067 .apply(OrderEventAny::PendingCancel(pending_cancel))
4068 .unwrap();
4069 order
4070 .apply(OrderEventAny::CancelRejected(cancel_rejected))
4071 .unwrap();
4072 assert_eq!(order.status(), OrderStatus::Accepted);
4073 }
4074
4075 #[rstest]
4076 #[case(true, false, "client_order_id")]
4077 #[case(false, true, "strategy_id")]
4078 fn test_update_identity_mismatch_precedes_field_validation(
4079 #[case] wrong_client: bool,
4080 #[case] wrong_strategy: bool,
4081 #[case] expected_field: &str,
4082 ) {
4083 let init = OrderInitializedSpec::builder().build();
4084 let submitted = OrderSubmittedSpec::builder().build();
4085 let accepted = OrderAcceptedSpec::builder().build();
4086 let mut order: MarketOrder = init.try_into().unwrap();
4087 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4088 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4089 let state = (
4090 order.status(),
4091 order.previous_status(),
4092 order.ts_last(),
4093 order.events().len(),
4094 );
4095 let client_order_id = if wrong_client {
4096 ClientOrderId::from("O-INVALID")
4097 } else {
4098 order.client_order_id()
4099 };
4100 let strategy_id = if wrong_strategy {
4101 StrategyId::from("OTHER-001")
4102 } else {
4103 order.strategy_id()
4104 };
4105 let updated = OrderUpdatedSpec::builder()
4108 .client_order_id(client_order_id)
4109 .strategy_id(strategy_id)
4110 .price(Price::new(95.0, 2))
4111 .build();
4112
4113 let result = order.apply(OrderEventAny::Updated(updated));
4114
4115 let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
4117 else {
4118 panic!("expected an identity predicate violation, was {result:?}");
4119 };
4120 assert!(
4121 message.contains(expected_field),
4122 "message did not name {expected_field}: {message}"
4123 );
4124 assert_eq!(order.status(), state.0);
4125 assert_eq!(order.previous_status(), state.1);
4126 assert_eq!(order.ts_last(), state.2);
4127 assert_eq!(order.events().len(), state.3);
4128 }
4129
4130 #[rstest]
4131 fn test_update_invalid_transition_precedes_field_validation() {
4132 let init = OrderInitializedSpec::builder().build();
4133 let denied = OrderDeniedSpec::builder().build();
4134 let mut order: MarketOrder = init.try_into().unwrap();
4135 order.apply(OrderEventAny::Denied(denied)).unwrap();
4136 let state = (
4137 order.status(),
4138 order.previous_status(),
4139 order.ts_last(),
4140 order.events().len(),
4141 );
4142 let updated = OrderUpdatedSpec::builder()
4145 .price(Price::new(95.0, 2))
4146 .build();
4147
4148 let result = order.apply(OrderEventAny::Updated(updated));
4149
4150 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
4151 assert_eq!(order.status(), state.0);
4152 assert_eq!(order.previous_status(), state.1);
4153 assert_eq!(order.ts_last(), state.2);
4154 assert_eq!(order.events().len(), state.3);
4155 }
4156
4157 #[rstest]
4158 fn test_direct_update_panics_on_invalid_field_before_mutation() {
4159 let init = OrderInitializedSpec::builder()
4160 .quantity(Quantity::from(100_000))
4161 .build();
4162 let mut order: MarketOrder = init.try_into().unwrap();
4163 let original_quantity = order.quantity();
4164 let updated = OrderUpdatedSpec::builder()
4169 .quantity(Quantity::from(50_000))
4170 .price(Price::new(95.0, 2))
4171 .build();
4172
4173 let result =
4174 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| order.update(&updated)));
4175
4176 assert!(result.is_err());
4177 assert_eq!(order.quantity(), original_quantity);
4178 }
4179
4180 #[rstest]
4181 fn test_partially_filled_order_can_be_updated() {
4182 let init = OrderInitializedSpec::builder()
4185 .quantity(Quantity::from(100_000))
4186 .build();
4187 let submitted = OrderSubmittedSpec::builder().build();
4188 let accepted = OrderAcceptedSpec::builder().build();
4189 let partial_fill = OrderFilledSpec::builder()
4190 .last_qty(Quantity::from(40_000))
4191 .build();
4192 let updated = OrderUpdatedSpec::builder()
4193 .quantity(Quantity::from(80_000)) .build();
4195
4196 let mut order: MarketOrder = init.try_into().unwrap();
4197 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4198 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4199 order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
4200
4201 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
4202 assert_eq!(order.filled_qty(), Quantity::from(40_000));
4203
4204 order.apply(OrderEventAny::Updated(updated)).unwrap();
4205
4206 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
4207 assert_eq!(order.quantity(), Quantity::from(80_000));
4208 assert_eq!(order.leaves_qty(), Quantity::from(40_000)); }
4210
4211 #[rstest]
4212 fn test_reconciliation_update_closes_order_at_filled_quantity() {
4213 let mut order: MarketOrder = OrderInitializedSpec::builder()
4214 .quantity(Quantity::from(100))
4215 .build()
4216 .try_into()
4217 .unwrap();
4218 order
4219 .apply(OrderEventAny::Accepted(
4220 OrderAcceptedSpec::builder().build(),
4221 ))
4222 .unwrap();
4223 order
4224 .apply(OrderEventAny::Filled(
4225 OrderFilledSpec::builder()
4226 .last_qty(Quantity::from(40))
4227 .build(),
4228 ))
4229 .unwrap();
4230 let updated = OrderEventAny::Updated(
4231 OrderUpdatedSpec::builder()
4232 .quantity(Quantity::from(40))
4233 .reconciliation(true)
4234 .ts_event(UnixNanos::from(5_000))
4235 .build(),
4236 );
4237
4238 order.apply(updated.clone()).unwrap();
4239 let replayed =
4240 OrderAny::from_events(order.events().into_iter().cloned().collect()).unwrap();
4241
4242 assert_eq!(order.status(), OrderStatus::Filled);
4243 assert_eq!(order.previous_status(), Some(OrderStatus::PartiallyFilled));
4244 assert_eq!(order.quantity(), Quantity::from(40));
4245 assert_eq!(order.filled_qty(), Quantity::from(40));
4246 assert_eq!(order.leaves_qty(), Quantity::from(0));
4247 assert_eq!(order.ts_closed(), Some(UnixNanos::from(5_000)));
4248 assert_eq!(order.ts_last(), UnixNanos::from(5_000));
4249 assert_eq!(order.event_count(), 4);
4250 assert_eq!(order.last_event(), &updated);
4251 assert_eq!(replayed.status(), order.status());
4252 assert_eq!(replayed.previous_status(), order.previous_status());
4253 assert_eq!(replayed.quantity(), order.quantity());
4254 assert_eq!(replayed.filled_qty(), order.filled_qty());
4255 assert_eq!(replayed.leaves_qty(), order.leaves_qty());
4256 assert_eq!(replayed.ts_closed(), order.ts_closed());
4257 assert_eq!(replayed.ts_last(), order.ts_last());
4258 }
4259
4260 #[rstest]
4261 fn test_triggered_order_can_be_updated() {
4262 let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
4265 let submitted = OrderSubmittedSpec::builder().build();
4266 let accepted = OrderAcceptedSpec::builder().build();
4267 let triggered = OrderTriggeredSpec::builder().build();
4268 let updated = OrderUpdatedSpec::builder()
4269 .quantity(Quantity::from(80_000))
4270 .build();
4271
4272 let mut order = OrderTestBuilder::new(OrderType::StopLimit)
4273 .instrument_id(instrument_id)
4274 .quantity(Quantity::from(100_000))
4275 .price(Price::from("0.99500"))
4276 .trigger_price(Price::from("1.00000"))
4277 .trigger_type(TriggerType::LastPrice)
4278 .build();
4279 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4280 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4281 order.apply(OrderEventAny::Triggered(triggered)).unwrap();
4282
4283 assert_eq!(order.status(), OrderStatus::Triggered);
4284
4285 order.apply(OrderEventAny::Updated(updated)).unwrap();
4286
4287 assert_eq!(order.status(), OrderStatus::Triggered);
4288 assert_eq!(order.quantity(), Quantity::from(80_000));
4289 }
4290
4291 #[rstest]
4292 fn test_order_updated_with_is_quote_quantity_clears_flag() {
4293 let init = OrderInitializedSpec::builder()
4294 .quantity(Quantity::new(10.0, 6))
4295 .quote_quantity(true)
4296 .build();
4297 let submitted = OrderSubmittedSpec::builder().build();
4298 let accepted = OrderAcceptedSpec::builder().build();
4299 let updated = OrderUpdatedSpec::builder()
4300 .quantity(Quantity::new(47.393_365, 6))
4301 .is_quote_quantity(false)
4302 .build();
4303
4304 let mut order: MarketOrder = init.try_into().unwrap();
4305 assert!(order.is_quote_quantity());
4306
4307 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4308 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4309 order.apply(OrderEventAny::Updated(updated)).unwrap();
4310
4311 assert!(!order.is_quote_quantity());
4312 assert_eq!(order.quantity(), Quantity::new(47.393_365, 6));
4313 assert_eq!(order.leaves_qty(), Quantity::new(47.393_365, 6));
4314 }
4315
4316 #[rstest]
4317 fn test_order_updated_default_is_quote_quantity_clears_flag() {
4318 let init = OrderInitializedSpec::builder()
4319 .quantity(Quantity::new(10.0, 6))
4320 .quote_quantity(true)
4321 .build();
4322 let submitted = OrderSubmittedSpec::builder().build();
4323 let accepted = OrderAcceptedSpec::builder().build();
4324 let updated = OrderUpdatedSpec::builder()
4326 .quantity(Quantity::new(8.0, 6))
4327 .build();
4328
4329 let mut order: MarketOrder = init.try_into().unwrap();
4330 assert!(order.is_quote_quantity());
4331
4332 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4333 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4334 order.apply(OrderEventAny::Updated(updated)).unwrap();
4335
4336 assert!(!order.is_quote_quantity());
4337 assert_eq!(order.quantity(), Quantity::new(8.0, 6));
4338 }
4339
4340 #[rstest]
4341 fn test_canceled_order_accepts_repeated_cancel_after_partial_fill() {
4342 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
4343 let submitted = OrderSubmittedSpec::builder().build();
4344 let accepted = OrderAcceptedSpec::builder().build();
4345 let canceled1 = OrderCanceledSpec::builder().build();
4346 let fill = OrderFilledSpec::builder()
4347 .last_qty(Quantity::from(50_000))
4348 .trade_id(TradeId::from("FILL-1"))
4349 .build();
4350 let canceled2 = OrderCanceledSpec::builder().build();
4351
4352 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4353 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4354 order.apply(OrderEventAny::Canceled(canceled1)).unwrap();
4355 assert_eq!(order.status(), OrderStatus::Canceled);
4356 assert!(order.is_closed());
4357
4358 order.apply(OrderEventAny::Filled(fill)).unwrap();
4359 assert_eq!(order.status(), OrderStatus::Canceled);
4360 assert_eq!(order.filled_qty(), Quantity::from(50_000));
4361 assert_eq!(order.leaves_qty(), Quantity::from(50_000));
4362 assert!(order.is_closed());
4363 assert!(!order.is_open());
4364
4365 order.apply(OrderEventAny::Canceled(canceled2)).unwrap();
4366 assert_eq!(order.status(), OrderStatus::Canceled);
4367 assert_eq!(order.filled_qty(), Quantity::from(50_000));
4368 assert_eq!(order.leaves_qty(), Quantity::from(50_000));
4369 assert_eq!(
4370 order
4371 .events()
4372 .iter()
4373 .filter(|event| matches!(event, OrderEventAny::Canceled(_)))
4374 .count(),
4375 2
4376 );
4377 assert!(order.is_closed());
4378 assert!(!order.is_open());
4379 }
4380
4381 #[rstest]
4382 fn test_updated_restores_status_before_pending_cancel() {
4383 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
4384 let submitted = OrderSubmittedSpec::builder().build();
4385 let accepted = OrderAcceptedSpec::builder().build();
4386 let pending_cancel = OrderPendingCancelSpec::builder().build();
4387 let fill = OrderFilledSpec::builder()
4388 .last_qty(Quantity::from(10_000))
4389 .build();
4390 let updated = OrderUpdatedSpec::builder()
4391 .quantity(Quantity::from(150_000))
4392 .build();
4393
4394 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4395 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4396 let ts_accepted = order.ts_accepted();
4397 let venue_order_ids: Vec<VenueOrderId> =
4398 order.venue_order_ids().into_iter().copied().collect();
4399 order
4400 .apply(OrderEventAny::PendingCancel(pending_cancel))
4401 .unwrap();
4402 order.apply(OrderEventAny::Filled(fill)).unwrap();
4403 assert_eq!(order.status(), OrderStatus::PendingCancel);
4404 assert_eq!(order.previous_status(), Some(OrderStatus::PartiallyFilled));
4405 order.apply(OrderEventAny::Updated(updated)).unwrap();
4406
4407 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
4408 assert_eq!(order.quantity(), Quantity::from(150_000));
4409 assert_eq!(order.filled_qty(), Quantity::from(10_000));
4410 assert_eq!(order.leaves_qty(), Quantity::from(140_000));
4411 assert_eq!(order.ts_accepted(), ts_accepted);
4412 assert_eq!(
4413 order
4414 .venue_order_ids()
4415 .into_iter()
4416 .copied()
4417 .collect::<Vec<_>>(),
4418 venue_order_ids,
4419 );
4420 }
4421
4422 #[rstest]
4423 fn test_apply_triggered_to_stop_market_order_returns_error() {
4424 let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
4425 let submitted = OrderSubmittedSpec::builder().build();
4426 let accepted = OrderAcceptedSpec::builder().build();
4427 let triggered = OrderTriggeredSpec::builder().build();
4428
4429 let mut order = OrderTestBuilder::new(OrderType::StopMarket)
4430 .instrument_id(instrument_id)
4431 .quantity(Quantity::from(1))
4432 .trigger_price(Price::from("1.00000"))
4433 .trigger_type(TriggerType::LastPrice)
4434 .build();
4435 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4436 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4437
4438 let result = order.apply(OrderEventAny::Triggered(triggered));
4439 assert!(result.is_err());
4440 assert_eq!(order.status(), OrderStatus::Accepted);
4441 }
4442
4443 #[rstest]
4444 fn test_apply_triggered_to_stop_limit_order_succeeds() {
4445 let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
4446 let submitted = OrderSubmittedSpec::builder().build();
4447 let accepted = OrderAcceptedSpec::builder().build();
4448 let triggered = OrderTriggeredSpec::builder().build();
4449
4450 let mut order = OrderTestBuilder::new(OrderType::StopLimit)
4451 .instrument_id(instrument_id)
4452 .quantity(Quantity::from(1))
4453 .price(Price::from("0.99500"))
4454 .trigger_price(Price::from("1.00000"))
4455 .trigger_type(TriggerType::LastPrice)
4456 .build();
4457 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4458 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4459 order.apply(OrderEventAny::Triggered(triggered)).unwrap();
4460
4461 assert_eq!(order.status(), OrderStatus::Triggered);
4462 }
4463
4464 #[rstest]
4465 fn test_to_order_status_report_for_accepted_limit_order() {
4466 let order = OrderTestBuilder::new(OrderType::Limit)
4467 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
4468 .side(OrderSide::Buy)
4469 .quantity(Quantity::from(100_000))
4470 .price(Price::from("1.00000"))
4471 .build();
4472 let accepted = TestOrderStubs::make_accepted_order(&order);
4473
4474 let report = accepted.to_order_status_report(None).unwrap();
4475
4476 assert_eq!(report.account_id, AccountId::from("SIM-001"));
4477 assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
4478 assert_eq!(report.client_order_id, Some(accepted.client_order_id()));
4479 assert_eq!(report.venue_order_id, VenueOrderId::from("V-001"));
4480 assert_eq!(report.order_side, Some(OrderSide::Buy));
4481 assert_eq!(report.order_type, OrderType::Limit);
4482 assert_eq!(report.time_in_force, accepted.time_in_force());
4483 assert_eq!(report.order_status, OrderStatus::Accepted);
4484 assert_eq!(report.quantity, Quantity::from(100_000));
4485 assert_eq!(report.filled_qty, Quantity::from(0));
4486 assert_eq!(report.price, Some(Price::from("1.00000")));
4487 assert_eq!(report.avg_px, None);
4488 assert_eq!(report.ts_accepted, accepted.ts_accepted().unwrap());
4489 assert_eq!(report.ts_last, accepted.ts_last());
4490 assert_eq!(report.ts_init, accepted.ts_init());
4491 }
4492
4493 #[rstest]
4494 fn test_to_order_status_report_for_filled_market_order(audusd_sim: CurrencyPair) {
4495 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
4496 let order = OrderTestBuilder::new(OrderType::Market)
4497 .instrument_id(audusd_sim.id())
4498 .side(OrderSide::Buy)
4499 .quantity(Quantity::from(100_000))
4500 .build();
4501 let filled = TestOrderStubs::make_filled_order(&order, &audusd_sim, LiquiditySide::Maker);
4502 let report_id = UUID4::new();
4503
4504 let report = filled.to_order_status_report(Some(report_id)).unwrap();
4505
4506 assert_eq!(report.report_id, report_id);
4507 assert_eq!(report.order_status, OrderStatus::Filled);
4508 assert_eq!(report.quantity, Quantity::from(100_000));
4509 assert_eq!(report.filled_qty, Quantity::from(100_000));
4510 assert_eq!(report.price, None);
4511 assert_eq!(report.avg_px, Some(dec!(1)));
4512 assert_eq!(report.venue_position_id, Some(PositionId::from("1")));
4513 assert_eq!(report.ts_accepted, filled.ts_accepted().unwrap());
4514 assert_eq!(report.ts_last, filled.ts_last());
4515 }
4516
4517 #[rstest]
4518 fn test_to_order_status_report_maps_optional_fields() {
4519 let order = OrderTestBuilder::new(OrderType::StopLimit)
4520 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
4521 .side(OrderSide::Sell)
4522 .quantity(Quantity::from(100_000))
4523 .price(Price::from("0.99500"))
4524 .trigger_price(Price::from("1.00000"))
4525 .trigger_type(TriggerType::LastPrice)
4526 .time_in_force(TimeInForce::Gtd)
4527 .expire_time(UnixNanos::from(5_000_000_000))
4528 .display_qty(Quantity::from(10_000))
4529 .post_only(true)
4530 .reduce_only(true)
4531 .contingency_type(ContingencyType::Oto)
4532 .order_list_id(OrderListId::from("OL-001"))
4533 .linked_order_ids(vec![ClientOrderId::from("O-CHILD")])
4534 .parent_order_id(ClientOrderId::from("O-PARENT"))
4535 .build();
4536 let accepted = TestOrderStubs::make_accepted_order(&order);
4537
4538 let report = accepted.to_order_status_report(None).unwrap();
4539
4540 assert_eq!(report.price, Some(Price::from("0.99500")));
4541 assert_eq!(report.trigger_price, Some(Price::from("1.00000")));
4542 assert_eq!(report.trigger_type, Some(TriggerType::LastPrice));
4543 assert_eq!(report.expire_time, Some(UnixNanos::from(5_000_000_000)));
4544 assert_eq!(report.display_qty, Some(Quantity::from(10_000)));
4545 assert!(report.post_only);
4546 assert!(report.reduce_only);
4547 assert_eq!(report.contingency_type, Some(ContingencyType::Oto));
4548 assert_eq!(report.order_list_id, Some(OrderListId::from("OL-001")));
4549 assert_eq!(
4550 report.linked_order_ids,
4551 Some(vec![ClientOrderId::from("O-CHILD")])
4552 );
4553 assert_eq!(
4554 report.parent_order_id,
4555 Some(ClientOrderId::from("O-PARENT"))
4556 );
4557 }
4558
4559 #[rstest]
4560 fn test_to_order_status_report_maps_ts_triggered() {
4561 let order = OrderTestBuilder::new(OrderType::StopLimit)
4562 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
4563 .side(OrderSide::Buy)
4564 .quantity(Quantity::from(100_000))
4565 .price(Price::from("0.99500"))
4566 .trigger_price(Price::from("1.00000"))
4567 .trigger_type(TriggerType::LastPrice)
4568 .build();
4569 let mut order = TestOrderStubs::make_accepted_order(&order);
4570 let triggered = OrderTriggeredSpec::builder()
4571 .ts_event(UnixNanos::from(1_500_000_000))
4572 .build();
4573 order.apply(OrderEventAny::Triggered(triggered)).unwrap();
4574
4575 let report = order.to_order_status_report(None).unwrap();
4576
4577 assert_eq!(report.order_status, OrderStatus::Triggered);
4578 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
4579 }
4580
4581 #[rstest]
4582 fn test_to_order_status_report_maps_rejection_reason() {
4583 let order = OrderTestBuilder::new(OrderType::Limit)
4584 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
4585 .side(OrderSide::Buy)
4586 .quantity(Quantity::from(100_000))
4587 .price(Price::from("1.00000"))
4588 .build();
4589 let mut order = TestOrderStubs::make_accepted_order(&order);
4590 let rejected = OrderRejectedSpec::builder()
4591 .reason(Ustr::from("INSUFFICIENT_MARGIN"))
4592 .build();
4593 order.apply(OrderEventAny::Rejected(rejected)).unwrap();
4594
4595 let report = order.to_order_status_report(None).unwrap();
4596
4597 assert_eq!(report.order_status, OrderStatus::Rejected);
4598 assert_eq!(
4599 report.cancel_reason,
4600 Some("INSUFFICIENT_MARGIN".to_string())
4601 );
4602 }
4603
4604 #[rstest]
4605 fn test_to_order_status_report_maps_distinct_timestamps() {
4606 let mut order = OrderTestBuilder::new(OrderType::Limit)
4607 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
4608 .side(OrderSide::Buy)
4609 .quantity(Quantity::from(100_000))
4610 .price(Price::from("1.00000"))
4611 .ts_init(UnixNanos::from(1_000))
4612 .build();
4613 let submitted = OrderSubmittedSpec::builder()
4614 .ts_event(UnixNanos::from(2_000))
4615 .build();
4616 let accepted = OrderAcceptedSpec::builder()
4617 .ts_event(UnixNanos::from(3_000))
4618 .build();
4619 let filled = OrderFilledSpec::builder()
4620 .last_qty(Quantity::from(50_000))
4621 .ts_event(UnixNanos::from(4_000))
4622 .build();
4623 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4624 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4625 order.apply(OrderEventAny::Filled(filled)).unwrap();
4626
4627 let report = order.to_order_status_report(None).unwrap();
4628
4629 assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
4630 assert_eq!(report.filled_qty, Quantity::from(50_000));
4631 assert_eq!(report.ts_accepted, UnixNanos::from(3_000));
4632 assert_eq!(report.ts_last, UnixNanos::from(4_000));
4633 assert_eq!(report.ts_init, UnixNanos::from(1_000));
4634 }
4635
4636 #[rstest]
4637 fn test_to_order_status_report_ts_accepted_falls_back_to_ts_last() {
4638 let mut order = OrderTestBuilder::new(OrderType::Limit)
4640 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
4641 .side(OrderSide::Buy)
4642 .quantity(Quantity::from(100_000))
4643 .price(Price::from("1.00000"))
4644 .build();
4645 let submitted = OrderSubmittedSpec::builder()
4646 .ts_event(UnixNanos::from(2_000))
4647 .build();
4648 let updated = OrderUpdatedSpec::builder()
4649 .venue_order_id(VenueOrderId::from("V-001"))
4650 .ts_event(UnixNanos::from(3_000))
4651 .build();
4652 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4653 order.apply(OrderEventAny::Updated(updated)).unwrap();
4654
4655 let report = order.to_order_status_report(None).unwrap();
4656
4657 assert_eq!(report.order_status, OrderStatus::Submitted);
4658 assert_eq!(report.venue_order_id, VenueOrderId::from("V-001"));
4659 assert_eq!(report.ts_accepted, UnixNanos::from(3_000));
4660 assert_eq!(report.ts_last, UnixNanos::from(3_000));
4661 }
4662
4663 #[rstest]
4664 fn test_to_order_status_report_maps_trailing_offsets() {
4665 let order = OrderTestBuilder::new(OrderType::TrailingStopLimit)
4666 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
4667 .side(OrderSide::Sell)
4668 .quantity(Quantity::from(100_000))
4669 .price(Price::from("0.99500"))
4670 .activation_price(Price::from("1.00500"))
4671 .trigger_price(Price::from("1.00000"))
4672 .trigger_type(TriggerType::LastPrice)
4673 .limit_offset(dec!(0.0001))
4674 .trailing_offset(dec!(0.0002))
4675 .trailing_offset_type(TrailingOffsetType::Price)
4676 .build();
4677 let accepted = TestOrderStubs::make_accepted_order(&order);
4678
4679 let report = accepted.to_order_status_report(None).unwrap();
4680
4681 assert_eq!(report.activation_price, Some(Price::from("1.00500")));
4682 assert_eq!(report.limit_offset, Some(dec!(0.0001)));
4683 assert_eq!(report.trailing_offset, Some(dec!(0.0002)));
4684 assert_eq!(report.trailing_offset_type, Some(TrailingOffsetType::Price),);
4685 }
4686
4687 #[rstest]
4688 fn test_to_order_status_report_returns_none_before_venue_ack() {
4689 let order = OrderTestBuilder::new(OrderType::Limit)
4690 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
4691 .side(OrderSide::Buy)
4692 .quantity(Quantity::from(100_000))
4693 .price(Price::from("1.00000"))
4694 .build();
4695
4696 assert!(order.to_order_status_report(None).is_none());
4697 }
4698
4699 #[rstest]
4700 fn test_to_order_status_report_returns_none_for_submitted_order() {
4701 let mut order = OrderTestBuilder::new(OrderType::Limit)
4703 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
4704 .side(OrderSide::Buy)
4705 .quantity(Quantity::from(100_000))
4706 .price(Price::from("1.00000"))
4707 .build();
4708 let submitted = OrderSubmittedSpec::builder().build();
4709 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4710
4711 assert!(order.account_id().is_some());
4712 assert!(order.to_order_status_report(None).is_none());
4713 }
4714}