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}