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