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