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