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