Skip to main content

nautilus_execution/matching_engine/
orders.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//! Shares pending order state and contingent commands within a simulated venue.
17
18use std::{cell::RefCell, collections::VecDeque, rc::Rc};
19
20use indexmap::{IndexMap, IndexSet};
21use nautilus_common::cache::Cache;
22use nautilus_model::{
23    events::OrderUpdated,
24    identifiers::{AccountId, ClientOrderId, InstrumentId},
25    orders::Order,
26    types::Quantity,
27};
28
29/// Pending order state shared by a simulated venue's instrument engines.
30///
31/// The venue clones one instance into each instrument engine before processing orders. It drains
32/// queued commands before the next input, after its engine calls return:
33///
34/// 1. Pops a command with [`Self::pop_command`].
35/// 1. Calls [`process_oto_command`](super::OrderMatchingEngine::process_oto_command) on the engine
36///    for [`OtoCommand::instrument_id`].
37/// 1. After a [`OtoCommand::Release`] with `parent_quantity: Some(quantity)`, calls
38///    [`process_oto_resize`](super::OrderMatchingEngine::process_oto_resize) on the engine for
39///    `parent_instrument_id`, using `parent_order_id` and `quantity`.
40///
41/// Each engine keeps its own book, pending fills, and fee evaluation.
42#[derive(Debug, Clone, Default)]
43pub struct MatchingOrders {
44    pub(super) pending_updates: Rc<RefCell<IndexMap<ClientOrderId, Vec<OrderUpdated>>>>,
45    pub(super) cached_filled_qty: Rc<RefCell<IndexMap<ClientOrderId, Quantity>>>,
46    pub(super) pending_oto_order_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
47    pub(super) released_oto_order_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
48    pub(super) rejected_oto_parent_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
49    pub(super) closed_oto_order_ids: Rc<RefCell<IndexSet<ClientOrderId>>>,
50    pub(super) commands: Rc<RefCell<VecDeque<OtoCommand>>>,
51}
52
53/// A contingent command delivered to the order's instrument engine.
54#[derive(Debug, Clone)]
55pub enum OtoCommand {
56    /// Releases a child; a parent quantity requests a sibling sizing recount after delivery.
57    Release {
58        instrument_id: InstrumentId,
59        client_order_id: ClientOrderId,
60        account_id: AccountId,
61        parent_instrument_id: InstrumentId,
62        parent_order_id: ClientOrderId,
63        parent_quantity: Option<Quantity>,
64    },
65    /// Recounts the parent's child allowance after a full-trigger child fill.
66    Resize {
67        instrument_id: InstrumentId,
68        parent_order_id: ClientOrderId,
69        quantity: Quantity,
70    },
71    /// Cancels a child ahead of all queued commands.
72    ///
73    /// `cancel_contingencies` controls cancellation of linked orders. The cascade skips the linked
74    /// order IDs in `excluded`.
75    Cancel {
76        instrument_id: InstrumentId,
77        client_order_id: ClientOrderId,
78        cancel_contingencies: bool,
79        excluded: Vec<ClientOrderId>,
80    },
81}
82
83impl OtoCommand {
84    /// Returns the instrument which owns this command.
85    pub fn instrument_id(&self) -> InstrumentId {
86        match self {
87            Self::Release { instrument_id, .. }
88            | Self::Resize { instrument_id, .. }
89            | Self::Cancel { instrument_id, .. } => *instrument_id,
90        }
91    }
92}
93
94impl MatchingOrders {
95    /// Removes the next command without holding a borrow during matching.
96    pub fn pop_command(&self) -> Option<OtoCommand> {
97        self.commands.borrow_mut().pop_front()
98    }
99
100    pub(super) fn reset(&self, instrument_id: InstrumentId, cache: &Cache) {
101        let keep = |id: &ClientOrderId| {
102            cache
103                .order(id)
104                .is_some_and(|order| order.instrument_id() != instrument_id)
105        };
106
107        self.pending_updates.borrow_mut().retain(|id, _| keep(id));
108        self.cached_filled_qty.borrow_mut().retain(|id, _| keep(id));
109        self.pending_oto_order_ids.borrow_mut().retain(keep);
110        self.released_oto_order_ids.borrow_mut().retain(keep);
111        self.rejected_oto_parent_ids.borrow_mut().retain(keep);
112        self.closed_oto_order_ids.borrow_mut().retain(keep);
113        self.commands.borrow_mut().retain(|command| {
114            command.instrument_id() != instrument_id && !matches!(command,
115                OtoCommand::Release { parent_instrument_id, .. } if *parent_instrument_id == instrument_id)
116        });
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use nautilus_model::{
123        enums::OrderType,
124        events::order::spec::OrderUpdatedSpec,
125        instruments::{CryptoPerpetual, Instrument, stubs::crypto_perpetual_ethusdt},
126        orders::OrderTestBuilder,
127    };
128    use rstest::rstest;
129
130    use super::*;
131
132    #[rstest]
133    fn test_reset_retains_other_instrument_state(crypto_perpetual_ethusdt: CryptoPerpetual) {
134        let first = crypto_perpetual_ethusdt.id();
135        let second = InstrumentId::from("BTCUSDT-PERP.BINANCE");
136        let first_id = ClientOrderId::from("FIRST");
137        let second_id = ClientOrderId::from("SECOND");
138        let orders = MatchingOrders::default();
139        let mut cache = Cache::default();
140        let update = OrderUpdatedSpec::builder()
141            .instrument_id(second)
142            .client_order_id(second_id)
143            .quantity(Quantity::from("4.560"))
144            .build();
145
146        for (instrument, id, qty) in [(first, first_id, "1.230"), (second, second_id, "4.560")] {
147            let order = OrderTestBuilder::new(OrderType::Market)
148                .instrument_id(instrument)
149                .client_order_id(id)
150                .quantity(Quantity::from(qty))
151                .build();
152            cache.add_order(order, None, None, false).unwrap();
153
154            let pending = if id == second_id {
155                update
156            } else {
157                OrderUpdatedSpec::builder()
158                    .instrument_id(instrument)
159                    .client_order_id(id)
160                    .quantity(Quantity::from(qty))
161                    .build()
162            };
163
164            orders
165                .pending_updates
166                .borrow_mut()
167                .insert(id, vec![pending]);
168            orders
169                .cached_filled_qty
170                .borrow_mut()
171                .insert(id, Quantity::from(qty));
172            orders.pending_oto_order_ids.borrow_mut().insert(id);
173            orders.released_oto_order_ids.borrow_mut().insert(id);
174            orders.rejected_oto_parent_ids.borrow_mut().insert(id);
175            orders.closed_oto_order_ids.borrow_mut().insert(id);
176        }
177
178        orders.commands.borrow_mut().push_back(OtoCommand::Release {
179            instrument_id: second,
180            client_order_id: second_id,
181            account_id: AccountId::from("SIM-001"),
182            parent_instrument_id: first,
183            parent_order_id: first_id,
184            parent_quantity: Some(Quantity::from("1.230")),
185        });
186
187        orders.commands.borrow_mut().push_back(OtoCommand::Cancel {
188            instrument_id: second,
189            client_order_id: second_id,
190            cancel_contingencies: true,
191            excluded: vec![first_id],
192        });
193
194        for (instrument_id, parent_order_id, quantity) in [
195            (first, first_id, Quantity::from("2.340")),
196            (second, second_id, Quantity::from("5.670")),
197        ] {
198            orders.commands.borrow_mut().push_back(OtoCommand::Resize {
199                instrument_id,
200                parent_order_id,
201                quantity,
202            });
203        }
204
205        orders.reset(first, &cache);
206        assert_eq!(
207            *orders.pending_updates.borrow(),
208            IndexMap::from([(second_id, vec![update])])
209        );
210        assert_eq!(
211            *orders.cached_filled_qty.borrow(),
212            IndexMap::from([(second_id, Quantity::from("4.560"))])
213        );
214        assert_eq!(
215            *orders.pending_oto_order_ids.borrow(),
216            IndexSet::from([second_id])
217        );
218        assert_eq!(
219            *orders.released_oto_order_ids.borrow(),
220            IndexSet::from([second_id])
221        );
222        assert_eq!(
223            *orders.rejected_oto_parent_ids.borrow(),
224            IndexSet::from([second_id])
225        );
226        assert_eq!(
227            *orders.closed_oto_order_ids.borrow(),
228            IndexSet::from([second_id])
229        );
230
231        let OtoCommand::Cancel {
232            instrument_id,
233            client_order_id,
234            cancel_contingencies,
235            excluded,
236        } = orders.pop_command().unwrap()
237        else {
238            panic!("Expected remaining peer cancellation");
239        };
240
241        assert_eq!(instrument_id, second);
242        assert_eq!(client_order_id, second_id);
243        assert!(cancel_contingencies);
244        assert_eq!(excluded, vec![first_id]);
245
246        let OtoCommand::Resize {
247            instrument_id,
248            parent_order_id,
249            quantity,
250        } = orders.pop_command().unwrap()
251        else {
252            panic!("Expected remaining peer resize");
253        };
254
255        assert_eq!(instrument_id, second);
256        assert_eq!(parent_order_id, second_id);
257        assert_eq!(quantity, Quantity::from("5.670"));
258        assert!(orders.pop_command().is_none());
259    }
260}