Skip to main content

nautilus_model/orders/
mod.rs

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