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