Skip to main content

nautilus_execution/matching_engine/
inflight.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//! Tracks submitted orders which have not yet reached the simulated venue.
17
18use std::{cell::RefCell, rc::Rc};
19
20use ahash::AHashSet;
21use nautilus_common::messages::execution::TradingCommand;
22use nautilus_model::identifiers::ClientOrderId;
23
24/// Shared receipt state for a venue's command queues and matching engines.
25#[derive(Debug, Clone, Default)]
26pub struct InflightOrders {
27    orders: Rc<RefCell<AHashSet<ClientOrderId>>>,
28}
29
30impl InflightOrders {
31    /// Marks a queued submit's orders as awaiting venue receipt.
32    ///
33    /// Non-submit commands are ignored.
34    pub fn insert(&self, command: &TradingCommand) {
35        self.orders.borrow_mut().extend(submit_ids(command));
36    }
37
38    /// Releases all orders in a submit immediately before the venue processes it.
39    ///
40    /// The first receipt releases the order even if a duplicate submit remains queued.
41    /// Non-submit commands are ignored.
42    pub fn remove(&self, command: &TradingCommand) {
43        let mut orders = self.orders.borrow_mut();
44        for id in submit_ids(command) {
45            orders.remove(id);
46        }
47    }
48
49    /// Returns whether the order is still awaiting venue receipt.
50    pub fn contains(&self, id: ClientOrderId) -> bool {
51        self.orders.borrow().contains(&id)
52    }
53
54    /// Clears receipt state when the venue discards its queues.
55    pub fn clear(&self) {
56        self.orders.borrow_mut().clear();
57    }
58}
59
60fn submit_ids(command: &TradingCommand) -> &[ClientOrderId] {
61    match command {
62        TradingCommand::SubmitOrder(command) => std::slice::from_ref(&command.client_order_id),
63        TradingCommand::SubmitOrderList(command) => &command.order_list.client_order_ids,
64        _ => &[],
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use nautilus_common::messages::execution::{SubmitOrder, SubmitOrderList};
71    use nautilus_core::UUID4;
72    use nautilus_model::{
73        enums::OrderType,
74        identifiers::{InstrumentId, OrderListId, StrategyId, TraderId},
75        orders::{Order, OrderList, OrderTestBuilder},
76        types::Quantity,
77    };
78    use rstest::rstest;
79
80    use super::*;
81
82    #[rstest]
83    #[case::single(false)]
84    #[case::list(true)]
85    fn test_first_receipt_releases_duplicate_submits(#[case] list: bool) {
86        let orders: Vec<_> = ["O-1", "O-2"]
87            .iter()
88            .map(|id| {
89                OrderTestBuilder::new(OrderType::Market)
90                    .instrument_id(InstrumentId::from("ETHUSDT.BINANCE"))
91                    .quantity(Quantity::from("1.000"))
92                    .client_order_id(ClientOrderId::from(*id))
93                    .order_list_id(OrderListId::from("OL-1"))
94                    .build()
95            })
96            .collect();
97
98        let trader_id = TraderId::from("TRADER-001");
99
100        let command = if list {
101            TradingCommand::SubmitOrderList(SubmitOrderList::new(
102                trader_id,
103                None,
104                StrategyId::from("STRATEGY-001"),
105                OrderList::from_orders(&orders, 0.into()),
106                orders
107                    .iter()
108                    .map(|order| order.init_event().clone())
109                    .collect(),
110                None,
111                None,
112                None,
113                UUID4::new(),
114                0.into(),
115                None,
116            ))
117        } else {
118            TradingCommand::SubmitOrder(SubmitOrder::from_order(
119                &orders[0],
120                trader_id,
121                None,
122                None,
123                UUID4::new(),
124                0.into(),
125            ))
126        };
127
128        let queue = InflightOrders::default();
129        let engine = queue.clone();
130        queue.insert(&command);
131        queue.insert(&command);
132        assert!(engine.contains(orders[0].client_order_id()));
133        assert_eq!(engine.contains(orders[1].client_order_id()), list);
134
135        queue.remove(&command);
136        assert!(!engine.contains(orders[0].client_order_id()));
137        assert!(!engine.contains(orders[1].client_order_id()));
138        queue.remove(&command);
139        assert!(!engine.contains(orders[0].client_order_id()));
140
141        queue.insert(&command);
142        queue.clear();
143        assert!(!engine.contains(orders[0].client_order_id()));
144        assert!(!engine.contains(orders[1].client_order_id()));
145    }
146}