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}