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