Skip to main content

nautilus_model/orders/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Order types for the trading domain model.
17//!
18//! Each order type wraps an [`OrderCore`] carrying the state common to all of them and adds its
19//! own type-specific fields. [`OrderAny`] dispatches over the concrete types.
20//!
21//! Orders are event sourced. [`OrderCore::apply`] validates the status transition, runs the
22//! handler for that event, then appends it, so [`OrderAny::from_events`] replays a stream to
23//! reconstruct its event-derived state.
24//!
25//! A fill-void correction rebuilds the derived fill state from the fills that survive it.
26//! `avg_px` folds those fills in `Decimal` and keeps the quotient at that precision, so no float
27//! conversion enters the next average and a rebuild agrees with the incremental update over the
28//! same fills. `slippage` derives from `avg_px` in the same arithmetic.
29
30pub 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
48// Re-exports
49use 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
95/// Order types that have stop/trigger prices.
96pub const STOP_ORDER_TYPES: &[OrderType] = &[
97    OrderType::StopMarket,
98    OrderType::StopLimit,
99    OrderType::MarketIfTouched,
100    OrderType::LimitIfTouched,
101];
102
103/// Order types that have limit prices.
104pub const LIMIT_ORDER_TYPES: &[OrderType] = &[
105    OrderType::Limit,
106    OrderType::StopLimit,
107    OrderType::LimitIfTouched,
108    OrderType::TrailingStopLimit,
109];
110
111/// Order types that support the TRIGGERED order status.
112///
113/// Market-style stops (`StopMarket`, `MarketIfTouched`, `TrailingStopMarket`) execute
114/// immediately on trigger and have no intermediate TRIGGERED state.
115pub const TRIGGERABLE_ORDER_TYPES: &[OrderType] = &[
116    OrderType::StopLimit,
117    OrderType::TrailingStopLimit,
118    OrderType::LimitIfTouched,
119];
120
121/// Order statuses for locally active orders (pre-submission to venue).
122pub 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/// Converts an `IndexMap` with `Ustr` keys and values to `String` keys and values.
157#[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/// Converts an `IndexMap` with `String` keys and values to `Ustr` keys and values.
165#[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    /// Transitions the order state machine based on the given `event`.
197    ///
198    /// # Errors
199    ///
200    /// Returns an error if the state transition is invalid from the current status.
201    #[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,  // Emulated orders
206            (Self::Initialized, OrderEventAny::Released(_)) => Self::Released,  // Emulated orders
207            (Self::Initialized, OrderEventAny::Submitted(_)) => Self::Submitted,
208            (Self::Initialized, OrderEventAny::Rejected(_)) => Self::Rejected,  // External orders
209            (Self::Initialized, OrderEventAny::Accepted(_)) => Self::Accepted,  // External orders
210            (Self::Initialized, OrderEventAny::Canceled(_)) => Self::Canceled,  // External orders
211            (Self::Initialized, OrderEventAny::Expired(_)) => Self::Expired,  // External orders
212            (Self::Initialized, OrderEventAny::Triggered(_)) => Self::Triggered, // External orders
213            (Self::Initialized, OrderEventAny::Updated(_)) => Self::Initialized, // In-place modification
214            (Self::Emulated, OrderEventAny::Canceled(_)) => Self::Canceled,  // Emulated orders
215            (Self::Emulated, OrderEventAny::Expired(_)) => Self::Expired,  // Emulated orders
216            (Self::Emulated, OrderEventAny::Updated(_)) => Self::Emulated, // In-place modification
217            (Self::Emulated, OrderEventAny::Released(_)) => Self::Released,  // Emulated orders
218            (Self::Released, OrderEventAny::Submitted(_)) => Self::Submitted,  // Emulated orders
219            (Self::Released, OrderEventAny::Denied(_)) => Self::Denied,  // Emulated orders
220            (Self::Released, OrderEventAny::Canceled(_)) => Self::Canceled,  // Execution algo
221            (Self::Released, OrderEventAny::Updated(_)) => Self::Released, // In-place modification
222            (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,  // FOK and IOC cases
226            (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,  // StopLimit order
230            (Self::Accepted, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
231            (Self::Accepted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
232            (Self::Accepted, OrderEventAny::CancelRejected(_)) => Self::Accepted,  // Acceptance can overtake a cancel rejection
233            (Self::Accepted, OrderEventAny::Canceled(_)) => Self::Canceled,
234            (Self::Accepted, OrderEventAny::Triggered(_)) => Self::Triggered,
235            (Self::Accepted, OrderEventAny::Updated(_)) => Self::Accepted,  // Updates should preserve state
236            (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,  // Real world possibility
240            (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,  // Real world possibility
248            (Self::PendingUpdate, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,  // Allow multiple requests
249            (Self::PendingUpdate, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
250            (Self::PendingUpdate, OrderEventAny::ModifyRejected(_)) => Self::PendingUpdate,  // Handled by modify_rejected to restore previous_status
251            (Self::PendingUpdate, OrderEventAny::Updated(_)) => Self::PendingUpdate,  // Handled by updated to restore previous_status
252            (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,  // Allow multiple requests
256            (Self::PendingCancel, OrderEventAny::ModifyRejected(_)) => Self::PendingCancel, // Preserve in-flight cancellation
257            (Self::PendingCancel, OrderEventAny::CancelRejected(_)) => Self::PendingCancel,  // Handled by cancel_rejected to restore previous_status
258            (Self::PendingCancel, OrderEventAny::Canceled(_)) => Self::Canceled,
259            (Self::PendingCancel, OrderEventAny::Expired(_)) => Self::Expired,
260            (Self::PendingCancel, OrderEventAny::Accepted(_)) => Self::Accepted,  // Allow failed cancel requests
261            (Self::PendingCancel, OrderEventAny::Updated(_)) => Self::PendingCancel,  // Handled by updated to restore previous_status
262            (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    /// Calculates potential overfill quantity without mutating order state.
340    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    /// Applies the `event` to the order.
362    ///
363    /// # Errors
364    ///
365    /// Returns an error if the event is invalid for the current order status.
366    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    /// Returns `true` if a fill with matching `trade_id`, side, qty, and price already exists.
389    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    /// Builds an [`OrderStatusReport`] snapshot from the order's current state.
527    ///
528    /// Returns `None` if the order has no `venue_order_id` or `account_id`:
529    /// a status report describes a venue-acknowledged order, and both fields
530    /// are set by the time a venue acknowledges the order.
531    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        // The trait exposes no trigger or rejection metadata accessors,
611        // recover the last occurrences from the event history.
612        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        // `cancel_reason` is the report's only reason channel,
628        // reconciliation reads it for rejected orders.
629        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>; // TODO: Temporary on trait
641    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    /// Creates a new [`OrderCore`] instance.
747    #[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    /// Applies the `event` to the order.
797    ///
798    /// # Errors
799    ///
800    /// Returns an error if the event is invalid for the current order status, or if
801    /// `event.client_order_id()` or `event.strategy_id()` does not match the order.
802    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        // Check for duplicate fill before state transition to maintain consistency
830        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        // Reject an update carrying fields the order type cannot hold before any
886        // mutation. Runs after the identity and transition checks so an identity
887        // mismatch or invalid transition is reported ahead of a field violation.
888        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        // Save current status as previous_status for ALL transitions except:
908        // - Initialized (no prior state exists)
909        // - ModifyRejected/CancelRejected (need to preserve the pre Pending state)
910        // - When already in Pending* state (avoid overwriting the pre Pending state when receiving multiple pending requests)
911        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            // Rejected by the pre-commit check above; kept for exhaustiveness
927            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        // Do nothing else
957    }
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        // Do nothing else
981    }
982
983    fn pending_cancel(&self, _event: &OrderPendingCancel) {
984        // Do nothing else
985    }
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        // Calculate overfill if any
1269        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            // Set ts_accepted to time of first fill if not previously set
1323            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    // `Order::apply` appends the event after its handler runs, so a correction or fill still in
1360    // flight arrives as `additional` / `pending` rather than through `self.events`.
1361    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    /// Returns the opposite order side.
1431    #[must_use]
1432    pub fn opposite_side(side: OrderSide) -> OrderSide {
1433        side.opposite()
1434    }
1435
1436    /// Returns the order side needed to close a position.
1437    #[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    // TODO: WIP
1519    // fn test_display_market_order() {
1520    //     let order = MarketOrder::default();
1521    //     assert_eq!(order.events().len(), 1);
1522    //     assert_eq!(
1523    //         stringify!(order.events().get(0)),
1524    //         stringify!(OrderInitialized)
1525    //     );
1526    // }
1527
1528    #[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        // Options and spreads can legitimately trade at negative prices. The
1634        // weighted average-price update must not panic when `last_px` or the
1635        // prior `avg_px` is below zero.
1636        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        // Weighted avg: (50_000 * -5.0 + 50_000 * -7.0) / 100_000 = -6.0
1662        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        // (1 * 0.07) + (2 * 0.13) + (3 * 0.29) = 1.20 over 6 filled, so the exact average is
1711        // 0.20. Accumulating the notional in f64 returns 0.19999999999999998 instead.
1712        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        // (1 * 1.00) + (2 * 2.00) = 5.00 over 3 filled. The exact quotient repeats, so `Decimal`
1732        // carries it to its full 28-place scale where the widest `f64` holds 1.6666666666666667.
1733        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        // Slippage only accrues against the order: a buy filled above the reference price and a
1760        // sell filled below it. The opposite cases are price improvement and stay `None`.
1761        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        // (50_000 * 1.00001) + (30_000 * 1.00002) + (20_000 * 1.00003) = 100_001.7 over
1869        // 100_000 filled, so the exact average is 1.000017 whichever order the fills arrive
1870        // in. The f64 fold returns 1.0000170000000002 for the ascending sequence only.
1871        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        // (1 * 0.05) + (3 * 0.15) = 0.50 over 4 filled: 0.125 exactly, whether the fold runs
1911        // incrementally over the fills, as the rebuild after the void drops the third fill, or
1912        // over the whole event stream on replay. The f64 rebuild returns 0.12499999999999999.
1913        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        // Surviving (2 * 1.00) + (2 * 1.14) = 4.28 over 4 filled: 1.07 exactly, where the f64
1942        // rebuild returns 1.0699999999999998.
1943        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        // The reopened void leaves 2 of the first fill, so the replacement fill folds against
1979        // the surviving quantity: (2 * 1.00) + (2 * 1.14) = 4.28 over 4 filled, or 1.07
1980        // exactly. The f64 update returns 1.0699999999999998, which slippage then inherits.
1981        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        // Same weighting as `test_avg_px_invariant_to_fill_arrival_order`, at a quantity
2017        // precision only the wider raw backing can hold.
2018        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        // Pins the exactness bound: a 16-decimal quantity against a 12-decimal price needs the
2037        // 28 fractional digits Decimal carries, so no product rounds.
2038        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        // (2e-16 * 1e-12) + (6e-16 * 3e-12) = 2e-27 over 8e-16 filled: 2.5e-12 exactly.
2048        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        // Create order with distinct timestamps to verify parameter ordering
2927        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        // Verify timestamps are in correct positions
2944        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        // Test external order flow: Initialized -> Accepted (no Submitted)
2980        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        // Verify account_id is initially None
2988        assert_eq!(order.account_id(), None);
2989
2990        // Apply accepted event directly (external order case)
2991        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2992
2993        // Verify account_id is now set from the accepted event
2994        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        // Test normal order flow: Initialized -> Submitted -> Accepted
3001        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        // After submitted, account_id should be set
3013        assert_eq!(order.account_id(), Some(AccountId::from("SUBMITTED-001")));
3014
3015        // Apply accepted event
3016        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
3017
3018        // account_id should now be updated to the accepted event's account_id
3019        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        // Test that overfill is tracked on the order
3026        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)) // Overfill: 110k > 100k
3033            .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        // Order should track overfill
3041        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        // Test multiple fills resulting in overfill
3050        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)) // Total 110k > 100k
3061            .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        // After first fill, no overfill
3070        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        // After second fill, overfill detected
3077        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        // Test that exact fill doesn't trigger overfill tracking
3086        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)) // Exact fill
3093            .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        // No overfill
3101        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        // Simulates real exchange scenario with fractional fills:
3109        // Order for 2450.5 units, partially filled 1202.5, then fill of 1285.5 arrives
3110        // Total filled: 2488.0, overfill: 37.5
3111        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")) // 1202.5 + 1285.5 = 2488 > 2450.5
3122            .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        // After first fill, no overfill
3131        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        // After second fill, overfill detected and tracked
3139        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        // Fill qty less than order qty - no overfill
3154        let overfill = order.calculate_overfill(Quantity::from(50_000));
3155        assert_eq!(overfill, Quantity::from(0));
3156
3157        // Fill qty equals order qty - no overfill
3158        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        // Fill qty exceeds order qty
3171        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        // Order is 60k filled, 40k remaining
3192        // Fill of 50k would overfill by 10k
3193        let overfill = order.calculate_overfill(Quantity::from(50_000));
3194        assert_eq!(overfill, Quantity::from(10_000));
3195
3196        // Fill of 40k would not overfill
3197        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        // Simulates the exact scenario from user's log
3210        // Order for 2450.5, if fill of 2488.0 arrives
3211        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        // After filling 0.072 of 1.000, another 0.072 fill should not overfill
3232        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")) // Same trade_id as fill1
3250            .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        // Verify first fill applied successfully
3258        assert_eq!(order.filled_qty(), Quantity::from(50_000));
3259        assert_eq!(order.status(), OrderStatus::PartiallyFilled);
3260
3261        // Applying duplicate fill should return DuplicateFill error
3262        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        // Order state should be unchanged after rejected duplicate
3272        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")) // Different trade_id
3390            .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        // Both fills should be applied
3399        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        // A distinct account and timestamp so the `submitted` handler's writes are
3536        // observable: with identical specs the assertions below cannot tell whether
3537        // the handler ran at all.
3538        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        // The order holds PendingUpdate and keeps the pre-update status, so the
3563        // in-flight modify can still be resolved.
3564        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        // Carry both a mismatched identity and a price a MarketOrder cannot hold:
3740        // the identity mismatch must be reported ahead of the field violation.
3741        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        // The specific identity predicate must surface, not the field violation.
3750        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        // No (Denied, Updated) transition exists, and the price is invalid for a
3777        // MarketOrder: the transition check must win over the field violation.
3778        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        // A distinct quantity so any mutation would be observable, alongside a
3799        // price the type cannot hold. The low-level mutation path retains a
3800        // defensive guard: a direct update() panics on the invalid field before
3801        // it reaches the quantity assignment.
3802        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        // Test that a partially filled order can receive an Updated event
3817        // and remain in PartiallyFilled status
3818        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)) // Reduce to 80k (still > 40k filled)
3828            .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)); // 80k - 40k filled
3843    }
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        // Test that a triggered order can receive an Updated event
3897        // and remain in Triggered status
3898        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        // Builder defaults is_quote_quantity to false
3959        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        // Venue ack can arrive through an update before any acceptance event
4273        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        // Inflight orders have an account_id but no venue_order_id yet
4336        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}