Skip to main content

nautilus_model/orders/
mod.rs

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