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