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