Skip to main content

nautilus_execution/engine/
stubs.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
16use std::{
17    cell::{Cell, RefCell},
18    rc::Rc,
19};
20
21use async_trait::async_trait;
22use nautilus_common::{
23    cache::Cache,
24    clients::ExecutionClient,
25    clock::{Clock, TestClock},
26    messages::execution::{
27        BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, ModifyOrder,
28        QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
29    },
30};
31use nautilus_core::{Params, UnixNanos};
32use nautilus_model::{
33    accounts::AccountAny,
34    enums::OmsType,
35    identifiers::{
36        AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, Venue, VenueOrderId,
37    },
38    instruments::InstrumentAny,
39    types::{AccountBalance, MarginBalance},
40};
41
42/// A stub execution client for testing purposes.
43///
44/// This client provides a minimal implementation of the `ExecutionClient` trait
45/// that can be used in unit tests without requiring actual venue connectivity.
46#[derive(Clone, Debug)]
47#[allow(dead_code)]
48pub struct StubExecutionClient {
49    client_id: ClientId,
50    account_id: AccountId,
51    venue: Venue,
52    oms_type: OmsType,
53    is_connected: bool,
54    clock: Rc<RefCell<dyn Clock>>,
55    cache: Rc<RefCell<Cache>>,
56    received_instruments: Rc<RefCell<Vec<InstrumentAny>>>,
57    start_count: Rc<Cell<usize>>,
58    stop_count: Rc<Cell<usize>>,
59    reset_count: Rc<Cell<usize>>,
60    dispose_count: Rc<Cell<usize>>,
61    submitted_order_ids: Rc<RefCell<Vec<ClientOrderId>>>,
62    modified_order_ids: Rc<RefCell<Vec<ClientOrderId>>>,
63    queried_account_ids: Rc<RefCell<Vec<AccountId>>>,
64    registered_external_order_ids: Rc<RefCell<Vec<ClientOrderId>>>,
65    handles_all_order_venues: bool,
66    submit_order_error: Option<String>,
67    submit_order_list_error: Option<String>,
68}
69
70impl StubExecutionClient {
71    /// Creates a new [`StubExecutionClient`] instance.
72    #[allow(dead_code)]
73    pub fn new(
74        client_id: ClientId,
75        account_id: AccountId,
76        venue: Venue,
77        oms_type: OmsType,
78        clock: Option<Rc<RefCell<dyn Clock>>>,
79    ) -> Self {
80        Self {
81            client_id,
82            account_id,
83            venue,
84            oms_type,
85            is_connected: false,
86            clock: clock.unwrap_or_else(|| Rc::new(RefCell::new(TestClock::new()))),
87            cache: Rc::new(RefCell::new(Cache::new(None, None))),
88            received_instruments: Rc::new(RefCell::new(Vec::new())),
89            start_count: Rc::new(Cell::new(0)),
90            stop_count: Rc::new(Cell::new(0)),
91            reset_count: Rc::new(Cell::new(0)),
92            dispose_count: Rc::new(Cell::new(0)),
93            submitted_order_ids: Rc::new(RefCell::new(Vec::new())),
94            modified_order_ids: Rc::new(RefCell::new(Vec::new())),
95            queried_account_ids: Rc::new(RefCell::new(Vec::new())),
96            registered_external_order_ids: Rc::new(RefCell::new(Vec::new())),
97            handles_all_order_venues: false,
98            submit_order_error: None,
99            submit_order_list_error: None,
100        }
101    }
102
103    /// Configures this stub to accept orders for any instrument venue.
104    #[must_use]
105    pub fn with_handles_all_order_venues(mut self) -> Self {
106        self.handles_all_order_venues = true;
107        self
108    }
109
110    /// Configures this stub to fail single-order submissions.
111    #[must_use]
112    pub fn with_submit_order_error(mut self, error: impl Into<String>) -> Self {
113        self.submit_order_error = Some(error.into());
114        self
115    }
116
117    /// Configures this stub to fail order-list submissions.
118    #[must_use]
119    pub fn with_submit_order_list_error(mut self, error: impl Into<String>) -> Self {
120        self.submit_order_list_error = Some(error.into());
121        self
122    }
123
124    /// Returns a shared handle to the order IDs registered via
125    /// [`ExecutionClient::register_external_order`].
126    #[must_use]
127    pub fn registered_external_order_ids(&self) -> Rc<RefCell<Vec<ClientOrderId>>> {
128        self.registered_external_order_ids.clone()
129    }
130
131    /// Returns a shared handle to the instruments delivered via [`ExecutionClient::on_instrument`].
132    #[must_use]
133    pub fn received_instruments(&self) -> Rc<RefCell<Vec<InstrumentAny>>> {
134        self.received_instruments.clone()
135    }
136
137    /// Returns a shared handle to the submitted order IDs.
138    #[must_use]
139    pub fn submitted_order_ids(&self) -> Rc<RefCell<Vec<ClientOrderId>>> {
140        self.submitted_order_ids.clone()
141    }
142
143    /// Returns a shared handle to the modified order IDs.
144    #[must_use]
145    pub fn modified_order_ids(&self) -> Rc<RefCell<Vec<ClientOrderId>>> {
146        self.modified_order_ids.clone()
147    }
148
149    /// Returns a shared handle to the queried account IDs.
150    #[must_use]
151    pub fn queried_account_ids(&self) -> Rc<RefCell<Vec<AccountId>>> {
152        self.queried_account_ids.clone()
153    }
154
155    /// Returns the number of times [`ExecutionClient::start`] was invoked.
156    #[must_use]
157    pub fn start_count(&self) -> usize {
158        self.start_count.get()
159    }
160
161    /// Returns the number of times [`ExecutionClient::stop`] was invoked.
162    #[must_use]
163    pub fn stop_count(&self) -> usize {
164        self.stop_count.get()
165    }
166
167    /// Returns the number of times [`ExecutionClient::reset`] was invoked.
168    #[must_use]
169    pub fn reset_count(&self) -> usize {
170        self.reset_count.get()
171    }
172
173    /// Returns the number of times [`ExecutionClient::dispose`] was invoked.
174    #[must_use]
175    pub fn dispose_count(&self) -> usize {
176        self.dispose_count.get()
177    }
178}
179
180#[async_trait(?Send)]
181impl ExecutionClient for StubExecutionClient {
182    fn is_connected(&self) -> bool {
183        self.is_connected
184    }
185
186    fn client_id(&self) -> ClientId {
187        self.client_id
188    }
189
190    fn account_id(&self) -> AccountId {
191        self.account_id
192    }
193
194    fn venue(&self) -> Venue {
195        self.venue
196    }
197
198    fn handles_order_venue(&self, venue: Venue) -> bool {
199        self.handles_all_order_venues || self.venue == venue
200    }
201
202    fn oms_type(&self) -> OmsType {
203        self.oms_type
204    }
205
206    fn get_account(&self) -> Option<AccountAny> {
207        None // Stub implementation returns None
208    }
209
210    fn generate_account_state(
211        &self,
212        _balances: Vec<AccountBalance>,
213        _margins: Vec<MarginBalance>,
214        _reported: bool,
215        _ts_event: UnixNanos,
216        _info: Option<Params>,
217    ) -> anyhow::Result<()> {
218        Ok(()) // Stub implementation always succeeds
219    }
220
221    fn start(&mut self) -> anyhow::Result<()> {
222        self.is_connected = true;
223        self.start_count.set(self.start_count.get() + 1);
224        Ok(())
225    }
226
227    fn stop(&mut self) -> anyhow::Result<()> {
228        self.is_connected = false;
229        self.stop_count.set(self.stop_count.get() + 1);
230        Ok(())
231    }
232
233    fn reset(&mut self) -> anyhow::Result<()> {
234        self.reset_count.set(self.reset_count.get() + 1);
235        Ok(())
236    }
237
238    fn dispose(&mut self) -> anyhow::Result<()> {
239        self.dispose_count.set(self.dispose_count.get() + 1);
240        Ok(())
241    }
242
243    fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
244        if let Some(error) = &self.submit_order_error {
245            anyhow::bail!("{error}");
246        }
247
248        self.submitted_order_ids
249            .borrow_mut()
250            .push(cmd.client_order_id);
251
252        Ok(()) // Stub implementation always succeeds
253    }
254
255    fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
256        if let Some(error) = &self.submit_order_list_error {
257            anyhow::bail!("{error}");
258        }
259
260        self.submitted_order_ids
261            .borrow_mut()
262            .extend(cmd.order_list.client_order_ids);
263
264        Ok(()) // Stub implementation always succeeds
265    }
266
267    fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
268        self.modified_order_ids
269            .borrow_mut()
270            .push(cmd.client_order_id);
271
272        Ok(()) // Stub implementation always succeeds
273    }
274
275    fn batch_modify_orders(&self, cmd: BatchModifyOrders) -> anyhow::Result<()> {
276        self.modified_order_ids.borrow_mut().extend(
277            cmd.modifies
278                .into_iter()
279                .map(|modify| modify.client_order_id),
280        );
281
282        Ok(()) // Stub implementation always succeeds
283    }
284
285    fn cancel_order(&self, _cmd: CancelOrder) -> anyhow::Result<()> {
286        Ok(()) // Stub implementation always succeeds
287    }
288
289    fn cancel_all_orders(&self, _cmd: CancelAllOrders) -> anyhow::Result<()> {
290        Ok(()) // Stub implementation always succeeds
291    }
292
293    fn batch_cancel_orders(&self, _cmd: BatchCancelOrders) -> anyhow::Result<()> {
294        Ok(()) // Stub implementation always succeeds
295    }
296
297    fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
298        self.queried_account_ids.borrow_mut().push(cmd.account_id);
299
300        Ok(()) // Stub implementation always succeeds
301    }
302
303    fn query_order(&self, _cmd: QueryOrder) -> anyhow::Result<()> {
304        Ok(()) // Stub implementation always succeeds
305    }
306
307    fn register_external_order(
308        &self,
309        client_order_id: ClientOrderId,
310        _venue_order_id: VenueOrderId,
311        _instrument_id: InstrumentId,
312        _strategy_id: StrategyId,
313        _ts_init: UnixNanos,
314    ) {
315        self.registered_external_order_ids
316            .borrow_mut()
317            .push(client_order_id);
318    }
319
320    fn on_instrument(&mut self, instrument: InstrumentAny) {
321        self.received_instruments.borrow_mut().push(instrument);
322    }
323}