1pub mod any;
36pub mod limit;
37pub mod limit_if_touched;
38pub mod list;
39pub mod market;
40pub mod market_if_touched;
41pub mod market_to_limit;
42pub mod stop_limit;
43pub mod stop_market;
44pub mod trailing_stop_limit;
45pub mod trailing_stop_market;
46
47#[cfg(any(test, feature = "test-support"))]
48pub mod builder;
49
50#[cfg(any(test, feature = "test-support"))]
51pub mod stubs;
52
53use ahash::{AHashMap, AHashSet};
55use enum_dispatch::enum_dispatch;
56use indexmap::IndexMap;
57use nautilus_core::{
58 UUID4, UnixNanos,
59 correctness::{CorrectnessError, check_predicate_false},
60};
61use rust_decimal::Decimal;
62use serde::{Deserialize, Serialize};
63use ustr::Ustr;
64
65#[cfg(any(test, feature = "test-support"))]
66pub use crate::orders::builder::OrderTestBuilder;
67pub use crate::orders::{
68 any::{LimitOrderAny, OrderAny, OrderReplayError, PassiveOrderAny, StopOrderAny},
69 limit::LimitOrder,
70 limit_if_touched::LimitIfTouchedOrder,
71 list::{OrderList, OrderListValidationError},
72 market::MarketOrder,
73 market_if_touched::MarketIfTouchedOrder,
74 market_to_limit::MarketToLimitOrder,
75 stop_limit::StopLimitOrder,
76 stop_market::StopMarketOrder,
77 trailing_stop_limit::TrailingStopLimitOrder,
78 trailing_stop_market::TrailingStopMarketOrder,
79};
80use crate::{
81 enums::{
82 ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide,
83 TimeInForce, TrailingOffsetType, TriggerType,
84 },
85 events::{
86 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
87 OrderEventAny, OrderExpired, OrderFillVoided, OrderFilled, OrderInitialized,
88 OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased,
89 OrderSubmitted, OrderTriggered, OrderUpdated,
90 },
91 identifiers::{
92 AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
93 StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
94 },
95 orderbook::OwnBookOrder,
96 reports::OrderStatusReport,
97 types::{Currency, Money, Price, Quantity},
98};
99
100pub const STOP_ORDER_TYPES: &[OrderType] = &[
102 OrderType::StopMarket,
103 OrderType::StopLimit,
104 OrderType::MarketIfTouched,
105 OrderType::LimitIfTouched,
106];
107
108pub const LIMIT_ORDER_TYPES: &[OrderType] = &[
110 OrderType::Limit,
111 OrderType::StopLimit,
112 OrderType::LimitIfTouched,
113 OrderType::TrailingStopLimit,
114];
115
116pub const TRIGGERABLE_ORDER_TYPES: &[OrderType] = &[
121 OrderType::StopLimit,
122 OrderType::TrailingStopLimit,
123 OrderType::LimitIfTouched,
124];
125
126pub const LOCAL_ACTIVE_ORDER_STATUSES: &[OrderStatus] = &[
128 OrderStatus::Initialized,
129 OrderStatus::Emulated,
130 OrderStatus::Released,
131];
132
133#[derive(thiserror::Error, Debug)]
134pub enum OrderError {
135 #[error("Order not found: {0}")]
136 NotFound(ClientOrderId),
137 #[error("Order invariant failed: must have a side for this operation")]
138 NoOrderSide,
139 #[error("Invalid event for order type")]
140 InvalidOrderEvent,
141 #[error("Invalid order state transition")]
142 InvalidStateTransition,
143 #[error("Order was already initialized")]
144 AlreadyInitialized,
145 #[error("Order had no previous state")]
146 NoPreviousState,
147 #[error("Duplicate fill: trade_id {0} already applied to order")]
148 DuplicateFill(TradeId),
149 #[error("Duplicate fill void: trade_id {0} already has this cumulative correction")]
150 DuplicateFillVoid(TradeId),
151 #[error("Fill void for trade_id {0} is older than the applied correction")]
152 StaleFillVoid(TradeId),
153 #[error("Fill void quantity exceeds the original fill for trade_id {0}")]
154 OverVoid(TradeId),
155 #[error("Fill void commission is missing or conflicts with trade_id {0}")]
156 InvalidFillVoidCommission(TradeId),
157 #[error("{0}")]
158 Invariant(#[from] CorrectnessError),
159}
160
161#[must_use]
163pub fn ustr_indexmap_to_str(h: IndexMap<Ustr, Ustr>) -> IndexMap<String, String> {
164 h.into_iter()
165 .map(|(k, v)| (k.to_string(), v.to_string()))
166 .collect()
167}
168
169#[must_use]
171pub fn str_indexmap_to_ustr(h: IndexMap<String, String>) -> IndexMap<Ustr, Ustr> {
172 h.into_iter()
173 .map(|(k, v)| (Ustr::from(&k), Ustr::from(&v)))
174 .collect()
175}
176
177#[inline]
178pub(crate) fn check_display_qty(
179 display_qty: Option<Quantity>,
180 quantity: Quantity,
181) -> Result<(), OrderError> {
182 if let Some(q) = display_qty {
183 check_predicate_false(q > quantity, "`display_qty` may not exceed `quantity`")?;
184 }
185 Ok(())
186}
187
188#[inline]
189pub(crate) fn check_time_in_force(
190 time_in_force: TimeInForce,
191 expire_time: Option<UnixNanos>,
192) -> Result<(), OrderError> {
193 check_predicate_false(
194 time_in_force == TimeInForce::Gtd && expire_time.unwrap_or_default() == 0,
195 "`expire_time` is required for `GTD` order",
196 )?;
197 Ok(())
198}
199
200impl OrderStatus {
201 #[rustfmt::skip]
207 pub fn transition(&mut self, event: &OrderEventAny) -> Result<Self, OrderError> {
208 let new_state = match (self, event) {
209 (Self::Initialized, OrderEventAny::Denied(_)) => Self::Denied,
210 (Self::Initialized, OrderEventAny::Emulated(_)) => Self::Emulated, (Self::Initialized, OrderEventAny::Released(_)) => Self::Released, (Self::Initialized, OrderEventAny::Submitted(_)) => Self::Submitted,
213 (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,
228 (Self::Submitted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
229 (Self::Submitted, OrderEventAny::ModifyRejected(_)) => Self::Submitted,
230 (Self::Submitted, OrderEventAny::CancelRejected(_)) => Self::Submitted, (Self::Submitted, OrderEventAny::Rejected(_)) => Self::Rejected,
232 (Self::Submitted, OrderEventAny::Canceled(_)) => Self::Canceled, (Self::Submitted, OrderEventAny::Accepted(_)) => Self::Accepted,
234 (Self::Submitted, OrderEventAny::Updated(_)) => Self::Submitted,
235 (Self::Submitted, OrderEventAny::Filled(_)) => Self::Filled,
236 (Self::Accepted, OrderEventAny::Rejected(_)) => Self::Rejected, (Self::Accepted, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
238 (Self::Accepted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
239 (Self::Accepted, OrderEventAny::ModifyRejected(_)) => Self::Accepted,
240 (Self::Accepted, OrderEventAny::CancelRejected(_)) => Self::Accepted, (Self::Accepted, OrderEventAny::Canceled(_)) => Self::Canceled,
242 (Self::Accepted, OrderEventAny::Triggered(_)) => Self::Triggered,
243 (Self::Accepted, OrderEventAny::Updated(_)) => Self::Accepted, (Self::Accepted, OrderEventAny::Expired(_)) => Self::Expired,
245 (Self::Accepted, OrderEventAny::Filled(_)) => Self::Filled,
246 (Self::Accepted, OrderEventAny::FillVoided(_)) => Self::Accepted,
247 (Self::Canceled, OrderEventAny::Filled(_)) => Self::Filled, (Self::Canceled, OrderEventAny::FillVoided(_)) => Self::Canceled,
249 (Self::Canceled, OrderEventAny::Updated(_)) => Self::Canceled,
250 (Self::PendingUpdate, OrderEventAny::Rejected(_)) => Self::Rejected,
251 (Self::PendingUpdate, OrderEventAny::Accepted(_)) => Self::Accepted,
252 (Self::PendingUpdate, OrderEventAny::Canceled(_)) => Self::Canceled,
253 (Self::PendingUpdate, OrderEventAny::Expired(_)) => Self::Expired,
254 (Self::PendingUpdate, OrderEventAny::Triggered(_)) => Self::Triggered,
255 (Self::PendingUpdate, OrderEventAny::Submitted(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
258 (Self::PendingUpdate, OrderEventAny::CancelRejected(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::ModifyRejected(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::Updated(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::Filled(_)) => Self::Filled,
262 (Self::PendingUpdate, OrderEventAny::FillVoided(_)) => Self::PendingUpdate,
263 (Self::PendingCancel, OrderEventAny::Rejected(_)) => Self::Rejected,
264 (Self::PendingCancel, OrderEventAny::PendingCancel(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::ModifyRejected(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::CancelRejected(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::Canceled(_)) => Self::Canceled,
268 (Self::PendingCancel, OrderEventAny::Expired(_)) => Self::Expired,
269 (Self::PendingCancel, OrderEventAny::Accepted(_)) => Self::Accepted, (Self::PendingCancel, OrderEventAny::Updated(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::Filled(_)) => Self::Filled,
272 (Self::PendingCancel, OrderEventAny::FillVoided(_)) => Self::PendingCancel,
273 (Self::Triggered, OrderEventAny::Rejected(_)) => Self::Rejected,
274 (Self::Triggered, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
275 (Self::Triggered, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
276 (Self::Triggered, OrderEventAny::ModifyRejected(_)) => Self::Triggered,
277 (Self::Triggered, OrderEventAny::CancelRejected(_)) => Self::Triggered,
278 (Self::Triggered, OrderEventAny::Canceled(_)) => Self::Canceled,
279 (Self::Triggered, OrderEventAny::Expired(_)) => Self::Expired,
280 (Self::Triggered, OrderEventAny::Filled(_)) => Self::Filled,
281 (Self::Triggered, OrderEventAny::Updated(_)) => Self::Triggered,
282 (Self::Triggered, OrderEventAny::FillVoided(_)) => Self::Triggered,
283 (Self::PartiallyFilled, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
284 (Self::PartiallyFilled, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
285 (Self::PartiallyFilled, OrderEventAny::ModifyRejected(_)) => Self::PartiallyFilled,
286 (Self::PartiallyFilled, OrderEventAny::CancelRejected(_)) => Self::PartiallyFilled,
287 (Self::PartiallyFilled, OrderEventAny::Canceled(_)) => Self::Canceled,
288 (Self::PartiallyFilled, OrderEventAny::Expired(_)) => Self::Expired,
289 (Self::PartiallyFilled, OrderEventAny::Filled(_)) => Self::Filled,
290 (Self::PartiallyFilled, OrderEventAny::Accepted(_)) => Self::Accepted,
291 (Self::PartiallyFilled, OrderEventAny::Updated(_)) => Self::PartiallyFilled,
292 (Self::PartiallyFilled, OrderEventAny::FillVoided(_)) => Self::PartiallyFilled,
293 (Self::Filled, OrderEventAny::FillVoided(_)) => Self::Voided,
294 (Self::Filled, OrderEventAny::Updated(_)) => Self::Filled,
295 (Self::Expired, OrderEventAny::Filled(_)) => Self::Filled, (Self::Expired, OrderEventAny::FillVoided(_)) => Self::Expired,
297 (Self::Expired, OrderEventAny::Updated(_)) => Self::Expired,
298 (Self::Voided, OrderEventAny::FillVoided(_)) => Self::Voided,
299 _ => return Err(OrderError::InvalidStateTransition),
300 };
301 Ok(new_state)
302 }
303}
304
305#[enum_dispatch]
306pub trait Order: 'static + Send {
307 fn into_any(self) -> OrderAny;
308 fn status(&self) -> OrderStatus;
309 fn trader_id(&self) -> TraderId;
310 fn strategy_id(&self) -> StrategyId;
311 fn instrument_id(&self) -> InstrumentId;
312 fn symbol(&self) -> Symbol;
313 fn venue(&self) -> Venue;
314 fn client_order_id(&self) -> ClientOrderId;
315 fn venue_order_id(&self) -> Option<VenueOrderId>;
316 fn position_id(&self) -> Option<PositionId>;
317 fn account_id(&self) -> Option<AccountId>;
318 fn last_trade_id(&self) -> Option<TradeId>;
319 fn order_side(&self) -> OrderSide;
320 fn order_type(&self) -> OrderType;
321 fn quantity(&self) -> Quantity;
322 fn time_in_force(&self) -> TimeInForce;
323 fn expire_time(&self) -> Option<UnixNanos>;
324 fn price(&self) -> Option<Price>;
325 fn activation_price(&self) -> Option<Price> {
326 None
327 }
328 fn trigger_price(&self) -> Option<Price>;
329 fn trigger_type(&self) -> Option<TriggerType>;
330 fn liquidity_side(&self) -> Option<LiquiditySide>;
331 fn is_post_only(&self) -> bool;
332 fn is_reduce_only(&self) -> bool;
333 fn is_quote_quantity(&self) -> bool;
334 fn display_qty(&self) -> Option<Quantity>;
335 fn limit_offset(&self) -> Option<Decimal>;
336 fn trailing_offset(&self) -> Option<Decimal>;
337 fn trailing_offset_type(&self) -> Option<TrailingOffsetType>;
338 fn emulation_trigger(&self) -> Option<TriggerType>;
339 fn trigger_instrument_id(&self) -> Option<InstrumentId>;
340 fn contingency_type(&self) -> Option<ContingencyType>;
341 fn order_list_id(&self) -> Option<OrderListId>;
342 fn linked_order_ids(&self) -> Option<&[ClientOrderId]>;
343 fn parent_order_id(&self) -> Option<ClientOrderId>;
344 fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId>;
345 fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>>;
346 fn exec_spawn_id(&self) -> Option<ClientOrderId>;
347 fn tags(&self) -> Option<&[Ustr]>;
348 fn filled_qty(&self) -> Quantity;
349 fn voided_qty(&self) -> Quantity;
350 fn non_reopened_voided_qty(&self) -> Quantity;
351 fn leaves_qty(&self) -> Quantity;
352 fn overfill_qty(&self) -> Quantity;
353
354 fn calculate_overfill(&self, fill_qty: Quantity) -> Quantity {
356 let potential_filled = self.filled_qty() + fill_qty;
357 let quantity = self.quantity();
358 if potential_filled > quantity {
359 potential_filled - quantity
360 } else {
361 Quantity::zero(fill_qty.precision)
362 }
363 }
364
365 fn avg_px(&self) -> Option<Decimal>;
366 fn slippage(&self) -> Option<Decimal>;
367 fn init_id(&self) -> UUID4;
368 fn ts_init(&self) -> UnixNanos;
369 fn ts_submitted(&self) -> Option<UnixNanos>;
370 fn ts_accepted(&self) -> Option<UnixNanos>;
371 fn ts_closed(&self) -> Option<UnixNanos>;
372 fn ts_last(&self) -> UnixNanos;
373
374 fn commissions(&self) -> &IndexMap<Currency, Money>;
375
376 fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError>;
382
383 fn update(&mut self, event: &OrderUpdated);
384
385 fn events(&self) -> Vec<&OrderEventAny>;
386
387 fn last_event(&self) -> &OrderEventAny {
388 self.events()
389 .last()
390 .expect("Order invariant violated: no events")
391 }
392
393 fn event_count(&self) -> usize {
394 self.events().len()
395 }
396
397 fn venue_order_ids(&self) -> Vec<&VenueOrderId>;
398
399 fn trade_ids(&self) -> Vec<&TradeId>;
400
401 fn has_price(&self) -> bool;
402
403 fn is_duplicate_fill(&self, fill: &OrderFilled) -> bool {
405 let mut latest = None;
406
407 for event in self.events() {
408 if let OrderEventAny::Filled(existing) = event
409 && existing.trade_id == fill.trade_id
410 && existing.order_side == fill.order_side
411 {
412 latest = Some(existing);
413 }
414 }
415
416 latest.is_some_and(|existing| {
417 existing.last_qty == fill.last_qty && existing.last_px == fill.last_px
418 })
419 }
420
421 fn is_buy(&self) -> bool {
422 self.order_side() == OrderSide::Buy
423 }
424
425 fn is_sell(&self) -> bool {
426 self.order_side() == OrderSide::Sell
427 }
428
429 fn is_passive(&self) -> bool {
430 self.order_type() != OrderType::Market
431 }
432
433 fn is_aggressive(&self) -> bool {
434 self.order_type() == OrderType::Market
435 }
436
437 fn is_emulated(&self) -> bool {
438 self.status() == OrderStatus::Emulated
439 }
440
441 fn is_active_local(&self) -> bool {
442 matches!(
443 self.status(),
444 OrderStatus::Initialized | OrderStatus::Emulated | OrderStatus::Released
445 )
446 }
447
448 fn is_primary(&self) -> bool {
449 self.exec_algorithm_id().is_some()
450 && self
451 .exec_spawn_id()
452 .is_some_and(|spawn_id| self.client_order_id() == spawn_id)
453 }
454
455 fn is_spawned(&self) -> bool {
456 self.exec_algorithm_id().is_some()
457 && self
458 .exec_spawn_id()
459 .is_some_and(|spawn_id| self.client_order_id() != spawn_id)
460 }
461
462 fn is_contingency(&self) -> bool {
463 self.contingency_type().is_some()
464 }
465
466 fn is_parent_order(&self) -> bool {
467 match self.contingency_type() {
468 Some(c) => c == ContingencyType::Oto,
469 None => false,
470 }
471 }
472
473 fn is_child_order(&self) -> bool {
474 self.parent_order_id().is_some()
475 }
476
477 fn is_open(&self) -> bool {
478 if self.emulation_trigger().is_some() {
479 return false;
480 }
481
482 matches!(
483 self.status(),
484 OrderStatus::Accepted
485 | OrderStatus::Triggered
486 | OrderStatus::PendingCancel
487 | OrderStatus::PendingUpdate
488 | OrderStatus::PartiallyFilled
489 )
490 }
491
492 fn is_canceled(&self) -> bool {
493 self.status() == OrderStatus::Canceled
494 }
495
496 fn is_closed(&self) -> bool {
497 matches!(
498 self.status(),
499 OrderStatus::Denied
500 | OrderStatus::Rejected
501 | OrderStatus::Canceled
502 | OrderStatus::Expired
503 | OrderStatus::Filled
504 | OrderStatus::Voided
505 )
506 }
507
508 fn is_inflight(&self) -> bool {
509 if self.emulation_trigger().is_some() {
510 return false;
511 }
512
513 matches!(
514 self.status(),
515 OrderStatus::Submitted | OrderStatus::PendingCancel | OrderStatus::PendingUpdate
516 )
517 }
518
519 fn is_pending_update(&self) -> bool {
520 self.status() == OrderStatus::PendingUpdate
521 }
522
523 fn is_pending_cancel(&self) -> bool {
524 self.status() == OrderStatus::PendingCancel
525 }
526
527 fn to_own_book_order(&self) -> OwnBookOrder {
528 OwnBookOrder::new(
529 self.trader_id(),
530 self.client_order_id(),
531 self.venue_order_id(),
532 self.order_side(),
533 self.price().expect("`OwnBookOrder` must have a price"),
534 self.leaves_qty(),
535 self.order_type(),
536 self.time_in_force(),
537 self.status(),
538 self.ts_last(),
539 self.ts_accepted().unwrap_or_default(),
540 self.ts_submitted().unwrap_or_default(),
541 self.ts_init(),
542 )
543 }
544
545 fn to_order_status_report(&self, report_id: Option<UUID4>) -> Option<OrderStatusReport> {
551 let account_id = self.account_id()?;
552 let venue_order_id = self.venue_order_id()?;
553
554 let mut report = OrderStatusReport::new(
555 account_id,
556 self.instrument_id(),
557 Some(self.client_order_id()),
558 venue_order_id,
559 self.order_side().into(),
560 self.order_type(),
561 self.time_in_force(),
562 self.status(),
563 self.quantity(),
564 self.filled_qty(),
565 self.ts_accepted().unwrap_or_else(|| self.ts_last()),
566 self.ts_last(),
567 self.ts_init(),
568 report_id,
569 )
570 .with_post_only(self.is_post_only())
571 .with_reduce_only(self.is_reduce_only());
572
573 if let Some(price) = self.price() {
574 report = report.with_price(price);
575 }
576
577 if let Some(activation_price) = self.activation_price() {
578 report = report.with_activation_price(activation_price);
579 }
580
581 if let Some(trigger_price) = self.trigger_price() {
582 report = report.with_trigger_price(trigger_price);
583 }
584
585 if let Some(trigger_type) = self.trigger_type() {
586 report = report.with_trigger_type(trigger_type);
587 }
588
589 if let Some(limit_offset) = self.limit_offset() {
590 report = report.with_limit_offset(limit_offset);
591 }
592
593 if let Some(trailing_offset) = self.trailing_offset() {
594 report = report.with_trailing_offset(trailing_offset);
595 }
596
597 if let Some(trailing_offset_type) = self.trailing_offset_type() {
598 report = report.with_trailing_offset_type(trailing_offset_type);
599 }
600
601 if let Some(display_qty) = self.display_qty() {
602 report = report.with_display_qty(display_qty);
603 }
604
605 if let Some(expire_time) = self.expire_time() {
606 report = report.with_expire_time(expire_time);
607 }
608
609 if let Some(contingency_type) = self.contingency_type() {
610 report = report.with_contingency_type(contingency_type);
611 }
612
613 if let Some(order_list_id) = self.order_list_id() {
614 report = report.with_order_list_id(order_list_id);
615 }
616
617 if let Some(linked_order_ids) = self.linked_order_ids() {
618 report = report.with_linked_order_ids(linked_order_ids.iter().copied());
619 }
620
621 if let Some(parent_order_id) = self.parent_order_id() {
622 report = report.with_parent_order_id(parent_order_id);
623 }
624
625 if let Some(position_id) = self.position_id() {
626 report = report.with_venue_position_id(position_id);
627 }
628
629 let mut ts_triggered = None;
632 let mut rejected_reason = None;
633
634 for event in self.events() {
635 match event {
636 OrderEventAny::Triggered(triggered) => ts_triggered = Some(triggered.ts_event),
637 OrderEventAny::Rejected(rejected) => rejected_reason = Some(rejected.reason),
638 _ => {}
639 }
640 }
641
642 if let Some(ts_triggered) = ts_triggered {
643 report = report.with_ts_triggered(ts_triggered);
644 }
645
646 if let Some(reason) = rejected_reason {
649 report = report.with_cancel_reason(reason.to_string());
650 }
651
652 if let Some(avg_px) = self.avg_px() {
653 report = report.with_avg_px(avg_px);
654 }
655
656 Some(report)
657 }
658
659 fn is_triggered(&self) -> Option<bool>; fn set_position_id(&mut self, position_id: Option<PositionId>);
661 fn set_quantity(&mut self, quantity: Quantity);
662 fn set_leaves_qty(&mut self, leaves_qty: Quantity);
663 fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>);
664 fn set_is_quote_quantity(&mut self, is_quote_quantity: bool);
665 fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide);
666 fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool;
667 fn previous_status(&self) -> Option<OrderStatus>;
668}
669
670impl<T> From<&T> for OrderInitialized
671where
672 T: Order,
673{
674 fn from(order: &T) -> Self {
675 Self {
676 trader_id: order.trader_id(),
677 strategy_id: order.strategy_id(),
678 instrument_id: order.instrument_id(),
679 client_order_id: order.client_order_id(),
680 order_side: order.order_side(),
681 order_type: order.order_type(),
682 quantity: order.quantity(),
683 price: order.price(),
684 activation_price: order.activation_price(),
685 trigger_price: order.trigger_price(),
686 trigger_type: order.trigger_type(),
687 time_in_force: order.time_in_force(),
688 expire_time: order.expire_time(),
689 post_only: order.is_post_only(),
690 reduce_only: order.is_reduce_only(),
691 quote_quantity: order.is_quote_quantity(),
692 display_qty: order.display_qty(),
693 limit_offset: order.limit_offset(),
694 trailing_offset: order.trailing_offset(),
695 trailing_offset_type: order.trailing_offset_type(),
696 emulation_trigger: order.emulation_trigger(),
697 trigger_instrument_id: order.trigger_instrument_id(),
698 contingency_type: order.contingency_type(),
699 order_list_id: order.order_list_id(),
700 linked_order_ids: order.linked_order_ids().map(<[ClientOrderId]>::to_vec),
701 parent_order_id: order.parent_order_id(),
702 exec_algorithm_id: order.exec_algorithm_id(),
703 exec_algorithm_params: order.exec_algorithm_params().map(ToOwned::to_owned),
704 exec_spawn_id: order.exec_spawn_id(),
705 tags: order.tags().map(<[Ustr]>::to_vec),
706 event_id: order.init_id(),
707 ts_event: order.ts_init(),
708 ts_init: order.ts_init(),
709 reconciliation: false,
710 causation_id: None,
711 }
712 }
713}
714
715#[derive(Clone, Debug, Serialize, Deserialize)]
721pub struct OrderCore {
722 events: Vec<OrderEventAny>,
723 #[serde(skip)]
724 fill_totals: Option<FillTotals>,
725 pub commissions: IndexMap<Currency, Money>,
726 pub venue_order_ids: Vec<VenueOrderId>,
727 pub trade_ids: Vec<TradeId>,
728 pub previous_status: Option<OrderStatus>,
729 pub status: OrderStatus,
730 pub trader_id: TraderId,
731 pub strategy_id: StrategyId,
732 pub instrument_id: InstrumentId,
733 pub client_order_id: ClientOrderId,
734 pub venue_order_id: Option<VenueOrderId>,
735 pub position_id: Option<PositionId>,
736 pub account_id: Option<AccountId>,
737 pub last_trade_id: Option<TradeId>,
738 pub side: OrderSide,
739 pub order_type: OrderType,
740 pub quantity: Quantity,
741 pub time_in_force: TimeInForce,
742 pub liquidity_side: Option<LiquiditySide>,
743 pub is_reduce_only: bool,
744 pub is_quote_quantity: bool,
745 #[serde(default, with = "crate::enums::serde_option_trigger_type")]
746 pub emulation_trigger: Option<TriggerType>,
747 #[serde(default, with = "crate::enums::serde_option_contingency_type")]
748 pub contingency_type: Option<ContingencyType>,
749 pub order_list_id: Option<OrderListId>,
750 pub linked_order_ids: Option<Vec<ClientOrderId>>,
751 pub parent_order_id: Option<ClientOrderId>,
752 pub exec_algorithm_id: Option<ExecAlgorithmId>,
753 pub exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
754 pub exec_spawn_id: Option<ClientOrderId>,
755 pub tags: Option<Vec<Ustr>>,
756 pub filled_qty: Quantity,
757 #[serde(default)]
758 pub voided_qty: Quantity,
759 #[serde(default)]
762 pub non_reopened_voided_qty: Quantity,
763 pub leaves_qty: Quantity,
764 pub overfill_qty: Quantity,
765 pub avg_px: Option<Decimal>,
766 pub slippage: Option<Decimal>,
767 pub init_id: UUID4,
768 pub ts_init: UnixNanos,
769 pub ts_submitted: Option<UnixNanos>,
770 pub ts_accepted: Option<UnixNanos>,
771 pub ts_closed: Option<UnixNanos>,
772 pub ts_last: UnixNanos,
773}
774
775impl OrderCore {
776 #[must_use]
778 pub fn new(init: OrderInitialized) -> Self {
779 let events: Vec<OrderEventAny> = vec![OrderEventAny::Initialized(init.clone())];
780 Self {
781 events,
782 fill_totals: Some(FillTotals::default()),
783 commissions: IndexMap::new(),
784 venue_order_ids: Vec::new(),
785 trade_ids: Vec::new(),
786 previous_status: None,
787 status: OrderStatus::Initialized,
788 trader_id: init.trader_id,
789 strategy_id: init.strategy_id,
790 instrument_id: init.instrument_id,
791 client_order_id: init.client_order_id,
792 venue_order_id: None,
793 position_id: None,
794 account_id: None,
795 last_trade_id: None,
796 side: init.order_side,
797 order_type: init.order_type,
798 quantity: init.quantity,
799 time_in_force: init.time_in_force,
800 liquidity_side: Some(LiquiditySide::NoLiquiditySide),
801 is_reduce_only: init.reduce_only,
802 is_quote_quantity: init.quote_quantity,
803 emulation_trigger: init.emulation_trigger,
804 contingency_type: init.contingency_type,
805 order_list_id: init.order_list_id,
806 linked_order_ids: init.linked_order_ids,
807 parent_order_id: init.parent_order_id,
808 exec_algorithm_id: init.exec_algorithm_id,
809 exec_algorithm_params: init.exec_algorithm_params,
810 exec_spawn_id: init.exec_spawn_id,
811 tags: init.tags,
812 filled_qty: Quantity::zero(init.quantity.precision),
813 voided_qty: Quantity::zero(init.quantity.precision),
814 non_reopened_voided_qty: Quantity::zero(init.quantity.precision),
815 leaves_qty: init.quantity,
816 overfill_qty: Quantity::zero(init.quantity.precision),
817 avg_px: None,
818 slippage: None,
819 init_id: init.event_id,
820 ts_init: init.ts_event,
821 ts_submitted: None,
822 ts_accepted: None,
823 ts_closed: None,
824 ts_last: init.ts_event,
825 }
826 }
827
828 pub fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
837 if self.client_order_id != event.client_order_id() {
838 return Err(CorrectnessError::PredicateViolation {
839 message: format!(
840 "Event client_order_id {} does not match order client_order_id {}",
841 event.client_order_id(),
842 self.client_order_id
843 ),
844 }
845 .into());
846 }
847
848 if self.strategy_id != event.strategy_id() {
849 return Err(CorrectnessError::PredicateViolation {
850 message: format!(
851 "Event strategy_id {} does not match order strategy_id {}",
852 event.strategy_id(),
853 self.strategy_id
854 ),
855 }
856 .into());
857 }
858
859 if let OrderEventAny::FillVoided(event) = &event {
860 self.validate_fill_void(event)?;
861 }
862
863 let mut has_prior_fill_void = false;
864
865 if let OrderEventAny::Filled(fill) = &event {
867 has_prior_fill_void = self.has_prior_fill_void(fill.trade_id);
868 if self.is_price_restatement_fill(fill)? {
869 self.restate_fill_price(event);
870 return Ok(());
871 }
872 }
873
874 if matches!(event, OrderEventAny::Triggered(_))
875 && !TRIGGERABLE_ORDER_TYPES.contains(&self.order_type)
876 {
877 return Err(OrderError::InvalidOrderEvent);
878 }
879
880 match &event {
881 OrderEventAny::Initialized(_) => return Err(OrderError::AlreadyInitialized),
882
883 OrderEventAny::Expired(_)
885 if matches!(self.status, OrderStatus::Expired | OrderStatus::Filled)
886 && self
887 .events
888 .iter()
889 .rposition(|event| matches!(event, OrderEventAny::Expired(_)))
890 .is_some_and(|index| {
891 self.events[index + 1..]
892 .iter()
893 .any(|event| matches!(event, OrderEventAny::Filled(_)))
894 }) =>
895 {
896 self.events.push(event);
898 return Ok(());
899 }
900 _ => {}
901 }
902
903 let is_reclose_after_fill = self.status == OrderStatus::Canceled
904 && matches!(event, OrderEventAny::Canceled(_))
905 && self
906 .events
907 .iter()
908 .rev()
909 .take_while(|event| !matches!(event, OrderEventAny::Canceled(_)))
910 .any(|event| matches!(event, OrderEventAny::Filled(_)));
911
912 let new_status = if is_reclose_after_fill {
913 OrderStatus::Canceled
914 } else {
915 self.status.transition(&event)?
916 };
917
918 if let OrderEventAny::Filled(fill) = &event
919 && self.filled_qty.checked_add(fill.last_qty).is_none()
920 {
921 return Err(CorrectnessError::PredicateViolation {
922 message: format!(
923 "filled quantity overflowed Quantity raw bounds: {} + {}",
924 self.filled_qty, fill.last_qty
925 ),
926 }
927 .into());
928 }
929
930 if let OrderEventAny::Filled(fill) = &event
931 && let Some(commission) = fill.commission
932 && let Some(total) = self.commissions.get(&commission.currency)
933 && total.checked_add(commission).is_none()
934 {
935 return Err(CorrectnessError::PredicateViolation {
936 message: format!(
937 "cumulative commission overflowed Money bounds: {total} + {commission}"
938 ),
939 }
940 .into());
941 }
942
943 let rejection_status = if matches!(
944 (&event, self.status),
945 (OrderEventAny::ModifyRejected(_), OrderStatus::PendingUpdate)
946 | (OrderEventAny::CancelRejected(_), OrderStatus::PendingCancel)
947 ) {
948 self.previous_status.ok_or(OrderError::NoPreviousState)?
949 } else {
950 self.status
951 };
952
953 if let OrderEventAny::Updated(update) = &event {
957 let invalid_fields = match self.order_type {
958 OrderType::Market => update.price.is_some() || update.trigger_price.is_some(),
959 OrderType::StopMarket
960 | OrderType::MarketIfTouched
961 | OrderType::TrailingStopMarket => update.price.is_some(),
962 OrderType::Limit | OrderType::MarketToLimit => update.trigger_price.is_some(),
963 OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit => {
964 false
965 }
966 };
967
968 if invalid_fields {
969 return Err(OrderError::InvalidOrderEvent);
970 }
971 }
972
973 let source_status = self.status;
974
975 if !matches!(
980 event,
981 OrderEventAny::Initialized(_)
982 | OrderEventAny::ModifyRejected(_)
983 | OrderEventAny::CancelRejected(_)
984 ) && !matches!(
985 self.status,
986 OrderStatus::PendingUpdate | OrderStatus::PendingCancel
987 ) {
988 self.previous_status = Some(self.status);
989 }
990
991 self.status = new_status;
992
993 match &event {
994 OrderEventAny::Initialized(_) => return Err(OrderError::AlreadyInitialized),
996 OrderEventAny::Denied(event) => self.denied(event),
997 OrderEventAny::Emulated(event) => self.emulated(event),
998 OrderEventAny::Released(event) => self.released(event),
999 OrderEventAny::Submitted(event) => self.submitted(event),
1000 OrderEventAny::Rejected(event) => self.rejected(event),
1001 OrderEventAny::Accepted(event) => self.accepted(event),
1002 OrderEventAny::PendingUpdate(event) => self.pending_update(event),
1003 OrderEventAny::PendingCancel(event) => self.pending_cancel(event),
1004 OrderEventAny::ModifyRejected(event) => self.modify_rejected(event, rejection_status),
1005 OrderEventAny::CancelRejected(event) => self.cancel_rejected(event, rejection_status),
1006 OrderEventAny::Updated(event) => self.updated(event),
1007 OrderEventAny::Triggered(event) => self.triggered(event),
1008 OrderEventAny::Canceled(event) => self.canceled(event),
1009 OrderEventAny::Expired(event) => self.expired(event),
1010 OrderEventAny::Filled(event) => self.filled(event, source_status),
1011 OrderEventAny::FillVoided(event) => self.fill_voided(event, source_status),
1012 }
1013
1014 if has_prior_fill_void {
1015 self.fill_totals = None;
1018 }
1019
1020 self.ts_last = event.ts_event();
1021 self.events.push(event);
1022 Ok(())
1023 }
1024
1025 fn denied(&mut self, event: &OrderDenied) {
1026 self.ts_closed = Some(event.ts_event);
1027 }
1028
1029 fn emulated(&self, _event: &OrderEmulated) {
1030 }
1032
1033 fn released(&mut self, _event: &OrderReleased) {
1034 self.emulation_trigger = None;
1035 }
1036
1037 fn submitted(&mut self, event: &OrderSubmitted) {
1038 self.account_id = Some(event.account_id);
1039 self.ts_submitted = Some(event.ts_event);
1040 }
1041
1042 fn accepted(&mut self, event: &OrderAccepted) {
1043 self.account_id = Some(event.account_id);
1044 self.venue_order_id = Some(event.venue_order_id);
1045 self.venue_order_ids.push(event.venue_order_id);
1046 self.ts_accepted = Some(event.ts_event);
1047 }
1048
1049 fn rejected(&mut self, event: &OrderRejected) {
1050 self.ts_closed = Some(event.ts_event);
1051 }
1052
1053 fn pending_update(&self, _event: &OrderPendingUpdate) {
1054 }
1056
1057 fn pending_cancel(&self, _event: &OrderPendingCancel) {
1058 }
1060
1061 fn modify_rejected(&mut self, _event: &OrderModifyRejected, rejection_status: OrderStatus) {
1062 if self.status == OrderStatus::PendingUpdate {
1063 self.status = rejection_status;
1064 }
1065 }
1066
1067 fn cancel_rejected(&mut self, _event: &OrderCancelRejected, previous_status: OrderStatus) {
1068 self.status = previous_status;
1069 }
1070
1071 fn triggered(&self, _event: &OrderTriggered) {}
1072
1073 fn canceled(&mut self, event: &OrderCanceled) {
1074 self.ts_closed = Some(event.ts_event);
1075 }
1076
1077 fn expired(&mut self, event: &OrderExpired) {
1078 self.ts_closed = Some(event.ts_event);
1079 }
1080
1081 fn validate_fill_void(&self, event: &OrderFillVoided) -> Result<(), OrderError> {
1082 let fill = self.events.iter().find_map(|candidate| match candidate {
1083 OrderEventAny::Filled(fill) if fill.trade_id == event.trade_id => Some(fill),
1084 _ => None,
1085 });
1086
1087 if event.voided_qty.is_zero() {
1088 return Err(OrderError::OverVoid(event.trade_id));
1089 }
1090
1091 let previous = self.latest_fill_void(event.trade_id);
1092 if let Some(previous) = previous {
1093 if event.voided_qty < previous.voided_qty {
1094 return Err(OrderError::StaleFillVoid(event.trade_id));
1095 }
1096
1097 if event.voided_qty == previous.voided_qty
1098 && event.commission_voided == previous.commission_voided
1099 && event.is_reopened == previous.is_reopened
1100 {
1101 return Err(OrderError::DuplicateFillVoid(event.trade_id));
1102 }
1103 }
1104
1105 let Some(fill) = fill else {
1106 if event.is_reopened {
1107 return Err(OrderError::InvalidOrderEvent);
1108 }
1109
1110 if !self.matches_fill_void_identity(event) {
1111 return Err(OrderError::InvalidOrderEvent);
1112 }
1113
1114 if event.voided_qty > self.quantity
1115 || event
1116 .commission_voided
1117 .is_some_and(|commission| !commission.is_zero())
1118 {
1119 return Err(OrderError::OverVoid(event.trade_id));
1120 }
1121 return Ok(());
1122 };
1123
1124 if fill.venue_order_id != event.venue_order_id
1125 || fill.account_id != event.account_id
1126 || fill.order_side != event.order_side
1127 || fill.order_type != event.order_type
1128 || fill.last_px != event.last_px
1129 || fill.currency != event.currency
1130 || fill.liquidity_side != event.liquidity_side
1131 || fill.position_id != event.position_id
1132 {
1133 return Err(OrderError::InvalidOrderEvent);
1134 }
1135
1136 if event.voided_qty > fill.last_qty {
1137 return Err(OrderError::OverVoid(event.trade_id));
1138 }
1139
1140 self.validate_fill_void_commission(fill, event, previous)
1141 }
1142
1143 fn matches_fill_void_identity(&self, event: &OrderFillVoided) -> bool {
1144 event.trader_id == self.trader_id
1145 && event.instrument_id == self.instrument_id
1146 && event.order_side == self.side
1147 && event.order_type == self.order_type
1148 && (self.venue_order_id == Some(event.venue_order_id)
1149 || self.venue_order_ids.contains(&event.venue_order_id))
1150 && self.account_id == Some(event.account_id)
1151 }
1152
1153 fn validate_fill_void_commission(
1154 &self,
1155 fill: &OrderFilled,
1156 event: &OrderFillVoided,
1157 previous: Option<&OrderFillVoided>,
1158 ) -> Result<(), OrderError> {
1159 match (fill.commission, event.commission_voided) {
1160 (None, None) => Ok(()),
1161 (None, Some(commission)) if commission.is_zero() => Ok(()),
1162 (Some(_), None) if previous.and_then(|prior| prior.commission_voided).is_none() => {
1163 Ok(())
1164 }
1165 (Some(commission), Some(voided))
1166 if commission.currency == voided.currency
1167 && commission.is_positive() == voided.is_positive()
1168 && commission.is_negative() == voided.is_negative()
1169 && voided.abs() <= commission.abs()
1170 && previous
1171 .and_then(|prior| prior.commission_voided)
1172 .is_none_or(|prior| voided.abs() >= prior.abs()) =>
1173 {
1174 Ok(())
1175 }
1176 _ => Err(OrderError::InvalidFillVoidCommission(event.trade_id)),
1177 }
1178 }
1179
1180 fn latest_fill_void(&self, trade_id: TradeId) -> Option<&OrderFillVoided> {
1181 self.events
1182 .iter()
1183 .rev()
1184 .find_map(|candidate| match candidate {
1185 OrderEventAny::FillVoided(event) if event.trade_id == trade_id => Some(event),
1186 _ => None,
1187 })
1188 }
1189
1190 fn fill_voided(&mut self, event: &OrderFillVoided, source_status: OrderStatus) {
1191 let has_local_fill = self.events.iter().any(
1192 |candidate| matches!(candidate, OrderEventAny::Filled(fill) if fill.trade_id == event.trade_id),
1193 );
1194 let non_reopened_voided_qty = self.recompute_fill_state(Some(event));
1195 let unfilled_qty = self.quantity.saturating_sub(self.filled_qty);
1196 let working_leaves = unfilled_qty.saturating_sub(non_reopened_voided_qty);
1197
1198 match source_status {
1199 OrderStatus::Voided => {
1200 self.status = OrderStatus::Voided;
1201 self.leaves_qty = Quantity::zero(self.quantity.precision);
1202 }
1203 _ if !has_local_fill && !event.is_reopened => {
1204 self.status = OrderStatus::Voided;
1205 self.leaves_qty = Quantity::zero(self.quantity.precision);
1206 }
1207 OrderStatus::Canceled | OrderStatus::Expired => {
1208 self.status = source_status;
1209 self.leaves_qty = unfilled_qty;
1210 }
1211 _ if working_leaves.is_zero() => {
1212 self.status = OrderStatus::Voided;
1213 self.leaves_qty = working_leaves;
1214 }
1215 OrderStatus::PendingUpdate | OrderStatus::PendingCancel | OrderStatus::Triggered => {
1216 self.status = source_status;
1217 self.leaves_qty = working_leaves;
1218 }
1219 _ => {
1220 self.status = if self.filled_qty.is_zero() {
1221 OrderStatus::Accepted
1222 } else {
1223 OrderStatus::PartiallyFilled
1224 };
1225 self.leaves_qty = working_leaves;
1226 }
1227 }
1228
1229 if self.status.is_open() {
1230 self.ts_closed = None;
1231 } else if self.status.is_closed() {
1232 self.ts_closed = self.ts_closed.or(Some(event.ts_event));
1233 }
1234 }
1235
1236 fn recompute_fill_state(&mut self, additional: Option<&OrderFillVoided>) -> Quantity {
1237 let corrections = self.fill_corrections(additional);
1238
1239 let mut totals = FillTotals::default();
1240 let mut filled = Quantity::zero(self.quantity.precision);
1241 let mut voided = Quantity::zero(self.quantity.precision);
1242 let mut non_reopened_voided = Quantity::zero(self.quantity.precision);
1243
1244 let mut commissions = IndexMap::<Currency, Money>::new();
1245 let mut trade_ids = Vec::new();
1246 let mut last_trade_id = None;
1247 let mut position_id = None;
1248 let mut liquidity_side = Some(LiquiditySide::NoLiquiditySide);
1249 let mut matched_corrections = AHashSet::new();
1250 let prices = self.accounting_prices();
1251
1252 for fill in self.quantity_fills() {
1253 let correction = corrections.get(&fill.trade_id).copied();
1254 if correction.is_some() {
1255 matched_corrections.insert(fill.trade_id);
1256 }
1257 let removed = Self::removed_fill_qty(fill, correction);
1258 let effective = fill.last_qty - removed;
1259 voided = voided.saturating_add(removed);
1260 if correction.is_some_and(|event| !event.is_reopened) {
1261 non_reopened_voided = non_reopened_voided.saturating_add(removed);
1262 }
1263
1264 if !effective.is_zero() {
1265 let price = prices
1266 .get(&fill.trade_id)
1267 .copied()
1268 .unwrap_or(fill.last_px.as_decimal());
1269 totals.add(effective.as_decimal(), price);
1270 filled = filled.saturating_add(effective);
1271 trade_ids.push(fill.trade_id);
1272 last_trade_id = Some(fill.trade_id);
1273 position_id = fill.position_id;
1274 liquidity_side = Some(fill.liquidity_side);
1275 }
1276
1277 if let Some(commission) = fill.commission {
1278 let surviving = correction
1279 .and_then(|event| event.commission_voided)
1280 .filter(|voided| !voided.is_zero())
1281 .map_or(commission, |voided| commission - voided);
1282
1283 if !surviving.is_zero() {
1284 commissions
1285 .entry(surviving.currency)
1286 .and_modify(|total| *total = *total + surviving)
1287 .or_insert(surviving);
1288 }
1289 }
1290 }
1291
1292 for (trade_id, correction) in corrections {
1293 if !matched_corrections.contains(&trade_id) {
1294 voided = voided.saturating_add(correction.voided_qty);
1295 }
1296 }
1297
1298 self.filled_qty = filled;
1299 self.voided_qty = voided;
1300 self.non_reopened_voided_qty = non_reopened_voided;
1301 self.overfill_qty = self.filled_qty.saturating_sub(self.quantity);
1302 self.avg_px = totals.average();
1303 self.fill_totals = Some(totals);
1304 self.commissions = commissions;
1305 self.trade_ids = trade_ids;
1306 self.last_trade_id = last_trade_id;
1307 self.position_id = position_id;
1308 self.liquidity_side = liquidity_side;
1309
1310 non_reopened_voided
1311 }
1312
1313 fn updated(&mut self, event: &OrderUpdated) {
1314 if matches!(
1315 self.status,
1316 OrderStatus::PendingUpdate | OrderStatus::PendingCancel,
1317 ) && let Some(previous) = self.previous_status
1318 {
1319 self.status = previous;
1320 }
1321
1322 if event.reconciliation && !self.filled_qty.is_zero() && event.quantity == self.filled_qty {
1323 self.status = OrderStatus::Filled;
1324 self.ts_closed = Some(event.ts_event);
1325 }
1326
1327 if let Some(venue_order_id) = &event.venue_order_id
1328 && (self.venue_order_id.is_none()
1329 || venue_order_id != self.venue_order_id.as_ref().unwrap())
1330 {
1331 self.venue_order_id = Some(*venue_order_id);
1332 self.venue_order_ids.push(*venue_order_id);
1333 }
1334
1335 self.is_quote_quantity = event.is_quote_quantity;
1336 }
1337
1338 pub(crate) fn apply_updated_quantity(&mut self, new_quantity: Quantity) {
1342 self.quantity = new_quantity;
1343 let unfilled_qty = self.quantity.saturating_sub(self.filled_qty);
1344 let voided_qty = self.non_reopened_voided_qty;
1345
1346 self.leaves_qty = if voided_qty > unfilled_qty {
1349 Quantity::zero(unfilled_qty.precision.max(voided_qty.precision))
1350 } else {
1351 unfilled_qty - voided_qty
1352 };
1353 }
1354
1355 fn filled(&mut self, event: &OrderFilled, source_status: OrderStatus) {
1356 let new_filled_qty = self
1357 .filled_qty
1358 .checked_add(event.last_qty)
1359 .expect("fill quantity bounds pre-checked");
1360
1361 if new_filled_qty > self.quantity {
1363 let overfill = new_filled_qty - self.quantity;
1364 self.overfill_qty = self.overfill_qty.saturating_add(overfill);
1365 }
1366
1367 let new_leaves_qty = self.leaves_qty.saturating_sub(event.last_qty);
1368
1369 if new_filled_qty >= self.quantity {
1370 self.status = OrderStatus::Filled;
1371 self.ts_closed = Some(event.ts_event);
1372 } else if new_leaves_qty.is_zero() && !self.voided_qty.is_zero() {
1373 self.status = OrderStatus::Voided;
1374 self.ts_closed = Some(event.ts_event);
1375 } else if matches!(source_status, OrderStatus::Canceled | OrderStatus::Expired) {
1376 self.status = source_status;
1377 } else {
1378 self.status = OrderStatus::PartiallyFilled;
1379
1380 if matches!(
1381 source_status,
1382 OrderStatus::PendingUpdate | OrderStatus::PendingCancel,
1383 ) {
1384 self.previous_status = Some(self.status);
1385 self.status = source_status;
1386 }
1387 }
1388
1389 let is_historical_venue_order_id = self.venue_order_id.is_some_and(|current| {
1390 current != event.venue_order_id && self.venue_order_ids.contains(&event.venue_order_id)
1391 });
1392
1393 if !is_historical_venue_order_id {
1394 self.venue_order_id = Some(event.venue_order_id);
1395 }
1396 self.position_id = event.position_id;
1397 self.trade_ids.push(event.trade_id);
1398 self.last_trade_id = Some(event.trade_id);
1399 self.liquidity_side = Some(event.liquidity_side);
1400 self.filled_qty = new_filled_qty;
1401 self.leaves_qty = new_leaves_qty;
1402 self.ts_last = event.ts_event;
1403
1404 if let Some(commission) = event.commission {
1405 let commission_currency = commission.currency;
1406 if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) {
1407 *existing_commission = *existing_commission + commission;
1408 } else {
1409 self.commissions.insert(commission_currency, commission);
1410 }
1411 }
1412
1413 if self.ts_accepted.is_none() {
1414 self.ts_accepted = Some(event.ts_event);
1416 }
1417
1418 if self.fill_totals.is_none() {
1419 self.fill_totals = Some(self.fill_totals_from_events());
1420 }
1421
1422 let totals = self.fill_totals.as_mut().expect("fill totals initialized");
1423 totals.add(event.last_qty.as_decimal(), event.last_px.as_decimal());
1424 self.avg_px = totals.average();
1425
1426 debug_assert!(
1427 matches!(
1428 self.status,
1429 OrderStatus::PartiallyFilled | OrderStatus::Filled | OrderStatus::Voided
1430 ) || (self.status == source_status
1431 && matches!(
1432 source_status,
1433 OrderStatus::Canceled
1434 | OrderStatus::Expired
1435 | OrderStatus::PendingUpdate
1436 | OrderStatus::PendingCancel
1437 )),
1438 "Invariant: status must reflect the fill or preserve a closed or pending source status after fill handler (status={:?})",
1439 self.status
1440 );
1441 debug_assert!(
1442 self.venue_order_id.is_some()
1443 && self.last_trade_id.is_some()
1444 && !self.trade_ids.is_empty(),
1445 "Invariant: venue_order_id, last_trade_id, and trade_ids must be set after fill"
1446 );
1447 debug_assert!(
1448 self.filled_qty
1449 .saturating_add(self.voided_qty)
1450 .saturating_add(self.leaves_qty)
1451 >= self.quantity,
1452 "Invariant: filled_qty + voided_qty + leaves_qty >= quantity (filled={}, voided={}, leaves={}, quantity={})",
1453 self.filled_qty,
1454 self.voided_qty,
1455 self.leaves_qty,
1456 self.quantity
1457 );
1458 }
1459
1460 fn has_prior_fill_void(&self, trade_id: TradeId) -> bool {
1461 self.events.iter().any(|candidate| {
1462 matches!(
1463 candidate,
1464 OrderEventAny::FillVoided(existing) if existing.trade_id == trade_id
1465 )
1466 })
1467 }
1468
1469 fn is_price_restatement_fill(&self, fill: &OrderFilled) -> Result<bool, OrderError> {
1470 let mut latest = None;
1471
1472 for candidate in &self.events {
1473 let OrderEventAny::Filled(existing) = candidate else {
1474 continue;
1475 };
1476
1477 if existing.trade_id != fill.trade_id {
1478 continue;
1479 }
1480
1481 latest = Some(existing.last_px);
1482 }
1483
1484 match latest {
1485 Some(price) if price == fill.last_px => Err(OrderError::DuplicateFill(fill.trade_id)),
1486 Some(_) => Ok(true),
1487 None => Ok(false),
1488 }
1489 }
1490
1491 fn restate_fill_price(&mut self, event: OrderEventAny) {
1492 self.ts_last = event.ts_event();
1493 self.events.push(event);
1494 let totals = self.fill_totals_from_events();
1495 self.avg_px = totals.average();
1496 self.fill_totals = Some(totals);
1497 }
1498
1499 fn fill_totals_from_events(&self) -> FillTotals {
1500 let corrections = self.fill_corrections(None);
1501 let prices = self.accounting_prices();
1502 let mut totals = FillTotals::default();
1503
1504 for fill in self.quantity_fills() {
1505 let removed = Self::removed_fill_qty(fill, corrections.get(&fill.trade_id).copied());
1506 let effective = (fill.last_qty - removed).as_decimal();
1507 if !effective.is_zero() {
1508 let price = prices
1509 .get(&fill.trade_id)
1510 .copied()
1511 .unwrap_or(fill.last_px.as_decimal());
1512 totals.add(effective, price);
1513 }
1514 }
1515
1516 totals
1517 }
1518
1519 fn quantity_fills(&self) -> Vec<&OrderFilled> {
1520 let mut seen = AHashSet::new();
1521 let mut fills = Vec::new();
1522
1523 for candidate in &self.events {
1524 let OrderEventAny::Filled(fill) = candidate else {
1525 continue;
1526 };
1527
1528 if seen.insert(fill.trade_id) {
1529 fills.push(fill);
1530 }
1531 }
1532
1533 fills
1534 }
1535
1536 fn accounting_prices(&self) -> AHashMap<TradeId, Decimal> {
1537 let mut prices = AHashMap::new();
1538
1539 for candidate in &self.events {
1540 if let OrderEventAny::Filled(fill) = candidate {
1541 prices.insert(fill.trade_id, fill.last_px.as_decimal());
1542 }
1543 }
1544
1545 prices
1546 }
1547
1548 fn fill_corrections<'a>(
1549 &'a self,
1550 additional: Option<&'a OrderFillVoided>,
1551 ) -> IndexMap<TradeId, &'a OrderFillVoided> {
1552 let mut corrections = IndexMap::new();
1553
1554 for candidate in &self.events {
1555 if let OrderEventAny::FillVoided(event) = candidate {
1556 corrections.insert(event.trade_id, event);
1557 }
1558 }
1559
1560 if let Some(event) = additional {
1561 corrections.insert(event.trade_id, event);
1562 }
1563
1564 corrections
1565 }
1566
1567 fn removed_fill_qty(fill: &OrderFilled, correction: Option<&OrderFillVoided>) -> Quantity {
1568 correction.map_or_else(
1569 || Quantity::zero(fill.last_qty.precision),
1570 |event| event.voided_qty.min(fill.last_qty),
1571 )
1572 }
1573
1574 pub fn set_slippage(&mut self, price: Price) {
1575 self.slippage = self.avg_px.and_then(|avg_px| {
1576 let current_price = price.as_decimal();
1577 match self.side {
1578 OrderSide::Buy if avg_px > current_price => avg_px.checked_sub(current_price),
1579 OrderSide::Sell if avg_px < current_price => current_price.checked_sub(avg_px),
1580 _ => None,
1581 }
1582 });
1583 }
1584
1585 #[must_use]
1587 pub fn opposite_side(side: OrderSide) -> OrderSide {
1588 side.opposite()
1589 }
1590
1591 #[must_use]
1593 pub fn closing_side(side: PositionSide) -> Option<OrderSide> {
1594 match side {
1595 PositionSide::Long => Some(OrderSide::Sell),
1596 PositionSide::Short => Some(OrderSide::Buy),
1597 PositionSide::Flat => None,
1598 }
1599 }
1600
1601 #[must_use]
1602 pub fn signed_decimal_qty(&self) -> Decimal {
1603 match self.side {
1604 OrderSide::Buy => self.quantity.as_decimal(),
1605 OrderSide::Sell => -self.quantity.as_decimal(),
1606 }
1607 }
1608
1609 #[must_use]
1610 pub fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
1611 if side == PositionSide::Flat {
1612 return false;
1613 }
1614
1615 match (self.side, side) {
1616 (OrderSide::Buy, PositionSide::Long) => false,
1617 (OrderSide::Buy, PositionSide::Short) => self.leaves_qty <= position_qty,
1618 (OrderSide::Sell, PositionSide::Short) => false,
1619 (OrderSide::Sell, PositionSide::Long) => self.leaves_qty <= position_qty,
1620 (_, PositionSide::Flat) => false,
1621 }
1622 }
1623
1624 #[must_use]
1625 pub fn commission(&self, currency: &Currency) -> Option<Money> {
1626 self.commissions.get(currency).copied()
1627 }
1628
1629 #[must_use]
1630 pub fn commissions(&self) -> IndexMap<Currency, Money> {
1631 self.commissions.clone()
1632 }
1633
1634 #[must_use]
1635 pub fn commissions_vec(&self) -> Vec<Money> {
1636 self.commissions.values().copied().collect()
1637 }
1638
1639 #[must_use]
1641 pub fn events(&self) -> &[OrderEventAny] {
1642 &self.events
1643 }
1644
1645 pub fn prepend_events(&mut self, events: impl IntoIterator<Item = OrderEventAny>) {
1650 self.fill_totals = None;
1651 self.events.splice(..0, events);
1652 }
1653
1654 #[must_use]
1655 pub fn init_event(&self) -> Option<OrderEventAny> {
1656 self.events.first().cloned()
1657 }
1658}
1659
1660#[derive(Clone, Debug, Default)]
1663struct FillTotals {
1664 notional: Decimal,
1665 quantity: Decimal,
1666}
1667
1668impl FillTotals {
1669 fn add(&mut self, quantity: Decimal, price: Decimal) {
1670 self.notional = self.notional.saturating_add(quantity.saturating_mul(price));
1671 self.quantity = self.quantity.saturating_add(quantity);
1672 }
1673
1674 fn average(&self) -> Option<Decimal> {
1675 self.notional.checked_div(self.quantity)
1676 }
1677}
1678
1679#[cfg(test)]
1680mod tests {
1681 use std::str::FromStr;
1682
1683 use rstest::rstest;
1684 use rust_decimal_macros::dec;
1685 use strum::IntoEnumIterator;
1686
1687 use super::*;
1688 use crate::{
1689 enums::{
1690 LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide, TrailingOffsetType,
1691 TriggerType,
1692 },
1693 events::order::spec::{
1694 OrderAcceptedSpec, OrderCancelRejectedSpec, OrderCanceledSpec, OrderDeniedSpec,
1695 OrderExpiredSpec, OrderFillVoidedSpec, OrderFilledSpec, OrderInitializedSpec,
1696 OrderModifyRejectedSpec, OrderPendingCancelSpec, OrderPendingUpdateSpec,
1697 OrderRejectedSpec, OrderSubmittedSpec, OrderTriggeredSpec, OrderUpdatedSpec,
1698 },
1699 identifiers::InstrumentId,
1700 instruments::{CurrencyPair, Instrument, InstrumentAny, stubs::audusd_sim},
1701 orders::{MarketOrder, builder::OrderTestBuilder, stubs::TestOrderStubs},
1702 stubs::TestDefault,
1703 types::{
1704 Price, Quantity,
1705 money::{MONEY_RAW_MAX, MONEY_RAW_MIN, MoneyRaw},
1706 quantity::{QUANTITY_RAW_MAX, QuantityRaw},
1707 },
1708 };
1709
1710 #[rstest]
1721 #[case(OrderSide::Buy, OrderSide::Sell)]
1722 #[case(OrderSide::Sell, OrderSide::Buy)]
1723 fn test_order_opposite_side(#[case] order_side: OrderSide, #[case] expected_side: OrderSide) {
1724 let result = OrderCore::opposite_side(order_side);
1725 assert_eq!(result, expected_side);
1726 }
1727
1728 #[rstest]
1729 #[case(PositionSide::Long, Some(OrderSide::Sell))]
1730 #[case(PositionSide::Short, Some(OrderSide::Buy))]
1731 #[case(PositionSide::Flat, None)]
1732 fn test_closing_side(
1733 #[case] position_side: PositionSide,
1734 #[case] expected_side: Option<OrderSide>,
1735 ) {
1736 let result = OrderCore::closing_side(position_side);
1737 assert_eq!(result, expected_side);
1738 }
1739
1740 #[rstest]
1741 #[case(OrderSide::Buy, dec!(10_000))]
1742 #[case(OrderSide::Sell, dec!(-10_000))]
1743 fn test_signed_decimal_qty(#[case] order_side: OrderSide, #[case] expected: Decimal) {
1744 let order: MarketOrder = OrderInitializedSpec::builder()
1745 .order_side(order_side)
1746 .quantity(Quantity::from(10_000))
1747 .build()
1748 .try_into()
1749 .unwrap();
1750
1751 let result = order.signed_decimal_qty();
1752 assert_eq!(result, expected);
1753 }
1754
1755 #[rustfmt::skip]
1756 #[rstest]
1757 #[case(OrderSide::Buy, Quantity::from(100), PositionSide::Long, Quantity::from(50), false)]
1758 #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(50), true)]
1759 #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(100), true)]
1760 #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)]
1761 #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)]
1762 #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(50), true)]
1763 #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(100), true)]
1764 #[case(OrderSide::Sell, Quantity::from(100), PositionSide::Short, Quantity::from(50), false)]
1765 fn test_would_reduce_only(
1766 #[case] order_side: OrderSide,
1767 #[case] order_qty: Quantity,
1768 #[case] position_side: PositionSide,
1769 #[case] position_qty: Quantity,
1770 #[case] expected: bool,
1771 ) {
1772 let order: MarketOrder = OrderInitializedSpec::builder()
1773 .order_side(order_side)
1774 .quantity(order_qty)
1775 .build()
1776 .try_into()
1777 .unwrap();
1778
1779 assert_eq!(
1780 order.would_reduce_only(position_side, position_qty),
1781 expected
1782 );
1783 }
1784
1785 #[rstest]
1786 fn test_order_state_transition_denied() {
1787 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
1788 let denied = OrderDeniedSpec::builder().build();
1789 let event = OrderEventAny::Denied(denied);
1790
1791 order.apply(event.clone()).unwrap();
1792
1793 assert_eq!(order.status, OrderStatus::Denied);
1794 assert!(order.is_closed());
1795 assert!(!order.is_open());
1796 assert_eq!(order.event_count(), 2);
1797 assert_eq!(order.last_event(), &event);
1798 }
1799
1800 #[rstest]
1801 fn test_order_life_cycle_to_filled() {
1802 let init = OrderInitializedSpec::builder().build();
1803 let submitted = OrderSubmittedSpec::builder().build();
1804 let accepted = OrderAcceptedSpec::builder().build();
1805 let filled = OrderFilledSpec::builder().build();
1806
1807 let mut order: MarketOrder = init.clone().try_into().unwrap();
1808 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1809 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1810 order.apply(OrderEventAny::Filled(filled)).unwrap();
1811
1812 assert_eq!(order.client_order_id, init.client_order_id);
1813 assert_eq!(order.status(), OrderStatus::Filled);
1814 assert_eq!(order.filled_qty(), Quantity::from(100_000));
1815 assert_eq!(order.leaves_qty(), Quantity::from(0));
1816 assert_eq!(order.avg_px(), Some(dec!(1.0)));
1817 assert!(!order.is_open());
1818 assert!(order.is_closed());
1819 assert_eq!(order.commission(&Currency::USD()), None);
1820 assert_eq!(order.commissions(), &IndexMap::new());
1821 }
1822
1823 #[rstest]
1824 fn test_order_life_cycle_fills_with_negative_prices() {
1825 let init = OrderInitializedSpec::builder()
1829 .quantity(Quantity::from(100_000))
1830 .build();
1831 let submitted = OrderSubmittedSpec::builder().build();
1832 let accepted = OrderAcceptedSpec::builder().build();
1833 let fill1 = OrderFilledSpec::builder()
1834 .last_qty(Quantity::from(50_000))
1835 .last_px(Price::from("-5.00000"))
1836 .trade_id(TradeId::from("TRADE-1"))
1837 .build();
1838 let fill2 = OrderFilledSpec::builder()
1839 .last_qty(Quantity::from(50_000))
1840 .last_px(Price::from("-7.00000"))
1841 .trade_id(TradeId::from("TRADE-2"))
1842 .build();
1843
1844 let mut order: MarketOrder = init.try_into().unwrap();
1845 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1846 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1847 order.apply(OrderEventAny::Filled(fill1)).unwrap();
1848 order.apply(OrderEventAny::Filled(fill2)).unwrap();
1849
1850 assert_eq!(order.status(), OrderStatus::Filled);
1851 assert_eq!(order.filled_qty(), Quantity::from(100_000));
1852 assert_eq!(order.leaves_qty(), Quantity::from(0));
1853 assert_eq!(order.avg_px(), Some(dec!(-6.0)));
1855 }
1856
1857 fn fill(trade_id: &str, last_qty: &str, last_px: &str) -> (TradeId, Quantity, Price) {
1858 (
1859 TradeId::from(trade_id),
1860 Quantity::from(last_qty),
1861 Price::from(last_px),
1862 )
1863 }
1864
1865 fn market_order_with_fills(
1866 order_side: OrderSide,
1867 quantity: Quantity,
1868 fills: &[(TradeId, Quantity, Price)],
1869 ) -> MarketOrder {
1870 let mut order: MarketOrder = OrderInitializedSpec::builder()
1871 .order_side(order_side)
1872 .quantity(quantity)
1873 .build()
1874 .try_into()
1875 .unwrap();
1876 order
1877 .apply(OrderEventAny::Accepted(
1878 OrderAcceptedSpec::builder().build(),
1879 ))
1880 .unwrap();
1881
1882 for (trade_id, last_qty, last_px) in fills {
1883 order
1884 .apply(OrderEventAny::Filled(
1885 OrderFilledSpec::builder()
1886 .order_side(order_side)
1887 .trade_id(*trade_id)
1888 .last_qty(*last_qty)
1889 .last_px(*last_px)
1890 .build(),
1891 ))
1892 .unwrap();
1893 }
1894
1895 order
1896 }
1897
1898 #[rstest]
1899 #[case::cloned("clone")]
1900 #[case::snapshot("snapshot")]
1901 #[case::replayed("replay")]
1902 fn test_fill_totals_survive_corrections_and_restoration(#[case] restoration: &str) {
1903 let mut order = market_order_with_fills(OrderSide::Buy, Quantity::from(20), &[]);
1904 let first = OrderFilledSpec::builder()
1905 .trade_id(TradeId::from("T-1"))
1906 .last_qty(Quantity::from(4))
1907 .last_px(Price::from("1.00"))
1908 .build();
1909 let second = OrderFilledSpec::builder()
1910 .trade_id(TradeId::from("T-2"))
1911 .last_qty(Quantity::from(2))
1912 .last_px(Price::from("2.00"))
1913 .build();
1914 let third = OrderFilledSpec::builder()
1915 .trade_id(TradeId::from("T-3"))
1916 .last_qty(Quantity::from(1))
1917 .last_px(Price::from("-3.00"))
1918 .build();
1919 let fourth = OrderFilledSpec::builder()
1920 .trade_id(TradeId::from("T-4"))
1921 .last_qty(Quantity::from(2))
1922 .last_px(Price::from("0.15"))
1923 .build();
1924 let steps = [
1925 (
1926 OrderEventAny::Filled(first.clone()),
1927 4,
1928 0,
1929 16,
1930 Some(dec!(1)),
1931 ),
1932 (
1933 OrderEventAny::Filled(second.clone()),
1934 6,
1935 0,
1936 14,
1937 Some(dec!(1.3333333333333333333333333333)),
1938 ),
1939 (
1940 OrderEventAny::FillVoided(matching_fill_void(&first, Quantity::from(1), None)),
1941 5,
1942 1,
1943 15,
1944 Some(dec!(1.4)),
1945 ),
1946 (
1947 OrderEventAny::FillVoided(matching_fill_void(&first, Quantity::from(3), None)),
1948 3,
1949 3,
1950 17,
1951 Some(dec!(1.6666666666666666666666666667)),
1952 ),
1953 (
1954 OrderEventAny::Filled(third.clone()),
1955 4,
1956 3,
1957 16,
1958 Some(dec!(0.5)),
1959 ),
1960 (
1961 OrderEventAny::FillVoided(matching_fill_void(&first, Quantity::from(4), None)),
1962 3,
1963 4,
1964 17,
1965 Some(dec!(0.3333333333333333333333333333)),
1966 ),
1967 (
1968 OrderEventAny::FillVoided(matching_fill_void(&second, Quantity::from(2), None)),
1969 1,
1970 6,
1971 19,
1972 Some(dec!(-3)),
1973 ),
1974 (
1975 OrderEventAny::FillVoided(matching_fill_void(&third, Quantity::from(1), None)),
1976 0,
1977 7,
1978 20,
1979 None,
1980 ),
1981 (OrderEventAny::Filled(fourth), 2, 7, 18, Some(dec!(0.15))),
1982 ];
1983 let mut expected_events = OrderCore::events(&order).to_vec();
1984
1985 for (event, filled, voided, leaves, average) in steps {
1986 let mut restored = match restoration {
1987 "clone" => order.clone(),
1988 "snapshot" => {
1989 let snapshot = serde_json::to_value(&order).unwrap();
1990 assert!(
1991 !snapshot["core"]
1992 .as_object()
1993 .unwrap()
1994 .contains_key("fill_totals")
1995 );
1996 serde_json::from_value(snapshot).unwrap()
1997 }
1998 "replay" => {
1999 let replayed = OrderAny::from_events(expected_events.clone()).unwrap();
2000
2001 let OrderAny::Market(replayed) = replayed else {
2002 panic!("Expected a market order");
2003 };
2004
2005 replayed
2006 }
2007 _ => unreachable!(),
2008 };
2009
2010 order.apply(event.clone()).unwrap();
2011 restored.apply(event.clone()).unwrap();
2012 expected_events.push(event);
2013
2014 assert_eq!(restored.avg_px(), average);
2015 assert_eq!(restored.filled_qty(), Quantity::from(filled));
2016 assert_eq!(restored.voided_qty(), Quantity::from(voided));
2017 assert_eq!(restored.leaves_qty(), Quantity::from(leaves));
2018 assert_eq!(OrderCore::events(&restored), expected_events);
2019 assert_eq!(
2020 serde_json::to_value(&restored).unwrap(),
2021 serde_json::to_value(&order).unwrap()
2022 );
2023 order = restored;
2024 }
2025 }
2026
2027 #[rstest]
2028 fn test_prepended_events_invalidate_fill_totals_without_applying_state() {
2029 let mut order = market_order_with_fills(
2030 OrderSide::Buy,
2031 Quantity::from(10),
2032 &[fill("T-CURRENT", "1", "2.00")],
2033 );
2034 let historical = [
2035 OrderEventAny::Filled(
2036 OrderFilledSpec::builder()
2037 .trade_id(TradeId::from("T-PAST-1"))
2038 .last_qty(Quantity::from(2))
2039 .last_px(Price::from("3.00"))
2040 .build(),
2041 ),
2042 OrderEventAny::Filled(
2043 OrderFilledSpec::builder()
2044 .trade_id(TradeId::from("T-PAST-2"))
2045 .last_qty(Quantity::from(1))
2046 .last_px(Price::from("4.00"))
2047 .build(),
2048 ),
2049 ];
2050 let mut expected_events = historical.to_vec();
2051 expected_events.extend_from_slice(OrderCore::events(&order));
2052 order.prepend_events(historical);
2053 assert_eq!(OrderCore::events(&order), expected_events);
2054 assert_eq!(order.avg_px(), Some(dec!(2)));
2055 assert_eq!(order.filled_qty(), Quantity::from(1));
2056 assert_eq!(order.leaves_qty(), Quantity::from(9));
2057
2058 let next = OrderEventAny::Filled(
2059 OrderFilledSpec::builder()
2060 .trade_id(TradeId::from("T-NEXT"))
2061 .last_qty(Quantity::from(2))
2062 .last_px(Price::from("3.00"))
2063 .build(),
2064 );
2065 expected_events.push(next.clone());
2066 order.apply(next).unwrap();
2067
2068 assert_eq!(OrderCore::events(&order), expected_events);
2069 assert_eq!(order.avg_px(), Some(dec!(3)));
2070 assert_eq!(order.filled_qty(), Quantity::from(3));
2071 assert_eq!(order.leaves_qty(), Quantity::from(7));
2072 assert_eq!(order.voided_qty(), Quantity::from(0));
2073 }
2074
2075 #[rstest]
2076 fn test_restored_fill_totals_use_history_instead_of_snapshot_average() {
2077 let order = market_order_with_fills(
2078 OrderSide::Buy,
2079 Quantity::from(10),
2080 &[fill("T-1", "1", "0.10"), fill("T-2", "2", "0.20")],
2081 );
2082 let mut snapshot = serde_json::to_value(&order).unwrap();
2083 snapshot["core"]["avg_px"] = serde_json::to_value(dec!(99)).unwrap();
2084 let mut restored: MarketOrder = serde_json::from_value(snapshot).unwrap();
2085 restored
2086 .apply(OrderEventAny::Filled(
2087 OrderFilledSpec::builder()
2088 .trade_id(TradeId::from("T-3"))
2089 .last_qty(Quantity::from(2))
2090 .last_px(Price::from("0.35"))
2091 .build(),
2092 ))
2093 .unwrap();
2094
2095 assert_eq!(restored.avg_px(), Some(dec!(0.24)));
2096 assert_eq!(restored.filled_qty(), Quantity::from(5));
2097 assert_eq!(restored.leaves_qty(), Quantity::from(5));
2098 assert_eq!(restored.voided_qty(), Quantity::from(0));
2099 }
2100
2101 #[rstest]
2102 fn test_restored_unknown_fill_void_applies_after_matching_fill() {
2103 let mut order = market_order_with_fills(OrderSide::Buy, Quantity::from(10), &[]);
2104 let first = OrderFilledSpec::builder()
2105 .trade_id(TradeId::from("T-RESTORED"))
2106 .last_qty(Quantity::from(4))
2107 .last_px(Price::from("1.00"))
2108 .build();
2109 let mut correction = matching_fill_void(&first, Quantity::from(2), None);
2110 correction.is_reopened = false;
2111 order.apply(OrderEventAny::FillVoided(correction)).unwrap();
2112 let mut snapshot = serde_json::to_value(&order).unwrap();
2113 snapshot["core"]["status"] = serde_json::to_value(OrderStatus::Accepted).unwrap();
2114 snapshot["core"]["leaves_qty"] = serde_json::to_value(Quantity::from(8)).unwrap();
2115 let mut restored: MarketOrder = serde_json::from_value(snapshot).unwrap();
2116 restored.apply(OrderEventAny::Filled(first)).unwrap();
2117 assert_eq!(restored.avg_px(), Some(dec!(1)));
2118
2119 restored
2120 .apply(OrderEventAny::Filled(
2121 OrderFilledSpec::builder()
2122 .trade_id(TradeId::from("T-NEXT"))
2123 .last_qty(Quantity::from(2))
2124 .last_px(Price::from("3.00"))
2125 .build(),
2126 ))
2127 .unwrap();
2128
2129 assert_eq!(restored.avg_px(), Some(dec!(2)));
2130 assert_eq!(restored.filled_qty(), Quantity::from(6));
2131 assert_eq!(restored.voided_qty(), Quantity::from(2));
2132 assert_eq!(restored.leaves_qty(), Quantity::from(2));
2133 }
2134
2135 #[rstest]
2136 #[case::positive(Decimal::MAX, Decimal::ONE, Decimal::MAX)]
2137 #[case::negative(Decimal::MIN, -Decimal::ONE, Decimal::MIN)]
2138 fn test_fill_totals_preserve_saturation_order(
2139 #[case] price: Decimal,
2140 #[case] increment: Decimal,
2141 #[case] saturated: Decimal,
2142 ) {
2143 let mut totals = FillTotals::default();
2144 totals.add(dec!(2), price);
2145 totals.add(Decimal::ONE, increment);
2146 totals.add(Decimal::ONE, -increment);
2147
2148 assert_eq!(totals.notional, saturated - increment);
2149 assert_eq!(totals.quantity, dec!(4));
2150 assert_eq!(
2151 totals.average(),
2152 (saturated - increment).checked_div(dec!(4))
2153 );
2154 }
2155
2156 #[rstest]
2157 fn test_fill_totals_preserve_quantity_saturation() {
2158 let mut totals = FillTotals::default();
2159 totals.add(Decimal::MAX, Decimal::ZERO);
2160 totals.add(Decimal::ONE, Decimal::ONE);
2161
2162 assert_eq!(totals.notional, Decimal::ONE);
2163 assert_eq!(totals.quantity, Decimal::MAX);
2164 assert_eq!(totals.average(), Decimal::ONE.checked_div(Decimal::MAX));
2165 }
2166
2167 #[rstest]
2168 #[case(OrderSide::Buy)]
2169 #[case(OrderSide::Sell)]
2170 fn test_avg_px_weighted_average_is_exact(#[case] order_side: OrderSide) {
2171 let order = market_order_with_fills(
2174 order_side,
2175 Quantity::from(6),
2176 &[
2177 fill("TRADE-1", "1", "0.07"),
2178 fill("TRADE-2", "2", "0.13"),
2179 fill("TRADE-3", "3", "0.29"),
2180 ],
2181 );
2182
2183 assert_eq!(order.status(), OrderStatus::Filled);
2184 assert_eq!(order.filled_qty(), Quantity::from(6));
2185 assert_eq!(order.leaves_qty(), Quantity::from(0));
2186 assert_eq!(order.voided_qty(), Quantity::from(0));
2187 assert_eq!(order.avg_px(), Some(dec!(0.2)));
2188 }
2189
2190 #[rstest]
2191 fn test_avg_px_keeps_a_quotient_no_f64_can_hold() {
2192 let order = market_order_with_fills(
2195 OrderSide::Buy,
2196 Quantity::from(3),
2197 &[fill("TRADE-1", "1", "1.00"), fill("TRADE-2", "2", "2.00")],
2198 );
2199
2200 assert_eq!(order.status(), OrderStatus::Filled);
2201 assert_eq!(order.filled_qty(), Quantity::from(3));
2202 assert_eq!(order.avg_px(), Some(dec!(1.6666666666666666666666666667)));
2203 assert_ne!(
2204 order.avg_px(),
2205 Some(Decimal::from_f64_retain(5.0_f64 / 3.0).unwrap())
2206 );
2207 }
2208
2209 #[rstest]
2210 #[case(OrderSide::Buy, "1.07", "1.00", Some(dec!(0.07)))]
2211 #[case(OrderSide::Sell, "0.93", "1.00", Some(dec!(0.07)))]
2212 #[case(OrderSide::Buy, "0.93", "1.00", None)]
2213 #[case(OrderSide::Sell, "1.07", "1.00", None)]
2214 fn test_set_slippage_by_side(
2215 #[case] order_side: OrderSide,
2216 #[case] fill_px: &str,
2217 #[case] reference_px: &str,
2218 #[case] expected: Option<Decimal>,
2219 ) {
2220 let mut order =
2223 market_order_with_fills(order_side, Quantity::from(1), &[fill("T-1", "1", fill_px)]);
2224
2225 order.set_slippage(Price::from(reference_px));
2226
2227 assert_eq!(order.avg_px(), Some(Decimal::from_str(fill_px).unwrap()));
2228 assert_eq!(order.slippage(), expected);
2229 }
2230
2231 #[rstest]
2232 #[case::limit(OrderType::Limit)]
2233 #[case::limit_if_touched(OrderType::LimitIfTouched)]
2234 #[case::market_if_touched(OrderType::MarketIfTouched)]
2235 #[case::market_to_limit(OrderType::MarketToLimit)]
2236 #[case::stop_limit(OrderType::StopLimit)]
2237 #[case::stop_market(OrderType::StopMarket)]
2238 #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
2239 #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
2240 fn test_fill_void_recomputes_slippage(#[case] order_type: OrderType) {
2241 let venue_order_id = VenueOrderId::from("V-SLIPPAGE");
2242 let account_id = AccountId::from("SIM-SLIPPAGE");
2243 let mut order = OrderTestBuilder::new(order_type)
2244 .instrument_id(InstrumentId::from("ETHUSDT-LINEAR.BYBIT"))
2245 .client_order_id(ClientOrderId::from("O-SLIPPAGE"))
2246 .side(OrderSide::Buy)
2247 .quantity(Quantity::from(10))
2248 .price(Price::from("100.00"))
2249 .trigger_price(Price::from("100.00"))
2250 .trigger_type(TriggerType::LastPrice)
2251 .limit_offset(dec!(1))
2252 .trailing_offset(dec!(1))
2253 .trailing_offset_type(TrailingOffsetType::Price)
2254 .build();
2255 let accepted = OrderAcceptedSpec::builder()
2256 .trader_id(order.trader_id())
2257 .strategy_id(order.strategy_id())
2258 .instrument_id(order.instrument_id())
2259 .client_order_id(order.client_order_id())
2260 .venue_order_id(venue_order_id)
2261 .account_id(account_id)
2262 .build();
2263 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2264
2265 if order_type == OrderType::MarketToLimit {
2266 order
2267 .apply(OrderEventAny::Updated(
2268 OrderUpdatedSpec::builder()
2269 .trader_id(order.trader_id())
2270 .strategy_id(order.strategy_id())
2271 .instrument_id(order.instrument_id())
2272 .client_order_id(order.client_order_id())
2273 .quantity(order.quantity())
2274 .venue_order_id(venue_order_id)
2275 .account_id(account_id)
2276 .price(Price::from("100.00"))
2277 .build(),
2278 ))
2279 .unwrap();
2280 }
2281
2282 let fill = |trade_id: &str, last_px: &str| {
2283 OrderFilledSpec::builder()
2284 .trader_id(order.trader_id())
2285 .strategy_id(order.strategy_id())
2286 .instrument_id(order.instrument_id())
2287 .client_order_id(order.client_order_id())
2288 .venue_order_id(venue_order_id)
2289 .account_id(account_id)
2290 .trade_id(TradeId::from(trade_id))
2291 .order_side(order.order_side())
2292 .order_type(order.order_type())
2293 .last_qty(Quantity::from(5))
2294 .last_px(Price::from(last_px))
2295 .build()
2296 };
2297 let unfavorable_fill = fill("T-SLIPPAGE-1", "110.00");
2298 let reference_fill = fill("T-SLIPPAGE-2", "100.00");
2299 order
2300 .apply(OrderEventAny::Filled(unfavorable_fill.clone()))
2301 .unwrap();
2302 order.apply(OrderEventAny::Filled(reference_fill)).unwrap();
2303 assert_eq!(order.avg_px(), Some(dec!(105)));
2304 assert_eq!(order.slippage(), Some(dec!(5)));
2305
2306 let correction = matching_fill_void(&unfavorable_fill, unfavorable_fill.last_qty, None);
2307 let event_count = order.event_count();
2308 order.apply(OrderEventAny::FillVoided(correction)).unwrap();
2309 let replayed =
2310 OrderAny::from_events(order.events().into_iter().cloned().collect()).unwrap();
2311
2312 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2313 assert_eq!(order.filled_qty(), Quantity::from(5));
2314 assert_eq!(order.voided_qty(), Quantity::from(5));
2315 assert_eq!(order.leaves_qty(), Quantity::from(5));
2316 assert_eq!(order.avg_px(), Some(dec!(100)));
2317 assert_eq!(order.slippage(), None);
2318 assert_eq!(order.event_count(), event_count + 1);
2319 assert_eq!(replayed.status(), order.status());
2320 assert_eq!(replayed.filled_qty(), order.filled_qty());
2321 assert_eq!(replayed.voided_qty(), order.voided_qty());
2322 assert_eq!(replayed.leaves_qty(), order.leaves_qty());
2323 assert_eq!(replayed.avg_px(), order.avg_px());
2324 assert_eq!(replayed.slippage(), order.slippage());
2325 }
2326
2327 #[rstest]
2328 fn test_avg_px_invariant_to_fill_arrival_order() {
2329 let mut fills = [
2333 fill("TRADE-1", "50000", "1.00001"),
2334 fill("TRADE-2", "30000", "1.00002"),
2335 fill("TRADE-3", "20000", "1.00003"),
2336 ];
2337
2338 let ascending = market_order_with_fills(OrderSide::Buy, Quantity::from(100_000), &fills);
2339 fills.reverse();
2340 let descending = market_order_with_fills(OrderSide::Buy, Quantity::from(100_000), &fills);
2341
2342 assert_eq!(ascending.avg_px(), Some(dec!(1.000_017)));
2343 assert_eq!(descending.avg_px(), ascending.avg_px());
2344 assert_eq!(descending.filled_qty(), ascending.filled_qty());
2345 }
2346
2347 #[rstest]
2348 fn test_avg_px_after_fill_void_matches_order_without_voided_fill() {
2349 let surviving = [fill("TRADE-1", "1", "0.05"), fill("TRADE-2", "3", "0.15")];
2350 let voided = fill("TRADE-VOIDED", "2", "0.13");
2351
2352 let expected = market_order_with_fills(OrderSide::Buy, Quantity::from(6), &surviving);
2353 let mut corrected = market_order_with_fills(
2354 OrderSide::Buy,
2355 Quantity::from(6),
2356 &[surviving[0], surviving[1], voided],
2357 );
2358 corrected
2359 .apply(OrderEventAny::FillVoided(
2360 OrderFillVoidedSpec::builder()
2361 .trade_id(voided.0)
2362 .voided_qty(voided.1)
2363 .last_px(voided.2)
2364 .build(),
2365 ))
2366 .unwrap();
2367
2368 let replayed =
2369 OrderAny::from_events(corrected.events().into_iter().cloned().collect()).unwrap();
2370
2371 assert_eq!(expected.avg_px(), Some(dec!(0.125)));
2375 assert_eq!(corrected.avg_px(), expected.avg_px());
2376 assert_eq!(replayed.avg_px(), expected.avg_px());
2377 assert_eq!(corrected.status(), OrderStatus::Voided);
2378 assert_eq!(corrected.filled_qty(), Quantity::from(4));
2379 assert_eq!(corrected.voided_qty(), Quantity::from(2));
2380 assert_eq!(corrected.leaves_qty(), Quantity::from(0));
2381 assert_eq!(replayed.filled_qty(), corrected.filled_qty());
2382 assert_eq!(replayed.voided_qty(), corrected.voided_qty());
2383 }
2384
2385 #[rstest]
2386 #[case::lower("1")]
2387 #[case::equal("1.000")]
2388 fn test_fractional_fill_correction_preserves_quantity_precision(#[case] quantity: &str) {
2389 let mut order = market_order_with_fills(
2390 OrderSide::Buy,
2391 Quantity::from(quantity),
2392 &[fill("TRADE-1", "0.750", "1.00")],
2393 );
2394 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2395 assert_eq!(order.filled_qty().as_decimal(), dec!(0.750));
2396 assert_eq!(order.leaves_qty().as_decimal(), dec!(0.250));
2397
2398 order
2399 .apply(OrderEventAny::FillVoided(
2400 OrderFillVoidedSpec::builder()
2401 .trade_id(TradeId::from("TRADE-1"))
2402 .voided_qty(Quantity::from("0.125"))
2403 .last_px(Price::from("1.00"))
2404 .build(),
2405 ))
2406 .unwrap();
2407
2408 assert_eq!(order.filled_qty().as_decimal(), dec!(0.625));
2409 assert_eq!(order.voided_qty().as_decimal(), dec!(0.125));
2410 assert_eq!(order.leaves_qty().as_decimal(), dec!(0.250));
2411 assert_eq!(order.overfill_qty().as_decimal(), dec!(0));
2412 }
2413
2414 #[rstest]
2415 #[case::lower("1")]
2416 #[case::equal("1.000")]
2417 fn test_fractional_overfill_preserves_quantity_precision(
2418 #[case] quantity: &str,
2419 #[values(false, true)] partial_fill_first: bool,
2420 ) {
2421 let fills = if partial_fill_first {
2422 vec![
2423 fill("TRADE-1", "0.750", "1.00"),
2424 fill("TRADE-2", "0.500", "1.00"),
2425 ]
2426 } else {
2427 vec![fill("TRADE-1", "1.250", "1.00")]
2428 };
2429 let order = market_order_with_fills(OrderSide::Buy, Quantity::from(quantity), &fills);
2430
2431 assert_eq!(order.status(), OrderStatus::Filled);
2432 assert_eq!(order.filled_qty().as_decimal(), dec!(1.250));
2433 assert_eq!(order.overfill_qty().as_decimal(), dec!(0.250));
2434 assert_eq!(order.leaves_qty().as_decimal(), dec!(0));
2435 }
2436
2437 #[rstest]
2438 fn test_avg_px_recomputed_from_partially_voided_fill() {
2439 let mut order = market_order_with_fills(
2440 OrderSide::Buy,
2441 Quantity::from(6),
2442 &[fill("TRADE-1", "4", "1.00"), fill("TRADE-2", "2", "1.14")],
2443 );
2444 order
2445 .apply(OrderEventAny::FillVoided(
2446 OrderFillVoidedSpec::builder()
2447 .trade_id(TradeId::from("TRADE-1"))
2448 .voided_qty(Quantity::from(2))
2449 .last_px(Price::from("1.00"))
2450 .build(),
2451 ))
2452 .unwrap();
2453
2454 assert_eq!(order.status(), OrderStatus::Voided);
2457 assert_eq!(order.filled_qty(), Quantity::from(4));
2458 assert_eq!(order.voided_qty(), Quantity::from(2));
2459 assert_eq!(order.leaves_qty(), Quantity::from(0));
2460 assert_eq!(order.avg_px(), Some(dec!(1.07)));
2461 }
2462
2463 #[rstest]
2464 fn test_avg_px_over_reopened_fill_void_and_replacement_fill() {
2465 let mut order = market_order_with_fills(
2466 OrderSide::Buy,
2467 Quantity::from(6),
2468 &[fill("TRADE-1", "4", "1.00")],
2469 );
2470 order
2471 .apply(OrderEventAny::FillVoided(
2472 OrderFillVoidedSpec::builder()
2473 .trade_id(TradeId::from("TRADE-1"))
2474 .voided_qty(Quantity::from(2))
2475 .last_px(Price::from("1.00"))
2476 .is_reopened(true)
2477 .build(),
2478 ))
2479 .unwrap();
2480 order
2481 .apply(OrderEventAny::Filled(
2482 OrderFilledSpec::builder()
2483 .trade_id(TradeId::from("TRADE-2"))
2484 .last_qty(Quantity::from(2))
2485 .last_px(Price::from("1.14"))
2486 .build(),
2487 ))
2488 .unwrap();
2489 order.set_slippage(Price::from("1.00"));
2490
2491 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2495 assert_eq!(order.filled_qty(), Quantity::from(4));
2496 assert_eq!(order.voided_qty(), Quantity::from(2));
2497 assert_eq!(order.leaves_qty(), Quantity::from(2));
2498 assert_eq!(order.avg_px(), Some(dec!(1.07)));
2499 assert_eq!(order.slippage(), Some(dec!(0.07)));
2500 }
2501
2502 #[rstest]
2503 fn test_avg_px_cleared_when_every_fill_is_voided() {
2504 let mut order = market_order_with_fills(
2505 OrderSide::Buy,
2506 Quantity::from(6),
2507 &[fill("TRADE-1", "4", "1.50")],
2508 );
2509 order
2510 .apply(OrderEventAny::FillVoided(
2511 OrderFillVoidedSpec::builder()
2512 .trade_id(TradeId::from("TRADE-1"))
2513 .voided_qty(Quantity::from(4))
2514 .last_px(Price::from("1.50"))
2515 .build(),
2516 ))
2517 .unwrap();
2518
2519 assert_eq!(order.status(), OrderStatus::Accepted);
2520 assert_eq!(order.filled_qty(), Quantity::from(0));
2521 assert_eq!(order.voided_qty(), Quantity::from(4));
2522 assert_eq!(order.leaves_qty(), Quantity::from(2));
2523 assert_eq!(order.avg_px(), None);
2524 }
2525
2526 #[cfg(feature = "high-precision")]
2527 #[rstest]
2528 fn test_avg_px_exact_over_high_precision_quantities() {
2529 let order = market_order_with_fills(
2532 OrderSide::Buy,
2533 Quantity::from("0.000000000010"),
2534 &[
2535 fill("TRADE-1", "0.000000000005", "1.00001"),
2536 fill("TRADE-2", "0.000000000003", "1.00002"),
2537 fill("TRADE-3", "0.000000000002", "1.00003"),
2538 ],
2539 );
2540
2541 assert_eq!(order.status(), OrderStatus::Filled);
2542 assert_eq!(order.filled_qty(), Quantity::from("0.000000000010"));
2543 assert_eq!(order.avg_px(), Some(dec!(1.000_017)));
2544 }
2545
2546 #[cfg(feature = "high-precision")]
2547 #[rstest]
2548 fn test_avg_px_exact_at_the_widest_representable_fill_scale() {
2549 let order = market_order_with_fills(
2552 OrderSide::Buy,
2553 Quantity::from("0.0000000000000008"),
2554 &[
2555 fill("TRADE-1", "0.0000000000000002", "0.000000000001"),
2556 fill("TRADE-2", "0.0000000000000006", "0.000000000003"),
2557 ],
2558 );
2559
2560 assert_eq!(order.status(), OrderStatus::Filled);
2562 assert_eq!(order.filled_qty(), Quantity::from("0.0000000000000008"));
2563 assert_eq!(order.avg_px(), Some(dec!(2.5e-12)));
2564 }
2565
2566 #[rstest]
2567 fn test_order_life_cycle_accumulates_fill_commissions() {
2568 let init = OrderInitializedSpec::builder()
2569 .quantity(Quantity::from(100_000))
2570 .build();
2571 let submitted = OrderSubmittedSpec::builder().build();
2572 let accepted = OrderAcceptedSpec::builder().build();
2573 let fill1 = OrderFilledSpec::builder()
2574 .last_qty(Quantity::from(50_000))
2575 .trade_id(TradeId::from("TRADE-1"))
2576 .commission(Money::from("1.25 USD"))
2577 .build();
2578 let fill2 = OrderFilledSpec::builder()
2579 .last_qty(Quantity::from(50_000))
2580 .trade_id(TradeId::from("TRADE-2"))
2581 .commission(Money::from("1.35 USD"))
2582 .build();
2583
2584 let mut order: MarketOrder = init.try_into().unwrap();
2585 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2586 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2587 order.apply(OrderEventAny::Filled(fill1)).unwrap();
2588 order.apply(OrderEventAny::Filled(fill2)).unwrap();
2589
2590 assert_eq!(order.status(), OrderStatus::Filled);
2591 assert_eq!(
2592 order.commission(&Currency::USD()),
2593 Some(Money::from("2.60 USD"))
2594 );
2595 assert_eq!(order.commissions_vec(), vec![Money::from("2.60 USD")]);
2596 }
2597
2598 #[rstest]
2599 fn test_fill_void_reopens_only_when_explicitly_marked() {
2600 let init = OrderInitializedSpec::builder()
2601 .quantity(Quantity::from(100_000))
2602 .build();
2603 let filled = OrderFilledSpec::builder()
2604 .trade_id(TradeId::from("TRADE-VOID"))
2605 .last_qty(Quantity::from(100_000))
2606 .commission(Money::from("1.00 USD"))
2607 .build();
2608 let fill_voided = OrderFillVoidedSpec::builder()
2609 .trade_id(filled.trade_id)
2610 .voided_qty(Quantity::from(40_000))
2611 .commission_voided(Money::from("0.40 USD"))
2612 .build();
2613 let later_fill = OrderFilledSpec::builder()
2614 .trade_id(TradeId::from("TRADE-AFTER-REOPEN"))
2615 .last_qty(Quantity::from(40_000))
2616 .build();
2617 let mut order: MarketOrder = init.try_into().unwrap();
2618 order
2619 .apply(OrderEventAny::Accepted(
2620 OrderAcceptedSpec::builder().build(),
2621 ))
2622 .unwrap();
2623 order.apply(OrderEventAny::Filled(filled)).unwrap();
2624
2625 let mut terminal = order.clone();
2626 terminal
2627 .apply(OrderEventAny::FillVoided(fill_voided.clone()))
2628 .unwrap();
2629 let fill_while_terminal = terminal.apply(OrderEventAny::Filled(later_fill.clone()));
2630
2631 order
2632 .apply(OrderEventAny::FillVoided(OrderFillVoided {
2633 is_reopened: true,
2634 ..fill_voided
2635 }))
2636 .unwrap();
2637 let status_after_correction = order.status();
2638 order.apply(OrderEventAny::Filled(later_fill)).unwrap();
2639
2640 assert!(matches!(
2641 fill_while_terminal,
2642 Err(OrderError::InvalidStateTransition)
2643 ));
2644 assert_eq!(terminal.status(), OrderStatus::Voided);
2645 assert_eq!(status_after_correction, OrderStatus::PartiallyFilled);
2646 assert_eq!(order.status(), OrderStatus::Filled);
2647 assert_eq!(order.filled_qty(), Quantity::from(100_000));
2648 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2649 assert_eq!(order.leaves_qty(), Quantity::from(0));
2650 assert_eq!(
2651 order.commission(&Currency::USD()),
2652 Some(Money::from("0.60 USD"))
2653 );
2654 }
2655
2656 #[rstest]
2657 fn test_fill_void_preserves_existing_working_remainder() {
2658 let init = OrderInitializedSpec::builder()
2659 .quantity(Quantity::from(100_000))
2660 .build();
2661 let filled = OrderFilledSpec::builder()
2662 .trade_id(TradeId::from("TRADE-VOID"))
2663 .last_qty(Quantity::from(40_000))
2664 .build();
2665 let later_fill = OrderFilledSpec::builder()
2666 .trade_id(TradeId::from("TRADE-REMAINDER"))
2667 .last_qty(Quantity::from(60_000))
2668 .build();
2669 let mut order: MarketOrder = init.try_into().unwrap();
2670 order
2671 .apply(OrderEventAny::Accepted(
2672 OrderAcceptedSpec::builder().build(),
2673 ))
2674 .unwrap();
2675 order.apply(OrderEventAny::Filled(filled.clone())).unwrap();
2676
2677 order
2678 .apply(OrderEventAny::FillVoided(
2679 OrderFillVoidedSpec::builder()
2680 .trade_id(filled.trade_id)
2681 .voided_qty(filled.last_qty)
2682 .build(),
2683 ))
2684 .unwrap();
2685
2686 assert_eq!(order.status(), OrderStatus::Accepted);
2687 assert_eq!(order.filled_qty(), Quantity::from(0));
2688 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2689 assert_eq!(order.leaves_qty(), Quantity::from(60_000));
2690 assert!(order.is_open());
2691
2692 order.apply(OrderEventAny::Filled(later_fill)).unwrap();
2693
2694 assert_eq!(order.status(), OrderStatus::Voided);
2695 assert_eq!(order.filled_qty(), Quantity::from(60_000));
2696 assert_eq!(order.voided_qty(), Quantity::from(40_000));
2697 assert_eq!(order.leaves_qty(), Quantity::from(0));
2698 assert!(order.is_closed());
2699 }
2700
2701 #[rstest]
2702 fn test_order_updated_after_non_reopened_fill_void_keeps_void_out_of_leaves(
2703 #[values(
2704 OrderType::Market,
2705 OrderType::Limit,
2706 OrderType::LimitIfTouched,
2707 OrderType::MarketIfTouched,
2708 OrderType::MarketToLimit,
2709 OrderType::StopLimit,
2710 OrderType::StopMarket,
2711 OrderType::TrailingStopLimit,
2712 OrderType::TrailingStopMarket
2713 )]
2714 order_type: OrderType,
2715 #[values(8, 10, 12)] updated_qty: u64,
2716 ) {
2717 let venue_order_id = VenueOrderId::from("V-VOID-UPDATE");
2718 let account_id = AccountId::from("SIM-VOID-UPDATE");
2719 let mut order = OrderTestBuilder::new(order_type)
2720 .instrument_id(InstrumentId::from("ETHUSDT-LINEAR.BYBIT"))
2721 .client_order_id(ClientOrderId::from("O-VOID-UPDATE"))
2722 .side(OrderSide::Buy)
2723 .quantity(Quantity::from(10))
2724 .price(Price::from("100.00"))
2725 .trigger_price(Price::from("100.00"))
2726 .trigger_type(TriggerType::LastPrice)
2727 .limit_offset(dec!(1))
2728 .trailing_offset(dec!(1))
2729 .trailing_offset_type(TrailingOffsetType::Price)
2730 .build();
2731 order
2732 .apply(OrderEventAny::Accepted(
2733 OrderAcceptedSpec::builder()
2734 .trader_id(order.trader_id())
2735 .strategy_id(order.strategy_id())
2736 .instrument_id(order.instrument_id())
2737 .client_order_id(order.client_order_id())
2738 .venue_order_id(venue_order_id)
2739 .account_id(account_id)
2740 .build(),
2741 ))
2742 .unwrap();
2743 let fill = |trade_id: &str, last_qty: u64| {
2744 OrderFilledSpec::builder()
2745 .trader_id(order.trader_id())
2746 .strategy_id(order.strategy_id())
2747 .instrument_id(order.instrument_id())
2748 .client_order_id(order.client_order_id())
2749 .venue_order_id(venue_order_id)
2750 .account_id(account_id)
2751 .trade_id(TradeId::from(trade_id))
2752 .order_side(order.order_side())
2753 .order_type(order.order_type())
2754 .last_qty(Quantity::from(last_qty))
2755 .last_px(Price::from("100.00"))
2756 .build()
2757 };
2758 let expected_leaves = updated_qty - 4;
2760 let first_fill = fill("T-VOID-UPDATE-1", 4);
2761 let remainder_fill = fill("T-VOID-UPDATE-2", expected_leaves);
2762 order
2763 .apply(OrderEventAny::Filled(first_fill.clone()))
2764 .unwrap();
2765 order
2766 .apply(OrderEventAny::FillVoided(OrderFillVoided {
2767 is_reopened: false,
2768 ..matching_fill_void(&first_fill, Quantity::from(1), None)
2769 }))
2770 .unwrap();
2771 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2772 assert_eq!(order.filled_qty(), Quantity::from(3));
2773 assert_eq!(order.leaves_qty(), Quantity::from(6));
2774
2775 order
2776 .apply(OrderEventAny::Updated(
2777 OrderUpdatedSpec::builder()
2778 .trader_id(order.trader_id())
2779 .strategy_id(order.strategy_id())
2780 .instrument_id(order.instrument_id())
2781 .client_order_id(order.client_order_id())
2782 .quantity(Quantity::from(updated_qty))
2783 .venue_order_id(venue_order_id)
2784 .account_id(account_id)
2785 .build(),
2786 ))
2787 .unwrap();
2788
2789 assert_eq!(order.leaves_qty(), Quantity::from(expected_leaves));
2790
2791 order.apply(OrderEventAny::Filled(remainder_fill)).unwrap();
2792
2793 assert_eq!(order.status(), OrderStatus::Voided);
2794 assert_eq!(order.leaves_qty(), Quantity::from(0));
2795 assert!(order.is_closed());
2796 }
2797
2798 fn accepted_order_with_first_fill() -> (OrderAny, VenueOrderId, AccountId, OrderFilled) {
2801 let venue_order_id = VenueOrderId::from("V-VOID-UPDATE");
2802 let account_id = AccountId::from("SIM-VOID-UPDATE");
2803 let mut order = OrderTestBuilder::new(OrderType::Limit)
2804 .instrument_id(InstrumentId::from("ETHUSDT-LINEAR.BYBIT"))
2805 .client_order_id(ClientOrderId::from("O-VOID-UPDATE"))
2806 .side(OrderSide::Buy)
2807 .quantity(Quantity::from(10))
2808 .price(Price::from("100.00"))
2809 .build();
2810 order
2811 .apply(OrderEventAny::Accepted(
2812 OrderAcceptedSpec::builder()
2813 .trader_id(order.trader_id())
2814 .strategy_id(order.strategy_id())
2815 .instrument_id(order.instrument_id())
2816 .client_order_id(order.client_order_id())
2817 .venue_order_id(venue_order_id)
2818 .account_id(account_id)
2819 .build(),
2820 ))
2821 .unwrap();
2822 let first_fill = OrderFilledSpec::builder()
2823 .trader_id(order.trader_id())
2824 .strategy_id(order.strategy_id())
2825 .instrument_id(order.instrument_id())
2826 .client_order_id(order.client_order_id())
2827 .venue_order_id(venue_order_id)
2828 .account_id(account_id)
2829 .trade_id(TradeId::from("T-VOID-UPDATE-1"))
2830 .order_side(order.order_side())
2831 .order_type(order.order_type())
2832 .last_qty(Quantity::from(4))
2833 .last_px(Price::from("100.00"))
2834 .build();
2835 order
2836 .apply(OrderEventAny::Filled(first_fill.clone()))
2837 .unwrap();
2838 (order, venue_order_id, account_id, first_fill)
2839 }
2840
2841 #[rstest]
2842 fn test_order_updated_after_reopened_fill_void_keeps_void_in_leaves() {
2843 let (mut order, venue_order_id, account_id, first_fill) = accepted_order_with_first_fill();
2844
2845 order
2846 .apply(OrderEventAny::FillVoided(OrderFillVoided {
2847 is_reopened: true,
2848 ..matching_fill_void(&first_fill, Quantity::from(1), None)
2849 }))
2850 .unwrap();
2851 assert_eq!(order.filled_qty(), Quantity::from(3));
2852 assert_eq!(order.non_reopened_voided_qty(), Quantity::from(0));
2853 assert_eq!(order.leaves_qty(), Quantity::from(7));
2855
2856 order
2857 .apply(OrderEventAny::Updated(
2858 OrderUpdatedSpec::builder()
2859 .trader_id(order.trader_id())
2860 .strategy_id(order.strategy_id())
2861 .instrument_id(order.instrument_id())
2862 .client_order_id(order.client_order_id())
2863 .quantity(Quantity::from(12))
2864 .venue_order_id(venue_order_id)
2865 .account_id(account_id)
2866 .build(),
2867 ))
2868 .unwrap();
2869
2870 assert_eq!(order.non_reopened_voided_qty(), Quantity::from(0));
2871 assert_eq!(order.leaves_qty(), Quantity::from(9));
2872 }
2873
2874 #[rstest]
2875 fn test_order_updated_uses_latest_superseding_correction() {
2876 let (mut order, venue_order_id, account_id, first_fill) = accepted_order_with_first_fill();
2877
2878 order
2879 .apply(OrderEventAny::FillVoided(OrderFillVoided {
2880 is_reopened: false,
2881 ..matching_fill_void(&first_fill, Quantity::from(1), None)
2882 }))
2883 .unwrap();
2884 assert_eq!(order.non_reopened_voided_qty(), Quantity::from(1));
2885
2886 order
2889 .apply(OrderEventAny::FillVoided(OrderFillVoided {
2890 is_reopened: false,
2891 ..matching_fill_void(&first_fill, Quantity::from(3), None)
2892 }))
2893 .unwrap();
2894 assert_eq!(order.filled_qty(), Quantity::from(1));
2895 assert_eq!(order.non_reopened_voided_qty(), Quantity::from(3));
2896
2897 order
2898 .apply(OrderEventAny::Updated(
2899 OrderUpdatedSpec::builder()
2900 .trader_id(order.trader_id())
2901 .strategy_id(order.strategy_id())
2902 .instrument_id(order.instrument_id())
2903 .client_order_id(order.client_order_id())
2904 .quantity(Quantity::from(12))
2905 .venue_order_id(venue_order_id)
2906 .account_id(account_id)
2907 .build(),
2908 ))
2909 .unwrap();
2910
2911 assert_eq!(order.leaves_qty(), Quantity::from(8));
2913 }
2914
2915 #[rstest]
2916 fn test_order_updated_to_zero_leaves_sets_no_new_terminal_transition() {
2917 let (mut order, venue_order_id, account_id, first_fill) = accepted_order_with_first_fill();
2918
2919 order
2920 .apply(OrderEventAny::FillVoided(OrderFillVoided {
2921 is_reopened: false,
2922 ..matching_fill_void(&first_fill, Quantity::from(1), None)
2923 }))
2924 .unwrap();
2925 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2926 assert_eq!(order.filled_qty(), Quantity::from(3));
2927 assert_eq!(order.non_reopened_voided_qty(), Quantity::from(1));
2928
2929 order
2932 .apply(OrderEventAny::Updated(
2933 OrderUpdatedSpec::builder()
2934 .trader_id(order.trader_id())
2935 .strategy_id(order.strategy_id())
2936 .instrument_id(order.instrument_id())
2937 .client_order_id(order.client_order_id())
2938 .quantity(Quantity::from(4))
2939 .venue_order_id(venue_order_id)
2940 .account_id(account_id)
2941 .reconciliation(false)
2942 .build(),
2943 ))
2944 .unwrap();
2945
2946 assert_eq!(order.leaves_qty(), Quantity::from(0));
2947 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
2948 assert!(order.ts_closed().is_none());
2949 assert!(!order.is_closed());
2950
2951 order
2953 .apply(OrderEventAny::Canceled(
2954 OrderCanceledSpec::builder()
2955 .trader_id(order.trader_id())
2956 .strategy_id(order.strategy_id())
2957 .instrument_id(order.instrument_id())
2958 .client_order_id(order.client_order_id())
2959 .venue_order_id(venue_order_id)
2960 .account_id(account_id)
2961 .build(),
2962 ))
2963 .unwrap();
2964 assert_eq!(order.status(), OrderStatus::Canceled);
2965 }
2966
2967 #[rstest]
2968 fn test_reconciliation_to_filled_exception_fires_with_non_reopened_void_present() {
2969 let (mut order, venue_order_id, account_id, first_fill) = accepted_order_with_first_fill();
2970
2971 order
2972 .apply(OrderEventAny::FillVoided(OrderFillVoided {
2973 is_reopened: false,
2974 ..matching_fill_void(&first_fill, Quantity::from(1), None)
2975 }))
2976 .unwrap();
2977 assert_eq!(order.filled_qty(), Quantity::from(3));
2978
2979 order
2982 .apply(OrderEventAny::Updated(
2983 OrderUpdatedSpec::builder()
2984 .trader_id(order.trader_id())
2985 .strategy_id(order.strategy_id())
2986 .instrument_id(order.instrument_id())
2987 .client_order_id(order.client_order_id())
2988 .quantity(Quantity::from(3))
2989 .venue_order_id(venue_order_id)
2990 .account_id(account_id)
2991 .reconciliation(true)
2992 .build(),
2993 ))
2994 .unwrap();
2995
2996 assert_eq!(order.status(), OrderStatus::Filled);
2997 assert!(order.ts_closed().is_some());
2998 assert_eq!(order.leaves_qty(), Quantity::from(0));
2999 }
3000
3001 #[rstest]
3002 fn test_order_updated_restores_pending_status_with_non_reopened_void_present() {
3003 let (mut order, venue_order_id, account_id, first_fill) = accepted_order_with_first_fill();
3004
3005 order
3006 .apply(OrderEventAny::FillVoided(OrderFillVoided {
3007 is_reopened: false,
3008 ..matching_fill_void(&first_fill, Quantity::from(1), None)
3009 }))
3010 .unwrap();
3011 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3012
3013 order
3014 .apply(OrderEventAny::PendingUpdate(
3015 OrderPendingUpdateSpec::builder()
3016 .trader_id(order.trader_id())
3017 .strategy_id(order.strategy_id())
3018 .instrument_id(order.instrument_id())
3019 .client_order_id(order.client_order_id())
3020 .account_id(account_id)
3021 .build(),
3022 ))
3023 .unwrap();
3024 assert_eq!(order.status(), OrderStatus::PendingUpdate);
3025
3026 order
3027 .apply(OrderEventAny::Updated(
3028 OrderUpdatedSpec::builder()
3029 .trader_id(order.trader_id())
3030 .strategy_id(order.strategy_id())
3031 .instrument_id(order.instrument_id())
3032 .client_order_id(order.client_order_id())
3033 .quantity(Quantity::from(12))
3034 .venue_order_id(venue_order_id)
3035 .account_id(account_id)
3036 .build(),
3037 ))
3038 .unwrap();
3039
3040 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3042 assert_eq!(order.leaves_qty(), Quantity::from(8));
3043 }
3044
3045 #[rstest]
3046 fn test_order_updated_after_fill_void_replays_to_same_state() {
3047 let (mut order, venue_order_id, account_id, first_fill) = accepted_order_with_first_fill();
3048
3049 order
3050 .apply(OrderEventAny::FillVoided(OrderFillVoided {
3051 is_reopened: false,
3052 ..matching_fill_void(&first_fill, Quantity::from(1), None)
3053 }))
3054 .unwrap();
3055 order
3056 .apply(OrderEventAny::Updated(
3057 OrderUpdatedSpec::builder()
3058 .trader_id(order.trader_id())
3059 .strategy_id(order.strategy_id())
3060 .instrument_id(order.instrument_id())
3061 .client_order_id(order.client_order_id())
3062 .quantity(Quantity::from(12))
3063 .venue_order_id(venue_order_id)
3064 .account_id(account_id)
3065 .build(),
3066 ))
3067 .unwrap();
3068
3069 let replayed =
3070 OrderAny::from_events(order.events().into_iter().cloned().collect()).unwrap();
3071
3072 assert_eq!(replayed.status(), order.status());
3073 assert_eq!(replayed.filled_qty(), order.filled_qty());
3074 assert_eq!(
3075 replayed.non_reopened_voided_qty(),
3076 order.non_reopened_voided_qty()
3077 );
3078 assert_eq!(replayed.leaves_qty(), order.leaves_qty());
3079 assert_eq!(replayed.leaves_qty(), Quantity::from(8));
3080 }
3081
3082 #[rstest]
3083 fn test_canceled_order_with_voided_qty_becomes_voided_when_late_fill_exhausts_leaves() {
3084 let initial_trade_id = TradeId::from("TRADE-VOID");
3085 let late_trade_id = TradeId::from("TRADE-LATE");
3086 let init = OrderInitializedSpec::builder()
3087 .quantity(Quantity::from(100_000))
3088 .build();
3089 let accepted = OrderAcceptedSpec::builder()
3090 .ts_event(UnixNanos::from(1_000))
3091 .build();
3092 let filled = OrderFilledSpec::builder()
3093 .trade_id(initial_trade_id)
3094 .last_qty(Quantity::from(60_000))
3095 .ts_event(UnixNanos::from(2_000))
3096 .build();
3097 let fill_voided = OrderFillVoidedSpec::builder()
3098 .trade_id(initial_trade_id)
3099 .voided_qty(Quantity::from(20_000))
3100 .ts_event(UnixNanos::from(3_000))
3101 .is_reopened(false)
3102 .build();
3103 let canceled = OrderCanceledSpec::builder()
3104 .ts_event(UnixNanos::from(4_000))
3105 .build();
3106 let late_fill = OrderFilledSpec::builder()
3107 .trade_id(late_trade_id)
3108 .last_qty(Quantity::from(40_000))
3109 .ts_event(UnixNanos::from(5_000))
3110 .build();
3111 let mut order: MarketOrder = init.try_into().unwrap();
3112
3113 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3114 order.apply(OrderEventAny::Filled(filled)).unwrap();
3115 order.apply(OrderEventAny::FillVoided(fill_voided)).unwrap();
3116 order.apply(OrderEventAny::Canceled(canceled)).unwrap();
3117
3118 assert_eq!(order.status(), OrderStatus::Canceled);
3119 assert_eq!(order.filled_qty(), Quantity::from(40_000));
3120 assert_eq!(order.voided_qty(), Quantity::from(20_000));
3121 assert_eq!(order.leaves_qty(), Quantity::from(40_000));
3122 assert_eq!(order.ts_closed(), Some(UnixNanos::from(4_000)));
3123 assert_eq!(order.last_trade_id(), Some(initial_trade_id));
3124 assert!(order.is_closed());
3125 assert!(!order.is_open());
3126
3127 order.apply(OrderEventAny::Filled(late_fill)).unwrap();
3128
3129 assert_eq!(order.status(), OrderStatus::Voided);
3130 assert_eq!(order.filled_qty(), Quantity::from(80_000));
3131 assert_eq!(order.voided_qty(), Quantity::from(20_000));
3132 assert_eq!(order.leaves_qty(), Quantity::from(0));
3133 assert_eq!(order.overfill_qty(), Quantity::from(0));
3134 assert_eq!(order.ts_closed(), Some(UnixNanos::from(5_000)));
3135 assert_eq!(order.last_trade_id(), Some(late_trade_id));
3136 assert_eq!(order.trade_ids(), vec![&initial_trade_id, &late_trade_id]);
3137 assert!(order.is_closed());
3138 assert!(!order.is_open());
3139 }
3140
3141 #[rstest]
3142 fn test_reopened_fill_void_requires_existing_fill() {
3143 let trade_id = TradeId::from("TRADE-OUT-OF-ORDER");
3144 let init = OrderInitializedSpec::builder()
3145 .quantity(Quantity::from(100_000))
3146 .build();
3147 let early_void = OrderFillVoidedSpec::builder()
3148 .trade_id(trade_id)
3149 .voided_qty(Quantity::from(10_000))
3150 .is_reopened(true)
3151 .build();
3152 let late_fill = OrderFilledSpec::builder()
3153 .trade_id(trade_id)
3154 .last_qty(Quantity::from(40_000))
3155 .build();
3156 let mut order: MarketOrder = init.try_into().unwrap();
3157 order
3158 .apply(OrderEventAny::Accepted(
3159 OrderAcceptedSpec::builder().build(),
3160 ))
3161 .unwrap();
3162
3163 let result = order.apply(OrderEventAny::FillVoided(early_void));
3164 order.apply(OrderEventAny::Filled(late_fill)).unwrap();
3165
3166 assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
3167 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3168 assert_eq!(order.filled_qty(), Quantity::from(40_000));
3169 assert_eq!(order.voided_qty(), Quantity::from(0));
3170 assert_eq!(order.leaves_qty(), Quantity::from(60_000));
3171 }
3172
3173 #[rstest]
3174 #[case(OrderStatus::Accepted)]
3175 #[case(OrderStatus::Canceled)]
3176 #[case(OrderStatus::Expired)]
3177 fn test_terminal_fill_void_rejects_later_order_events(#[case] status_before_void: OrderStatus) {
3178 let trade_id = TradeId::from("TRADE-TERMINAL-VOID");
3179 let init = OrderInitializedSpec::builder()
3180 .quantity(Quantity::from(100_000))
3181 .build();
3182 let terminal_void = OrderFillVoidedSpec::builder()
3183 .trade_id(trade_id)
3184 .voided_qty(Quantity::from(40_000))
3185 .commission_voided(Money::from("0 USD"))
3186 .build();
3187 let late_fill = OrderFilledSpec::builder()
3188 .trade_id(trade_id)
3189 .last_qty(Quantity::from(40_000))
3190 .build();
3191 let mut order: MarketOrder = init.try_into().unwrap();
3192 order
3193 .apply(OrderEventAny::Accepted(
3194 OrderAcceptedSpec::builder().build(),
3195 ))
3196 .unwrap();
3197
3198 match status_before_void {
3199 OrderStatus::Accepted => {}
3200 OrderStatus::Canceled => order
3201 .apply(OrderEventAny::Canceled(
3202 OrderCanceledSpec::builder().build(),
3203 ))
3204 .unwrap(),
3205 OrderStatus::Expired => order
3206 .apply(OrderEventAny::Expired(OrderExpiredSpec::builder().build()))
3207 .unwrap(),
3208 _ => unreachable!(),
3209 }
3210
3211 order
3212 .apply(OrderEventAny::FillVoided(terminal_void))
3213 .unwrap();
3214 let update_result = order.apply(OrderEventAny::Updated(
3215 OrderUpdatedSpec::builder()
3216 .quantity(Quantity::from(120_000))
3217 .build(),
3218 ));
3219 let cancel_result = order.apply(OrderEventAny::Canceled(
3220 OrderCanceledSpec::builder().build(),
3221 ));
3222 let fill_result = order.apply(OrderEventAny::Filled(late_fill));
3223
3224 assert!(matches!(
3225 update_result,
3226 Err(OrderError::InvalidStateTransition)
3227 ));
3228 assert!(matches!(
3229 cancel_result,
3230 Err(OrderError::InvalidStateTransition)
3231 ));
3232 assert!(matches!(
3233 fill_result,
3234 Err(OrderError::InvalidStateTransition)
3235 ));
3236 assert_eq!(order.status(), OrderStatus::Voided);
3237 assert_eq!(order.filled_qty(), Quantity::from(0));
3238 assert_eq!(order.voided_qty(), Quantity::from(40_000));
3239 assert_eq!(order.leaves_qty(), Quantity::from(0));
3240 }
3241
3242 #[rstest]
3243 #[case(Quantity::from(100_001), None)]
3244 #[case(Quantity::from(100_000), Some(Money::from("1 USD")))]
3245 fn test_terminal_fill_void_rejects_invalid_economic_values(
3246 #[case] voided_qty: Quantity,
3247 #[case] commission_voided: Option<Money>,
3248 ) {
3249 let trade_id = TradeId::from("TRADE-TERMINAL-VOID");
3250 let init = OrderInitializedSpec::builder()
3251 .quantity(Quantity::from(100_000))
3252 .build();
3253 let mut order: MarketOrder = init.try_into().unwrap();
3254 order
3255 .apply(OrderEventAny::Accepted(
3256 OrderAcceptedSpec::builder().build(),
3257 ))
3258 .unwrap();
3259 let mut invalid = OrderFillVoidedSpec::builder()
3260 .trade_id(trade_id)
3261 .voided_qty(voided_qty)
3262 .build();
3263 invalid.commission_voided = commission_voided;
3264
3265 let result = order.apply(OrderEventAny::FillVoided(invalid));
3266
3267 assert!(matches!(
3268 result,
3269 Err(OrderError::OverVoid(error_trade_id)) if error_trade_id == trade_id
3270 ));
3271 assert_eq!(order.status(), OrderStatus::Accepted);
3272 assert_eq!(order.voided_qty(), Quantity::from(0));
3273 }
3274
3275 #[rstest]
3276 fn test_terminal_fill_void_accepts_current_venue_order_id_after_fill_before_accept() {
3277 let init = OrderInitializedSpec::builder()
3278 .quantity(Quantity::from(100_000))
3279 .build();
3280 let submitted = OrderSubmittedSpec::builder().build();
3281 let filled = OrderFilledSpec::builder()
3282 .account_id(submitted.account_id)
3283 .trade_id(TradeId::from("TRADE-APPLIED"))
3284 .last_qty(Quantity::from(40_000))
3285 .build();
3286 let terminal_void = OrderFillVoidedSpec::builder()
3287 .venue_order_id(filled.venue_order_id)
3288 .account_id(filled.account_id)
3289 .trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
3290 .voided_qty(Quantity::from(10_000))
3291 .build();
3292 let mut order: MarketOrder = init.try_into().unwrap();
3293 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3294 order.apply(OrderEventAny::Filled(filled)).unwrap();
3295
3296 order
3297 .apply(OrderEventAny::FillVoided(terminal_void))
3298 .unwrap();
3299
3300 assert_eq!(order.status(), OrderStatus::Voided);
3301 assert_eq!(order.filled_qty(), Quantity::from(40_000));
3302 assert_eq!(order.voided_qty(), Quantity::from(10_000));
3303 assert_eq!(order.leaves_qty(), Quantity::from(0));
3304 }
3305
3306 #[rstest]
3307 fn test_terminal_fill_void_requires_matching_order_identity() {
3308 let init = OrderInitializedSpec::builder()
3309 .quantity(Quantity::from(100_000))
3310 .build();
3311 let mut order: MarketOrder = init.try_into().unwrap();
3312 order
3313 .apply(OrderEventAny::Accepted(
3314 OrderAcceptedSpec::builder().build(),
3315 ))
3316 .unwrap();
3317 let mismatched = OrderFillVoidedSpec::builder()
3318 .instrument_id(InstrumentId::from("OTHER.SIM"))
3319 .trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
3320 .voided_qty(Quantity::from(100_000))
3321 .build();
3322
3323 let result = order.apply(OrderEventAny::FillVoided(mismatched));
3324
3325 assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
3326 assert_eq!(order.status(), OrderStatus::Accepted);
3327 assert_eq!(order.voided_qty(), Quantity::from(0));
3328 }
3329
3330 #[rstest]
3331 #[case(OrderStatus::Canceled)]
3332 #[case(OrderStatus::Expired)]
3333 fn test_fill_void_does_not_reopen_canceled_or_expired_order(
3334 #[case] terminal_status: OrderStatus,
3335 ) {
3336 let init = OrderInitializedSpec::builder()
3337 .quantity(Quantity::from(100_000))
3338 .build();
3339 let filled = OrderFilledSpec::builder()
3340 .trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
3341 .last_qty(Quantity::from(60_000))
3342 .build();
3343 let mut order: MarketOrder = init.try_into().unwrap();
3344 order
3345 .apply(OrderEventAny::Accepted(
3346 OrderAcceptedSpec::builder().build(),
3347 ))
3348 .unwrap();
3349 order.apply(OrderEventAny::Filled(filled.clone())).unwrap();
3350 match terminal_status {
3351 OrderStatus::Canceled => order
3352 .apply(OrderEventAny::Canceled(
3353 OrderCanceledSpec::builder().build(),
3354 ))
3355 .unwrap(),
3356 OrderStatus::Expired => order
3357 .apply(OrderEventAny::Expired(OrderExpiredSpec::builder().build()))
3358 .unwrap(),
3359 _ => unreachable!(),
3360 }
3361 let voided = OrderFillVoidedSpec::builder()
3362 .trade_id(filled.trade_id)
3363 .voided_qty(Quantity::from(20_000))
3364 .is_reopened(true)
3365 .build();
3366
3367 order.apply(OrderEventAny::FillVoided(voided)).unwrap();
3368
3369 assert_eq!(order.status(), terminal_status);
3370 assert_eq!(order.filled_qty(), Quantity::from(40_000));
3371 assert_eq!(order.voided_qty(), Quantity::from(20_000));
3372 assert!(order.is_closed());
3373 assert!(order.ts_closed().is_some());
3374 }
3375
3376 #[rstest]
3377 fn test_fill_void_rejects_duplicate_stale_and_over_void() {
3378 let init = OrderInitializedSpec::builder()
3379 .quantity(Quantity::from(100_000))
3380 .build();
3381 let filled = OrderFilledSpec::builder()
3382 .trade_id(TradeId::from("TRADE-VOID"))
3383 .last_qty(Quantity::from(100_000))
3384 .build();
3385 let mut order: MarketOrder = init.try_into().unwrap();
3386 order
3387 .apply(OrderEventAny::Accepted(
3388 OrderAcceptedSpec::builder().build(),
3389 ))
3390 .unwrap();
3391 order.apply(OrderEventAny::Filled(filled)).unwrap();
3392 order
3393 .apply(OrderEventAny::FillVoided(
3394 OrderFillVoidedSpec::builder()
3395 .trade_id(TradeId::from("TRADE-VOID"))
3396 .voided_qty(Quantity::from(40_000))
3397 .build(),
3398 ))
3399 .unwrap();
3400
3401 let duplicate = order.apply(OrderEventAny::FillVoided(
3402 OrderFillVoidedSpec::builder()
3403 .trade_id(TradeId::from("TRADE-VOID"))
3404 .voided_qty(Quantity::from(40_000))
3405 .build(),
3406 ));
3407 let stale = order.apply(OrderEventAny::FillVoided(
3408 OrderFillVoidedSpec::builder()
3409 .trade_id(TradeId::from("TRADE-VOID"))
3410 .voided_qty(Quantity::from(30_000))
3411 .build(),
3412 ));
3413 let over_void = order.apply(OrderEventAny::FillVoided(
3414 OrderFillVoidedSpec::builder()
3415 .trade_id(TradeId::from("TRADE-VOID"))
3416 .voided_qty(Quantity::from(100_001))
3417 .build(),
3418 ));
3419 assert!(matches!(
3420 duplicate,
3421 Err(OrderError::DuplicateFillVoid(trade_id))
3422 if trade_id == TradeId::from("TRADE-VOID")
3423 ));
3424 assert!(matches!(
3425 stale,
3426 Err(OrderError::StaleFillVoid(trade_id))
3427 if trade_id == TradeId::from("TRADE-VOID")
3428 ));
3429 assert!(matches!(
3430 over_void,
3431 Err(OrderError::OverVoid(trade_id))
3432 if trade_id == TradeId::from("TRADE-VOID")
3433 ));
3434 assert_eq!(order.filled_qty(), Quantity::from(60_000));
3435 assert_eq!(order.voided_qty(), Quantity::from(40_000));
3436 }
3437
3438 enum FillVoidIdentityField {
3439 VenueOrderId,
3440 AccountId,
3441 OrderSide,
3442 OrderType,
3443 LastPx,
3444 Currency,
3445 LiquiditySide,
3446 PositionId,
3447 }
3448
3449 #[rstest]
3450 #[case::venue_order_id(FillVoidIdentityField::VenueOrderId)]
3451 #[case::account_id(FillVoidIdentityField::AccountId)]
3452 #[case::order_side(FillVoidIdentityField::OrderSide)]
3453 #[case::order_type(FillVoidIdentityField::OrderType)]
3454 #[case::last_px(FillVoidIdentityField::LastPx)]
3455 #[case::currency(FillVoidIdentityField::Currency)]
3456 #[case::liquidity_side(FillVoidIdentityField::LiquiditySide)]
3457 #[case::position_id(FillVoidIdentityField::PositionId)]
3458 fn test_fill_void_rejects_known_fill_identity_mismatch(#[case] field: FillVoidIdentityField) {
3459 let (mut order, fill) = filled_market_order_for_void();
3460 let events_before: Vec<_> = order.events().into_iter().cloned().collect();
3461 let mut correction =
3462 matching_fill_void(&fill, Quantity::from(40), Some(Money::from("0.40 USD")));
3463
3464 match field {
3465 FillVoidIdentityField::VenueOrderId => {
3466 correction.venue_order_id = VenueOrderId::from("V-OTHER");
3467 }
3468 FillVoidIdentityField::AccountId => {
3469 correction.account_id = AccountId::from("SIM-OTHER");
3470 }
3471 FillVoidIdentityField::OrderSide => correction.order_side = OrderSide::Sell,
3472 FillVoidIdentityField::OrderType => correction.order_type = OrderType::Limit,
3473 FillVoidIdentityField::LastPx => correction.last_px = Price::from("1.50"),
3474 FillVoidIdentityField::Currency => correction.currency = Currency::EUR(),
3475 FillVoidIdentityField::LiquiditySide => {
3476 correction.liquidity_side = LiquiditySide::Maker;
3477 }
3478 FillVoidIdentityField::PositionId => {
3479 correction.position_id = Some(PositionId::from("P-OTHER"));
3480 }
3481 }
3482
3483 let result = order.apply(OrderEventAny::FillVoided(correction));
3484
3485 assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
3486 assert_fill_void_rejected_without_mutation(&order, &events_before);
3487 }
3488
3489 #[rstest]
3490 #[case::currency("0.40 EUR")]
3491 #[case::sign("-0.40 USD")]
3492 #[case::amount("1.01 USD")]
3493 fn test_fill_void_rejects_invalid_commission_without_mutation(#[case] commission: &str) {
3494 let (mut order, fill) = filled_market_order_for_void();
3495 let events_before: Vec<_> = order.events().into_iter().cloned().collect();
3496 let correction =
3497 matching_fill_void(&fill, Quantity::from(40), Some(Money::from(commission)));
3498
3499 let result = order.apply(OrderEventAny::FillVoided(correction));
3500
3501 assert!(matches!(
3502 result,
3503 Err(OrderError::InvalidFillVoidCommission(trade_id)) if trade_id == fill.trade_id
3504 ));
3505 assert_fill_void_rejected_without_mutation(&order, &events_before);
3506 }
3507
3508 #[rstest]
3509 fn test_fill_void_rejects_zero_quantity_without_mutation() {
3510 let (mut order, fill) = filled_market_order_for_void();
3511 let events_before: Vec<_> = order.events().into_iter().cloned().collect();
3512 let correction = matching_fill_void(
3513 &fill,
3514 Quantity::zero(fill.last_qty.precision),
3515 Some(Money::from("0.00 USD")),
3516 );
3517
3518 let result = order.apply(OrderEventAny::FillVoided(correction));
3519
3520 assert!(matches!(
3521 result,
3522 Err(OrderError::OverVoid(trade_id)) if trade_id == fill.trade_id
3523 ));
3524 assert_fill_void_rejected_without_mutation(&order, &events_before);
3525 }
3526
3527 #[rstest]
3528 fn test_fill_void_uses_latest_cumulative_correction() {
3529 let (mut order, fill) = filled_market_order_for_void();
3530 order
3531 .apply(OrderEventAny::FillVoided(matching_fill_void(
3532 &fill,
3533 Quantity::from(40),
3534 Some(Money::from("0.40 USD")),
3535 )))
3536 .unwrap();
3537 let event_count = order.event_count();
3538
3539 let decreasing_commission = order.apply(OrderEventAny::FillVoided(matching_fill_void(
3540 &fill,
3541 Quantity::from(50),
3542 Some(Money::from("0.30 USD")),
3543 )));
3544
3545 assert!(matches!(
3546 decreasing_commission,
3547 Err(OrderError::InvalidFillVoidCommission(trade_id)) if trade_id == fill.trade_id
3548 ));
3549 assert_eq!(order.event_count(), event_count);
3550 assert_eq!(order.filled_qty(), Quantity::from(60));
3551 assert_eq!(order.voided_qty(), Quantity::from(40));
3552 assert_eq!(
3553 order.commission(&Currency::USD()),
3554 Some(Money::from("0.60 USD")),
3555 );
3556
3557 order
3558 .apply(OrderEventAny::FillVoided(matching_fill_void(
3559 &fill,
3560 Quantity::from(60),
3561 Some(Money::from("0.60 USD")),
3562 )))
3563 .unwrap();
3564 let replayed =
3565 OrderAny::from_events(order.events().into_iter().cloned().collect()).unwrap();
3566
3567 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3568 assert_eq!(order.filled_qty(), Quantity::from(40));
3569 assert_eq!(order.voided_qty(), Quantity::from(60));
3570 assert_eq!(order.leaves_qty(), Quantity::from(60));
3571 assert_eq!(order.avg_px(), Some(dec!(1.25)));
3572 assert_eq!(
3573 order.commission(&Currency::USD()),
3574 Some(Money::from("0.40 USD")),
3575 );
3576 assert_eq!(order.trade_ids(), vec![&fill.trade_id]);
3577 assert_eq!(order.last_trade_id(), Some(fill.trade_id));
3578 assert_eq!(order.event_count(), event_count + 1);
3579 assert_eq!(replayed.status(), order.status());
3580 assert_eq!(replayed.filled_qty(), order.filled_qty());
3581 assert_eq!(replayed.voided_qty(), order.voided_qty());
3582 assert_eq!(replayed.leaves_qty(), order.leaves_qty());
3583 assert_eq!(replayed.avg_px(), order.avg_px());
3584 assert_eq!(replayed.commissions(), order.commissions());
3585 assert_eq!(replayed.trade_ids(), order.trade_ids());
3586 assert_eq!(replayed.last_trade_id(), order.last_trade_id());
3587 }
3588
3589 fn filled_market_order_for_void() -> (MarketOrder, OrderFilled) {
3590 let venue_order_id = VenueOrderId::from("V-FILL-VOID");
3591 let account_id = AccountId::from("SIM-FILL-VOID");
3592 let fill = OrderFilledSpec::builder()
3593 .venue_order_id(venue_order_id)
3594 .account_id(account_id)
3595 .trade_id(TradeId::from("T-FILL-VOID"))
3596 .last_qty(Quantity::from(100))
3597 .last_px(Price::from("1.25"))
3598 .position_id(PositionId::from("P-FILL-VOID"))
3599 .commission(Money::from("1.00 USD"))
3600 .build();
3601 let mut order: MarketOrder = OrderInitializedSpec::builder()
3602 .quantity(Quantity::from(100))
3603 .build()
3604 .try_into()
3605 .unwrap();
3606 order
3607 .apply(OrderEventAny::Accepted(
3608 OrderAcceptedSpec::builder()
3609 .venue_order_id(venue_order_id)
3610 .account_id(account_id)
3611 .build(),
3612 ))
3613 .unwrap();
3614 order.apply(OrderEventAny::Filled(fill.clone())).unwrap();
3615
3616 (order, fill)
3617 }
3618
3619 fn matching_fill_void(
3620 fill: &OrderFilled,
3621 voided_qty: Quantity,
3622 commission_voided: Option<Money>,
3623 ) -> OrderFillVoided {
3624 OrderFillVoidedSpec::builder()
3625 .trader_id(fill.trader_id)
3626 .strategy_id(fill.strategy_id)
3627 .instrument_id(fill.instrument_id)
3628 .client_order_id(fill.client_order_id)
3629 .venue_order_id(fill.venue_order_id)
3630 .account_id(fill.account_id)
3631 .trade_id(fill.trade_id)
3632 .voided_qty(voided_qty)
3633 .order_side(fill.order_side)
3634 .order_type(fill.order_type)
3635 .last_px(fill.last_px)
3636 .currency(fill.currency)
3637 .liquidity_side(fill.liquidity_side)
3638 .maybe_position_id(fill.position_id)
3639 .maybe_commission_voided(commission_voided)
3640 .is_reopened(true)
3641 .build()
3642 }
3643
3644 fn assert_fill_void_rejected_without_mutation(
3645 order: &MarketOrder,
3646 events_before: &[OrderEventAny],
3647 ) {
3648 assert_eq!(order.status(), OrderStatus::Filled);
3649 assert_eq!(order.filled_qty(), Quantity::from(100));
3650 assert_eq!(order.voided_qty(), Quantity::from(0));
3651 assert_eq!(order.leaves_qty(), Quantity::from(0));
3652 assert_eq!(order.avg_px(), Some(dec!(1.25)));
3653 assert_eq!(
3654 order.commission(&Currency::USD()),
3655 Some(Money::from("1.00 USD")),
3656 );
3657 assert_eq!(
3658 order.events().into_iter().cloned().collect::<Vec<_>>(),
3659 events_before,
3660 );
3661 }
3662
3663 #[rstest]
3664 fn test_unknown_fill_void_does_not_reverse_surviving_fill() {
3665 let init = OrderInitializedSpec::builder()
3666 .quantity(Quantity::from(100_000))
3667 .build();
3668 let filled = OrderFilledSpec::builder()
3669 .trade_id(TradeId::from("TRADE-APPLIED"))
3670 .last_qty(Quantity::from(60_000))
3671 .build();
3672 let voided = OrderFillVoidedSpec::builder()
3673 .trade_id(TradeId::from("TRADE-NEVER-APPLIED"))
3674 .voided_qty(Quantity::from(40_000))
3675 .build();
3676 let mut order: MarketOrder = init.try_into().unwrap();
3677 order
3678 .apply(OrderEventAny::Accepted(
3679 OrderAcceptedSpec::builder().build(),
3680 ))
3681 .unwrap();
3682 order.apply(OrderEventAny::Filled(filled)).unwrap();
3683
3684 order.apply(OrderEventAny::FillVoided(voided)).unwrap();
3685 order
3686 .apply(OrderEventAny::FillVoided(
3687 OrderFillVoidedSpec::builder()
3688 .trade_id(TradeId::from("TRADE-APPLIED"))
3689 .voided_qty(Quantity::from(10_000))
3690 .is_reopened(true)
3691 .build(),
3692 ))
3693 .unwrap();
3694 let late_fill = order.apply(OrderEventAny::Filled(
3695 OrderFilledSpec::builder()
3696 .trade_id(TradeId::from("TRADE-LATE"))
3697 .last_qty(Quantity::from(10_000))
3698 .build(),
3699 ));
3700
3701 assert_eq!(order.status(), OrderStatus::Voided);
3702 assert_eq!(order.filled_qty(), Quantity::from(50_000));
3703 assert_eq!(order.voided_qty(), Quantity::from(50_000));
3704 assert_eq!(order.leaves_qty(), Quantity::zero(0));
3705 assert!(order.is_closed());
3706 assert!(matches!(late_fill, Err(OrderError::InvalidStateTransition)));
3707 }
3708
3709 #[rstest]
3710 fn test_order_state_transition_to_canceled() {
3711 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
3712 let submitted = OrderSubmittedSpec::builder().build();
3713 let canceled = OrderCanceledSpec::builder().build();
3714
3715 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3716 order.apply(OrderEventAny::Canceled(canceled)).unwrap();
3717
3718 assert_eq!(order.status(), OrderStatus::Canceled);
3719 assert!(order.is_closed());
3720 assert!(!order.is_open());
3721 }
3722
3723 #[rstest]
3724 fn test_order_life_cycle_to_partially_filled() {
3725 let init = OrderInitializedSpec::builder().build();
3726 let submitted = OrderSubmittedSpec::builder().build();
3727 let accepted = OrderAcceptedSpec::builder().build();
3728 let filled = OrderFilledSpec::builder()
3729 .last_qty(Quantity::from(50_000))
3730 .build();
3731
3732 let mut order: MarketOrder = init.clone().try_into().unwrap();
3733 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3734 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3735 order.apply(OrderEventAny::Filled(filled)).unwrap();
3736
3737 assert_eq!(order.client_order_id, init.client_order_id);
3738 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3739 assert_eq!(order.filled_qty(), Quantity::from(50_000));
3740 assert_eq!(order.leaves_qty(), Quantity::from(50_000));
3741 assert!(order.is_open());
3742 assert!(!order.is_closed());
3743 }
3744
3745 #[rstest]
3746 fn test_order_commission_calculation() {
3747 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
3748 order
3749 .commissions
3750 .insert(Currency::USD(), Money::new(10.0, Currency::USD()));
3751
3752 assert_eq!(
3753 order.commission(&Currency::USD()),
3754 Some(Money::new(10.0, Currency::USD()))
3755 );
3756 assert_eq!(
3757 order.commissions_vec(),
3758 vec![Money::new(10.0, Currency::USD())]
3759 );
3760 }
3761
3762 #[rstest]
3763 fn test_order_is_primary() {
3764 let order: MarketOrder = OrderInitializedSpec::builder()
3765 .exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
3766 .exec_spawn_id(ClientOrderId::from("O-001"))
3767 .client_order_id(ClientOrderId::from("O-001"))
3768 .build()
3769 .try_into()
3770 .unwrap();
3771
3772 assert!(order.is_primary());
3773 assert!(!order.is_spawned());
3774 }
3775
3776 #[rstest]
3777 fn test_order_is_spawned() {
3778 let order: MarketOrder = OrderInitializedSpec::builder()
3779 .exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
3780 .exec_spawn_id(ClientOrderId::from("O-002"))
3781 .client_order_id(ClientOrderId::from("O-001"))
3782 .build()
3783 .try_into()
3784 .unwrap();
3785
3786 assert!(!order.is_primary());
3787 assert!(order.is_spawned());
3788 }
3789
3790 #[rstest]
3791 fn test_order_is_contingency() {
3792 let order: MarketOrder = OrderInitializedSpec::builder()
3793 .contingency_type(ContingencyType::Oto)
3794 .linked_order_ids(vec![ClientOrderId::from("O-LINKED")])
3795 .build()
3796 .try_into()
3797 .unwrap();
3798
3799 assert!(order.is_contingency());
3800 assert!(order.is_parent_order());
3801 assert!(!order.is_child_order());
3802 }
3803
3804 #[rstest]
3805 fn test_order_is_child_order() {
3806 let order: MarketOrder = OrderInitializedSpec::builder()
3807 .parent_order_id(ClientOrderId::from("PARENT-001"))
3808 .build()
3809 .try_into()
3810 .unwrap();
3811
3812 assert!(order.is_child_order());
3813 assert!(!order.is_parent_order());
3814 }
3815
3816 #[rstest]
3817 fn test_to_own_book_order_timestamp_ordering() {
3818 use crate::orders::limit::LimitOrder;
3819
3820 let init = OrderInitializedSpec::builder()
3822 .price(Price::from("100.00"))
3823 .build();
3824 let submitted = OrderSubmittedSpec::builder()
3825 .ts_event(UnixNanos::from(1_000_000))
3826 .build();
3827 let accepted = OrderAcceptedSpec::builder()
3828 .ts_event(UnixNanos::from(2_000_000))
3829 .build();
3830
3831 let mut order: LimitOrder = init.try_into().unwrap();
3832 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3833 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3834
3835 let own_book_order = order.to_own_book_order();
3836
3837 assert_eq!(own_book_order.ts_submitted, UnixNanos::from(1_000_000));
3839 assert_eq!(own_book_order.ts_accepted, UnixNanos::from(2_000_000));
3840 assert_eq!(own_book_order.ts_last, UnixNanos::from(2_000_000));
3841 }
3842
3843 #[rstest]
3844 fn test_to_own_book_order_partial_fill_uses_leaves_qty() {
3845 use crate::orders::limit::LimitOrder;
3846
3847 let init = OrderInitializedSpec::builder()
3848 .price(Price::from("100.00"))
3849 .quantity(Quantity::from(100_000))
3850 .build();
3851 let submitted = OrderSubmittedSpec::builder().build();
3852 let accepted = OrderAcceptedSpec::builder().build();
3853 let filled = OrderFilledSpec::builder()
3854 .order_type(OrderType::Limit)
3855 .last_qty(Quantity::from(40_000))
3856 .last_px(Price::from("100.00"))
3857 .build();
3858
3859 let mut order: LimitOrder = init.try_into().unwrap();
3860 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3861 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3862 order.apply(OrderEventAny::Filled(filled)).unwrap();
3863
3864 let own_book_order = order.to_own_book_order();
3865
3866 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3867 assert_eq!(order.leaves_qty(), Quantity::from(60_000));
3868 assert_eq!(own_book_order.size, Quantity::from(60_000));
3869 }
3870
3871 #[rstest]
3872 fn test_order_accepted_without_submitted_sets_account_id() {
3873 let init = OrderInitializedSpec::builder().build();
3875 let accepted = OrderAcceptedSpec::builder()
3876 .account_id(AccountId::from("EXTERNAL-001"))
3877 .build();
3878
3879 let mut order: MarketOrder = init.try_into().unwrap();
3880
3881 assert_eq!(order.account_id(), None);
3883
3884 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3886
3887 assert_eq!(order.account_id(), Some(AccountId::from("EXTERNAL-001")));
3889 assert_eq!(order.status(), OrderStatus::Accepted);
3890 }
3891
3892 #[rstest]
3893 fn test_order_accepted_after_submitted_preserves_account_id() {
3894 let init = OrderInitializedSpec::builder().build();
3896 let submitted = OrderSubmittedSpec::builder()
3897 .account_id(AccountId::from("SUBMITTED-001"))
3898 .build();
3899 let accepted = OrderAcceptedSpec::builder()
3900 .account_id(AccountId::from("ACCEPTED-001"))
3901 .build();
3902
3903 let mut order: MarketOrder = init.try_into().unwrap();
3904 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3905
3906 assert_eq!(order.account_id(), Some(AccountId::from("SUBMITTED-001")));
3908
3909 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3911
3912 assert_eq!(order.account_id(), Some(AccountId::from("ACCEPTED-001")));
3914 assert_eq!(order.status(), OrderStatus::Accepted);
3915 }
3916
3917 #[rstest]
3918 fn test_overfill_tracks_overfill_qty() {
3919 let init = OrderInitializedSpec::builder()
3921 .quantity(Quantity::from(100_000))
3922 .build();
3923 let submitted = OrderSubmittedSpec::builder().build();
3924 let accepted = OrderAcceptedSpec::builder().build();
3925 let overfill = OrderFilledSpec::builder()
3926 .last_qty(Quantity::from(110_000)) .build();
3928
3929 let mut order: MarketOrder = init.try_into().unwrap();
3930 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3931 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3932 order.apply(OrderEventAny::Filled(overfill)).unwrap();
3933
3934 assert_eq!(order.overfill_qty(), Quantity::from(10_000));
3936 assert_eq!(order.filled_qty(), Quantity::from(110_000));
3937 assert_eq!(order.leaves_qty(), Quantity::from(0));
3938 assert_eq!(order.status(), OrderStatus::Filled);
3939 }
3940
3941 #[rstest]
3942 fn test_partial_fill_then_overfill() {
3943 let init = OrderInitializedSpec::builder()
3945 .quantity(Quantity::from(100_000))
3946 .build();
3947 let submitted = OrderSubmittedSpec::builder().build();
3948 let accepted = OrderAcceptedSpec::builder().build();
3949 let fill1 = OrderFilledSpec::builder()
3950 .last_qty(Quantity::from(80_000))
3951 .trade_id(TradeId::from("TRADE-1"))
3952 .build();
3953 let fill2 = OrderFilledSpec::builder()
3954 .last_qty(Quantity::from(30_000)) .trade_id(TradeId::from("TRADE-2"))
3956 .build();
3957
3958 let mut order: MarketOrder = init.try_into().unwrap();
3959 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3960 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3961 order.apply(OrderEventAny::Filled(fill1)).unwrap();
3962
3963 assert_eq!(order.overfill_qty(), Quantity::from(0));
3965 assert_eq!(order.filled_qty(), Quantity::from(80_000));
3966 assert_eq!(order.leaves_qty(), Quantity::from(20_000));
3967
3968 order.apply(OrderEventAny::Filled(fill2)).unwrap();
3969
3970 assert_eq!(order.overfill_qty(), Quantity::from(10_000));
3972 assert_eq!(order.filled_qty(), Quantity::from(110_000));
3973 assert_eq!(order.leaves_qty(), Quantity::from(0));
3974 assert_eq!(order.status(), OrderStatus::Filled);
3975 }
3976
3977 #[rstest]
3978 fn test_exact_fill_no_overfill() {
3979 let init = OrderInitializedSpec::builder()
3981 .quantity(Quantity::from(100_000))
3982 .build();
3983 let submitted = OrderSubmittedSpec::builder().build();
3984 let accepted = OrderAcceptedSpec::builder().build();
3985 let filled = OrderFilledSpec::builder()
3986 .last_qty(Quantity::from(100_000)) .build();
3988
3989 let mut order: MarketOrder = init.try_into().unwrap();
3990 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
3991 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3992 order.apply(OrderEventAny::Filled(filled)).unwrap();
3993
3994 assert_eq!(order.overfill_qty(), Quantity::from(0));
3996 assert_eq!(order.filled_qty(), Quantity::from(100_000));
3997 assert_eq!(order.leaves_qty(), Quantity::from(0));
3998 }
3999
4000 #[rstest]
4001 fn test_partial_fill_then_overfill_with_fractional_quantities() {
4002 let init = OrderInitializedSpec::builder()
4006 .quantity(Quantity::from("2450.5"))
4007 .build();
4008 let submitted = OrderSubmittedSpec::builder().build();
4009 let accepted = OrderAcceptedSpec::builder().build();
4010 let fill1 = OrderFilledSpec::builder()
4011 .last_qty(Quantity::from("1202.5"))
4012 .trade_id(TradeId::from("TRADE-1"))
4013 .build();
4014 let fill2 = OrderFilledSpec::builder()
4015 .last_qty(Quantity::from("1285.5")) .trade_id(TradeId::from("TRADE-2"))
4017 .build();
4018
4019 let mut order: MarketOrder = init.try_into().unwrap();
4020 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4021 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4022 order.apply(OrderEventAny::Filled(fill1)).unwrap();
4023
4024 assert_eq!(order.overfill_qty(), Quantity::from(0));
4026 assert_eq!(order.filled_qty(), Quantity::from("1202.5"));
4027 assert_eq!(order.leaves_qty(), Quantity::from("1248.0"));
4028 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
4029
4030 order.apply(OrderEventAny::Filled(fill2)).unwrap();
4031
4032 assert_eq!(order.overfill_qty(), Quantity::from("37.5"));
4034 assert_eq!(order.filled_qty(), Quantity::from("2488.0"));
4035 assert_eq!(order.leaves_qty(), Quantity::from(0));
4036 assert_eq!(order.status(), OrderStatus::Filled);
4037 }
4038
4039 #[rstest]
4040 fn test_calculate_overfill_returns_zero_when_no_overfill() {
4041 let order: MarketOrder = OrderInitializedSpec::builder()
4042 .quantity(Quantity::from(100_000))
4043 .build()
4044 .try_into()
4045 .unwrap();
4046
4047 let overfill = order.calculate_overfill(Quantity::from(50_000));
4049 assert_eq!(overfill, Quantity::from(0));
4050
4051 let overfill = order.calculate_overfill(Quantity::from(100_000));
4053 assert_eq!(overfill, Quantity::from(0));
4054 }
4055
4056 #[rstest]
4057 fn test_calculate_overfill_returns_overfill_amount() {
4058 let order: MarketOrder = OrderInitializedSpec::builder()
4059 .quantity(Quantity::from(100_000))
4060 .build()
4061 .try_into()
4062 .unwrap();
4063
4064 let overfill = order.calculate_overfill(Quantity::from(110_000));
4066 assert_eq!(overfill, Quantity::from(10_000));
4067 }
4068
4069 #[rstest]
4070 fn test_calculate_overfill_accounts_for_existing_fills() {
4071 let init = OrderInitializedSpec::builder()
4072 .quantity(Quantity::from(100_000))
4073 .build();
4074 let submitted = OrderSubmittedSpec::builder().build();
4075 let accepted = OrderAcceptedSpec::builder().build();
4076 let partial_fill = OrderFilledSpec::builder()
4077 .last_qty(Quantity::from(60_000))
4078 .build();
4079
4080 let mut order: MarketOrder = init.try_into().unwrap();
4081 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4082 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4083 order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
4084
4085 let overfill = order.calculate_overfill(Quantity::from(50_000));
4088 assert_eq!(overfill, Quantity::from(10_000));
4089
4090 let overfill = order.calculate_overfill(Quantity::from(40_000));
4092 assert_eq!(overfill, Quantity::from(0));
4093 }
4094
4095 #[rstest]
4096 fn test_calculate_overfill_with_fractional_quantities() {
4097 let order: MarketOrder = OrderInitializedSpec::builder()
4098 .quantity(Quantity::from("2450.5"))
4099 .build()
4100 .try_into()
4101 .unwrap();
4102
4103 let overfill = order.calculate_overfill(Quantity::from("2488.0"));
4106 assert_eq!(overfill, Quantity::from("37.5"));
4107 }
4108
4109 #[rstest]
4110 fn test_calculate_overfill_zero_after_fractional_partial_fill() {
4111 let init = OrderInitializedSpec::builder()
4112 .quantity(Quantity::from("1.000"))
4113 .build();
4114 let submitted = OrderSubmittedSpec::builder().build();
4115 let accepted = OrderAcceptedSpec::builder().build();
4116 let partial_fill = OrderFilledSpec::builder()
4117 .last_qty(Quantity::from("0.072"))
4118 .build();
4119
4120 let mut order: MarketOrder = init.try_into().unwrap();
4121 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4122 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4123 order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
4124
4125 let overfill = order.calculate_overfill(Quantity::from("0.072"));
4127 assert_eq!(overfill, Quantity::from("0.000"));
4128 }
4129
4130 #[rstest]
4131 fn test_duplicate_fill_rejected() {
4132 let init = OrderInitializedSpec::builder()
4133 .quantity(Quantity::from(100_000))
4134 .build();
4135 let submitted = OrderSubmittedSpec::builder().build();
4136 let accepted = OrderAcceptedSpec::builder().build();
4137 let fill1 = OrderFilledSpec::builder()
4138 .last_qty(Quantity::from(50_000))
4139 .trade_id(TradeId::from("TRADE-001"))
4140 .build();
4141 let fill2_duplicate = OrderFilledSpec::builder()
4142 .last_qty(Quantity::from(50_000))
4143 .trade_id(TradeId::from("TRADE-001")) .build();
4145
4146 let mut order: MarketOrder = init.try_into().unwrap();
4147 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4148 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4149 order.apply(OrderEventAny::Filled(fill1)).unwrap();
4150
4151 assert_eq!(order.filled_qty(), Quantity::from(50_000));
4153 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
4154
4155 let result = order.apply(OrderEventAny::Filled(fill2_duplicate));
4157 assert!(result.is_err());
4158 match result.unwrap_err() {
4159 OrderError::DuplicateFill(trade_id) => {
4160 assert_eq!(trade_id, TradeId::from("TRADE-001"));
4161 }
4162 e => panic!("Expected DuplicateFill error, was: {e:?}"),
4163 }
4164
4165 assert_eq!(order.filled_qty(), Quantity::from(50_000));
4167 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
4168 }
4169
4170 #[rstest]
4171 fn test_same_trade_id_new_price_restates_average_and_void_matches_original_price() {
4172 let trade_id = TradeId::from("TRADE-1");
4173 let qty = Quantity::from("2");
4174 let original_px = Price::from("12");
4175 let restated_px = Price::from("9.47");
4176 let mut order =
4177 market_order_with_fills(OrderSide::Buy, qty, &[(trade_id, qty, original_px)]);
4178 let restatement = OrderFilledSpec::builder()
4179 .order_side(OrderSide::Buy)
4180 .trade_id(trade_id)
4181 .last_qty(qty)
4182 .last_px(restated_px)
4183 .event_id(UUID4::new())
4184 .build();
4185
4186 order
4187 .apply(OrderEventAny::Filled(restatement.clone()))
4188 .unwrap();
4189 let repeated = order.apply(OrderEventAny::Filled(restatement));
4190 assert!(matches!(repeated, Err(OrderError::DuplicateFill(_))));
4191
4192 let stored_px = order.events().iter().find_map(|event| match event {
4193 OrderEventAny::Filled(fill) if fill.last_px == original_px => Some(fill.last_px),
4194 _ => None,
4195 });
4196
4197 let replayed =
4198 OrderAny::from_events(order.events().into_iter().cloned().collect()).unwrap();
4199
4200 assert_eq!(order.status(), OrderStatus::Filled);
4201 assert_eq!(order.filled_qty(), qty);
4202 assert!(order.leaves_qty().is_zero());
4203 let returned = OrderFilledSpec::builder()
4204 .order_side(OrderSide::Buy)
4205 .trade_id(trade_id)
4206 .last_qty(qty)
4207 .last_px(original_px)
4208 .event_id(UUID4::new())
4209 .build();
4210 order.apply(OrderEventAny::Filled(returned)).unwrap();
4211 assert_eq!(order.avg_px(), Some(dec!(12)));
4212 assert_eq!(order.filled_qty(), qty);
4213 assert_eq!(order.trade_ids().len(), 1);
4214 assert_eq!(stored_px, Some(original_px));
4215 assert_eq!(replayed.status(), OrderStatus::Filled);
4216 assert_eq!(replayed.filled_qty(), qty);
4217 assert_eq!(replayed.avg_px(), Some(dec!(9.47)));
4218
4219 order
4220 .apply(OrderEventAny::FillVoided(
4221 OrderFillVoidedSpec::builder()
4222 .trade_id(trade_id)
4223 .voided_qty(qty)
4224 .last_px(original_px)
4225 .order_side(OrderSide::Buy)
4226 .build(),
4227 ))
4228 .unwrap();
4229
4230 let mut mismatched =
4231 market_order_with_fills(OrderSide::Buy, qty, &[(trade_id, qty, original_px)]);
4232 mismatched
4233 .apply(OrderEventAny::Filled(
4234 OrderFilledSpec::builder()
4235 .order_side(OrderSide::Buy)
4236 .trade_id(trade_id)
4237 .last_qty(qty)
4238 .last_px(restated_px)
4239 .event_id(UUID4::new())
4240 .build(),
4241 ))
4242 .unwrap();
4243 let rejected = mismatched.apply(OrderEventAny::FillVoided(
4244 OrderFillVoidedSpec::builder()
4245 .trade_id(trade_id)
4246 .voided_qty(qty)
4247 .last_px(restated_px)
4248 .order_side(OrderSide::Buy)
4249 .build(),
4250 ));
4251
4252 assert!(matches!(rejected, Err(OrderError::InvalidOrderEvent)));
4253 }
4254
4255 #[rstest]
4256 fn test_fill_raw_overflow_rejected_without_mutation() {
4257 let unit = Quantity::from(1);
4258 let almost_max = Quantity::from_raw(QUANTITY_RAW_MAX - unit.raw(), 0);
4259 let max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
4260 let init = OrderInitializedSpec::builder().quantity(max).build();
4261 let accepted = OrderAcceptedSpec::builder().build();
4262 let fill1 = OrderFilledSpec::builder()
4263 .last_qty(almost_max)
4264 .trade_id(TradeId::from("TRADE-001"))
4265 .build();
4266 let fill2 = OrderFilledSpec::builder()
4267 .last_qty(Quantity::from(2))
4268 .trade_id(TradeId::from("TRADE-002"))
4269 .build();
4270
4271 let mut order: MarketOrder = init.try_into().unwrap();
4272 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4273 order.apply(OrderEventAny::Filled(fill1)).unwrap();
4274
4275 assert_eq!(order.filled_qty(), almost_max);
4276 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
4277
4278 let result = order.apply(OrderEventAny::Filled(fill2));
4279 let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
4280 else {
4281 panic!("Expected typed invariant error, was: {result:?}");
4282 };
4283 assert_eq!(
4284 message,
4285 format!("filled quantity overflowed Quantity raw bounds: {almost_max} + 2")
4286 );
4287 assert_eq!(order.filled_qty(), almost_max);
4288 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
4289 assert_eq!(order.event_count(), 3);
4290 }
4291
4292 #[rstest]
4293 #[case::domain_overflow(1)]
4294 #[case::integer_overflow(Money::from("1 USD").raw())]
4295 fn test_fill_commission_overflow_rejected_without_mutation(
4296 #[case] increment: MoneyRaw,
4297 #[values(false, true)] negative: bool,
4298 ) {
4299 let usd = Currency::USD();
4300
4301 let bound = if negative {
4302 MONEY_RAW_MIN
4303 } else {
4304 MONEY_RAW_MAX
4305 };
4306
4307 let commission = Money::from_raw_checked(bound, usd).unwrap();
4308 let additional =
4309 Money::from_raw_checked(if negative { -increment } else { increment }, usd).unwrap();
4310 let init = OrderInitializedSpec::builder()
4311 .quantity(Quantity::from(2))
4312 .build();
4313 let mut order: MarketOrder = init.try_into().unwrap();
4314 order
4315 .apply(OrderEventAny::Accepted(
4316 OrderAcceptedSpec::builder().build(),
4317 ))
4318 .unwrap();
4319 let first = OrderFilledSpec::builder()
4320 .trade_id(TradeId::from("TRADE-1"))
4321 .last_qty(Quantity::from(1))
4322 .commission(commission)
4323 .ts_event(11.into())
4324 .build();
4325 let second = OrderFilledSpec::builder()
4326 .trade_id(TradeId::from("TRADE-2"))
4327 .last_qty(Quantity::from(1))
4328 .commission(additional)
4329 .position_id(PositionId::from("POSITION-2"))
4330 .ts_event(29.into())
4331 .build();
4332 order.apply(OrderEventAny::Filled(first)).unwrap();
4333 let before = serde_json::to_value(&order).unwrap();
4334 let totals_before = order
4335 .fill_totals
4336 .as_ref()
4337 .map(|totals| (totals.notional, totals.quantity));
4338
4339 let result = order.apply(OrderEventAny::Filled(second));
4340
4341 let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
4342 else {
4343 panic!("Expected typed invariant error, was: {result:?}");
4344 };
4345
4346 assert_eq!(
4347 message,
4348 format!("cumulative commission overflowed Money bounds: {commission} + {additional}")
4349 );
4350 assert_eq!(serde_json::to_value(&order).unwrap(), before);
4351 assert_eq!(
4352 order
4353 .fill_totals
4354 .as_ref()
4355 .map(|totals| (totals.notional, totals.quantity)),
4356 totals_before
4357 );
4358 }
4359
4360 #[rstest]
4361 fn test_fill_commission_bounds_survive_corrections_and_replay() {
4362 let usd = Currency::USD();
4363 let aud = Currency::AUD();
4364 let unit = Money::from("1 USD");
4365 let max = Money::from_raw_checked(MONEY_RAW_MAX, usd).unwrap();
4366 let other_max = Money::from_raw_checked(MONEY_RAW_MAX, aud).unwrap();
4367 let init = OrderInitializedSpec::builder()
4368 .quantity(Quantity::from(6))
4369 .build();
4370 let mut order: MarketOrder = init.try_into().unwrap();
4371 order
4372 .apply(OrderEventAny::Accepted(
4373 OrderAcceptedSpec::builder().build(),
4374 ))
4375 .unwrap();
4376 let first = OrderFilledSpec::builder()
4377 .trade_id(TradeId::from("TRADE-1"))
4378 .last_qty(Quantity::from(2))
4379 .commission(max - unit)
4380 .build();
4381 order.apply(OrderEventAny::Filled(first.clone())).unwrap();
4382 order
4383 .apply(OrderEventAny::Filled(
4384 OrderFilledSpec::builder()
4385 .trade_id(TradeId::from("TRADE-2"))
4386 .last_qty(Quantity::from(1))
4387 .commission(unit)
4388 .build(),
4389 ))
4390 .unwrap();
4391 order
4392 .apply(OrderEventAny::Filled(
4393 OrderFilledSpec::builder()
4394 .trade_id(TradeId::from("TRADE-3"))
4395 .last_qty(Quantity::from(1))
4396 .commission(other_max)
4397 .build(),
4398 ))
4399 .unwrap();
4400 order
4401 .apply(OrderEventAny::FillVoided(matching_fill_void(
4402 &first,
4403 Quantity::from(1),
4404 Some(unit),
4405 )))
4406 .unwrap();
4407 assert_eq!(order.commission(&usd), Some(max - unit));
4408 order
4409 .apply(OrderEventAny::Filled(
4410 OrderFilledSpec::builder()
4411 .trade_id(TradeId::from("TRADE-4"))
4412 .last_qty(Quantity::from(1))
4413 .commission(unit)
4414 .build(),
4415 ))
4416 .unwrap();
4417 let replayed =
4418 OrderAny::from_events(order.events().into_iter().cloned().collect()).unwrap();
4419 let before = serde_json::to_value(&order).unwrap();
4420 let overflow = OrderFilledSpec::builder()
4421 .trade_id(TradeId::from("TRADE-5"))
4422 .last_qty(Quantity::from(1))
4423 .commission(unit)
4424 .build();
4425
4426 let result = order.apply(OrderEventAny::Filled(overflow));
4427
4428 assert!(matches!(result, Err(OrderError::Invariant(_))));
4429 assert_eq!(serde_json::to_value(&order).unwrap(), before);
4430 assert_eq!(order.commission(&usd), Some(max));
4431 assert_eq!(order.commission(&aud), Some(other_max));
4432 assert_eq!(order.filled_qty(), Quantity::from(4));
4433 assert_eq!(order.leaves_qty(), Quantity::from(2));
4434 assert_eq!(replayed.commissions(), order.commissions());
4435 assert_eq!(replayed.filled_qty(), order.filled_qty());
4436 assert_eq!(replayed.leaves_qty(), order.leaves_qty());
4437 }
4438
4439 #[rstest]
4440 fn test_late_fill_on_filled_order_is_invalid_transition_not_overflow() {
4441 let max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
4442 let init = OrderInitializedSpec::builder().quantity(max).build();
4443 let accepted = OrderAcceptedSpec::builder().build();
4444 let fill1 = OrderFilledSpec::builder()
4445 .last_qty(max)
4446 .trade_id(TradeId::from("TRADE-001"))
4447 .build();
4448 let fill2 = OrderFilledSpec::builder()
4449 .last_qty(Quantity::from(1))
4450 .trade_id(TradeId::from("TRADE-002"))
4451 .build();
4452
4453 let mut order: MarketOrder = init.try_into().unwrap();
4454 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4455 order.apply(OrderEventAny::Filled(fill1)).unwrap();
4456
4457 let result = order.apply(OrderEventAny::Filled(fill2));
4458 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
4459 assert_eq!(order.filled_qty(), max);
4460 assert_eq!(order.status(), OrderStatus::Filled);
4461 }
4462
4463 #[rstest]
4464 fn test_quantity_saturating_add_clamps_undef_sentinel() {
4465 let qty = Quantity::from_raw(QuantityRaw::MAX, 0).saturating_add(Quantity::zero(0));
4466
4467 assert_eq!(qty, Quantity::from_raw(QUANTITY_RAW_MAX, 0));
4468 assert!(!qty.is_undefined());
4469 }
4470
4471 #[rstest]
4472 fn test_check_display_qty_returns_typed_invariant_with_stable_display() {
4473 let error = check_display_qty(Some(Quantity::from(2)), Quantity::from(1)).unwrap_err();
4474
4475 match error {
4476 OrderError::Invariant(CorrectnessError::PredicateViolation { ref message }) => {
4477 assert_eq!(message, "`display_qty` may not exceed `quantity`");
4478 }
4479 other => panic!("Expected typed invariant error, was: {other:?}"),
4480 }
4481
4482 assert_eq!(error.to_string(), "`display_qty` may not exceed `quantity`");
4483 }
4484
4485 #[rstest]
4486 fn test_check_time_in_force_returns_typed_invariant_with_stable_display() {
4487 let error = check_time_in_force(TimeInForce::Gtd, None).unwrap_err();
4488
4489 match error {
4490 OrderError::Invariant(CorrectnessError::PredicateViolation { ref message }) => {
4491 assert_eq!(message, "`expire_time` is required for `GTD` order");
4492 }
4493 other => panic!("Expected typed invariant error, was: {other:?}"),
4494 }
4495
4496 assert_eq!(
4497 error.to_string(),
4498 "`expire_time` is required for `GTD` order"
4499 );
4500 }
4501
4502 #[rstest]
4503 fn test_different_trade_ids_allowed() {
4504 let init = OrderInitializedSpec::builder()
4505 .quantity(Quantity::from(100_000))
4506 .build();
4507 let submitted = OrderSubmittedSpec::builder().build();
4508 let accepted = OrderAcceptedSpec::builder().build();
4509 let fill1 = OrderFilledSpec::builder()
4510 .last_qty(Quantity::from(50_000))
4511 .trade_id(TradeId::from("TRADE-001"))
4512 .build();
4513 let fill2 = OrderFilledSpec::builder()
4514 .last_qty(Quantity::from(50_000))
4515 .trade_id(TradeId::from("TRADE-002")) .build();
4517
4518 let mut order: MarketOrder = init.try_into().unwrap();
4519 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4520 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4521 order.apply(OrderEventAny::Filled(fill1)).unwrap();
4522 order.apply(OrderEventAny::Filled(fill2)).unwrap();
4523
4524 assert_eq!(order.filled_qty(), Quantity::from(100_000));
4526 assert_eq!(order.status(), OrderStatus::Filled);
4527 assert_eq!(order.trade_ids.len(), 2);
4528 }
4529
4530 #[rstest]
4531 fn test_pending_update_order_restores_status_on_updated() {
4532 let init = OrderInitializedSpec::builder()
4533 .quantity(Quantity::from(100_000))
4534 .build();
4535 let submitted = OrderSubmittedSpec::builder().build();
4536 let accepted = OrderAcceptedSpec::builder().build();
4537 let pending_update = OrderPendingUpdateSpec::builder().build();
4538 let fill = OrderFilledSpec::builder()
4539 .last_qty(Quantity::from(10_000))
4540 .build();
4541 let updated = OrderUpdatedSpec::builder()
4542 .quantity(Quantity::from(50_000))
4543 .build();
4544
4545 let mut order: MarketOrder = init.try_into().unwrap();
4546 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4547 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4548
4549 assert_eq!(order.status(), OrderStatus::Accepted);
4550
4551 order
4552 .apply(OrderEventAny::PendingUpdate(pending_update))
4553 .unwrap();
4554 order.apply(OrderEventAny::Filled(fill)).unwrap();
4555 assert_eq!(order.status(), OrderStatus::PendingUpdate);
4556 assert_eq!(order.previous_status(), Some(OrderStatus::PartiallyFilled));
4557 assert_eq!(order.filled_qty(), Quantity::from(10_000));
4558
4559 order.apply(OrderEventAny::Updated(updated)).unwrap();
4560
4561 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
4562 assert_eq!(order.quantity(), Quantity::from(50_000));
4563 assert_eq!(order.leaves_qty(), Quantity::from(40_000));
4564 }
4565
4566 #[rstest]
4567 fn test_pending_cancel_supersedes_pending_update_without_losing_previous_status() {
4568 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
4569 order
4570 .apply(OrderEventAny::Submitted(
4571 OrderSubmittedSpec::builder().build(),
4572 ))
4573 .unwrap();
4574 order
4575 .apply(OrderEventAny::Accepted(
4576 OrderAcceptedSpec::builder().build(),
4577 ))
4578 .unwrap();
4579 order
4580 .apply(OrderEventAny::PendingUpdate(
4581 OrderPendingUpdateSpec::builder().build(),
4582 ))
4583 .unwrap();
4584
4585 assert_eq!(order.status(), OrderStatus::PendingUpdate);
4586 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
4587
4588 order
4589 .apply(OrderEventAny::PendingCancel(
4590 OrderPendingCancelSpec::builder().build(),
4591 ))
4592 .unwrap();
4593
4594 assert_eq!(order.status(), OrderStatus::PendingCancel);
4595 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
4596
4597 let repeated = OrderEventAny::PendingCancel(OrderPendingCancelSpec::builder().build());
4598 order.apply(repeated.clone()).unwrap();
4599
4600 assert_eq!(order.status(), OrderStatus::PendingCancel);
4601 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
4602 assert_eq!(order.event_count(), 6);
4603 assert_eq!(order.last_event(), &repeated);
4604 }
4605
4606 #[rstest]
4607 fn test_late_fill_for_historical_venue_order_keeps_current_venue_order_id() {
4608 let old_venue_order_id = VenueOrderId::from("V-OLD");
4609 let new_venue_order_id = VenueOrderId::from("V-NEW");
4610 let init = OrderInitializedSpec::builder()
4611 .quantity(Quantity::from(10))
4612 .build();
4613 let accepted = OrderAcceptedSpec::builder()
4614 .venue_order_id(old_venue_order_id)
4615 .build();
4616 let updated = OrderUpdatedSpec::builder()
4617 .quantity(Quantity::from(10))
4618 .venue_order_id(new_venue_order_id)
4619 .build();
4620 let late_fill = OrderFilledSpec::builder()
4621 .venue_order_id(old_venue_order_id)
4622 .last_qty(Quantity::from(2))
4623 .trade_id(TradeId::from("T-LATE"))
4624 .build();
4625
4626 let mut order: MarketOrder = init.try_into().unwrap();
4627 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4628 order.apply(OrderEventAny::Updated(updated)).unwrap();
4629 order.apply(OrderEventAny::Filled(late_fill)).unwrap();
4630
4631 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
4632 assert_eq!(order.quantity(), Quantity::from(10));
4633 assert_eq!(order.filled_qty(), Quantity::from(2));
4634 assert_eq!(order.leaves_qty(), Quantity::from(8));
4635 assert_eq!(order.venue_order_id(), Some(new_venue_order_id));
4636 assert_eq!(
4637 order
4638 .venue_order_ids()
4639 .into_iter()
4640 .copied()
4641 .collect::<Vec<_>>(),
4642 vec![old_venue_order_id, new_venue_order_id],
4643 );
4644 let OrderEventAny::Filled(event) = order.last_event() else {
4645 panic!("last event was not the late fill");
4646 };
4647 assert_eq!(event.venue_order_id, old_venue_order_id);
4648 }
4649
4650 #[rstest]
4651 fn test_pending_update_accepts_delayed_submitted() {
4652 let init = OrderInitializedSpec::builder()
4653 .quantity(Quantity::from(100_000))
4654 .build();
4655 let submitted = OrderSubmittedSpec::builder()
4656 .ts_event(UnixNanos::from(1_000))
4657 .build();
4658 let accepted = OrderAcceptedSpec::builder().build();
4659 let pending_update = OrderPendingUpdateSpec::builder().build();
4660
4661 let delayed_submitted = OrderSubmittedSpec::builder()
4665 .account_id(AccountId::from("SIM-002"))
4666 .ts_event(UnixNanos::from(3_000))
4667 .build();
4668 let updated = OrderUpdatedSpec::builder()
4669 .quantity(Quantity::from(50_000))
4670 .build();
4671
4672 let mut order: MarketOrder = init.try_into().unwrap();
4673 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4674 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4675 order
4676 .apply(OrderEventAny::PendingUpdate(pending_update))
4677 .unwrap();
4678
4679 assert_eq!(order.status(), OrderStatus::PendingUpdate);
4680 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
4681 assert_eq!(order.account_id(), Some(AccountId::from("SIM-001")));
4682 assert_eq!(order.ts_submitted(), Some(UnixNanos::from(1_000)));
4683
4684 order
4685 .apply(OrderEventAny::Submitted(delayed_submitted))
4686 .unwrap();
4687
4688 assert_eq!(order.status(), OrderStatus::PendingUpdate);
4691 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
4692
4693 assert_eq!(order.account_id(), Some(AccountId::from("SIM-002")));
4694 assert_eq!(order.ts_submitted(), Some(UnixNanos::from(3_000)));
4695
4696 order.apply(OrderEventAny::Updated(updated)).unwrap();
4697
4698 assert_eq!(order.status(), OrderStatus::Accepted);
4699 assert_eq!(order.quantity(), Quantity::from(50_000));
4700 }
4701
4702 #[rstest]
4703 #[case::submitted(false, OrderStatus::Submitted)]
4704 #[case::accepted(true, OrderStatus::Accepted)]
4705 fn test_rejected_event_preserves_status_restoration(
4706 #[case] accepted_first: bool,
4707 #[case] restored_status: OrderStatus,
4708 ) {
4709 let init = OrderInitializedSpec::builder().build();
4710 let submitted = OrderSubmittedSpec::builder().build();
4711 let accepted = OrderAcceptedSpec::builder().build();
4712 let invalid = OrderSubmittedSpec::builder().build();
4713 let pending_update = OrderPendingUpdateSpec::builder().build();
4714 let modify_rejected = OrderModifyRejectedSpec::builder().build();
4715 let mut order: MarketOrder = init.try_into().unwrap();
4716 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4717 if accepted_first {
4718 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4719 }
4720
4721 let previous_status = order.previous_status();
4722
4723 let result = order.apply(OrderEventAny::Submitted(invalid));
4724
4725 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
4726 assert_eq!(order.previous_status(), previous_status);
4727
4728 order
4729 .apply(OrderEventAny::PendingUpdate(pending_update))
4730 .unwrap();
4731 order
4732 .apply(OrderEventAny::ModifyRejected(modify_rejected))
4733 .unwrap();
4734 assert_eq!(order.status(), restored_status);
4735 }
4736
4737 #[rstest]
4738 #[case::submitted(OrderStatus::Submitted)]
4739 #[case::accepted(OrderStatus::Accepted)]
4740 #[case::partially_filled(OrderStatus::PartiallyFilled)]
4741 fn test_modify_rejected_after_venue_update(#[case] expected_status: OrderStatus) {
4742 let mut order = OrderTestBuilder::new(OrderType::Limit)
4743 .instrument_id(InstrumentId::test_default())
4744 .quantity(Quantity::from(10))
4745 .price(Price::from("98.00"))
4746 .build();
4747 order
4748 .apply(OrderEventAny::Submitted(
4749 OrderSubmittedSpec::builder().build(),
4750 ))
4751 .unwrap();
4752
4753 if expected_status != OrderStatus::Submitted {
4754 order
4755 .apply(OrderEventAny::Accepted(
4756 OrderAcceptedSpec::builder().build(),
4757 ))
4758 .unwrap();
4759 }
4760
4761 let filled_qty = if expected_status == OrderStatus::PartiallyFilled {
4762 order
4763 .apply(OrderEventAny::Filled(
4764 OrderFilledSpec::builder()
4765 .order_type(OrderType::Limit)
4766 .last_qty(Quantity::from(3))
4767 .build(),
4768 ))
4769 .unwrap();
4770 Quantity::from(3)
4771 } else {
4772 Quantity::from(0)
4773 };
4774
4775 order
4776 .apply(OrderEventAny::PendingUpdate(
4777 OrderPendingUpdateSpec::builder().build(),
4778 ))
4779 .unwrap();
4780 order
4781 .apply(OrderEventAny::Updated(
4782 OrderUpdatedSpec::builder()
4783 .quantity(Quantity::from(20))
4784 .price(Price::from("97.00"))
4785 .build(),
4786 ))
4787 .unwrap();
4788 assert_eq!(order.status(), expected_status);
4789 let previous_status = order.previous_status();
4790 let events_before = order.events().len();
4791
4792 let rejection = OrderEventAny::ModifyRejected(OrderModifyRejectedSpec::builder().build());
4793 order.apply(rejection.clone()).unwrap();
4794
4795 assert_eq!(order.status(), expected_status);
4796 assert_eq!(order.previous_status(), previous_status);
4797 assert_eq!(order.quantity(), Quantity::from(20));
4798 assert_eq!(order.price(), Some(Price::from("97.00")));
4799 assert_eq!(order.filled_qty(), filled_qty);
4800 assert_eq!(order.leaves_qty(), Quantity::from(20) - filled_qty);
4801 assert_eq!(order.events().len(), events_before + 1);
4802 assert_eq!(order.last_event(), &rejection);
4803 }
4804
4805 #[rstest]
4806 fn test_modify_rejected_preserves_triggered_with_stale_previous_status() {
4807 let mut order = OrderTestBuilder::new(OrderType::StopLimit)
4808 .instrument_id(InstrumentId::test_default())
4809 .quantity(Quantity::from(10))
4810 .price(Price::from("98.00"))
4811 .trigger_price(Price::from("99.00"))
4812 .build();
4813 order
4814 .apply(OrderEventAny::Submitted(
4815 OrderSubmittedSpec::builder().build(),
4816 ))
4817 .unwrap();
4818 order
4819 .apply(OrderEventAny::Accepted(
4820 OrderAcceptedSpec::builder().build(),
4821 ))
4822 .unwrap();
4823 order
4824 .apply(OrderEventAny::PendingUpdate(
4825 OrderPendingUpdateSpec::builder().build(),
4826 ))
4827 .unwrap();
4828 order
4829 .apply(OrderEventAny::Updated(
4830 OrderUpdatedSpec::builder()
4831 .quantity(Quantity::from(20))
4832 .price(Price::from("97.00"))
4833 .trigger_price(Price::from("98.00"))
4834 .build(),
4835 ))
4836 .unwrap();
4837 order
4838 .apply(OrderEventAny::Triggered(
4839 OrderTriggeredSpec::builder().build(),
4840 ))
4841 .unwrap();
4842 assert_eq!(order.status(), OrderStatus::Triggered);
4843 assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
4844
4845 let rejection = OrderEventAny::ModifyRejected(OrderModifyRejectedSpec::builder().build());
4846 order.apply(rejection.clone()).unwrap();
4847
4848 assert_eq!(order.status(), OrderStatus::Triggered);
4849 assert_eq!(order.quantity(), Quantity::from(20));
4850 assert_eq!(order.price(), Some(Price::from("97.00")));
4851 assert_eq!(order.trigger_price(), Some(Price::from("98.00")));
4852 assert_eq!(order.last_event(), &rejection);
4853 }
4854
4855 #[rstest]
4856 #[case::submitted(OrderStatus::Submitted)]
4857 #[case::accepted(OrderStatus::Accepted)]
4858 #[case::pending_update(OrderStatus::PendingUpdate)]
4859 #[case::pending_cancel(OrderStatus::PendingCancel)]
4860 fn test_modify_rejected_without_previous_status(#[case] status: OrderStatus) {
4861 let mut order: MarketOrder = OrderInitializedSpec::builder()
4862 .quantity(Quantity::from(10))
4863 .build()
4864 .try_into()
4865 .unwrap();
4866
4867 if status == OrderStatus::Submitted {
4868 order
4869 .apply(OrderEventAny::Submitted(
4870 OrderSubmittedSpec::builder().build(),
4871 ))
4872 .unwrap();
4873 } else {
4874 order
4875 .apply(OrderEventAny::Accepted(
4876 OrderAcceptedSpec::builder().build(),
4877 ))
4878 .unwrap();
4879 }
4880
4881 match status {
4882 OrderStatus::PendingUpdate => order
4883 .apply(OrderEventAny::PendingUpdate(
4884 OrderPendingUpdateSpec::builder().build(),
4885 ))
4886 .unwrap(),
4887 OrderStatus::PendingCancel => order
4888 .apply(OrderEventAny::PendingCancel(
4889 OrderPendingCancelSpec::builder().build(),
4890 ))
4891 .unwrap(),
4892 _ => {}
4893 }
4894
4895 order.previous_status = None;
4896 let events_before = order.events().len();
4897 let rejection = OrderEventAny::ModifyRejected(OrderModifyRejectedSpec::builder().build());
4898 let result = order.apply(rejection.clone());
4899
4900 if status == OrderStatus::PendingUpdate {
4901 assert!(matches!(result, Err(OrderError::NoPreviousState)));
4902 assert_eq!(order.events().len(), events_before);
4903 } else {
4904 result.unwrap();
4905 assert_eq!(order.last_event(), &rejection);
4906 }
4907
4908 assert_eq!(order.status(), status);
4909 assert_eq!(order.quantity(), Quantity::from(10));
4910 assert_eq!(order.previous_status(), None);
4911 }
4912
4913 #[rstest]
4914 #[case::submitted(false, OrderStatus::Submitted)]
4915 #[case::accepted(true, OrderStatus::Accepted)]
4916 fn test_modify_rejected_preserves_pending_cancel(
4917 #[case] accepted_first: bool,
4918 #[case] restored_status: OrderStatus,
4919 ) {
4920 let init = OrderInitializedSpec::builder().build();
4921 let submitted = OrderSubmittedSpec::builder().build();
4922 let accepted = OrderAcceptedSpec::builder().build();
4923 let pending_update = OrderPendingUpdateSpec::builder().build();
4924 let pending_cancel = OrderPendingCancelSpec::builder().build();
4925 let modify_rejected = OrderModifyRejectedSpec::builder().build();
4926 let cancel_rejected = OrderCancelRejectedSpec::builder().build();
4927 let mut order: MarketOrder = init.try_into().unwrap();
4928
4929 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
4930 if accepted_first {
4931 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
4932 }
4933
4934 order
4935 .apply(OrderEventAny::PendingUpdate(pending_update))
4936 .unwrap();
4937 order
4938 .apply(OrderEventAny::PendingCancel(pending_cancel))
4939 .unwrap();
4940 order
4941 .apply(OrderEventAny::ModifyRejected(modify_rejected))
4942 .unwrap();
4943
4944 assert_eq!(order.status(), OrderStatus::PendingCancel);
4945 assert_eq!(order.previous_status(), Some(restored_status));
4946
4947 order
4948 .apply(OrderEventAny::CancelRejected(cancel_rejected))
4949 .unwrap();
4950
4951 assert_eq!(order.status(), restored_status);
4952 }
4953
4954 #[rstest]
4955 fn test_modify_rejected_transition_matrix() {
4956 let event = OrderEventAny::ModifyRejected(OrderModifyRejectedSpec::builder().build());
4957
4958 for status in OrderStatus::iter() {
4959 let expected = matches!(
4960 status,
4961 OrderStatus::Submitted
4962 | OrderStatus::Accepted
4963 | OrderStatus::Triggered
4964 | OrderStatus::PartiallyFilled
4965 | OrderStatus::PendingUpdate
4966 | OrderStatus::PendingCancel
4967 );
4968 let mut current = status;
4969 let result = current.transition(&event);
4970
4971 if expected {
4972 assert_eq!(result.unwrap(), status, "status={status}");
4973 } else {
4974 assert!(
4975 matches!(result, Err(OrderError::InvalidStateTransition)),
4976 "status={status}"
4977 );
4978 }
4979
4980 assert_eq!(current, status);
4981 }
4982 }
4983
4984 #[rstest]
4985 fn test_cancel_rejected_transition_matrix() {
4986 let event = OrderEventAny::CancelRejected(OrderCancelRejectedSpec::builder().build());
4987
4988 for status in OrderStatus::iter() {
4989 let expected = match status {
4990 OrderStatus::Submitted
4991 | OrderStatus::Accepted
4992 | OrderStatus::Triggered
4993 | OrderStatus::PartiallyFilled
4994 | OrderStatus::PendingUpdate
4995 | OrderStatus::PendingCancel => Some(status),
4996 OrderStatus::Initialized
4997 | OrderStatus::Emulated
4998 | OrderStatus::Released
4999 | OrderStatus::Denied
5000 | OrderStatus::Rejected
5001 | OrderStatus::Canceled
5002 | OrderStatus::Expired
5003 | OrderStatus::Filled
5004 | OrderStatus::Voided => None,
5005 };
5006
5007 let mut current = status;
5008 let result = current.transition(&event);
5009
5010 if let Some(expected) = expected {
5011 assert_eq!(result.unwrap(), expected, "status={status}");
5012 } else {
5013 assert!(
5014 matches!(result, Err(OrderError::InvalidStateTransition)),
5015 "status={status}"
5016 );
5017 }
5018
5019 assert_eq!(current, status);
5020 }
5021 }
5022
5023 #[rstest]
5024 #[case::submitted(&[], OrderStatus::Submitted, false)]
5025 #[case::accepted(&[OrderStatus::Accepted], OrderStatus::Accepted, false)]
5026 #[case::pending_submitted(&[OrderStatus::PendingCancel], OrderStatus::Submitted, false)]
5027 #[case::pending_accepted(&[OrderStatus::Accepted, OrderStatus::PendingCancel], OrderStatus::Accepted, false)]
5028 #[case::acceptance_overtakes_submitted_cancel(&[OrderStatus::PendingCancel, OrderStatus::Accepted], OrderStatus::Accepted, false)]
5029 #[case::acceptance_overtakes_accepted_cancel(&[OrderStatus::Accepted, OrderStatus::PendingCancel, OrderStatus::Accepted], OrderStatus::Accepted, false)]
5030 #[case::triggered(&[OrderStatus::Accepted, OrderStatus::Triggered], OrderStatus::Triggered, false)]
5031 #[case::partially_filled(&[OrderStatus::Accepted, OrderStatus::PartiallyFilled], OrderStatus::PartiallyFilled, false)]
5032 #[case::pending_triggered(&[OrderStatus::Accepted, OrderStatus::Triggered, OrderStatus::PendingCancel], OrderStatus::Triggered, false)]
5033 #[case::pending_partially_filled(&[OrderStatus::Accepted, OrderStatus::PartiallyFilled, OrderStatus::PendingCancel], OrderStatus::PartiallyFilled, false)]
5034 #[case::fill_overtakes_cancel(&[OrderStatus::Accepted, OrderStatus::PendingCancel, OrderStatus::PartiallyFilled], OrderStatus::PartiallyFilled, false)]
5035 #[case::pending_update_accepted_updated(&[OrderStatus::Accepted, OrderStatus::PendingUpdate], OrderStatus::PendingUpdate, false)]
5036 #[case::pending_update_accepted_rejected(&[OrderStatus::Accepted, OrderStatus::PendingUpdate], OrderStatus::PendingUpdate, true)]
5037 #[case::pending_update_triggered_updated(&[OrderStatus::Accepted, OrderStatus::Triggered, OrderStatus::PendingUpdate], OrderStatus::PendingUpdate, false)]
5038 #[case::pending_update_triggered_rejected(&[OrderStatus::Accepted, OrderStatus::Triggered, OrderStatus::PendingUpdate], OrderStatus::PendingUpdate, true)]
5039 #[case::pending_update_partially_filled_updated(&[OrderStatus::Accepted, OrderStatus::PartiallyFilled, OrderStatus::PendingUpdate], OrderStatus::PendingUpdate, false)]
5040 #[case::pending_update_partially_filled_rejected(&[OrderStatus::Accepted, OrderStatus::PartiallyFilled, OrderStatus::PendingUpdate], OrderStatus::PendingUpdate, true)]
5041 fn test_cancel_rejected_preserves_order_state(
5042 #[case] statuses: &[OrderStatus],
5043 #[case] expected_status: OrderStatus,
5044 #[case] modify_rejected: bool,
5045 ) {
5046 let order_type = if statuses.contains(&OrderStatus::Triggered) {
5047 OrderType::StopLimit
5048 } else {
5049 OrderType::Market
5050 };
5051
5052 let mut order = OrderTestBuilder::new(order_type)
5053 .instrument_id(InstrumentId::test_default())
5054 .quantity(Quantity::from(100_000))
5055 .price(Price::from("1.00000"))
5056 .trigger_price(Price::from("1.00000"))
5057 .trigger_type(TriggerType::LastPrice)
5058 .build();
5059 order
5060 .apply(OrderEventAny::Submitted(
5061 OrderSubmittedSpec::builder().build(),
5062 ))
5063 .unwrap();
5064
5065 for status in statuses {
5066 let source_status = order.status();
5067
5068 let event = match status {
5069 OrderStatus::Accepted => {
5070 OrderEventAny::Accepted(OrderAcceptedSpec::builder().build())
5071 }
5072 OrderStatus::Triggered => {
5073 OrderEventAny::Triggered(OrderTriggeredSpec::builder().build())
5074 }
5075 OrderStatus::PartiallyFilled => OrderEventAny::Filled(
5076 OrderFilledSpec::builder()
5077 .order_type(order_type)
5078 .last_qty(Quantity::from(40_000))
5079 .build(),
5080 ),
5081 OrderStatus::PendingUpdate => {
5082 OrderEventAny::PendingUpdate(OrderPendingUpdateSpec::builder().build())
5083 }
5084 OrderStatus::PendingCancel => {
5085 OrderEventAny::PendingCancel(OrderPendingCancelSpec::builder().build())
5086 }
5087 _ => panic!("Unsupported setup status: {status}"),
5088 };
5089
5090 order.apply(event).unwrap();
5091
5092 let expected = if *status == OrderStatus::PartiallyFilled
5093 && source_status == OrderStatus::PendingCancel
5094 {
5095 OrderStatus::PendingCancel
5096 } else {
5097 *status
5098 };
5099
5100 assert_eq!(order.status(), expected);
5101 }
5102
5103 let previous_status = order.previous_status();
5104 let quantity = order.quantity();
5105 let filled_qty = order.filled_qty();
5106 let leaves_qty = order.leaves_qty();
5107 let venue_order_id = order.venue_order_id();
5108 let event_count = order.events().len();
5109 let cancel_rejected =
5110 OrderEventAny::CancelRejected(OrderCancelRejectedSpec::builder().build());
5111
5112 order.apply(cancel_rejected.clone()).unwrap();
5113
5114 assert_eq!(order.status(), expected_status);
5115 assert_eq!(order.previous_status(), previous_status);
5116 assert_eq!(order.quantity(), quantity);
5117 assert_eq!(order.filled_qty(), filled_qty);
5118 assert_eq!(order.leaves_qty(), leaves_qty);
5119 assert_eq!(order.venue_order_id(), venue_order_id);
5120 assert_eq!(order.events().len(), event_count + 1);
5121 assert_eq!(order.last_event(), &cancel_rejected);
5122
5123 if expected_status == OrderStatus::Submitted {
5124 order
5125 .apply(OrderEventAny::Accepted(
5126 OrderAcceptedSpec::builder().build(),
5127 ))
5128 .unwrap();
5129 assert_eq!(order.status(), OrderStatus::Accepted);
5130 }
5131
5132 if expected_status == OrderStatus::PendingUpdate {
5133 let restored_status = previous_status.unwrap();
5134
5135 let resolution = if modify_rejected {
5136 OrderEventAny::ModifyRejected(OrderModifyRejectedSpec::builder().build())
5137 } else {
5138 OrderEventAny::Updated(
5139 OrderUpdatedSpec::builder()
5140 .quantity(Quantity::from(80_000))
5141 .build(),
5142 )
5143 };
5144
5145 order.apply(resolution).unwrap();
5146
5147 assert_eq!(order.status(), restored_status);
5148 assert_eq!(order.previous_status(), previous_status);
5149 assert_eq!(
5150 order.quantity(),
5151 if modify_rejected {
5152 quantity
5153 } else {
5154 Quantity::from(80_000)
5155 }
5156 );
5157 assert_eq!(order.filled_qty(), filled_qty);
5158 assert_eq!(order.leaves_qty(), order.quantity() - filled_qty);
5159 assert_eq!(order.venue_order_id(), venue_order_id);
5160 }
5161
5162 order
5163 .apply(OrderEventAny::PendingCancel(
5164 OrderPendingCancelSpec::builder().build(),
5165 ))
5166 .unwrap();
5167 order
5168 .apply(OrderEventAny::Canceled(
5169 OrderCanceledSpec::builder().build(),
5170 ))
5171 .unwrap();
5172
5173 assert_eq!(order.status(), OrderStatus::Canceled);
5174 }
5175
5176 #[rstest]
5177 fn test_rejected_event_preserves_cancel_rejected_restoration() {
5178 let init = OrderInitializedSpec::builder().build();
5179 let submitted = OrderSubmittedSpec::builder().build();
5180 let accepted = OrderAcceptedSpec::builder().build();
5181 let invalid = OrderSubmittedSpec::builder().build();
5182 let pending_cancel = OrderPendingCancelSpec::builder().build();
5183 let cancel_rejected = OrderCancelRejectedSpec::builder().build();
5184 let mut order: MarketOrder = init.try_into().unwrap();
5185 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5186 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
5187 let previous_status = order.previous_status();
5188
5189 let result = order.apply(OrderEventAny::Submitted(invalid));
5190
5191 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
5192 assert_eq!(order.previous_status(), previous_status);
5193
5194 order
5195 .apply(OrderEventAny::PendingCancel(pending_cancel))
5196 .unwrap();
5197 order
5198 .apply(OrderEventAny::CancelRejected(cancel_rejected))
5199 .unwrap();
5200 assert_eq!(order.status(), OrderStatus::Accepted);
5201 }
5202
5203 #[rstest]
5204 #[case(true, false, "client_order_id")]
5205 #[case(false, true, "strategy_id")]
5206 fn test_update_identity_mismatch_precedes_field_validation(
5207 #[case] wrong_client: bool,
5208 #[case] wrong_strategy: bool,
5209 #[case] expected_field: &str,
5210 ) {
5211 let init = OrderInitializedSpec::builder().build();
5212 let submitted = OrderSubmittedSpec::builder().build();
5213 let accepted = OrderAcceptedSpec::builder().build();
5214 let mut order: MarketOrder = init.try_into().unwrap();
5215 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5216 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
5217 let state = (
5218 order.status(),
5219 order.previous_status(),
5220 order.ts_last(),
5221 order.events().len(),
5222 );
5223 let client_order_id = if wrong_client {
5224 ClientOrderId::from("O-INVALID")
5225 } else {
5226 order.client_order_id()
5227 };
5228 let strategy_id = if wrong_strategy {
5229 StrategyId::from("OTHER-001")
5230 } else {
5231 order.strategy_id()
5232 };
5233 let updated = OrderUpdatedSpec::builder()
5236 .client_order_id(client_order_id)
5237 .strategy_id(strategy_id)
5238 .price(Price::new(95.0, 2))
5239 .build();
5240
5241 let result = order.apply(OrderEventAny::Updated(updated));
5242
5243 let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
5245 else {
5246 panic!("expected an identity predicate violation, was {result:?}");
5247 };
5248 assert!(
5249 message.contains(expected_field),
5250 "message did not name {expected_field}: {message}"
5251 );
5252 assert_eq!(order.status(), state.0);
5253 assert_eq!(order.previous_status(), state.1);
5254 assert_eq!(order.ts_last(), state.2);
5255 assert_eq!(order.events().len(), state.3);
5256 }
5257
5258 #[rstest]
5259 fn test_update_invalid_transition_precedes_field_validation() {
5260 let init = OrderInitializedSpec::builder().build();
5261 let denied = OrderDeniedSpec::builder().build();
5262 let mut order: MarketOrder = init.try_into().unwrap();
5263 order.apply(OrderEventAny::Denied(denied)).unwrap();
5264 let state = (
5265 order.status(),
5266 order.previous_status(),
5267 order.ts_last(),
5268 order.events().len(),
5269 );
5270 let updated = OrderUpdatedSpec::builder()
5273 .price(Price::new(95.0, 2))
5274 .build();
5275
5276 let result = order.apply(OrderEventAny::Updated(updated));
5277
5278 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
5279 assert_eq!(order.status(), state.0);
5280 assert_eq!(order.previous_status(), state.1);
5281 assert_eq!(order.ts_last(), state.2);
5282 assert_eq!(order.events().len(), state.3);
5283 }
5284
5285 #[rstest]
5286 fn test_direct_update_panics_on_invalid_field_before_mutation() {
5287 let init = OrderInitializedSpec::builder()
5288 .quantity(Quantity::from(100_000))
5289 .build();
5290 let mut order: MarketOrder = init.try_into().unwrap();
5291 let original_quantity = order.quantity();
5292 let updated = OrderUpdatedSpec::builder()
5297 .quantity(Quantity::from(50_000))
5298 .price(Price::new(95.0, 2))
5299 .build();
5300
5301 let result =
5302 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| order.update(&updated)));
5303
5304 assert!(result.is_err());
5305 assert_eq!(order.quantity(), original_quantity);
5306 }
5307
5308 #[rstest]
5309 fn test_partially_filled_order_can_be_updated() {
5310 let init = OrderInitializedSpec::builder()
5313 .quantity(Quantity::from(100_000))
5314 .build();
5315 let submitted = OrderSubmittedSpec::builder().build();
5316 let accepted = OrderAcceptedSpec::builder().build();
5317 let partial_fill = OrderFilledSpec::builder()
5318 .last_qty(Quantity::from(40_000))
5319 .build();
5320 let updated = OrderUpdatedSpec::builder()
5321 .quantity(Quantity::from(80_000)) .build();
5323
5324 let mut order: MarketOrder = init.try_into().unwrap();
5325 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5326 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
5327 order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
5328
5329 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
5330 assert_eq!(order.filled_qty(), Quantity::from(40_000));
5331
5332 order.apply(OrderEventAny::Updated(updated)).unwrap();
5333
5334 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
5335 assert_eq!(order.quantity(), Quantity::from(80_000));
5336 assert_eq!(order.leaves_qty(), Quantity::from(40_000)); }
5338
5339 #[rstest]
5340 fn test_reconciliation_update_closes_order_at_filled_quantity() {
5341 let mut order: MarketOrder = OrderInitializedSpec::builder()
5342 .quantity(Quantity::from(100))
5343 .build()
5344 .try_into()
5345 .unwrap();
5346 order
5347 .apply(OrderEventAny::Accepted(
5348 OrderAcceptedSpec::builder().build(),
5349 ))
5350 .unwrap();
5351 order
5352 .apply(OrderEventAny::Filled(
5353 OrderFilledSpec::builder()
5354 .last_qty(Quantity::from(40))
5355 .build(),
5356 ))
5357 .unwrap();
5358 let updated = OrderEventAny::Updated(
5359 OrderUpdatedSpec::builder()
5360 .quantity(Quantity::from(40))
5361 .reconciliation(true)
5362 .ts_event(UnixNanos::from(5_000))
5363 .build(),
5364 );
5365
5366 order.apply(updated.clone()).unwrap();
5367 let replayed =
5368 OrderAny::from_events(order.events().into_iter().cloned().collect()).unwrap();
5369
5370 assert_eq!(order.status(), OrderStatus::Filled);
5371 assert_eq!(order.previous_status(), Some(OrderStatus::PartiallyFilled));
5372 assert_eq!(order.quantity(), Quantity::from(40));
5373 assert_eq!(order.filled_qty(), Quantity::from(40));
5374 assert_eq!(order.leaves_qty(), Quantity::from(0));
5375 assert_eq!(order.ts_closed(), Some(UnixNanos::from(5_000)));
5376 assert_eq!(order.ts_last(), UnixNanos::from(5_000));
5377 assert_eq!(order.event_count(), 4);
5378 assert_eq!(order.last_event(), &updated);
5379 assert_eq!(replayed.status(), order.status());
5380 assert_eq!(replayed.previous_status(), order.previous_status());
5381 assert_eq!(replayed.quantity(), order.quantity());
5382 assert_eq!(replayed.filled_qty(), order.filled_qty());
5383 assert_eq!(replayed.leaves_qty(), order.leaves_qty());
5384 assert_eq!(replayed.ts_closed(), order.ts_closed());
5385 assert_eq!(replayed.ts_last(), order.ts_last());
5386 }
5387
5388 #[rstest]
5389 fn test_triggered_order_can_be_updated() {
5390 let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
5393 let submitted = OrderSubmittedSpec::builder().build();
5394 let accepted = OrderAcceptedSpec::builder().build();
5395 let triggered = OrderTriggeredSpec::builder().build();
5396 let updated = OrderUpdatedSpec::builder()
5397 .quantity(Quantity::from(80_000))
5398 .build();
5399
5400 let mut order = OrderTestBuilder::new(OrderType::StopLimit)
5401 .instrument_id(instrument_id)
5402 .quantity(Quantity::from(100_000))
5403 .price(Price::from("0.99500"))
5404 .trigger_price(Price::from("1.00000"))
5405 .trigger_type(TriggerType::LastPrice)
5406 .build();
5407 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5408 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
5409 order.apply(OrderEventAny::Triggered(triggered)).unwrap();
5410
5411 assert_eq!(order.status(), OrderStatus::Triggered);
5412
5413 order.apply(OrderEventAny::Updated(updated)).unwrap();
5414
5415 assert_eq!(order.status(), OrderStatus::Triggered);
5416 assert_eq!(order.quantity(), Quantity::from(80_000));
5417 }
5418
5419 #[rstest]
5420 fn test_order_updated_with_is_quote_quantity_clears_flag() {
5421 let init = OrderInitializedSpec::builder()
5422 .quantity(Quantity::new(10.0, 6))
5423 .quote_quantity(true)
5424 .build();
5425 let submitted = OrderSubmittedSpec::builder().build();
5426 let accepted = OrderAcceptedSpec::builder().build();
5427 let updated = OrderUpdatedSpec::builder()
5428 .quantity(Quantity::new(47.393_365, 6))
5429 .is_quote_quantity(false)
5430 .build();
5431
5432 let mut order: MarketOrder = init.try_into().unwrap();
5433 assert!(order.is_quote_quantity());
5434
5435 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5436 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
5437 order.apply(OrderEventAny::Updated(updated)).unwrap();
5438
5439 assert!(!order.is_quote_quantity());
5440 assert_eq!(order.quantity(), Quantity::new(47.393_365, 6));
5441 assert_eq!(order.leaves_qty(), Quantity::new(47.393_365, 6));
5442 }
5443
5444 #[rstest]
5445 fn test_order_updated_default_is_quote_quantity_clears_flag() {
5446 let init = OrderInitializedSpec::builder()
5447 .quantity(Quantity::new(10.0, 6))
5448 .quote_quantity(true)
5449 .build();
5450 let submitted = OrderSubmittedSpec::builder().build();
5451 let accepted = OrderAcceptedSpec::builder().build();
5452 let updated = OrderUpdatedSpec::builder()
5454 .quantity(Quantity::new(8.0, 6))
5455 .build();
5456
5457 let mut order: MarketOrder = init.try_into().unwrap();
5458 assert!(order.is_quote_quantity());
5459
5460 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5461 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
5462 order.apply(OrderEventAny::Updated(updated)).unwrap();
5463
5464 assert!(!order.is_quote_quantity());
5465 assert_eq!(order.quantity(), Quantity::new(8.0, 6));
5466 }
5467
5468 #[rstest]
5469 #[case::partial(40_000, OrderStatus::Expired, 10)]
5470 #[case::complete(100_000, OrderStatus::Filled, 20)]
5471 fn test_expired_order_receiving_fill(
5472 #[case] fill_qty: u64,
5473 #[case] expected_status: OrderStatus,
5474 #[case] closed_at: u64,
5475 ) {
5476 let mut order: LimitOrder = OrderInitializedSpec::builder()
5477 .order_type(OrderType::Limit)
5478 .quantity(Quantity::from(100_000))
5479 .price(Price::from("1.00000"))
5480 .time_in_force(TimeInForce::Gtd)
5481 .expire_time(UnixNanos::from(10))
5482 .build()
5483 .try_into()
5484 .unwrap();
5485 order
5486 .apply(OrderEventAny::Accepted(
5487 OrderAcceptedSpec::builder().build(),
5488 ))
5489 .unwrap();
5490 let expired = OrderEventAny::Expired(
5491 OrderExpiredSpec::builder()
5492 .ts_event(UnixNanos::from(10))
5493 .build(),
5494 );
5495 order.apply(expired).unwrap();
5496 let fill = OrderEventAny::Filled(
5497 OrderFilledSpec::builder()
5498 .order_type(OrderType::Limit)
5499 .last_qty(Quantity::from(fill_qty))
5500 .last_px(Price::from("1.25000"))
5501 .commission(Money::from("2 USD"))
5502 .ts_event(UnixNanos::from(20))
5503 .build(),
5504 );
5505 let mut persisted_events: Vec<_> = order.events().into_iter().cloned().collect();
5506 order.apply(fill.clone()).unwrap();
5507 persisted_events.push(order.last_event().clone());
5508 let repeated_expiry = OrderEventAny::Expired(
5509 OrderExpiredSpec::builder()
5510 .ts_event(UnixNanos::from(30))
5511 .build(),
5512 );
5513 order.apply(repeated_expiry.clone()).unwrap();
5514 persisted_events.push(order.last_event().clone());
5515 let replayed = OrderAny::from_events(persisted_events).unwrap();
5516
5517 assert_eq!(order.status(), expected_status);
5518 assert_eq!(order.filled_qty(), Quantity::from(fill_qty));
5519 assert_eq!(order.leaves_qty(), Quantity::from(100_000 - fill_qty));
5520 assert_eq!(order.avg_px(), Some(dec!(1.25)));
5521 assert_eq!(
5522 order.commissions().get(&Currency::USD()),
5523 Some(&Money::from("2 USD"))
5524 );
5525 assert_eq!(order.ts_closed(), Some(UnixNanos::from(closed_at)));
5526 assert_eq!(order.ts_last(), UnixNanos::from(20));
5527 assert_eq!(order.events().len(), 5);
5528 assert_eq!(order.last_event(), &repeated_expiry);
5529 assert_eq!(replayed.status(), expected_status);
5530 assert_eq!(replayed.filled_qty(), Quantity::from(fill_qty));
5531 assert_eq!(replayed.leaves_qty(), Quantity::from(100_000 - fill_qty));
5532 assert_eq!(replayed.avg_px(), Some(dec!(1.25)));
5533 assert_eq!(replayed.commissions(), order.commissions());
5534 assert_eq!(replayed.ts_closed(), Some(UnixNanos::from(closed_at)));
5535 assert_eq!(replayed.ts_last(), UnixNanos::from(20));
5536 assert_eq!(replayed.events(), order.events());
5537 assert!(matches!(
5538 order.apply(repeated_expiry),
5539 Err(OrderError::InvalidStateTransition)
5540 ));
5541 assert_eq!(order.events(), replayed.events());
5542 assert!(order.is_closed());
5543 assert!(!order.is_open());
5544 assert!(matches!(
5545 order.apply(fill),
5546 Err(OrderError::DuplicateFill(_))
5547 ));
5548 assert_eq!(order.filled_qty(), Quantity::from(fill_qty));
5549 }
5550
5551 #[rstest]
5552 fn test_expired_order_rejects_expiry_without_new_fill() {
5553 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
5554 order
5555 .apply(OrderEventAny::Accepted(
5556 OrderAcceptedSpec::builder().build(),
5557 ))
5558 .unwrap();
5559 order
5560 .apply(OrderEventAny::Expired(
5561 OrderExpiredSpec::builder()
5562 .ts_event(UnixNanos::from(10))
5563 .build(),
5564 ))
5565 .unwrap();
5566 let events: Vec<_> = order.events().into_iter().cloned().collect();
5567
5568 let result = order.apply(OrderEventAny::Expired(
5569 OrderExpiredSpec::builder()
5570 .ts_event(UnixNanos::from(30))
5571 .build(),
5572 ));
5573
5574 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
5575 assert_eq!(order.status(), OrderStatus::Expired);
5576 assert_eq!(order.ts_closed(), Some(UnixNanos::from(10)));
5577 assert_eq!(order.ts_last(), UnixNanos::from(10));
5578 assert_eq!(order.events(), events.iter().collect::<Vec<_>>());
5579 }
5580
5581 #[rstest]
5582 fn test_filled_order_rejects_expiry_without_prior_expiry() {
5583 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
5584 order
5585 .apply(OrderEventAny::Accepted(
5586 OrderAcceptedSpec::builder().build(),
5587 ))
5588 .unwrap();
5589 order
5590 .apply(OrderEventAny::Filled(OrderFilledSpec::builder().build()))
5591 .unwrap();
5592
5593 let result = order.apply(OrderEventAny::Expired(OrderExpiredSpec::builder().build()));
5594
5595 assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
5596 assert_eq!(order.status(), OrderStatus::Filled);
5597 assert_eq!(order.filled_qty(), Quantity::from(100_000));
5598 assert_eq!(order.events().len(), 3);
5599 }
5600
5601 #[rstest]
5602 fn test_canceled_order_accepts_repeated_cancel_after_partial_fill() {
5603 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
5604 let submitted = OrderSubmittedSpec::builder().build();
5605 let accepted = OrderAcceptedSpec::builder().build();
5606 let canceled1 = OrderCanceledSpec::builder().build();
5607 let fill = OrderFilledSpec::builder()
5608 .last_qty(Quantity::from(50_000))
5609 .trade_id(TradeId::from("FILL-1"))
5610 .build();
5611 let canceled2 = OrderCanceledSpec::builder().build();
5612
5613 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5614 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
5615 order.apply(OrderEventAny::Canceled(canceled1)).unwrap();
5616 assert_eq!(order.status(), OrderStatus::Canceled);
5617 assert!(order.is_closed());
5618
5619 order.apply(OrderEventAny::Filled(fill)).unwrap();
5620 assert_eq!(order.status(), OrderStatus::Canceled);
5621 assert_eq!(order.filled_qty(), Quantity::from(50_000));
5622 assert_eq!(order.leaves_qty(), Quantity::from(50_000));
5623 assert!(order.is_closed());
5624 assert!(!order.is_open());
5625
5626 order.apply(OrderEventAny::Canceled(canceled2)).unwrap();
5627 assert_eq!(order.status(), OrderStatus::Canceled);
5628 assert_eq!(order.filled_qty(), Quantity::from(50_000));
5629 assert_eq!(order.leaves_qty(), Quantity::from(50_000));
5630 assert_eq!(
5631 order
5632 .events()
5633 .iter()
5634 .filter(|event| matches!(event, OrderEventAny::Canceled(_)))
5635 .count(),
5636 2
5637 );
5638 assert!(order.is_closed());
5639 assert!(!order.is_open());
5640 }
5641
5642 #[rstest]
5643 fn test_updated_restores_status_before_pending_cancel() {
5644 let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
5645 let submitted = OrderSubmittedSpec::builder().build();
5646 let accepted = OrderAcceptedSpec::builder().build();
5647 let pending_cancel = OrderPendingCancelSpec::builder().build();
5648 let fill = OrderFilledSpec::builder()
5649 .last_qty(Quantity::from(10_000))
5650 .build();
5651 let updated = OrderUpdatedSpec::builder()
5652 .quantity(Quantity::from(150_000))
5653 .build();
5654
5655 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5656 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
5657 let ts_accepted = order.ts_accepted();
5658 let venue_order_ids: Vec<VenueOrderId> =
5659 order.venue_order_ids().into_iter().copied().collect();
5660 order
5661 .apply(OrderEventAny::PendingCancel(pending_cancel))
5662 .unwrap();
5663 order.apply(OrderEventAny::Filled(fill)).unwrap();
5664 assert_eq!(order.status(), OrderStatus::PendingCancel);
5665 assert_eq!(order.previous_status(), Some(OrderStatus::PartiallyFilled));
5666 order.apply(OrderEventAny::Updated(updated)).unwrap();
5667
5668 assert_eq!(order.status(), OrderStatus::PartiallyFilled);
5669 assert_eq!(order.quantity(), Quantity::from(150_000));
5670 assert_eq!(order.filled_qty(), Quantity::from(10_000));
5671 assert_eq!(order.leaves_qty(), Quantity::from(140_000));
5672 assert_eq!(order.ts_accepted(), ts_accepted);
5673 assert_eq!(
5674 order
5675 .venue_order_ids()
5676 .into_iter()
5677 .copied()
5678 .collect::<Vec<_>>(),
5679 venue_order_ids,
5680 );
5681 }
5682
5683 #[rstest]
5684 fn test_apply_triggered_to_stop_market_order_returns_error() {
5685 let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
5686 let submitted = OrderSubmittedSpec::builder().build();
5687 let accepted = OrderAcceptedSpec::builder().build();
5688 let triggered = OrderTriggeredSpec::builder().build();
5689
5690 let mut order = OrderTestBuilder::new(OrderType::StopMarket)
5691 .instrument_id(instrument_id)
5692 .quantity(Quantity::from(1))
5693 .trigger_price(Price::from("1.00000"))
5694 .trigger_type(TriggerType::LastPrice)
5695 .build();
5696 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5697 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
5698
5699 let result = order.apply(OrderEventAny::Triggered(triggered));
5700 assert!(result.is_err());
5701 assert_eq!(order.status(), OrderStatus::Accepted);
5702 }
5703
5704 #[rstest]
5705 fn test_apply_triggered_to_stop_limit_order_succeeds() {
5706 let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
5707 let submitted = OrderSubmittedSpec::builder().build();
5708 let accepted = OrderAcceptedSpec::builder().build();
5709 let triggered = OrderTriggeredSpec::builder().build();
5710
5711 let mut order = OrderTestBuilder::new(OrderType::StopLimit)
5712 .instrument_id(instrument_id)
5713 .quantity(Quantity::from(1))
5714 .price(Price::from("0.99500"))
5715 .trigger_price(Price::from("1.00000"))
5716 .trigger_type(TriggerType::LastPrice)
5717 .build();
5718 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5719 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
5720 order.apply(OrderEventAny::Triggered(triggered)).unwrap();
5721
5722 assert_eq!(order.status(), OrderStatus::Triggered);
5723 }
5724
5725 #[rstest]
5726 fn test_to_order_status_report_for_accepted_limit_order() {
5727 let order = OrderTestBuilder::new(OrderType::Limit)
5728 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
5729 .side(OrderSide::Buy)
5730 .quantity(Quantity::from(100_000))
5731 .price(Price::from("1.00000"))
5732 .build();
5733 let accepted = TestOrderStubs::make_accepted_order(&order);
5734
5735 let report = accepted.to_order_status_report(None).unwrap();
5736
5737 assert_eq!(report.account_id, AccountId::from("SIM-001"));
5738 assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
5739 assert_eq!(report.client_order_id, Some(accepted.client_order_id()));
5740 assert_eq!(report.venue_order_id, VenueOrderId::from("V-001"));
5741 assert_eq!(report.order_side, Some(OrderSide::Buy));
5742 assert_eq!(report.order_type, OrderType::Limit);
5743 assert_eq!(report.time_in_force, accepted.time_in_force());
5744 assert_eq!(report.order_status, OrderStatus::Accepted);
5745 assert_eq!(report.quantity, Quantity::from(100_000));
5746 assert_eq!(report.filled_qty, Quantity::from(0));
5747 assert_eq!(report.price, Some(Price::from("1.00000")));
5748 assert_eq!(report.avg_px, None);
5749 assert_eq!(report.ts_accepted, accepted.ts_accepted().unwrap());
5750 assert_eq!(report.ts_last, accepted.ts_last());
5751 assert_eq!(report.ts_init, accepted.ts_init());
5752 }
5753
5754 #[rstest]
5755 fn test_to_order_status_report_for_filled_market_order(audusd_sim: CurrencyPair) {
5756 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
5757 let order = OrderTestBuilder::new(OrderType::Market)
5758 .instrument_id(audusd_sim.id())
5759 .side(OrderSide::Buy)
5760 .quantity(Quantity::from(100_000))
5761 .build();
5762 let filled = TestOrderStubs::make_filled_order(&order, &audusd_sim, LiquiditySide::Maker);
5763 let report_id = UUID4::new();
5764
5765 let report = filled.to_order_status_report(Some(report_id)).unwrap();
5766
5767 assert_eq!(report.report_id, report_id);
5768 assert_eq!(report.order_status, OrderStatus::Filled);
5769 assert_eq!(report.quantity, Quantity::from(100_000));
5770 assert_eq!(report.filled_qty, Quantity::from(100_000));
5771 assert_eq!(report.price, None);
5772 assert_eq!(report.avg_px, Some(dec!(1)));
5773 assert_eq!(report.venue_position_id, Some(PositionId::from("1")));
5774 assert_eq!(report.ts_accepted, filled.ts_accepted().unwrap());
5775 assert_eq!(report.ts_last, filled.ts_last());
5776 }
5777
5778 #[rstest]
5779 fn test_to_order_status_report_maps_optional_fields() {
5780 let order = OrderTestBuilder::new(OrderType::StopLimit)
5781 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
5782 .side(OrderSide::Sell)
5783 .quantity(Quantity::from(100_000))
5784 .price(Price::from("0.99500"))
5785 .trigger_price(Price::from("1.00000"))
5786 .trigger_type(TriggerType::LastPrice)
5787 .time_in_force(TimeInForce::Gtd)
5788 .expire_time(UnixNanos::from(5_000_000_000))
5789 .display_qty(Quantity::from(10_000))
5790 .post_only(true)
5791 .reduce_only(true)
5792 .contingency_type(ContingencyType::Oto)
5793 .order_list_id(OrderListId::from("OL-001"))
5794 .linked_order_ids(vec![ClientOrderId::from("O-CHILD")])
5795 .parent_order_id(ClientOrderId::from("O-PARENT"))
5796 .build();
5797 let accepted = TestOrderStubs::make_accepted_order(&order);
5798
5799 let report = accepted.to_order_status_report(None).unwrap();
5800
5801 assert_eq!(report.price, Some(Price::from("0.99500")));
5802 assert_eq!(report.trigger_price, Some(Price::from("1.00000")));
5803 assert_eq!(report.trigger_type, Some(TriggerType::LastPrice));
5804 assert_eq!(report.expire_time, Some(UnixNanos::from(5_000_000_000)));
5805 assert_eq!(report.display_qty, Some(Quantity::from(10_000)));
5806 assert!(report.post_only);
5807 assert!(report.reduce_only);
5808 assert_eq!(report.contingency_type, Some(ContingencyType::Oto));
5809 assert_eq!(report.order_list_id, Some(OrderListId::from("OL-001")));
5810 assert_eq!(
5811 report.linked_order_ids,
5812 Some(vec![ClientOrderId::from("O-CHILD")])
5813 );
5814 assert_eq!(
5815 report.parent_order_id,
5816 Some(ClientOrderId::from("O-PARENT"))
5817 );
5818 }
5819
5820 #[rstest]
5821 fn test_to_order_status_report_maps_ts_triggered() {
5822 let order = OrderTestBuilder::new(OrderType::StopLimit)
5823 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
5824 .side(OrderSide::Buy)
5825 .quantity(Quantity::from(100_000))
5826 .price(Price::from("0.99500"))
5827 .trigger_price(Price::from("1.00000"))
5828 .trigger_type(TriggerType::LastPrice)
5829 .build();
5830 let mut order = TestOrderStubs::make_accepted_order(&order);
5831 let triggered = OrderTriggeredSpec::builder()
5832 .ts_event(UnixNanos::from(1_500_000_000))
5833 .build();
5834 order.apply(OrderEventAny::Triggered(triggered)).unwrap();
5835
5836 let report = order.to_order_status_report(None).unwrap();
5837
5838 assert_eq!(report.order_status, OrderStatus::Triggered);
5839 assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
5840 }
5841
5842 #[rstest]
5843 fn test_to_order_status_report_maps_rejection_reason() {
5844 let order = OrderTestBuilder::new(OrderType::Limit)
5845 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
5846 .side(OrderSide::Buy)
5847 .quantity(Quantity::from(100_000))
5848 .price(Price::from("1.00000"))
5849 .build();
5850 let mut order = TestOrderStubs::make_accepted_order(&order);
5851 let rejected = OrderRejectedSpec::builder()
5852 .reason(Ustr::from("INSUFFICIENT_MARGIN"))
5853 .build();
5854 order.apply(OrderEventAny::Rejected(rejected)).unwrap();
5855
5856 let report = order.to_order_status_report(None).unwrap();
5857
5858 assert_eq!(report.order_status, OrderStatus::Rejected);
5859 assert_eq!(
5860 report.cancel_reason,
5861 Some("INSUFFICIENT_MARGIN".to_string())
5862 );
5863 }
5864
5865 #[rstest]
5866 fn test_to_order_status_report_maps_distinct_timestamps() {
5867 let mut order = OrderTestBuilder::new(OrderType::Limit)
5868 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
5869 .side(OrderSide::Buy)
5870 .quantity(Quantity::from(100_000))
5871 .price(Price::from("1.00000"))
5872 .ts_init(UnixNanos::from(1_000))
5873 .build();
5874 let submitted = OrderSubmittedSpec::builder()
5875 .ts_event(UnixNanos::from(2_000))
5876 .build();
5877 let accepted = OrderAcceptedSpec::builder()
5878 .ts_event(UnixNanos::from(3_000))
5879 .build();
5880 let filled = OrderFilledSpec::builder()
5881 .last_qty(Quantity::from(50_000))
5882 .ts_event(UnixNanos::from(4_000))
5883 .build();
5884 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5885 order.apply(OrderEventAny::Accepted(accepted)).unwrap();
5886 order.apply(OrderEventAny::Filled(filled)).unwrap();
5887
5888 let report = order.to_order_status_report(None).unwrap();
5889
5890 assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
5891 assert_eq!(report.filled_qty, Quantity::from(50_000));
5892 assert_eq!(report.ts_accepted, UnixNanos::from(3_000));
5893 assert_eq!(report.ts_last, UnixNanos::from(4_000));
5894 assert_eq!(report.ts_init, UnixNanos::from(1_000));
5895 }
5896
5897 #[rstest]
5898 fn test_to_order_status_report_ts_accepted_falls_back_to_ts_last() {
5899 let mut order = OrderTestBuilder::new(OrderType::Limit)
5901 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
5902 .side(OrderSide::Buy)
5903 .quantity(Quantity::from(100_000))
5904 .price(Price::from("1.00000"))
5905 .build();
5906 let submitted = OrderSubmittedSpec::builder()
5907 .ts_event(UnixNanos::from(2_000))
5908 .build();
5909 let updated = OrderUpdatedSpec::builder()
5910 .venue_order_id(VenueOrderId::from("V-001"))
5911 .ts_event(UnixNanos::from(3_000))
5912 .build();
5913 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5914 order.apply(OrderEventAny::Updated(updated)).unwrap();
5915
5916 let report = order.to_order_status_report(None).unwrap();
5917
5918 assert_eq!(report.order_status, OrderStatus::Submitted);
5919 assert_eq!(report.venue_order_id, VenueOrderId::from("V-001"));
5920 assert_eq!(report.ts_accepted, UnixNanos::from(3_000));
5921 assert_eq!(report.ts_last, UnixNanos::from(3_000));
5922 }
5923
5924 #[rstest]
5925 fn test_to_order_status_report_maps_trailing_offsets() {
5926 let order = OrderTestBuilder::new(OrderType::TrailingStopLimit)
5927 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
5928 .side(OrderSide::Sell)
5929 .quantity(Quantity::from(100_000))
5930 .price(Price::from("0.99500"))
5931 .activation_price(Price::from("1.00500"))
5932 .trigger_price(Price::from("1.00000"))
5933 .trigger_type(TriggerType::LastPrice)
5934 .limit_offset(dec!(0.0001))
5935 .trailing_offset(dec!(0.0002))
5936 .trailing_offset_type(TrailingOffsetType::Price)
5937 .build();
5938 let accepted = TestOrderStubs::make_accepted_order(&order);
5939
5940 let report = accepted.to_order_status_report(None).unwrap();
5941
5942 assert_eq!(report.activation_price, Some(Price::from("1.00500")));
5943 assert_eq!(report.limit_offset, Some(dec!(0.0001)));
5944 assert_eq!(report.trailing_offset, Some(dec!(0.0002)));
5945 assert_eq!(report.trailing_offset_type, Some(TrailingOffsetType::Price),);
5946 }
5947
5948 #[rstest]
5949 fn test_to_order_status_report_returns_none_before_venue_ack() {
5950 let order = OrderTestBuilder::new(OrderType::Limit)
5951 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
5952 .side(OrderSide::Buy)
5953 .quantity(Quantity::from(100_000))
5954 .price(Price::from("1.00000"))
5955 .build();
5956
5957 assert!(order.to_order_status_report(None).is_none());
5958 }
5959
5960 #[rstest]
5961 fn test_to_order_status_report_returns_none_for_submitted_order() {
5962 let mut order = OrderTestBuilder::new(OrderType::Limit)
5964 .instrument_id(InstrumentId::from("AUDUSD.SIM"))
5965 .side(OrderSide::Buy)
5966 .quantity(Quantity::from(100_000))
5967 .price(Price::from("1.00000"))
5968 .build();
5969 let submitted = OrderSubmittedSpec::builder().build();
5970 order.apply(OrderEventAny::Submitted(submitted)).unwrap();
5971
5972 assert!(order.account_id().is_some());
5973 assert!(order.to_order_status_report(None).is_none());
5974 }
5975}