Skip to main content

nautilus_common/clients/
execution.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//! Execution client trait definition.
17
18use async_trait::async_trait;
19use nautilus_core::UnixNanos;
20use nautilus_model::{
21    accounts::AccountAny,
22    enums::{LiquiditySide, OmsType},
23    identifiers::{
24        AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, Venue, VenueOrderId,
25    },
26    instruments::InstrumentAny,
27    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
28    types::{AccountBalance, MarginBalance, Money, Price, Quantity},
29};
30use rust_decimal::Decimal;
31
32use super::log_not_implemented;
33use crate::messages::execution::{
34    BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
35    GenerateOrderStatusReport, GenerateOrderStatusReports, GeneratePositionStatusReports,
36    ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
37};
38
39/// Default maximum absolute position difference tolerated during reconciliation.
40pub const DEFAULT_POSITION_RECONCILIATION_TOLERANCE: Decimal =
41    Decimal::from_parts(1, 0, 0, false, 8);
42
43/// Defines the interface for an execution client managing order operations.
44///
45/// # Thread Safety
46///
47/// Client instances are not intended to be sent across threads. The `?Send` bound
48/// allows implementations to hold non-Send state for any Python interop.
49#[async_trait(?Send)]
50pub trait ExecutionClient {
51    fn is_connected(&self) -> bool;
52    fn client_id(&self) -> ClientId;
53    fn account_id(&self) -> AccountId;
54    fn venue(&self) -> Venue;
55    fn oms_type(&self) -> OmsType;
56    fn get_account(&self) -> Option<AccountAny>;
57
58    /// Returns the maximum absolute position difference tolerated during reconciliation.
59    fn position_reconciliation_tolerance(&self) -> Decimal {
60        DEFAULT_POSITION_RECONCILIATION_TOLERANCE
61    }
62
63    /// Returns whether this client can execute orders for the given instrument venue.
64    ///
65    /// Single-venue clients should use the default behavior. Routing brokers can
66    /// override this when their client venue identifies the broker rather than
67    /// the instrument's exchange venue.
68    fn handles_order_venue(&self, venue: Venue) -> bool {
69        self.venue() == venue
70    }
71
72    /// Generates and publishes the account state event.
73    ///
74    /// Implementations may publish synchronously. Callers must release shared state borrows,
75    /// including clock and cache borrows, before calling this method because subscribers may
76    /// access the same state.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if generating the account state fails.
81    fn generate_account_state(
82        &self,
83        balances: Vec<AccountBalance>,
84        margins: Vec<MarginBalance>,
85        reported: bool,
86        ts_event: UnixNanos,
87    ) -> anyhow::Result<()>;
88
89    /// Starts the execution client.
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if the client fails to start.
94    fn start(&mut self) -> anyhow::Result<()>;
95
96    /// Stops the execution client.
97    ///
98    /// Implementations must be idempotent: the engine and node teardown paths
99    /// (e.g. backtest `end` -> `reset` -> `dispose`) may call `stop()` more
100    /// than once per run. Guard with an internal `is_stopped` check or
101    /// equivalent so repeated calls are safe.
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the client fails to stop.
106    fn stop(&mut self) -> anyhow::Result<()>;
107
108    /// Resets the execution client to its initial state.
109    ///
110    /// The default implementation is a no-op. Adapters with reconnectable state
111    /// (caches, sequence counters, in-flight orders) should override this.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if the client fails to reset.
116    fn reset(&mut self) -> anyhow::Result<()> {
117        Ok(())
118    }
119
120    /// Disposes of client resources and cleans up.
121    ///
122    /// The default implementation is a no-op. Adapters that hold async tasks,
123    /// background threads, or external handles should override this.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the client fails to dispose.
128    fn dispose(&mut self) -> anyhow::Result<()> {
129        Ok(())
130    }
131
132    /// Connects the client to the execution venue.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if connection fails.
137    async fn connect(&mut self) -> anyhow::Result<()> {
138        Ok(())
139    }
140
141    /// Disconnects the client from the execution venue.
142    ///
143    /// # Errors
144    ///
145    /// Returns an error if disconnection fails.
146    async fn disconnect(&mut self) -> anyhow::Result<()> {
147        Ok(())
148    }
149
150    /// Submits a single order command to the execution venue.
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if submission fails.
155    fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
156        log_not_implemented(&cmd);
157        Ok(())
158    }
159
160    /// Submits a list of orders to the execution venue.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if submission fails.
165    fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
166        log_not_implemented(&cmd);
167        Ok(())
168    }
169
170    /// Modifies an existing order.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if modification fails.
175    fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
176        log_not_implemented(&cmd);
177        Ok(())
178    }
179
180    /// Modifies a batch of orders.
181    ///
182    /// The default implementation fans out to [`Self::modify_order`] so existing execution
183    /// clients remain compatible until they add native batch support.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if any child modification fails.
188    fn batch_modify_orders(&self, cmd: BatchModifyOrders) -> anyhow::Result<()> {
189        for modify in cmd.modifies {
190            self.modify_order(modify)?;
191        }
192        Ok(())
193    }
194
195    /// Cancels a specific order.
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if cancellation fails.
200    fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
201        log_not_implemented(&cmd);
202        Ok(())
203    }
204
205    /// Cancels all orders.
206    ///
207    /// # Errors
208    ///
209    /// Returns an error if cancellation fails.
210    fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
211        log_not_implemented(&cmd);
212        Ok(())
213    }
214
215    /// Cancels a batch of orders.
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if batch cancellation fails.
220    fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
221        log_not_implemented(&cmd);
222        Ok(())
223    }
224
225    /// Queries the status of an account.
226    ///
227    /// # Errors
228    ///
229    /// Returns an error if the query fails.
230    fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
231        log_not_implemented(&cmd);
232        Ok(())
233    }
234
235    /// Queries the status of an order.
236    ///
237    /// # Errors
238    ///
239    /// Returns an error if the query fails.
240    fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
241        log_not_implemented(&cmd);
242        Ok(())
243    }
244
245    /// Generates a single order status report.
246    ///
247    /// # Errors
248    ///
249    /// Returns an error if report generation fails.
250    async fn generate_order_status_report(
251        &self,
252        cmd: &GenerateOrderStatusReport,
253    ) -> anyhow::Result<Option<OrderStatusReport>> {
254        log_not_implemented(cmd);
255        Ok(None)
256    }
257
258    /// Generates multiple order status reports.
259    ///
260    /// # Errors
261    ///
262    /// Returns an error if report generation fails.
263    async fn generate_order_status_reports(
264        &self,
265        cmd: &GenerateOrderStatusReports,
266    ) -> anyhow::Result<Vec<OrderStatusReport>> {
267        log_not_implemented(cmd);
268        Ok(Vec::new())
269    }
270
271    /// Generates fill reports based on execution results.
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if fill report generation fails.
276    async fn generate_fill_reports(
277        &self,
278        cmd: GenerateFillReports,
279    ) -> anyhow::Result<Vec<FillReport>> {
280        log_not_implemented(&cmd);
281        Ok(Vec::new())
282    }
283
284    /// Generates position status reports.
285    ///
286    /// # Errors
287    ///
288    /// Returns an error if generation fails.
289    async fn generate_position_status_reports(
290        &self,
291        cmd: &GeneratePositionStatusReports,
292    ) -> anyhow::Result<Vec<PositionStatusReport>> {
293        log_not_implemented(cmd);
294        Ok(Vec::new())
295    }
296
297    /// Generates mass status for executions.
298    ///
299    /// # Errors
300    ///
301    /// Returns an error if status generation fails.
302    async fn generate_mass_status(
303        &self,
304        lookback_mins: Option<u64>,
305    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
306        log_not_implemented(&lookback_mins);
307        Ok(None)
308    }
309
310    /// Registers an external order for tracking by the execution client.
311    ///
312    /// This is called after reconciliation creates an external order, allowing the
313    /// execution client to track it for subsequent events (e.g., cancellations).
314    fn register_external_order(
315        &self,
316        _client_order_id: ClientOrderId,
317        _venue_order_id: VenueOrderId,
318        _instrument_id: InstrumentId,
319        _strategy_id: StrategyId,
320        _ts_init: UnixNanos,
321    ) {
322        // Default no-op implementation
323    }
324
325    /// Handles an instrument update received via the message bus.
326    ///
327    /// Exec clients that need live instrument updates (e.g. for internal maps)
328    /// can override this to process instruments for their venue.
329    fn on_instrument(&mut self, _instrument: InstrumentAny) {
330        // Default no-op
331    }
332
333    /// Calculates the commission for a reconciliation fill.
334    ///
335    /// Override this method to provide venue-specific commission logic
336    /// for inferred fills generated during reconciliation.
337    ///
338    /// Returns `None` by default, signaling callers to use their own
339    /// generic commission formula.
340    #[expect(unused_variables)]
341    fn calculate_commission(
342        &self,
343        instrument: &InstrumentAny,
344        last_qty: Quantity,
345        last_px: Price,
346        liquidity_side: LiquiditySide,
347    ) -> Option<Money> {
348        None
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use std::{cell::RefCell, rc::Rc};
355
356    use nautilus_core::UUID4;
357    use nautilus_model::{
358        enums::OmsType,
359        identifiers::{TraderId, Venue},
360    };
361    use rstest::rstest;
362
363    use super::*;
364
365    struct RecordingExecutionClient {
366        modified_order_ids: Rc<RefCell<Vec<ClientOrderId>>>,
367    }
368
369    impl RecordingExecutionClient {
370        fn new(modified_order_ids: Rc<RefCell<Vec<ClientOrderId>>>) -> Self {
371            Self { modified_order_ids }
372        }
373    }
374
375    #[async_trait(?Send)]
376    impl ExecutionClient for RecordingExecutionClient {
377        fn is_connected(&self) -> bool {
378            true
379        }
380
381        fn client_id(&self) -> ClientId {
382            ClientId::from("TEST")
383        }
384
385        fn account_id(&self) -> AccountId {
386            AccountId::from("TEST-001")
387        }
388
389        fn venue(&self) -> Venue {
390            Venue::from("SIM")
391        }
392
393        fn oms_type(&self) -> OmsType {
394            OmsType::Netting
395        }
396
397        fn get_account(&self) -> Option<AccountAny> {
398            None
399        }
400
401        fn generate_account_state(
402            &self,
403            _balances: Vec<AccountBalance>,
404            _margins: Vec<MarginBalance>,
405            _reported: bool,
406            _ts_event: UnixNanos,
407        ) -> anyhow::Result<()> {
408            Ok(())
409        }
410
411        fn start(&mut self) -> anyhow::Result<()> {
412            Ok(())
413        }
414
415        fn stop(&mut self) -> anyhow::Result<()> {
416            Ok(())
417        }
418
419        fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
420            self.modified_order_ids
421                .borrow_mut()
422                .push(cmd.client_order_id);
423
424            Ok(())
425        }
426    }
427
428    #[rstest]
429    fn batch_modify_orders_default_fans_out_to_modify_order() {
430        let modified_order_ids = Rc::new(RefCell::new(Vec::new()));
431        let client = RecordingExecutionClient::new(modified_order_ids.clone());
432        let instrument_id = InstrumentId::from("AUD/USD.SIM");
433        let order1 = ClientOrderId::from("O-DEFAULT-BATCH-001");
434        let order2 = ClientOrderId::from("O-DEFAULT-BATCH-002");
435        let command = BatchModifyOrders::new(
436            TraderId::from("TRADER-001"),
437            Some(ClientId::from("TEST")),
438            StrategyId::from("S-001"),
439            instrument_id,
440            vec![
441                ModifyOrder::new(
442                    TraderId::from("TRADER-001"),
443                    Some(ClientId::from("TEST")),
444                    StrategyId::from("S-001"),
445                    instrument_id,
446                    order1,
447                    None,
448                    Some(Quantity::from("10")),
449                    Some(Price::from("1.00010")),
450                    None,
451                    UUID4::new(),
452                    UnixNanos::default(),
453                    None,
454                    None,
455                ),
456                ModifyOrder::new(
457                    TraderId::from("TRADER-001"),
458                    Some(ClientId::from("TEST")),
459                    StrategyId::from("S-001"),
460                    instrument_id,
461                    order2,
462                    None,
463                    Some(Quantity::from("20")),
464                    Some(Price::from("1.00020")),
465                    None,
466                    UUID4::new(),
467                    UnixNanos::default(),
468                    None,
469                    None,
470                ),
471            ],
472            UUID4::new(),
473            UnixNanos::default(),
474            None,
475            None,
476        );
477
478        client.batch_modify_orders(command).unwrap();
479
480        assert_eq!(modified_order_ids.borrow().as_slice(), &[order1, order2]);
481    }
482}