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