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
18pub mod any;
19#[cfg(any(test, feature = "stubs"))]
20pub mod builder;
21pub mod limit;
22pub mod limit_if_touched;
23pub mod list;
24pub mod market;
25pub mod market_if_touched;
26pub mod market_to_limit;
27pub mod stop_limit;
28pub mod stop_market;
29pub mod trailing_stop_limit;
30pub mod trailing_stop_market;
31
32#[cfg(any(test, feature = "stubs"))]
33pub mod stubs;
34
35// Re-exports
36use ahash::AHashSet;
37use enum_dispatch::enum_dispatch;
38use indexmap::IndexMap;
39use nautilus_core::{
40    UUID4, UnixNanos,
41    correctness::{CorrectnessError, check_predicate_false},
42};
43use rust_decimal::Decimal;
44use serde::{Deserialize, Serialize};
45use ustr::Ustr;
46
47#[cfg(any(test, feature = "stubs"))]
48pub use crate::orders::builder::OrderTestBuilder;
49pub use crate::orders::{
50    any::{LimitOrderAny, OrderAny, OrderReplayError, PassiveOrderAny, StopOrderAny},
51    limit::LimitOrder,
52    limit_if_touched::LimitIfTouchedOrder,
53    list::{OrderList, OrderListValidationError},
54    market::MarketOrder,
55    market_if_touched::MarketIfTouchedOrder,
56    market_to_limit::MarketToLimitOrder,
57    stop_limit::StopLimitOrder,
58    stop_market::StopMarketOrder,
59    trailing_stop_limit::TrailingStopLimitOrder,
60    trailing_stop_market::TrailingStopMarketOrder,
61};
62use crate::{
63    enums::{
64        ContingencyType, LiquiditySide, OrderSide, OrderSideSpecified, OrderStatus, OrderType,
65        PositionSide, TimeInForce, TrailingOffsetType, TriggerType,
66    },
67    events::{
68        OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
69        OrderEventAny, OrderExpired, OrderFilled, OrderInitialized, OrderModifyRejected,
70        OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased, OrderSubmitted,
71        OrderTriggered, OrderUpdated,
72    },
73    identifiers::{
74        AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
75        StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
76    },
77    orderbook::OwnBookOrder,
78    reports::OrderStatusReport,
79    types::{Currency, Money, Price, Quantity},
80};
81
82/// Order types that have stop/trigger prices.
83pub const STOP_ORDER_TYPES: &[OrderType] = &[
84    OrderType::StopMarket,
85    OrderType::StopLimit,
86    OrderType::MarketIfTouched,
87    OrderType::LimitIfTouched,
88];
89
90/// Order types that have limit prices.
91pub const LIMIT_ORDER_TYPES: &[OrderType] = &[
92    OrderType::Limit,
93    OrderType::StopLimit,
94    OrderType::LimitIfTouched,
95    OrderType::TrailingStopLimit,
96];
97
98/// Order types that support the TRIGGERED order status.
99///
100/// Market-style stops (`StopMarket`, `MarketIfTouched`, `TrailingStopMarket`) execute
101/// immediately on trigger and have no intermediate TRIGGERED state.
102pub const TRIGGERABLE_ORDER_TYPES: &[OrderType] = &[
103    OrderType::StopLimit,
104    OrderType::TrailingStopLimit,
105    OrderType::LimitIfTouched,
106];
107
108/// Order statuses for locally active orders (pre-submission to venue).
109pub const LOCAL_ACTIVE_ORDER_STATUSES: &[OrderStatus] = &[
110    OrderStatus::Initialized,
111    OrderStatus::Emulated,
112    OrderStatus::Released,
113];
114
115/// Order statuses that are safe for cancellation queries.
116///
117/// These are statuses where an order is working on the venue but not already
118/// in the process of being cancelled. Including `PENDING_CANCEL` in cancellation
119/// filters can cause duplicate cancel attempts or incorrect open order counts.
120///
121/// Note: `PENDING_UPDATE` is included as orders being updated can typically still
122/// be cancelled (update and cancel are independent operations on most venues).
123pub const CANCELLABLE_ORDER_STATUSES: &[OrderStatus] = &[
124    OrderStatus::Accepted,
125    OrderStatus::Triggered,
126    OrderStatus::PendingUpdate,
127    OrderStatus::PartiallyFilled,
128];
129
130/// Returns a cached `AHashSet` of cancellable order statuses for O(1) lookups.
131///
132/// For the small set (4 elements), using `CANCELLABLE_ORDER_STATUSES.contains()` may be
133/// equally fast due to better cache locality. Use this function when you need set operations
134/// or are building HashSet-based filters.
135///
136/// Note: This is a module-level convenience function. You can also use
137/// `OrderStatus::cancellable_statuses_set()` directly.
138#[must_use]
139pub fn cancellable_order_statuses_set() -> &'static AHashSet<OrderStatus> {
140    OrderStatus::cancellable_statuses_set()
141}
142
143#[derive(thiserror::Error, Debug)]
144pub enum OrderError {
145    #[error("Order not found: {0}")]
146    NotFound(ClientOrderId),
147    #[error("Order invariant failed: must have a side for this operation")]
148    NoOrderSide,
149    #[error("Invalid event for order type")]
150    InvalidOrderEvent,
151    #[error("Invalid order state transition")]
152    InvalidStateTransition,
153    #[error("Order was already initialized")]
154    AlreadyInitialized,
155    #[error("Order had no previous state")]
156    NoPreviousState,
157    #[error("Duplicate fill: trade_id {0} already applied to order")]
158    DuplicateFill(TradeId),
159    #[error("{0}")]
160    Invariant(#[from] CorrectnessError),
161}
162
163/// Converts an `IndexMap` with `Ustr` keys and values to `String` keys and values.
164#[must_use]
165pub fn ustr_indexmap_to_str(h: IndexMap<Ustr, Ustr>) -> IndexMap<String, String> {
166    h.into_iter()
167        .map(|(k, v)| (k.to_string(), v.to_string()))
168        .collect()
169}
170
171/// Converts an `IndexMap` with `String` keys and values to `Ustr` keys and values.
172#[must_use]
173pub fn str_indexmap_to_ustr(h: IndexMap<String, String>) -> IndexMap<Ustr, Ustr> {
174    h.into_iter()
175        .map(|(k, v)| (Ustr::from(&k), Ustr::from(&v)))
176        .collect()
177}
178
179#[inline]
180pub(crate) fn check_display_qty(
181    display_qty: Option<Quantity>,
182    quantity: Quantity,
183) -> Result<(), OrderError> {
184    if let Some(q) = display_qty {
185        check_predicate_false(q > quantity, "`display_qty` may not exceed `quantity`")?;
186    }
187    Ok(())
188}
189
190#[inline]
191pub(crate) fn check_time_in_force(
192    time_in_force: TimeInForce,
193    expire_time: Option<UnixNanos>,
194) -> Result<(), OrderError> {
195    check_predicate_false(
196        time_in_force == TimeInForce::Gtd && expire_time.unwrap_or_default() == 0,
197        "`expire_time` is required for `GTD` order",
198    )?;
199    Ok(())
200}
201
202impl OrderStatus {
203    /// Transitions the order state machine based on the given `event`.
204    ///
205    /// # Errors
206    ///
207    /// Returns an error if the state transition is invalid from the current status.
208    #[rustfmt::skip]
209    pub fn transition(&mut self, event: &OrderEventAny) -> Result<Self, OrderError> {
210        let new_state = match (self, event) {
211            (Self::Initialized, OrderEventAny::Denied(_)) => Self::Denied,
212            (Self::Initialized, OrderEventAny::Emulated(_)) => Self::Emulated,  // Emulated orders
213            (Self::Initialized, OrderEventAny::Released(_)) => Self::Released,  // Emulated orders
214            (Self::Initialized, OrderEventAny::Submitted(_)) => Self::Submitted,
215            (Self::Initialized, OrderEventAny::Rejected(_)) => Self::Rejected,  // External orders
216            (Self::Initialized, OrderEventAny::Accepted(_)) => Self::Accepted,  // External orders
217            (Self::Initialized, OrderEventAny::Canceled(_)) => Self::Canceled,  // External orders
218            (Self::Initialized, OrderEventAny::Expired(_)) => Self::Expired,  // External orders
219            (Self::Initialized, OrderEventAny::Triggered(_)) => Self::Triggered, // External orders
220            (Self::Initialized, OrderEventAny::Updated(_)) => Self::Initialized, // In-place modification
221            (Self::Emulated, OrderEventAny::Canceled(_)) => Self::Canceled,  // Emulated orders
222            (Self::Emulated, OrderEventAny::Expired(_)) => Self::Expired,  // Emulated orders
223            (Self::Emulated, OrderEventAny::Updated(_)) => Self::Emulated, // In-place modification
224            (Self::Emulated, OrderEventAny::Released(_)) => Self::Released,  // Emulated orders
225            (Self::Released, OrderEventAny::Submitted(_)) => Self::Submitted,  // Emulated orders
226            (Self::Released, OrderEventAny::Denied(_)) => Self::Denied,  // Emulated orders
227            (Self::Released, OrderEventAny::Canceled(_)) => Self::Canceled,  // Execution algo
228            (Self::Released, OrderEventAny::Updated(_)) => Self::Released, // In-place modification
229            (Self::Submitted, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
230            (Self::Submitted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
231            (Self::Submitted, OrderEventAny::Rejected(_)) => Self::Rejected,
232            (Self::Submitted, OrderEventAny::Canceled(_)) => Self::Canceled,  // FOK and IOC cases
233            (Self::Submitted, OrderEventAny::Accepted(_)) => Self::Accepted,
234            (Self::Submitted, OrderEventAny::Updated(_)) => Self::Submitted,
235            (Self::Submitted, OrderEventAny::Filled(_)) => Self::Filled,
236            (Self::Accepted, OrderEventAny::Rejected(_)) => Self::Rejected,  // StopLimit order
237            (Self::Accepted, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
238            (Self::Accepted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
239            (Self::Accepted, OrderEventAny::Canceled(_)) => Self::Canceled,
240            (Self::Accepted, OrderEventAny::Triggered(_)) => Self::Triggered,
241            (Self::Accepted, OrderEventAny::Updated(_)) => Self::Accepted,  // Updates should preserve state
242            (Self::Accepted, OrderEventAny::Expired(_)) => Self::Expired,
243            (Self::Accepted, OrderEventAny::Filled(_)) => Self::Filled,
244            (Self::Canceled, OrderEventAny::Filled(_)) => Self::Filled,  // Real world possibility
245            (Self::PendingUpdate, OrderEventAny::Rejected(_)) => Self::Rejected,
246            (Self::PendingUpdate, OrderEventAny::Accepted(_)) => Self::Accepted,
247            (Self::PendingUpdate, OrderEventAny::Canceled(_)) => Self::Canceled,
248            (Self::PendingUpdate, OrderEventAny::Expired(_)) => Self::Expired,
249            (Self::PendingUpdate, OrderEventAny::Triggered(_)) => Self::Triggered,
250            (Self::PendingUpdate, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,  // Allow multiple requests
251            (Self::PendingUpdate, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
252            (Self::PendingUpdate, OrderEventAny::ModifyRejected(_)) => Self::PendingUpdate,  // Handled by modify_rejected to restore previous_status
253            (Self::PendingUpdate, OrderEventAny::Updated(_)) => Self::PendingUpdate,  // Handled by updated to restore previous_status
254            (Self::PendingUpdate, OrderEventAny::Filled(_)) => Self::Filled,
255            (Self::PendingCancel, OrderEventAny::Rejected(_)) => Self::Rejected,
256            (Self::PendingCancel, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,  // Allow multiple requests
257            (Self::PendingCancel, OrderEventAny::CancelRejected(_)) => Self::PendingCancel,  // Handled by cancel_rejected to restore previous_status
258            (Self::PendingCancel, OrderEventAny::Canceled(_)) => Self::Canceled,
259            (Self::PendingCancel, OrderEventAny::Expired(_)) => Self::Expired,
260            (Self::PendingCancel, OrderEventAny::Accepted(_)) => Self::Accepted,  // Allow failed cancel requests
261            (Self::PendingCancel, OrderEventAny::Filled(_)) => Self::Filled,
262            (Self::Triggered, OrderEventAny::Rejected(_)) => Self::Rejected,
263            (Self::Triggered, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
264            (Self::Triggered, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
265            (Self::Triggered, OrderEventAny::Canceled(_)) => Self::Canceled,
266            (Self::Triggered, OrderEventAny::Expired(_)) => Self::Expired,
267            (Self::Triggered, OrderEventAny::Filled(_)) => Self::Filled,
268            (Self::Triggered, OrderEventAny::Updated(_)) => 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            _ => return Err(OrderError::InvalidStateTransition),
277        };
278        Ok(new_state)
279    }
280}
281
282#[enum_dispatch]
283pub trait Order: 'static + Send {
284    fn into_any(self) -> OrderAny;
285    fn status(&self) -> OrderStatus;
286    fn trader_id(&self) -> TraderId;
287    fn strategy_id(&self) -> StrategyId;
288    fn instrument_id(&self) -> InstrumentId;
289    fn symbol(&self) -> Symbol;
290    fn venue(&self) -> Venue;
291    fn client_order_id(&self) -> ClientOrderId;
292    fn venue_order_id(&self) -> Option<VenueOrderId>;
293    fn position_id(&self) -> Option<PositionId>;
294    fn account_id(&self) -> Option<AccountId>;
295    fn last_trade_id(&self) -> Option<TradeId>;
296    fn order_side(&self) -> OrderSide;
297    fn order_type(&self) -> OrderType;
298    fn quantity(&self) -> Quantity;
299    fn time_in_force(&self) -> TimeInForce;
300    fn expire_time(&self) -> Option<UnixNanos>;
301    fn price(&self) -> Option<Price>;
302    fn trigger_price(&self) -> Option<Price>;
303    fn activation_price(&self) -> Option<Price> {
304        None
305    }
306    fn trigger_type(&self) -> Option<TriggerType>;
307    fn liquidity_side(&self) -> Option<LiquiditySide>;
308    fn is_post_only(&self) -> bool;
309    fn is_reduce_only(&self) -> bool;
310    fn is_quote_quantity(&self) -> bool;
311    fn display_qty(&self) -> Option<Quantity>;
312    fn limit_offset(&self) -> Option<Decimal>;
313    fn trailing_offset(&self) -> Option<Decimal>;
314    fn trailing_offset_type(&self) -> Option<TrailingOffsetType>;
315    fn emulation_trigger(&self) -> Option<TriggerType>;
316    fn trigger_instrument_id(&self) -> Option<InstrumentId>;
317    fn contingency_type(&self) -> Option<ContingencyType>;
318    fn order_list_id(&self) -> Option<OrderListId>;
319    fn linked_order_ids(&self) -> Option<&[ClientOrderId]>;
320    fn parent_order_id(&self) -> Option<ClientOrderId>;
321    fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId>;
322    fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>>;
323    fn exec_spawn_id(&self) -> Option<ClientOrderId>;
324    fn tags(&self) -> Option<&[Ustr]>;
325    fn filled_qty(&self) -> Quantity;
326    fn leaves_qty(&self) -> Quantity;
327    fn overfill_qty(&self) -> Quantity;
328
329    /// Calculates potential overfill quantity without mutating order state.
330    fn calculate_overfill(&self, fill_qty: Quantity) -> Quantity {
331        let potential_filled = self.filled_qty() + fill_qty;
332        let quantity = self.quantity();
333        if potential_filled > quantity {
334            potential_filled - quantity
335        } else {
336            Quantity::zero(fill_qty.precision)
337        }
338    }
339
340    fn avg_px(&self) -> Option<f64>;
341    fn slippage(&self) -> Option<f64>;
342    fn init_id(&self) -> UUID4;
343    fn ts_init(&self) -> UnixNanos;
344    fn ts_submitted(&self) -> Option<UnixNanos>;
345    fn ts_accepted(&self) -> Option<UnixNanos>;
346    fn ts_closed(&self) -> Option<UnixNanos>;
347    fn ts_last(&self) -> UnixNanos;
348
349    fn order_side_specified(&self) -> OrderSideSpecified {
350        self.order_side().as_specified()
351    }
352    fn commissions(&self) -> &IndexMap<Currency, Money>;
353
354    /// Applies the `event` to the order.
355    ///
356    /// # Errors
357    ///
358    /// Returns an error if the event is invalid for the current order status.
359    fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError>;
360    fn update(&mut self, event: &OrderUpdated);
361
362    fn events(&self) -> Vec<&OrderEventAny>;
363
364    fn last_event(&self) -> &OrderEventAny {
365        self.events()
366            .last()
367            .expect("Order invariant violated: no events")
368    }
369
370    fn event_count(&self) -> usize {
371        self.events().len()
372    }
373
374    fn venue_order_ids(&self) -> Vec<&VenueOrderId>;
375
376    fn trade_ids(&self) -> Vec<&TradeId>;
377
378    fn has_price(&self) -> bool;
379
380    /// Returns `true` if a fill with matching `trade_id`, side, qty, and price already exists.
381    fn is_duplicate_fill(&self, fill: &OrderFilled) -> bool {
382        self.events().iter().any(|event| {
383            if let OrderEventAny::Filled(existing) = event {
384                existing.trade_id == fill.trade_id
385                    && existing.order_side == fill.order_side
386                    && existing.last_qty == fill.last_qty
387                    && existing.last_px == fill.last_px
388            } else {
389                false
390            }
391        })
392    }
393
394    fn is_buy(&self) -> bool {
395        self.order_side() == OrderSide::Buy
396    }
397
398    fn is_sell(&self) -> bool {
399        self.order_side() == OrderSide::Sell
400    }
401
402    fn is_passive(&self) -> bool {
403        self.order_type() != OrderType::Market
404    }
405
406    fn is_aggressive(&self) -> bool {
407        self.order_type() == OrderType::Market
408    }
409
410    fn is_emulated(&self) -> bool {
411        self.status() == OrderStatus::Emulated
412    }
413
414    fn is_active_local(&self) -> bool {
415        matches!(
416            self.status(),
417            OrderStatus::Initialized | OrderStatus::Emulated | OrderStatus::Released
418        )
419    }
420
421    fn is_primary(&self) -> bool {
422        self.exec_algorithm_id().is_some()
423            && self
424                .exec_spawn_id()
425                .is_some_and(|spawn_id| self.client_order_id() == spawn_id)
426    }
427
428    fn is_spawned(&self) -> bool {
429        self.exec_algorithm_id().is_some()
430            && self
431                .exec_spawn_id()
432                .is_some_and(|spawn_id| self.client_order_id() != spawn_id)
433    }
434
435    fn is_contingency(&self) -> bool {
436        self.contingency_type().is_some()
437    }
438
439    fn is_parent_order(&self) -> bool {
440        match self.contingency_type() {
441            Some(c) => c == ContingencyType::Oto,
442            None => false,
443        }
444    }
445
446    fn is_child_order(&self) -> bool {
447        self.parent_order_id().is_some()
448    }
449
450    fn is_open(&self) -> bool {
451        if let Some(emulation_trigger) = self.emulation_trigger()
452            && emulation_trigger != TriggerType::NoTrigger
453        {
454            return false;
455        }
456
457        matches!(
458            self.status(),
459            OrderStatus::Accepted
460                | OrderStatus::Triggered
461                | OrderStatus::PendingCancel
462                | OrderStatus::PendingUpdate
463                | OrderStatus::PartiallyFilled
464        )
465    }
466
467    fn is_canceled(&self) -> bool {
468        self.status() == OrderStatus::Canceled
469    }
470
471    fn is_closed(&self) -> bool {
472        matches!(
473            self.status(),
474            OrderStatus::Denied
475                | OrderStatus::Rejected
476                | OrderStatus::Canceled
477                | OrderStatus::Expired
478                | OrderStatus::Filled
479        )
480    }
481
482    fn is_inflight(&self) -> bool {
483        if let Some(emulation_trigger) = self.emulation_trigger()
484            && emulation_trigger != TriggerType::NoTrigger
485        {
486            return false;
487        }
488
489        matches!(
490            self.status(),
491            OrderStatus::Submitted | OrderStatus::PendingCancel | OrderStatus::PendingUpdate
492        )
493    }
494
495    fn is_pending_update(&self) -> bool {
496        self.status() == OrderStatus::PendingUpdate
497    }
498
499    fn is_pending_cancel(&self) -> bool {
500        self.status() == OrderStatus::PendingCancel
501    }
502
503    fn to_own_book_order(&self) -> OwnBookOrder {
504        OwnBookOrder::new(
505            self.trader_id(),
506            self.client_order_id(),
507            self.venue_order_id(),
508            self.order_side().as_specified(),
509            self.price().expect("`OwnBookOrder` must have a price"), // TBD
510            self.quantity(),
511            self.order_type(),
512            self.time_in_force(),
513            self.status(),
514            self.ts_last(),
515            self.ts_accepted().unwrap_or_default(),
516            self.ts_submitted().unwrap_or_default(),
517            self.ts_init(),
518        )
519    }
520
521    /// Builds an [`OrderStatusReport`] snapshot from the order's current state.
522    ///
523    /// Returns `None` if the order has no `venue_order_id` or `account_id`:
524    /// a status report describes a venue-acknowledged order, and both fields
525    /// are set by the time a venue acknowledges the order.
526    fn to_order_status_report(&self, report_id: Option<UUID4>) -> Option<OrderStatusReport> {
527        let account_id = self.account_id()?;
528        let venue_order_id = self.venue_order_id()?;
529
530        let mut report = OrderStatusReport::new(
531            account_id,
532            self.instrument_id(),
533            Some(self.client_order_id()),
534            venue_order_id,
535            self.order_side(),
536            self.order_type(),
537            self.time_in_force(),
538            self.status(),
539            self.quantity(),
540            self.filled_qty(),
541            self.ts_accepted().unwrap_or_else(|| self.ts_last()),
542            self.ts_last(),
543            self.ts_init(),
544            report_id,
545        )
546        .with_post_only(self.is_post_only())
547        .with_reduce_only(self.is_reduce_only());
548
549        if let Some(price) = self.price() {
550            report = report.with_price(price);
551        }
552
553        if let Some(trigger_price) = self.trigger_price() {
554            report = report.with_trigger_price(trigger_price);
555        }
556
557        if let Some(trigger_type) = self.trigger_type() {
558            report = report.with_trigger_type(trigger_type);
559        }
560
561        if let Some(limit_offset) = self.limit_offset() {
562            report = report.with_limit_offset(limit_offset);
563        }
564
565        if let Some(trailing_offset) = self.trailing_offset() {
566            report = report.with_trailing_offset(trailing_offset);
567        }
568
569        if let Some(trailing_offset_type) = self.trailing_offset_type() {
570            report = report.with_trailing_offset_type(trailing_offset_type);
571        }
572
573        if let Some(display_qty) = self.display_qty() {
574            report = report.with_display_qty(display_qty);
575        }
576
577        if let Some(expire_time) = self.expire_time() {
578            report = report.with_expire_time(expire_time);
579        }
580
581        if let Some(contingency_type) = self.contingency_type() {
582            report = report.with_contingency_type(contingency_type);
583        }
584
585        if let Some(order_list_id) = self.order_list_id() {
586            report = report.with_order_list_id(order_list_id);
587        }
588
589        if let Some(linked_order_ids) = self.linked_order_ids() {
590            report = report.with_linked_order_ids(linked_order_ids.iter().copied());
591        }
592
593        if let Some(parent_order_id) = self.parent_order_id() {
594            report = report.with_parent_order_id(parent_order_id);
595        }
596
597        if let Some(position_id) = self.position_id() {
598            report = report.with_venue_position_id(position_id);
599        }
600
601        // The trait exposes no trigger or rejection metadata accessors,
602        // recover the last occurrences from the event history.
603        let mut ts_triggered = None;
604        let mut rejected_reason = None;
605
606        for event in self.events() {
607            match event {
608                OrderEventAny::Triggered(triggered) => ts_triggered = Some(triggered.ts_event),
609                OrderEventAny::Rejected(rejected) => rejected_reason = Some(rejected.reason),
610                _ => {}
611            }
612        }
613
614        if let Some(ts_triggered) = ts_triggered {
615            report = report.with_ts_triggered(ts_triggered);
616        }
617
618        // `cancel_reason` is the report's only reason channel,
619        // reconciliation reads it for rejected orders.
620        if let Some(reason) = rejected_reason {
621            report = report.with_cancel_reason(reason.to_string());
622        }
623
624        // Skip a non-finite avg_px rather than fail the whole snapshot
625        if let Some(avg_px) = self.avg_px()
626            && let Ok(updated) = report.clone().with_avg_px(avg_px)
627        {
628            report = updated;
629        }
630
631        Some(report)
632    }
633
634    fn is_triggered(&self) -> Option<bool>; // TODO: Temporary on trait
635    fn set_position_id(&mut self, position_id: Option<PositionId>);
636    fn set_quantity(&mut self, quantity: Quantity);
637    fn set_leaves_qty(&mut self, leaves_qty: Quantity);
638    fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>);
639    fn set_is_quote_quantity(&mut self, is_quote_quantity: bool);
640    fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide);
641    fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool;
642    fn previous_status(&self) -> Option<OrderStatus>;
643}
644
645impl<T> From<&T> for OrderInitialized
646where
647    T: Order,
648{
649    fn from(order: &T) -> Self {
650        Self {
651            trader_id: order.trader_id(),
652            strategy_id: order.strategy_id(),
653            instrument_id: order.instrument_id(),
654            client_order_id: order.client_order_id(),
655            order_side: order.order_side(),
656            order_type: order.order_type(),
657            quantity: order.quantity(),
658            price: order.price(),
659            trigger_price: order.trigger_price(),
660            trigger_type: order.trigger_type(),
661            time_in_force: order.time_in_force(),
662            expire_time: order.expire_time(),
663            post_only: order.is_post_only(),
664            reduce_only: order.is_reduce_only(),
665            quote_quantity: order.is_quote_quantity(),
666            display_qty: order.display_qty(),
667            limit_offset: order.limit_offset(),
668            trailing_offset: order.trailing_offset(),
669            trailing_offset_type: order.trailing_offset_type(),
670            emulation_trigger: order.emulation_trigger(),
671            trigger_instrument_id: order.trigger_instrument_id(),
672            contingency_type: order.contingency_type(),
673            order_list_id: order.order_list_id(),
674            linked_order_ids: order.linked_order_ids().map(|x| x.to_vec()),
675            parent_order_id: order.parent_order_id(),
676            exec_algorithm_id: order.exec_algorithm_id(),
677            exec_algorithm_params: order.exec_algorithm_params().map(|x| x.to_owned()),
678            exec_spawn_id: order.exec_spawn_id(),
679            tags: order.tags().map(|x| x.to_vec()),
680            event_id: order.init_id(),
681            ts_event: order.ts_init(),
682            ts_init: order.ts_init(),
683            reconciliation: false,
684            causation_id: None,
685        }
686    }
687}
688
689#[derive(Clone, Debug, Serialize, Deserialize)]
690pub struct OrderCore {
691    pub events: Vec<OrderEventAny>,
692    pub commissions: IndexMap<Currency, Money>,
693    pub venue_order_ids: Vec<VenueOrderId>,
694    pub trade_ids: Vec<TradeId>,
695    pub previous_status: Option<OrderStatus>,
696    pub status: OrderStatus,
697    pub trader_id: TraderId,
698    pub strategy_id: StrategyId,
699    pub instrument_id: InstrumentId,
700    pub client_order_id: ClientOrderId,
701    pub venue_order_id: Option<VenueOrderId>,
702    pub position_id: Option<PositionId>,
703    pub account_id: Option<AccountId>,
704    pub last_trade_id: Option<TradeId>,
705    pub side: OrderSide,
706    pub order_type: OrderType,
707    pub quantity: Quantity,
708    pub time_in_force: TimeInForce,
709    pub liquidity_side: Option<LiquiditySide>,
710    pub is_reduce_only: bool,
711    pub is_quote_quantity: bool,
712    pub emulation_trigger: Option<TriggerType>,
713    pub contingency_type: Option<ContingencyType>,
714    pub order_list_id: Option<OrderListId>,
715    pub linked_order_ids: Option<Vec<ClientOrderId>>,
716    pub parent_order_id: Option<ClientOrderId>,
717    pub exec_algorithm_id: Option<ExecAlgorithmId>,
718    pub exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
719    pub exec_spawn_id: Option<ClientOrderId>,
720    pub tags: Option<Vec<Ustr>>,
721    pub filled_qty: Quantity,
722    pub leaves_qty: Quantity,
723    pub overfill_qty: Quantity,
724    pub avg_px: Option<f64>,
725    pub slippage: Option<f64>,
726    pub init_id: UUID4,
727    pub ts_init: UnixNanos,
728    pub ts_submitted: Option<UnixNanos>,
729    pub ts_accepted: Option<UnixNanos>,
730    pub ts_closed: Option<UnixNanos>,
731    pub ts_last: UnixNanos,
732}
733
734impl OrderCore {
735    /// Creates a new [`OrderCore`] instance.
736    #[must_use]
737    pub fn new(init: OrderInitialized) -> Self {
738        let events: Vec<OrderEventAny> = vec![OrderEventAny::Initialized(init.clone())];
739        Self {
740            events,
741            commissions: IndexMap::new(),
742            venue_order_ids: Vec::new(),
743            trade_ids: Vec::new(),
744            previous_status: None,
745            status: OrderStatus::Initialized,
746            trader_id: init.trader_id,
747            strategy_id: init.strategy_id,
748            instrument_id: init.instrument_id,
749            client_order_id: init.client_order_id,
750            venue_order_id: None,
751            position_id: None,
752            account_id: None,
753            last_trade_id: None,
754            side: init.order_side,
755            order_type: init.order_type,
756            quantity: init.quantity,
757            time_in_force: init.time_in_force,
758            liquidity_side: Some(LiquiditySide::NoLiquiditySide),
759            is_reduce_only: init.reduce_only,
760            is_quote_quantity: init.quote_quantity,
761            emulation_trigger: init.emulation_trigger.or(Some(TriggerType::NoTrigger)),
762            contingency_type: init
763                .contingency_type
764                .or(Some(ContingencyType::NoContingency)),
765            order_list_id: init.order_list_id,
766            linked_order_ids: init.linked_order_ids,
767            parent_order_id: init.parent_order_id,
768            exec_algorithm_id: init.exec_algorithm_id,
769            exec_algorithm_params: init.exec_algorithm_params,
770            exec_spawn_id: init.exec_spawn_id,
771            tags: init.tags,
772            filled_qty: Quantity::zero(init.quantity.precision),
773            leaves_qty: init.quantity,
774            overfill_qty: Quantity::zero(init.quantity.precision),
775            avg_px: None,
776            slippage: None,
777            init_id: init.event_id,
778            ts_init: init.ts_event,
779            ts_submitted: None,
780            ts_accepted: None,
781            ts_closed: None,
782            ts_last: init.ts_event,
783        }
784    }
785
786    /// Applies the `event` to the order.
787    ///
788    /// # Errors
789    ///
790    /// Returns an error if the event is invalid for the current order status, or if
791    /// `event.client_order_id()` or `event.strategy_id()` does not match the order.
792    pub fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
793        if self.client_order_id != event.client_order_id() {
794            return Err(CorrectnessError::PredicateViolation {
795                message: format!(
796                    "Event client_order_id {} does not match order client_order_id {}",
797                    event.client_order_id(),
798                    self.client_order_id
799                ),
800            }
801            .into());
802        }
803
804        if self.strategy_id != event.strategy_id() {
805            return Err(CorrectnessError::PredicateViolation {
806                message: format!(
807                    "Event strategy_id {} does not match order strategy_id {}",
808                    event.strategy_id(),
809                    self.strategy_id
810                ),
811            }
812            .into());
813        }
814
815        // Save current status as previous_status for ALL transitions except:
816        // - Initialized (no prior state exists)
817        // - ModifyRejected/CancelRejected (need to preserve the pre Pending state)
818        // - When already in Pending* state (avoid overwriting the pre Pending state when receiving multiple pending requests)
819        if !matches!(
820            event,
821            OrderEventAny::Initialized(_)
822                | OrderEventAny::ModifyRejected(_)
823                | OrderEventAny::CancelRejected(_)
824        ) && !matches!(
825            self.status,
826            OrderStatus::PendingUpdate | OrderStatus::PendingCancel
827        ) {
828            self.previous_status = Some(self.status);
829        }
830
831        // Check for duplicate fill before state transition to maintain consistency
832        if let OrderEventAny::Filled(fill) = &event
833            && self.trade_ids.contains(&fill.trade_id)
834        {
835            return Err(OrderError::DuplicateFill(fill.trade_id));
836        }
837
838        if matches!(event, OrderEventAny::Triggered(_))
839            && !TRIGGERABLE_ORDER_TYPES.contains(&self.order_type)
840        {
841            return Err(OrderError::InvalidOrderEvent);
842        }
843
844        let new_status = self.status.transition(&event)?;
845        self.status = new_status;
846
847        match &event {
848            OrderEventAny::Initialized(_) => return Err(OrderError::AlreadyInitialized),
849            OrderEventAny::Denied(event) => self.denied(event),
850            OrderEventAny::Emulated(event) => self.emulated(event),
851            OrderEventAny::Released(event) => self.released(event),
852            OrderEventAny::Submitted(event) => self.submitted(event),
853            OrderEventAny::Rejected(event) => self.rejected(event),
854            OrderEventAny::Accepted(event) => self.accepted(event),
855            OrderEventAny::PendingUpdate(event) => self.pending_update(event),
856            OrderEventAny::PendingCancel(event) => self.pending_cancel(event),
857            OrderEventAny::ModifyRejected(event) => self.modify_rejected(event)?,
858            OrderEventAny::CancelRejected(event) => self.cancel_rejected(event)?,
859            OrderEventAny::Updated(event) => self.updated(event),
860            OrderEventAny::Triggered(event) => self.triggered(event),
861            OrderEventAny::Canceled(event) => self.canceled(event),
862            OrderEventAny::Expired(event) => self.expired(event),
863            OrderEventAny::Filled(event) => self.filled(event),
864        }
865
866        self.ts_last = event.ts_event();
867        self.events.push(event);
868        Ok(())
869    }
870
871    fn denied(&mut self, event: &OrderDenied) {
872        self.ts_closed = Some(event.ts_event);
873    }
874
875    fn emulated(&self, _event: &OrderEmulated) {
876        // Do nothing else
877    }
878
879    fn released(&mut self, _event: &OrderReleased) {
880        self.emulation_trigger = None;
881    }
882
883    fn submitted(&mut self, event: &OrderSubmitted) {
884        self.account_id = Some(event.account_id);
885        self.ts_submitted = Some(event.ts_event);
886    }
887
888    fn accepted(&mut self, event: &OrderAccepted) {
889        self.account_id = Some(event.account_id);
890        self.venue_order_id = Some(event.venue_order_id);
891        self.venue_order_ids.push(event.venue_order_id);
892        self.ts_accepted = Some(event.ts_event);
893    }
894
895    fn rejected(&mut self, event: &OrderRejected) {
896        self.ts_closed = Some(event.ts_event);
897    }
898
899    fn pending_update(&self, _event: &OrderPendingUpdate) {
900        // Do nothing else
901    }
902
903    fn pending_cancel(&self, _event: &OrderPendingCancel) {
904        // Do nothing else
905    }
906
907    fn modify_rejected(&mut self, _event: &OrderModifyRejected) -> Result<(), OrderError> {
908        self.status = self.previous_status.ok_or(OrderError::NoPreviousState)?;
909        Ok(())
910    }
911
912    fn cancel_rejected(&mut self, _event: &OrderCancelRejected) -> Result<(), OrderError> {
913        self.status = self.previous_status.ok_or(OrderError::NoPreviousState)?;
914        Ok(())
915    }
916
917    fn triggered(&self, _event: &OrderTriggered) {}
918
919    fn canceled(&mut self, event: &OrderCanceled) {
920        self.ts_closed = Some(event.ts_event);
921    }
922
923    fn expired(&mut self, event: &OrderExpired) {
924        self.ts_closed = Some(event.ts_event);
925    }
926
927    fn updated(&mut self, event: &OrderUpdated) {
928        if self.status == OrderStatus::PendingUpdate
929            && let Some(previous) = self.previous_status
930        {
931            self.status = previous;
932        }
933
934        if let Some(venue_order_id) = &event.venue_order_id
935            && (self.venue_order_id.is_none()
936                || venue_order_id != self.venue_order_id.as_ref().unwrap())
937        {
938            self.venue_order_id = Some(*venue_order_id);
939            self.venue_order_ids.push(*venue_order_id);
940        }
941
942        self.is_quote_quantity = event.is_quote_quantity;
943    }
944
945    fn filled(&mut self, event: &OrderFilled) {
946        // Use saturating arithmetic to prevent overflow
947        let new_filled_qty = Quantity::from_raw(
948            self.filled_qty.raw.saturating_add(event.last_qty.raw),
949            self.filled_qty.precision,
950        );
951
952        // Calculate overfill if any
953        if new_filled_qty > self.quantity {
954            let overfill_raw = new_filled_qty.raw - self.quantity.raw;
955            self.overfill_qty = Quantity::from_raw(
956                self.overfill_qty.raw.saturating_add(overfill_raw),
957                self.filled_qty.precision,
958            );
959        }
960
961        if new_filled_qty < self.quantity {
962            self.status = OrderStatus::PartiallyFilled;
963        } else {
964            self.status = OrderStatus::Filled;
965            self.ts_closed = Some(event.ts_event);
966        }
967
968        self.venue_order_id = Some(event.venue_order_id);
969        self.position_id = event.position_id;
970        self.trade_ids.push(event.trade_id);
971        self.last_trade_id = Some(event.trade_id);
972        self.liquidity_side = Some(event.liquidity_side);
973        self.filled_qty = new_filled_qty;
974        self.leaves_qty = self.leaves_qty.saturating_sub(event.last_qty);
975        self.ts_last = event.ts_event;
976
977        if let Some(commission) = event.commission {
978            let commission_currency = commission.currency;
979            if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) {
980                *existing_commission = *existing_commission + commission;
981            } else {
982                self.commissions.insert(commission_currency, commission);
983            }
984        }
985
986        if self.ts_accepted.is_none() {
987            // Set ts_accepted to time of first fill if not previously set
988            self.ts_accepted = Some(event.ts_event);
989        }
990
991        self.set_avg_px(event.last_qty, event.last_px);
992
993        debug_assert!(
994            matches!(
995                self.status,
996                OrderStatus::PartiallyFilled | OrderStatus::Filled
997            ),
998            "Invariant: status must be PartiallyFilled or Filled after fill handler (status={:?})",
999            self.status
1000        );
1001        debug_assert!(
1002            self.venue_order_id.is_some()
1003                && self.last_trade_id.is_some()
1004                && !self.trade_ids.is_empty(),
1005            "Invariant: venue_order_id, last_trade_id and trade_ids must be set after fill"
1006        );
1007        debug_assert!(
1008            self.filled_qty.raw.saturating_add(self.leaves_qty.raw) >= self.quantity.raw,
1009            "Invariant: filled_qty + leaves_qty >= quantity (filled={}, leaves={}, quantity={})",
1010            self.filled_qty,
1011            self.leaves_qty,
1012            self.quantity
1013        );
1014    }
1015
1016    fn set_avg_px(&mut self, last_qty: Quantity, last_px: Price) {
1017        if self.avg_px.is_none() {
1018            self.avg_px = Some(last_px.as_f64());
1019            return;
1020        }
1021
1022        // Use previous filled quantity (before current fill) to avoid double-counting
1023        let prev_filled_qty = (self.filled_qty - last_qty).as_f64();
1024        let last_qty_f64 = last_qty.as_f64();
1025        let total_qty = prev_filled_qty + last_qty_f64;
1026
1027        debug_assert!(
1028            total_qty > 0.0,
1029            "Invariant: avg_px calc requires positive total_qty (prev={prev_filled_qty}, last={last_qty_f64})"
1030        );
1031
1032        let avg_px = self
1033            .avg_px
1034            .unwrap()
1035            .mul_add(prev_filled_qty, last_px.as_f64() * last_qty_f64)
1036            / total_qty;
1037        self.avg_px = Some(avg_px);
1038    }
1039
1040    pub fn set_slippage(&mut self, price: Price) {
1041        self.slippage = self.avg_px.and_then(|avg_px| {
1042            let current_price = price.as_f64();
1043            match self.side {
1044                OrderSide::Buy if avg_px > current_price => Some(avg_px - current_price),
1045                OrderSide::Sell if avg_px < current_price => Some(current_price - avg_px),
1046                _ => None,
1047            }
1048        });
1049    }
1050
1051    /// Returns the opposite order side.
1052    #[must_use]
1053    pub fn opposite_side(side: OrderSide) -> OrderSide {
1054        match side {
1055            OrderSide::Buy => OrderSide::Sell,
1056            OrderSide::Sell => OrderSide::Buy,
1057            OrderSide::NoOrderSide => OrderSide::NoOrderSide,
1058        }
1059    }
1060
1061    /// Returns the order side needed to close a position.
1062    #[must_use]
1063    pub fn closing_side(side: PositionSide) -> OrderSide {
1064        match side {
1065            PositionSide::Long => OrderSide::Sell,
1066            PositionSide::Short => OrderSide::Buy,
1067            PositionSide::Flat => OrderSide::NoOrderSide,
1068            PositionSide::NoPositionSide => OrderSide::NoOrderSide,
1069        }
1070    }
1071
1072    /// # Panics
1073    ///
1074    /// Panics if the order side is neither `Buy` nor `Sell`.
1075    #[must_use]
1076    pub fn signed_decimal_qty(&self) -> Decimal {
1077        match self.side {
1078            OrderSide::Buy => self.quantity.as_decimal(),
1079            OrderSide::Sell => -self.quantity.as_decimal(),
1080            OrderSide::NoOrderSide => panic!("Invalid order side"),
1081        }
1082    }
1083
1084    #[must_use]
1085    pub fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
1086        if side == PositionSide::Flat {
1087            return false;
1088        }
1089
1090        match (self.side, side) {
1091            (OrderSide::Buy, PositionSide::Long) => false,
1092            (OrderSide::Buy, PositionSide::Short) => self.leaves_qty <= position_qty,
1093            (OrderSide::Sell, PositionSide::Short) => false,
1094            (OrderSide::Sell, PositionSide::Long) => self.leaves_qty <= position_qty,
1095            _ => true,
1096        }
1097    }
1098
1099    #[must_use]
1100    pub fn commission(&self, currency: &Currency) -> Option<Money> {
1101        self.commissions.get(currency).copied()
1102    }
1103
1104    #[must_use]
1105    pub fn commissions(&self) -> IndexMap<Currency, Money> {
1106        self.commissions.clone()
1107    }
1108
1109    #[must_use]
1110    pub fn commissions_vec(&self) -> Vec<Money> {
1111        self.commissions.values().copied().collect()
1112    }
1113
1114    #[must_use]
1115    pub fn init_event(&self) -> Option<OrderEventAny> {
1116        self.events.first().cloned()
1117    }
1118}
1119
1120#[cfg(test)]
1121mod tests {
1122    use rstest::rstest;
1123    use rust_decimal_macros::dec;
1124
1125    use super::*;
1126    use crate::{
1127        enums::{LiquiditySide, OrderSide, OrderStatus, PositionSide, TriggerType},
1128        events::order::spec::{
1129            OrderAcceptedSpec, OrderCanceledSpec, OrderDeniedSpec, OrderFilledSpec,
1130            OrderInitializedSpec, OrderPendingUpdateSpec, OrderRejectedSpec, OrderSubmittedSpec,
1131            OrderTriggeredSpec, OrderUpdatedSpec,
1132        },
1133        identifiers::InstrumentId,
1134        instruments::{CurrencyPair, Instrument, InstrumentAny, stubs::audusd_sim},
1135        orders::{MarketOrder, builder::OrderTestBuilder, stubs::TestOrderStubs},
1136        types::{Price, Quantity},
1137    };
1138
1139    // TODO: WIP
1140    // fn test_display_market_order() {
1141    //     let order = MarketOrder::default();
1142    //     assert_eq!(order.events().len(), 1);
1143    //     assert_eq!(
1144    //         stringify!(order.events().get(0)),
1145    //         stringify!(OrderInitialized)
1146    //     );
1147    // }
1148
1149    #[rstest]
1150    #[case(OrderSide::Buy, OrderSide::Sell)]
1151    #[case(OrderSide::Sell, OrderSide::Buy)]
1152    #[case(OrderSide::NoOrderSide, OrderSide::NoOrderSide)]
1153    fn test_order_opposite_side(#[case] order_side: OrderSide, #[case] expected_side: OrderSide) {
1154        let result = OrderCore::opposite_side(order_side);
1155        assert_eq!(result, expected_side);
1156    }
1157
1158    #[rstest]
1159    #[case(PositionSide::Long, OrderSide::Sell)]
1160    #[case(PositionSide::Short, OrderSide::Buy)]
1161    #[case(PositionSide::NoPositionSide, OrderSide::NoOrderSide)]
1162    fn test_closing_side(#[case] position_side: PositionSide, #[case] expected_side: OrderSide) {
1163        let result = OrderCore::closing_side(position_side);
1164        assert_eq!(result, expected_side);
1165    }
1166
1167    #[rstest]
1168    #[case(OrderSide::Buy, dec!(10_000))]
1169    #[case(OrderSide::Sell, dec!(-10_000))]
1170    fn test_signed_decimal_qty(#[case] order_side: OrderSide, #[case] expected: Decimal) {
1171        let order: MarketOrder = OrderInitializedSpec::builder()
1172            .order_side(order_side)
1173            .quantity(Quantity::from(10_000))
1174            .build()
1175            .try_into()
1176            .unwrap();
1177
1178        let result = order.signed_decimal_qty();
1179        assert_eq!(result, expected);
1180    }
1181
1182    #[rustfmt::skip]
1183    #[rstest]
1184    #[case(OrderSide::Buy, Quantity::from(100), PositionSide::Long, Quantity::from(50), false)]
1185    #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(50), true)]
1186    #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(100), true)]
1187    #[case(OrderSide::Buy, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)]
1188    #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)]
1189    #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(50), true)]
1190    #[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(100), true)]
1191    #[case(OrderSide::Sell, Quantity::from(100), PositionSide::Short, Quantity::from(50), false)]
1192    fn test_would_reduce_only(
1193        #[case] order_side: OrderSide,
1194        #[case] order_qty: Quantity,
1195        #[case] position_side: PositionSide,
1196        #[case] position_qty: Quantity,
1197        #[case] expected: bool,
1198    ) {
1199        let order: MarketOrder = OrderInitializedSpec::builder()
1200            .order_side(order_side)
1201            .quantity(order_qty)
1202            .build()
1203            .try_into()
1204            .unwrap();
1205
1206        assert_eq!(
1207            order.would_reduce_only(position_side, position_qty),
1208            expected
1209        );
1210    }
1211
1212    #[rstest]
1213    fn test_order_state_transition_denied() {
1214        let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
1215        let denied = OrderDeniedSpec::builder().build();
1216        let event = OrderEventAny::Denied(denied);
1217
1218        order.apply(event.clone()).unwrap();
1219
1220        assert_eq!(order.status, OrderStatus::Denied);
1221        assert!(order.is_closed());
1222        assert!(!order.is_open());
1223        assert_eq!(order.event_count(), 2);
1224        assert_eq!(order.last_event(), &event);
1225    }
1226
1227    #[rstest]
1228    fn test_order_life_cycle_to_filled() {
1229        let init = OrderInitializedSpec::builder().build();
1230        let submitted = OrderSubmittedSpec::builder().build();
1231        let accepted = OrderAcceptedSpec::builder().build();
1232        let filled = OrderFilledSpec::builder().build();
1233
1234        let mut order: MarketOrder = init.clone().try_into().unwrap();
1235        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1236        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1237        order.apply(OrderEventAny::Filled(filled)).unwrap();
1238
1239        assert_eq!(order.client_order_id, init.client_order_id);
1240        assert_eq!(order.status(), OrderStatus::Filled);
1241        assert_eq!(order.filled_qty(), Quantity::from(100_000));
1242        assert_eq!(order.leaves_qty(), Quantity::from(0));
1243        assert_eq!(order.avg_px(), Some(1.0));
1244        assert!(!order.is_open());
1245        assert!(order.is_closed());
1246        assert_eq!(order.commission(&Currency::USD()), None);
1247        assert_eq!(order.commissions(), &IndexMap::new());
1248    }
1249
1250    #[rstest]
1251    fn test_order_life_cycle_fills_with_negative_prices() {
1252        // Options and spreads can legitimately trade at negative prices. The
1253        // weighted average-price update must not panic when `last_px` or the
1254        // prior `avg_px` is below zero.
1255        let init = OrderInitializedSpec::builder()
1256            .quantity(Quantity::from(100_000))
1257            .build();
1258        let submitted = OrderSubmittedSpec::builder().build();
1259        let accepted = OrderAcceptedSpec::builder().build();
1260        let fill1 = OrderFilledSpec::builder()
1261            .last_qty(Quantity::from(50_000))
1262            .last_px(Price::from("-5.00000"))
1263            .trade_id(TradeId::from("TRADE-1"))
1264            .build();
1265        let fill2 = OrderFilledSpec::builder()
1266            .last_qty(Quantity::from(50_000))
1267            .last_px(Price::from("-7.00000"))
1268            .trade_id(TradeId::from("TRADE-2"))
1269            .build();
1270
1271        let mut order: MarketOrder = init.try_into().unwrap();
1272        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1273        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1274        order.apply(OrderEventAny::Filled(fill1)).unwrap();
1275        order.apply(OrderEventAny::Filled(fill2)).unwrap();
1276
1277        assert_eq!(order.status(), OrderStatus::Filled);
1278        assert_eq!(order.filled_qty(), Quantity::from(100_000));
1279        assert_eq!(order.leaves_qty(), Quantity::from(0));
1280        // Weighted avg: (50_000 * -5.0 + 50_000 * -7.0) / 100_000 = -6.0
1281        assert_eq!(order.avg_px(), Some(-6.0));
1282    }
1283
1284    #[rstest]
1285    fn test_order_life_cycle_accumulates_fill_commissions() {
1286        let init = OrderInitializedSpec::builder()
1287            .quantity(Quantity::from(100_000))
1288            .build();
1289        let submitted = OrderSubmittedSpec::builder().build();
1290        let accepted = OrderAcceptedSpec::builder().build();
1291        let fill1 = OrderFilledSpec::builder()
1292            .last_qty(Quantity::from(50_000))
1293            .trade_id(TradeId::from("TRADE-1"))
1294            .commission(Money::from("1.25 USD"))
1295            .build();
1296        let fill2 = OrderFilledSpec::builder()
1297            .last_qty(Quantity::from(50_000))
1298            .trade_id(TradeId::from("TRADE-2"))
1299            .commission(Money::from("1.35 USD"))
1300            .build();
1301
1302        let mut order: MarketOrder = init.try_into().unwrap();
1303        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1304        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1305        order.apply(OrderEventAny::Filled(fill1)).unwrap();
1306        order.apply(OrderEventAny::Filled(fill2)).unwrap();
1307
1308        assert_eq!(order.status(), OrderStatus::Filled);
1309        assert_eq!(
1310            order.commission(&Currency::USD()),
1311            Some(Money::from("2.60 USD"))
1312        );
1313        assert_eq!(order.commissions_vec(), vec![Money::from("2.60 USD")]);
1314    }
1315
1316    #[rstest]
1317    fn test_order_state_transition_to_canceled() {
1318        let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
1319        let submitted = OrderSubmittedSpec::builder().build();
1320        let canceled = OrderCanceledSpec::builder().build();
1321
1322        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1323        order.apply(OrderEventAny::Canceled(canceled)).unwrap();
1324
1325        assert_eq!(order.status(), OrderStatus::Canceled);
1326        assert!(order.is_closed());
1327        assert!(!order.is_open());
1328    }
1329
1330    #[rstest]
1331    fn test_order_life_cycle_to_partially_filled() {
1332        let init = OrderInitializedSpec::builder().build();
1333        let submitted = OrderSubmittedSpec::builder().build();
1334        let accepted = OrderAcceptedSpec::builder().build();
1335        let filled = OrderFilledSpec::builder()
1336            .last_qty(Quantity::from(50_000))
1337            .build();
1338
1339        let mut order: MarketOrder = init.clone().try_into().unwrap();
1340        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1341        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1342        order.apply(OrderEventAny::Filled(filled)).unwrap();
1343
1344        assert_eq!(order.client_order_id, init.client_order_id);
1345        assert_eq!(order.status(), OrderStatus::PartiallyFilled);
1346        assert_eq!(order.filled_qty(), Quantity::from(50_000));
1347        assert_eq!(order.leaves_qty(), Quantity::from(50_000));
1348        assert!(order.is_open());
1349        assert!(!order.is_closed());
1350    }
1351
1352    #[rstest]
1353    fn test_order_commission_calculation() {
1354        let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
1355        order
1356            .commissions
1357            .insert(Currency::USD(), Money::new(10.0, Currency::USD()));
1358
1359        assert_eq!(
1360            order.commission(&Currency::USD()),
1361            Some(Money::new(10.0, Currency::USD()))
1362        );
1363        assert_eq!(
1364            order.commissions_vec(),
1365            vec![Money::new(10.0, Currency::USD())]
1366        );
1367    }
1368
1369    #[rstest]
1370    fn test_order_is_primary() {
1371        let order: MarketOrder = OrderInitializedSpec::builder()
1372            .exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
1373            .exec_spawn_id(ClientOrderId::from("O-001"))
1374            .client_order_id(ClientOrderId::from("O-001"))
1375            .build()
1376            .try_into()
1377            .unwrap();
1378
1379        assert!(order.is_primary());
1380        assert!(!order.is_spawned());
1381    }
1382
1383    #[rstest]
1384    fn test_order_is_spawned() {
1385        let order: MarketOrder = OrderInitializedSpec::builder()
1386            .exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
1387            .exec_spawn_id(ClientOrderId::from("O-002"))
1388            .client_order_id(ClientOrderId::from("O-001"))
1389            .build()
1390            .try_into()
1391            .unwrap();
1392
1393        assert!(!order.is_primary());
1394        assert!(order.is_spawned());
1395    }
1396
1397    #[rstest]
1398    fn test_order_is_contingency() {
1399        let order: MarketOrder = OrderInitializedSpec::builder()
1400            .contingency_type(ContingencyType::Oto)
1401            .build()
1402            .try_into()
1403            .unwrap();
1404
1405        assert!(order.is_contingency());
1406        assert!(order.is_parent_order());
1407        assert!(!order.is_child_order());
1408    }
1409
1410    #[rstest]
1411    fn test_order_is_child_order() {
1412        let order: MarketOrder = OrderInitializedSpec::builder()
1413            .parent_order_id(ClientOrderId::from("PARENT-001"))
1414            .build()
1415            .try_into()
1416            .unwrap();
1417
1418        assert!(order.is_child_order());
1419        assert!(!order.is_parent_order());
1420    }
1421
1422    #[rstest]
1423    fn test_to_own_book_order_timestamp_ordering() {
1424        use crate::orders::limit::LimitOrder;
1425
1426        // Create order with distinct timestamps to verify parameter ordering
1427        let init = OrderInitializedSpec::builder()
1428            .price(Price::from("100.00"))
1429            .build();
1430        let submitted = OrderSubmittedSpec::builder()
1431            .ts_event(UnixNanos::from(1_000_000))
1432            .build();
1433        let accepted = OrderAcceptedSpec::builder()
1434            .ts_event(UnixNanos::from(2_000_000))
1435            .build();
1436
1437        let mut order: LimitOrder = init.try_into().unwrap();
1438        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1439        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1440
1441        let own_book_order = order.to_own_book_order();
1442
1443        // Verify timestamps are in correct positions
1444        assert_eq!(own_book_order.ts_submitted, UnixNanos::from(1_000_000));
1445        assert_eq!(own_book_order.ts_accepted, UnixNanos::from(2_000_000));
1446        assert_eq!(own_book_order.ts_last, UnixNanos::from(2_000_000));
1447    }
1448
1449    #[rstest]
1450    fn test_order_accepted_without_submitted_sets_account_id() {
1451        // Test external order flow: Initialized -> Accepted (no Submitted)
1452        let init = OrderInitializedSpec::builder().build();
1453        let accepted = OrderAcceptedSpec::builder()
1454            .account_id(AccountId::from("EXTERNAL-001"))
1455            .build();
1456
1457        let mut order: MarketOrder = init.try_into().unwrap();
1458
1459        // Verify account_id is initially None
1460        assert_eq!(order.account_id(), None);
1461
1462        // Apply accepted event directly (external order case)
1463        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1464
1465        // Verify account_id is now set from the accepted event
1466        assert_eq!(order.account_id(), Some(AccountId::from("EXTERNAL-001")));
1467        assert_eq!(order.status(), OrderStatus::Accepted);
1468    }
1469
1470    #[rstest]
1471    fn test_order_accepted_after_submitted_preserves_account_id() {
1472        // Test normal order flow: Initialized -> Submitted -> Accepted
1473        let init = OrderInitializedSpec::builder().build();
1474        let submitted = OrderSubmittedSpec::builder()
1475            .account_id(AccountId::from("SUBMITTED-001"))
1476            .build();
1477        let accepted = OrderAcceptedSpec::builder()
1478            .account_id(AccountId::from("ACCEPTED-001"))
1479            .build();
1480
1481        let mut order: MarketOrder = init.try_into().unwrap();
1482        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1483
1484        // After submitted, account_id should be set
1485        assert_eq!(order.account_id(), Some(AccountId::from("SUBMITTED-001")));
1486
1487        // Apply accepted event
1488        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1489
1490        // account_id should now be updated to the accepted event's account_id
1491        assert_eq!(order.account_id(), Some(AccountId::from("ACCEPTED-001")));
1492        assert_eq!(order.status(), OrderStatus::Accepted);
1493    }
1494
1495    #[rstest]
1496    fn test_overfill_tracks_overfill_qty() {
1497        // Test that overfill is tracked on the order
1498        let init = OrderInitializedSpec::builder()
1499            .quantity(Quantity::from(100_000))
1500            .build();
1501        let submitted = OrderSubmittedSpec::builder().build();
1502        let accepted = OrderAcceptedSpec::builder().build();
1503        let overfill = OrderFilledSpec::builder()
1504            .last_qty(Quantity::from(110_000)) // Overfill: 110k > 100k
1505            .build();
1506
1507        let mut order: MarketOrder = init.try_into().unwrap();
1508        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1509        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1510        order.apply(OrderEventAny::Filled(overfill)).unwrap();
1511
1512        // Order should track overfill
1513        assert_eq!(order.overfill_qty(), Quantity::from(10_000));
1514        assert_eq!(order.filled_qty(), Quantity::from(110_000));
1515        assert_eq!(order.leaves_qty(), Quantity::from(0));
1516        assert_eq!(order.status(), OrderStatus::Filled);
1517    }
1518
1519    #[rstest]
1520    fn test_partial_fill_then_overfill() {
1521        // Test multiple fills resulting in overfill
1522        let init = OrderInitializedSpec::builder()
1523            .quantity(Quantity::from(100_000))
1524            .build();
1525        let submitted = OrderSubmittedSpec::builder().build();
1526        let accepted = OrderAcceptedSpec::builder().build();
1527        let fill1 = OrderFilledSpec::builder()
1528            .last_qty(Quantity::from(80_000))
1529            .trade_id(TradeId::from("TRADE-1"))
1530            .build();
1531        let fill2 = OrderFilledSpec::builder()
1532            .last_qty(Quantity::from(30_000)) // Total 110k > 100k
1533            .trade_id(TradeId::from("TRADE-2"))
1534            .build();
1535
1536        let mut order: MarketOrder = init.try_into().unwrap();
1537        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1538        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1539        order.apply(OrderEventAny::Filled(fill1)).unwrap();
1540
1541        // After first fill, no overfill
1542        assert_eq!(order.overfill_qty(), Quantity::from(0));
1543        assert_eq!(order.filled_qty(), Quantity::from(80_000));
1544        assert_eq!(order.leaves_qty(), Quantity::from(20_000));
1545
1546        order.apply(OrderEventAny::Filled(fill2)).unwrap();
1547
1548        // After second fill, overfill detected
1549        assert_eq!(order.overfill_qty(), Quantity::from(10_000));
1550        assert_eq!(order.filled_qty(), Quantity::from(110_000));
1551        assert_eq!(order.leaves_qty(), Quantity::from(0));
1552        assert_eq!(order.status(), OrderStatus::Filled);
1553    }
1554
1555    #[rstest]
1556    fn test_exact_fill_no_overfill() {
1557        // Test that exact fill doesn't trigger overfill tracking
1558        let init = OrderInitializedSpec::builder()
1559            .quantity(Quantity::from(100_000))
1560            .build();
1561        let submitted = OrderSubmittedSpec::builder().build();
1562        let accepted = OrderAcceptedSpec::builder().build();
1563        let filled = OrderFilledSpec::builder()
1564            .last_qty(Quantity::from(100_000)) // Exact fill
1565            .build();
1566
1567        let mut order: MarketOrder = init.try_into().unwrap();
1568        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1569        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1570        order.apply(OrderEventAny::Filled(filled)).unwrap();
1571
1572        // No overfill
1573        assert_eq!(order.overfill_qty(), Quantity::from(0));
1574        assert_eq!(order.filled_qty(), Quantity::from(100_000));
1575        assert_eq!(order.leaves_qty(), Quantity::from(0));
1576    }
1577
1578    #[rstest]
1579    fn test_partial_fill_then_overfill_with_fractional_quantities() {
1580        // Simulates real exchange scenario with fractional fills:
1581        // Order for 2450.5 units, partially filled 1202.5, then fill of 1285.5 arrives
1582        // Total filled: 2488.0, overfill: 37.5
1583        let init = OrderInitializedSpec::builder()
1584            .quantity(Quantity::from("2450.5"))
1585            .build();
1586        let submitted = OrderSubmittedSpec::builder().build();
1587        let accepted = OrderAcceptedSpec::builder().build();
1588        let fill1 = OrderFilledSpec::builder()
1589            .last_qty(Quantity::from("1202.5"))
1590            .trade_id(TradeId::from("TRADE-1"))
1591            .build();
1592        let fill2 = OrderFilledSpec::builder()
1593            .last_qty(Quantity::from("1285.5")) // 1202.5 + 1285.5 = 2488 > 2450.5
1594            .trade_id(TradeId::from("TRADE-2"))
1595            .build();
1596
1597        let mut order: MarketOrder = init.try_into().unwrap();
1598        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1599        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1600        order.apply(OrderEventAny::Filled(fill1)).unwrap();
1601
1602        // After first fill, no overfill
1603        assert_eq!(order.overfill_qty(), Quantity::from(0));
1604        assert_eq!(order.filled_qty(), Quantity::from("1202.5"));
1605        assert_eq!(order.leaves_qty(), Quantity::from("1248.0"));
1606        assert_eq!(order.status(), OrderStatus::PartiallyFilled);
1607
1608        order.apply(OrderEventAny::Filled(fill2)).unwrap();
1609
1610        // After second fill, overfill detected and tracked
1611        assert_eq!(order.overfill_qty(), Quantity::from("37.5"));
1612        assert_eq!(order.filled_qty(), Quantity::from("2488.0"));
1613        assert_eq!(order.leaves_qty(), Quantity::from(0));
1614        assert_eq!(order.status(), OrderStatus::Filled);
1615    }
1616
1617    #[rstest]
1618    fn test_calculate_overfill_returns_zero_when_no_overfill() {
1619        let order: MarketOrder = OrderInitializedSpec::builder()
1620            .quantity(Quantity::from(100_000))
1621            .build()
1622            .try_into()
1623            .unwrap();
1624
1625        // Fill qty less than order qty - no overfill
1626        let overfill = order.calculate_overfill(Quantity::from(50_000));
1627        assert_eq!(overfill, Quantity::from(0));
1628
1629        // Fill qty equals order qty - no overfill
1630        let overfill = order.calculate_overfill(Quantity::from(100_000));
1631        assert_eq!(overfill, Quantity::from(0));
1632    }
1633
1634    #[rstest]
1635    fn test_calculate_overfill_returns_overfill_amount() {
1636        let order: MarketOrder = OrderInitializedSpec::builder()
1637            .quantity(Quantity::from(100_000))
1638            .build()
1639            .try_into()
1640            .unwrap();
1641
1642        // Fill qty exceeds order qty
1643        let overfill = order.calculate_overfill(Quantity::from(110_000));
1644        assert_eq!(overfill, Quantity::from(10_000));
1645    }
1646
1647    #[rstest]
1648    fn test_calculate_overfill_accounts_for_existing_fills() {
1649        let init = OrderInitializedSpec::builder()
1650            .quantity(Quantity::from(100_000))
1651            .build();
1652        let submitted = OrderSubmittedSpec::builder().build();
1653        let accepted = OrderAcceptedSpec::builder().build();
1654        let partial_fill = OrderFilledSpec::builder()
1655            .last_qty(Quantity::from(60_000))
1656            .build();
1657
1658        let mut order: MarketOrder = init.try_into().unwrap();
1659        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1660        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1661        order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
1662
1663        // Order is 60k filled, 40k remaining
1664        // Fill of 50k would overfill by 10k
1665        let overfill = order.calculate_overfill(Quantity::from(50_000));
1666        assert_eq!(overfill, Quantity::from(10_000));
1667
1668        // Fill of 40k would not overfill
1669        let overfill = order.calculate_overfill(Quantity::from(40_000));
1670        assert_eq!(overfill, Quantity::from(0));
1671    }
1672
1673    #[rstest]
1674    fn test_calculate_overfill_with_fractional_quantities() {
1675        let order: MarketOrder = OrderInitializedSpec::builder()
1676            .quantity(Quantity::from("2450.5"))
1677            .build()
1678            .try_into()
1679            .unwrap();
1680
1681        // Simulates the exact scenario from user's log
1682        // Order for 2450.5, if fill of 2488.0 arrives
1683        let overfill = order.calculate_overfill(Quantity::from("2488.0"));
1684        assert_eq!(overfill, Quantity::from("37.5"));
1685    }
1686
1687    #[rstest]
1688    fn test_calculate_overfill_zero_after_fractional_partial_fill() {
1689        let init = OrderInitializedSpec::builder()
1690            .quantity(Quantity::from("1.000"))
1691            .build();
1692        let submitted = OrderSubmittedSpec::builder().build();
1693        let accepted = OrderAcceptedSpec::builder().build();
1694        let partial_fill = OrderFilledSpec::builder()
1695            .last_qty(Quantity::from("0.072"))
1696            .build();
1697
1698        let mut order: MarketOrder = init.try_into().unwrap();
1699        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1700        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1701        order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
1702
1703        // After filling 0.072 of 1.000, another 0.072 fill should not overfill
1704        let overfill = order.calculate_overfill(Quantity::from("0.072"));
1705        assert_eq!(overfill, Quantity::from("0.000"));
1706    }
1707
1708    #[rstest]
1709    fn test_duplicate_fill_rejected() {
1710        let init = OrderInitializedSpec::builder()
1711            .quantity(Quantity::from(100_000))
1712            .build();
1713        let submitted = OrderSubmittedSpec::builder().build();
1714        let accepted = OrderAcceptedSpec::builder().build();
1715        let fill1 = OrderFilledSpec::builder()
1716            .last_qty(Quantity::from(50_000))
1717            .trade_id(TradeId::from("TRADE-001"))
1718            .build();
1719        let fill2_duplicate = OrderFilledSpec::builder()
1720            .last_qty(Quantity::from(50_000))
1721            .trade_id(TradeId::from("TRADE-001")) // Same trade_id as fill1
1722            .build();
1723
1724        let mut order: MarketOrder = init.try_into().unwrap();
1725        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1726        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1727        order.apply(OrderEventAny::Filled(fill1)).unwrap();
1728
1729        // Verify first fill applied successfully
1730        assert_eq!(order.filled_qty(), Quantity::from(50_000));
1731        assert_eq!(order.status(), OrderStatus::PartiallyFilled);
1732
1733        // Applying duplicate fill should return DuplicateFill error
1734        let result = order.apply(OrderEventAny::Filled(fill2_duplicate));
1735        assert!(result.is_err());
1736        match result.unwrap_err() {
1737            OrderError::DuplicateFill(trade_id) => {
1738                assert_eq!(trade_id, TradeId::from("TRADE-001"));
1739            }
1740            e => panic!("Expected DuplicateFill error, was: {e:?}"),
1741        }
1742
1743        // Order state should be unchanged after rejected duplicate
1744        assert_eq!(order.filled_qty(), Quantity::from(50_000));
1745        assert_eq!(order.status(), OrderStatus::PartiallyFilled);
1746    }
1747
1748    #[rstest]
1749    fn test_check_display_qty_returns_typed_invariant_with_stable_display() {
1750        let error = check_display_qty(Some(Quantity::from(2)), Quantity::from(1)).unwrap_err();
1751
1752        match error {
1753            OrderError::Invariant(CorrectnessError::PredicateViolation { ref message }) => {
1754                assert_eq!(message, "`display_qty` may not exceed `quantity`");
1755            }
1756            other => panic!("Expected typed invariant error, was: {other:?}"),
1757        }
1758
1759        assert_eq!(error.to_string(), "`display_qty` may not exceed `quantity`");
1760    }
1761
1762    #[rstest]
1763    fn test_check_time_in_force_returns_typed_invariant_with_stable_display() {
1764        let error = check_time_in_force(TimeInForce::Gtd, None).unwrap_err();
1765
1766        match error {
1767            OrderError::Invariant(CorrectnessError::PredicateViolation { ref message }) => {
1768                assert_eq!(message, "`expire_time` is required for `GTD` order");
1769            }
1770            other => panic!("Expected typed invariant error, was: {other:?}"),
1771        }
1772
1773        assert_eq!(
1774            error.to_string(),
1775            "`expire_time` is required for `GTD` order"
1776        );
1777    }
1778
1779    #[rstest]
1780    fn test_different_trade_ids_allowed() {
1781        let init = OrderInitializedSpec::builder()
1782            .quantity(Quantity::from(100_000))
1783            .build();
1784        let submitted = OrderSubmittedSpec::builder().build();
1785        let accepted = OrderAcceptedSpec::builder().build();
1786        let fill1 = OrderFilledSpec::builder()
1787            .last_qty(Quantity::from(50_000))
1788            .trade_id(TradeId::from("TRADE-001"))
1789            .build();
1790        let fill2 = OrderFilledSpec::builder()
1791            .last_qty(Quantity::from(50_000))
1792            .trade_id(TradeId::from("TRADE-002")) // Different trade_id
1793            .build();
1794
1795        let mut order: MarketOrder = init.try_into().unwrap();
1796        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1797        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1798        order.apply(OrderEventAny::Filled(fill1)).unwrap();
1799        order.apply(OrderEventAny::Filled(fill2)).unwrap();
1800
1801        // Both fills should be applied
1802        assert_eq!(order.filled_qty(), Quantity::from(100_000));
1803        assert_eq!(order.status(), OrderStatus::Filled);
1804        assert_eq!(order.trade_ids.len(), 2);
1805    }
1806
1807    #[rstest]
1808    fn test_pending_update_order_restores_status_on_updated() {
1809        let init = OrderInitializedSpec::builder()
1810            .quantity(Quantity::from(100_000))
1811            .build();
1812        let submitted = OrderSubmittedSpec::builder().build();
1813        let accepted = OrderAcceptedSpec::builder().build();
1814        let pending_update = OrderPendingUpdateSpec::builder().build();
1815        let updated = OrderUpdatedSpec::builder()
1816            .quantity(Quantity::from(50_000))
1817            .build();
1818
1819        let mut order: MarketOrder = init.try_into().unwrap();
1820        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1821        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1822
1823        assert_eq!(order.status(), OrderStatus::Accepted);
1824
1825        order
1826            .apply(OrderEventAny::PendingUpdate(pending_update))
1827            .unwrap();
1828        assert_eq!(order.status(), OrderStatus::PendingUpdate);
1829
1830        order.apply(OrderEventAny::Updated(updated)).unwrap();
1831
1832        assert_eq!(order.status(), OrderStatus::Accepted);
1833        assert_eq!(order.quantity(), Quantity::from(50_000));
1834    }
1835
1836    #[rstest]
1837    fn test_partially_filled_order_can_be_updated() {
1838        // Test that a partially filled order can receive an Updated event
1839        // and remain in PartiallyFilled status
1840        let init = OrderInitializedSpec::builder()
1841            .quantity(Quantity::from(100_000))
1842            .build();
1843        let submitted = OrderSubmittedSpec::builder().build();
1844        let accepted = OrderAcceptedSpec::builder().build();
1845        let partial_fill = OrderFilledSpec::builder()
1846            .last_qty(Quantity::from(40_000))
1847            .build();
1848        let updated = OrderUpdatedSpec::builder()
1849            .quantity(Quantity::from(80_000)) // Reduce to 80k (still > 40k filled)
1850            .build();
1851
1852        let mut order: MarketOrder = init.try_into().unwrap();
1853        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1854        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1855        order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
1856
1857        assert_eq!(order.status(), OrderStatus::PartiallyFilled);
1858        assert_eq!(order.filled_qty(), Quantity::from(40_000));
1859
1860        order.apply(OrderEventAny::Updated(updated)).unwrap();
1861
1862        assert_eq!(order.status(), OrderStatus::PartiallyFilled);
1863        assert_eq!(order.quantity(), Quantity::from(80_000));
1864        assert_eq!(order.leaves_qty(), Quantity::from(40_000)); // 80k - 40k filled
1865    }
1866
1867    #[rstest]
1868    fn test_triggered_order_can_be_updated() {
1869        // Test that a triggered order can receive an Updated event
1870        // and remain in Triggered status
1871        let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
1872        let submitted = OrderSubmittedSpec::builder().build();
1873        let accepted = OrderAcceptedSpec::builder().build();
1874        let triggered = OrderTriggeredSpec::builder().build();
1875        let updated = OrderUpdatedSpec::builder()
1876            .quantity(Quantity::from(80_000))
1877            .build();
1878
1879        let mut order = OrderTestBuilder::new(OrderType::StopLimit)
1880            .instrument_id(instrument_id)
1881            .quantity(Quantity::from(100_000))
1882            .price(Price::from("0.99500"))
1883            .trigger_price(Price::from("1.00000"))
1884            .trigger_type(TriggerType::LastPrice)
1885            .build();
1886        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1887        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1888        order.apply(OrderEventAny::Triggered(triggered)).unwrap();
1889
1890        assert_eq!(order.status(), OrderStatus::Triggered);
1891
1892        order.apply(OrderEventAny::Updated(updated)).unwrap();
1893
1894        assert_eq!(order.status(), OrderStatus::Triggered);
1895        assert_eq!(order.quantity(), Quantity::from(80_000));
1896    }
1897
1898    #[rstest]
1899    fn test_order_updated_with_is_quote_quantity_clears_flag() {
1900        let init = OrderInitializedSpec::builder()
1901            .quantity(Quantity::new(10.0, 6))
1902            .quote_quantity(true)
1903            .build();
1904        let submitted = OrderSubmittedSpec::builder().build();
1905        let accepted = OrderAcceptedSpec::builder().build();
1906        let updated = OrderUpdatedSpec::builder()
1907            .quantity(Quantity::new(47.393_365, 6))
1908            .is_quote_quantity(false)
1909            .build();
1910
1911        let mut order: MarketOrder = init.try_into().unwrap();
1912        assert!(order.is_quote_quantity());
1913
1914        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1915        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1916        order.apply(OrderEventAny::Updated(updated)).unwrap();
1917
1918        assert!(!order.is_quote_quantity());
1919        assert_eq!(order.quantity(), Quantity::new(47.393_365, 6));
1920        assert_eq!(order.leaves_qty(), Quantity::new(47.393_365, 6));
1921    }
1922
1923    #[rstest]
1924    fn test_order_updated_default_is_quote_quantity_clears_flag() {
1925        let init = OrderInitializedSpec::builder()
1926            .quantity(Quantity::new(10.0, 6))
1927            .quote_quantity(true)
1928            .build();
1929        let submitted = OrderSubmittedSpec::builder().build();
1930        let accepted = OrderAcceptedSpec::builder().build();
1931        // Builder defaults is_quote_quantity to false
1932        let updated = OrderUpdatedSpec::builder()
1933            .quantity(Quantity::new(8.0, 6))
1934            .build();
1935
1936        let mut order: MarketOrder = init.try_into().unwrap();
1937        assert!(order.is_quote_quantity());
1938
1939        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1940        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1941        order.apply(OrderEventAny::Updated(updated)).unwrap();
1942
1943        assert!(!order.is_quote_quantity());
1944        assert_eq!(order.quantity(), Quantity::new(8.0, 6));
1945    }
1946
1947    #[rstest]
1948    fn test_canceled_then_partial_fill_then_canceled() {
1949        let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
1950        let submitted = OrderSubmittedSpec::builder().build();
1951        let accepted = OrderAcceptedSpec::builder().build();
1952        let canceled1 = OrderCanceledSpec::builder().build();
1953        let fill = OrderFilledSpec::builder()
1954            .last_qty(Quantity::from(50_000))
1955            .trade_id(TradeId::from("FILL-1"))
1956            .build();
1957        let canceled2 = OrderCanceledSpec::builder().build();
1958
1959        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1960        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1961        order.apply(OrderEventAny::Canceled(canceled1)).unwrap();
1962        assert_eq!(order.status(), OrderStatus::Canceled);
1963        assert!(order.is_closed());
1964
1965        // Fill arrives after cancel (real-world race condition)
1966        order.apply(OrderEventAny::Filled(fill)).unwrap();
1967        assert_eq!(order.status(), OrderStatus::PartiallyFilled);
1968        assert_eq!(order.filled_qty(), Quantity::from(50_000));
1969        assert!(order.is_open());
1970
1971        // Re-emitted cancel restores terminal state
1972        order.apply(OrderEventAny::Canceled(canceled2)).unwrap();
1973        assert_eq!(order.status(), OrderStatus::Canceled);
1974        assert!(order.is_closed());
1975    }
1976
1977    #[rstest]
1978    fn test_apply_triggered_to_stop_market_order_returns_error() {
1979        let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
1980        let submitted = OrderSubmittedSpec::builder().build();
1981        let accepted = OrderAcceptedSpec::builder().build();
1982        let triggered = OrderTriggeredSpec::builder().build();
1983
1984        let mut order = OrderTestBuilder::new(OrderType::StopMarket)
1985            .instrument_id(instrument_id)
1986            .quantity(Quantity::from(1))
1987            .trigger_price(Price::from("1.00000"))
1988            .trigger_type(TriggerType::LastPrice)
1989            .build();
1990        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
1991        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
1992
1993        let result = order.apply(OrderEventAny::Triggered(triggered));
1994        assert!(result.is_err());
1995        assert_eq!(order.status(), OrderStatus::Accepted);
1996    }
1997
1998    #[rstest]
1999    fn test_apply_triggered_to_stop_limit_order_succeeds() {
2000        let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
2001        let submitted = OrderSubmittedSpec::builder().build();
2002        let accepted = OrderAcceptedSpec::builder().build();
2003        let triggered = OrderTriggeredSpec::builder().build();
2004
2005        let mut order = OrderTestBuilder::new(OrderType::StopLimit)
2006            .instrument_id(instrument_id)
2007            .quantity(Quantity::from(1))
2008            .price(Price::from("0.99500"))
2009            .trigger_price(Price::from("1.00000"))
2010            .trigger_type(TriggerType::LastPrice)
2011            .build();
2012        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2013        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2014        order.apply(OrderEventAny::Triggered(triggered)).unwrap();
2015
2016        assert_eq!(order.status(), OrderStatus::Triggered);
2017    }
2018
2019    #[rstest]
2020    fn test_to_order_status_report_for_accepted_limit_order() {
2021        let order = OrderTestBuilder::new(OrderType::Limit)
2022            .instrument_id(InstrumentId::from("AUDUSD.SIM"))
2023            .side(OrderSide::Buy)
2024            .quantity(Quantity::from(100_000))
2025            .price(Price::from("1.00000"))
2026            .build();
2027        let accepted = TestOrderStubs::make_accepted_order(&order);
2028
2029        let report = accepted.to_order_status_report(None).unwrap();
2030
2031        assert_eq!(report.account_id, AccountId::from("SIM-001"));
2032        assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
2033        assert_eq!(report.client_order_id, Some(accepted.client_order_id()));
2034        assert_eq!(report.venue_order_id, VenueOrderId::from("V-001"));
2035        assert_eq!(report.order_side, OrderSide::Buy);
2036        assert_eq!(report.order_type, OrderType::Limit);
2037        assert_eq!(report.time_in_force, accepted.time_in_force());
2038        assert_eq!(report.order_status, OrderStatus::Accepted);
2039        assert_eq!(report.quantity, Quantity::from(100_000));
2040        assert_eq!(report.filled_qty, Quantity::from(0));
2041        assert_eq!(report.price, Some(Price::from("1.00000")));
2042        assert_eq!(report.avg_px, None);
2043        assert_eq!(report.ts_accepted, accepted.ts_accepted().unwrap());
2044        assert_eq!(report.ts_last, accepted.ts_last());
2045        assert_eq!(report.ts_init, accepted.ts_init());
2046    }
2047
2048    #[rstest]
2049    fn test_to_order_status_report_for_filled_market_order(audusd_sim: CurrencyPair) {
2050        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2051        let order = OrderTestBuilder::new(OrderType::Market)
2052            .instrument_id(audusd_sim.id())
2053            .side(OrderSide::Buy)
2054            .quantity(Quantity::from(100_000))
2055            .build();
2056        let filled = TestOrderStubs::make_filled_order(&order, &audusd_sim, LiquiditySide::Maker);
2057        let report_id = UUID4::new();
2058
2059        let report = filled.to_order_status_report(Some(report_id)).unwrap();
2060
2061        assert_eq!(report.report_id, report_id);
2062        assert_eq!(report.order_status, OrderStatus::Filled);
2063        assert_eq!(report.quantity, Quantity::from(100_000));
2064        assert_eq!(report.filled_qty, Quantity::from(100_000));
2065        assert_eq!(report.price, None);
2066        assert_eq!(report.avg_px, Some(dec!(1)));
2067        assert_eq!(report.venue_position_id, Some(PositionId::from("1")));
2068        assert_eq!(report.ts_accepted, filled.ts_accepted().unwrap());
2069        assert_eq!(report.ts_last, filled.ts_last());
2070    }
2071
2072    #[rstest]
2073    fn test_to_order_status_report_maps_optional_fields() {
2074        let order = OrderTestBuilder::new(OrderType::StopLimit)
2075            .instrument_id(InstrumentId::from("AUDUSD.SIM"))
2076            .side(OrderSide::Sell)
2077            .quantity(Quantity::from(100_000))
2078            .price(Price::from("0.99500"))
2079            .trigger_price(Price::from("1.00000"))
2080            .trigger_type(TriggerType::LastPrice)
2081            .time_in_force(TimeInForce::Gtd)
2082            .expire_time(UnixNanos::from(5_000_000_000))
2083            .display_qty(Quantity::from(10_000))
2084            .post_only(true)
2085            .reduce_only(true)
2086            .contingency_type(ContingencyType::Oto)
2087            .order_list_id(OrderListId::from("OL-001"))
2088            .linked_order_ids(vec![ClientOrderId::from("O-CHILD")])
2089            .parent_order_id(ClientOrderId::from("O-PARENT"))
2090            .build();
2091        let accepted = TestOrderStubs::make_accepted_order(&order);
2092
2093        let report = accepted.to_order_status_report(None).unwrap();
2094
2095        assert_eq!(report.price, Some(Price::from("0.99500")));
2096        assert_eq!(report.trigger_price, Some(Price::from("1.00000")));
2097        assert_eq!(report.trigger_type, Some(TriggerType::LastPrice));
2098        assert_eq!(report.expire_time, Some(UnixNanos::from(5_000_000_000)));
2099        assert_eq!(report.display_qty, Some(Quantity::from(10_000)));
2100        assert!(report.post_only);
2101        assert!(report.reduce_only);
2102        assert_eq!(report.contingency_type, ContingencyType::Oto);
2103        assert_eq!(report.order_list_id, Some(OrderListId::from("OL-001")));
2104        assert_eq!(
2105            report.linked_order_ids,
2106            Some(vec![ClientOrderId::from("O-CHILD")])
2107        );
2108        assert_eq!(
2109            report.parent_order_id,
2110            Some(ClientOrderId::from("O-PARENT"))
2111        );
2112    }
2113
2114    #[rstest]
2115    fn test_to_order_status_report_maps_ts_triggered() {
2116        let order = OrderTestBuilder::new(OrderType::StopLimit)
2117            .instrument_id(InstrumentId::from("AUDUSD.SIM"))
2118            .side(OrderSide::Buy)
2119            .quantity(Quantity::from(100_000))
2120            .price(Price::from("0.99500"))
2121            .trigger_price(Price::from("1.00000"))
2122            .trigger_type(TriggerType::LastPrice)
2123            .build();
2124        let mut order = TestOrderStubs::make_accepted_order(&order);
2125        let triggered = OrderTriggeredSpec::builder()
2126            .ts_event(UnixNanos::from(1_500_000_000))
2127            .build();
2128        order.apply(OrderEventAny::Triggered(triggered)).unwrap();
2129
2130        let report = order.to_order_status_report(None).unwrap();
2131
2132        assert_eq!(report.order_status, OrderStatus::Triggered);
2133        assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
2134    }
2135
2136    #[rstest]
2137    fn test_to_order_status_report_maps_rejection_reason() {
2138        let order = OrderTestBuilder::new(OrderType::Limit)
2139            .instrument_id(InstrumentId::from("AUDUSD.SIM"))
2140            .side(OrderSide::Buy)
2141            .quantity(Quantity::from(100_000))
2142            .price(Price::from("1.00000"))
2143            .build();
2144        let mut order = TestOrderStubs::make_accepted_order(&order);
2145        let rejected = OrderRejectedSpec::builder()
2146            .reason(Ustr::from("INSUFFICIENT_MARGIN"))
2147            .build();
2148        order.apply(OrderEventAny::Rejected(rejected)).unwrap();
2149
2150        let report = order.to_order_status_report(None).unwrap();
2151
2152        assert_eq!(report.order_status, OrderStatus::Rejected);
2153        assert_eq!(
2154            report.cancel_reason,
2155            Some("INSUFFICIENT_MARGIN".to_string())
2156        );
2157    }
2158
2159    #[rstest]
2160    fn test_to_order_status_report_maps_distinct_timestamps() {
2161        let mut order = OrderTestBuilder::new(OrderType::Limit)
2162            .instrument_id(InstrumentId::from("AUDUSD.SIM"))
2163            .side(OrderSide::Buy)
2164            .quantity(Quantity::from(100_000))
2165            .price(Price::from("1.00000"))
2166            .ts_init(UnixNanos::from(1_000))
2167            .build();
2168        let submitted = OrderSubmittedSpec::builder()
2169            .ts_event(UnixNanos::from(2_000))
2170            .build();
2171        let accepted = OrderAcceptedSpec::builder()
2172            .ts_event(UnixNanos::from(3_000))
2173            .build();
2174        let filled = OrderFilledSpec::builder()
2175            .last_qty(Quantity::from(50_000))
2176            .ts_event(UnixNanos::from(4_000))
2177            .build();
2178        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2179        order.apply(OrderEventAny::Accepted(accepted)).unwrap();
2180        order.apply(OrderEventAny::Filled(filled)).unwrap();
2181
2182        let report = order.to_order_status_report(None).unwrap();
2183
2184        assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
2185        assert_eq!(report.filled_qty, Quantity::from(50_000));
2186        assert_eq!(report.ts_accepted, UnixNanos::from(3_000));
2187        assert_eq!(report.ts_last, UnixNanos::from(4_000));
2188        assert_eq!(report.ts_init, UnixNanos::from(1_000));
2189    }
2190
2191    #[rstest]
2192    fn test_to_order_status_report_ts_accepted_falls_back_to_ts_last() {
2193        // Venue ack can arrive through an update before any acceptance event
2194        let mut order = OrderTestBuilder::new(OrderType::Limit)
2195            .instrument_id(InstrumentId::from("AUDUSD.SIM"))
2196            .side(OrderSide::Buy)
2197            .quantity(Quantity::from(100_000))
2198            .price(Price::from("1.00000"))
2199            .build();
2200        let submitted = OrderSubmittedSpec::builder()
2201            .ts_event(UnixNanos::from(2_000))
2202            .build();
2203        let updated = OrderUpdatedSpec::builder()
2204            .venue_order_id(VenueOrderId::from("V-001"))
2205            .ts_event(UnixNanos::from(3_000))
2206            .build();
2207        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2208        order.apply(OrderEventAny::Updated(updated)).unwrap();
2209
2210        let report = order.to_order_status_report(None).unwrap();
2211
2212        assert_eq!(report.order_status, OrderStatus::Submitted);
2213        assert_eq!(report.venue_order_id, VenueOrderId::from("V-001"));
2214        assert_eq!(report.ts_accepted, UnixNanos::from(3_000));
2215        assert_eq!(report.ts_last, UnixNanos::from(3_000));
2216    }
2217
2218    #[rstest]
2219    fn test_to_order_status_report_maps_trailing_offsets() {
2220        let order = OrderTestBuilder::new(OrderType::TrailingStopLimit)
2221            .instrument_id(InstrumentId::from("AUDUSD.SIM"))
2222            .side(OrderSide::Sell)
2223            .quantity(Quantity::from(100_000))
2224            .price(Price::from("0.99500"))
2225            .trigger_price(Price::from("1.00000"))
2226            .trigger_type(TriggerType::LastPrice)
2227            .limit_offset(dec!(0.0001))
2228            .trailing_offset(dec!(0.0002))
2229            .trailing_offset_type(TrailingOffsetType::Price)
2230            .build();
2231        let accepted = TestOrderStubs::make_accepted_order(&order);
2232
2233        let report = accepted.to_order_status_report(None).unwrap();
2234
2235        assert_eq!(report.limit_offset, Some(dec!(0.0001)));
2236        assert_eq!(report.trailing_offset, Some(dec!(0.0002)));
2237        assert_eq!(report.trailing_offset_type, TrailingOffsetType::Price);
2238    }
2239
2240    #[rstest]
2241    fn test_to_order_status_report_returns_none_before_venue_ack() {
2242        let order = OrderTestBuilder::new(OrderType::Limit)
2243            .instrument_id(InstrumentId::from("AUDUSD.SIM"))
2244            .side(OrderSide::Buy)
2245            .quantity(Quantity::from(100_000))
2246            .price(Price::from("1.00000"))
2247            .build();
2248
2249        assert!(order.to_order_status_report(None).is_none());
2250    }
2251
2252    #[rstest]
2253    fn test_to_order_status_report_returns_none_for_submitted_order() {
2254        // Inflight orders have an account_id but no venue_order_id yet
2255        let mut order = OrderTestBuilder::new(OrderType::Limit)
2256            .instrument_id(InstrumentId::from("AUDUSD.SIM"))
2257            .side(OrderSide::Buy)
2258            .quantity(Quantity::from(100_000))
2259            .price(Price::from("1.00000"))
2260            .build();
2261        let submitted = OrderSubmittedSpec::builder().build();
2262        order.apply(OrderEventAny::Submitted(submitted)).unwrap();
2263
2264        assert!(order.account_id().is_some());
2265        assert!(order.to_order_status_report(None).is_none());
2266    }
2267}