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