Skip to main content

nautilus_model/orders/
mod.rs

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