nautilus_execution/client/
base.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//! Base execution client functionality.
17
18// Under development
19#![allow(dead_code)]
20#![allow(unused_variables)]
21
22use std::{any::Any, cell::RefCell, fmt::Debug, rc::Rc};
23
24use nautilus_common::{
25    cache::Cache, clock::Clock, messages::ExecutionReport, msgbus,
26    msgbus::switchboard::MessagingSwitchboard,
27};
28use nautilus_core::{UUID4, UnixNanos};
29use nautilus_model::{
30    accounts::AccountAny,
31    enums::{AccountType, LiquiditySide, OmsType, OrderSide, OrderType},
32    events::{
33        AccountState, OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied,
34        OrderEventAny, OrderExpired, OrderFilled, OrderModifyRejected, OrderRejected,
35        OrderSubmitted, OrderTriggered, OrderUpdated,
36    },
37    identifiers::{
38        AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId,
39        TraderId, Venue, VenueOrderId,
40    },
41    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
42    types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
43};
44
45/// Base implementation for execution clients providing common functionality.
46///
47/// This struct provides the foundation for all execution clients, handling
48/// account state generation, order event creation, and message routing.
49/// Execution clients can inherit this base functionality and extend it
50/// with venue-specific implementations.
51#[derive(Clone)]
52pub struct ExecutionClientCore {
53    pub trader_id: TraderId,
54    pub client_id: ClientId,
55    pub venue: Venue,
56    pub oms_type: OmsType,
57    pub account_id: AccountId,
58    pub account_type: AccountType,
59    pub base_currency: Option<Currency>,
60    pub is_connected: bool,
61    clock: Rc<RefCell<dyn Clock>>,
62    cache: Rc<RefCell<Cache>>,
63}
64
65impl Debug for ExecutionClientCore {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.debug_struct(stringify!(ExecutionClientCore))
68            .field("client_id", &self.client_id)
69            .finish()
70    }
71}
72
73impl ExecutionClientCore {
74    /// Creates a new [`ExecutionClientCore`] instance.
75    #[allow(clippy::too_many_arguments)]
76    pub const fn new(
77        trader_id: TraderId,
78        client_id: ClientId,
79        venue: Venue,
80        oms_type: OmsType,
81        account_id: AccountId,
82        account_type: AccountType,
83        base_currency: Option<Currency>,
84        clock: Rc<RefCell<dyn Clock>>,
85        cache: Rc<RefCell<Cache>>,
86    ) -> Self {
87        Self {
88            trader_id,
89            client_id,
90            venue,
91            oms_type,
92            account_id,
93            account_type,
94            base_currency,
95            is_connected: false,
96            clock,
97            cache,
98        }
99    }
100
101    /// Sets the connection status of the execution client.
102    pub const fn set_connected(&mut self, is_connected: bool) {
103        self.is_connected = is_connected;
104    }
105
106    /// Sets the account identifier for the execution client.
107    pub const fn set_account_id(&mut self, account_id: AccountId) {
108        self.account_id = account_id;
109    }
110
111    /// Returns a reference to the clock.
112    #[must_use]
113    pub const fn clock(&self) -> &Rc<RefCell<dyn Clock>> {
114        &self.clock
115    }
116
117    /// Returns a reference to the cache.
118    #[must_use]
119    pub const fn cache(&self) -> &Rc<RefCell<Cache>> {
120        &self.cache
121    }
122
123    /// Returns the account associated with this execution client.
124    #[must_use]
125    pub fn get_account(&self) -> Option<AccountAny> {
126        self.cache.borrow().account(&self.account_id).cloned()
127    }
128
129    /// Generates and publishes the account state event.
130    ///
131    /// # Errors
132    ///
133    /// Returns an error if constructing or sending the account state fails.
134    pub fn generate_account_state(
135        &self,
136        balances: Vec<AccountBalance>,
137        margins: Vec<MarginBalance>,
138        reported: bool,
139        ts_event: UnixNanos,
140        // info:  TODO: Need to double check the use case here
141    ) -> anyhow::Result<()> {
142        let account_state = AccountState::new(
143            self.account_id,
144            self.account_type,
145            balances,
146            margins,
147            reported,
148            UUID4::new(),
149            ts_event,
150            self.clock.borrow().timestamp_ns(),
151            self.base_currency,
152        );
153        self.send_account_state(account_state);
154        Ok(())
155    }
156
157    pub fn generate_order_denied(
158        &self,
159        strategy_id: StrategyId,
160        instrument_id: InstrumentId,
161        client_order_id: ClientOrderId,
162        reason: &str,
163        ts_event: UnixNanos,
164    ) {
165        let event = OrderDenied::new(
166            self.trader_id,
167            strategy_id,
168            instrument_id,
169            client_order_id,
170            reason.into(),
171            UUID4::new(),
172            ts_event,
173            self.clock.borrow().timestamp_ns(),
174        );
175        self.send_order_event(OrderEventAny::Denied(event));
176    }
177
178    pub fn generate_order_submitted(
179        &self,
180        strategy_id: StrategyId,
181        instrument_id: InstrumentId,
182        client_order_id: ClientOrderId,
183        ts_event: UnixNanos,
184    ) {
185        let event = OrderSubmitted::new(
186            self.trader_id,
187            strategy_id,
188            instrument_id,
189            client_order_id,
190            self.account_id,
191            UUID4::new(),
192            ts_event,
193            self.clock.borrow().timestamp_ns(),
194        );
195        self.send_order_event(OrderEventAny::Submitted(event));
196    }
197
198    pub fn generate_order_rejected(
199        &self,
200        strategy_id: StrategyId,
201        instrument_id: InstrumentId,
202        client_order_id: ClientOrderId,
203        reason: &str,
204        ts_event: UnixNanos,
205        due_post_only: bool,
206    ) {
207        let event = OrderRejected::new(
208            self.trader_id,
209            strategy_id,
210            instrument_id,
211            client_order_id,
212            self.account_id,
213            reason.into(),
214            UUID4::new(),
215            ts_event,
216            self.clock.borrow().timestamp_ns(),
217            false,
218            due_post_only,
219        );
220        self.send_order_event(OrderEventAny::Rejected(event));
221    }
222
223    pub fn generate_order_accepted(
224        &self,
225        strategy_id: StrategyId,
226        instrument_id: InstrumentId,
227        client_order_id: ClientOrderId,
228        venue_order_id: VenueOrderId,
229        ts_event: UnixNanos,
230    ) {
231        let event = OrderAccepted::new(
232            self.trader_id,
233            strategy_id,
234            instrument_id,
235            client_order_id,
236            venue_order_id,
237            self.account_id,
238            UUID4::new(),
239            ts_event,
240            self.clock.borrow().timestamp_ns(),
241            false,
242        );
243        self.send_order_event(OrderEventAny::Accepted(event));
244    }
245
246    pub fn generate_order_modify_rejected(
247        &self,
248        strategy_id: StrategyId,
249        instrument_id: InstrumentId,
250        client_order_id: ClientOrderId,
251        venue_order_id: VenueOrderId,
252        reason: &str,
253        ts_event: UnixNanos,
254    ) {
255        let event = OrderModifyRejected::new(
256            self.trader_id,
257            strategy_id,
258            instrument_id,
259            client_order_id,
260            reason.into(),
261            UUID4::new(),
262            ts_event,
263            self.clock.borrow().timestamp_ns(),
264            false,
265            Some(venue_order_id),
266            Some(self.account_id),
267        );
268        self.send_order_event(OrderEventAny::ModifyRejected(event));
269    }
270
271    pub fn generate_order_cancel_rejected(
272        &self,
273        strategy_id: StrategyId,
274        instrument_id: InstrumentId,
275        client_order_id: ClientOrderId,
276        venue_order_id: VenueOrderId,
277        reason: &str,
278        ts_event: UnixNanos,
279    ) {
280        let event = OrderCancelRejected::new(
281            self.trader_id,
282            strategy_id,
283            instrument_id,
284            client_order_id,
285            reason.into(),
286            UUID4::new(),
287            ts_event,
288            self.clock.borrow().timestamp_ns(),
289            false,
290            Some(venue_order_id),
291            Some(self.account_id),
292        );
293        self.send_order_event(OrderEventAny::CancelRejected(event));
294    }
295
296    #[allow(clippy::too_many_arguments)]
297    pub fn generate_order_updated(
298        &self,
299        strategy_id: StrategyId,
300        instrument_id: InstrumentId,
301        client_order_id: ClientOrderId,
302        venue_order_id: VenueOrderId,
303        quantity: Quantity,
304        price: Price,
305        trigger_price: Option<Price>,
306        protection_price: Option<Price>,
307        ts_event: UnixNanos,
308        venue_order_id_modified: bool,
309    ) {
310        if !venue_order_id_modified {
311            let cache = self.cache.as_ref().borrow();
312            let existing_order_result = cache.venue_order_id(&client_order_id);
313            if let Some(existing_order) = existing_order_result
314                && *existing_order != venue_order_id
315            {
316                log::error!(
317                    "Existing venue order id {existing_order} does not match provided venue order id {venue_order_id}"
318                );
319            }
320        }
321
322        let event = OrderUpdated::new(
323            self.trader_id,
324            strategy_id,
325            instrument_id,
326            client_order_id,
327            quantity,
328            UUID4::new(),
329            ts_event,
330            self.clock.borrow().timestamp_ns(),
331            false,
332            Some(venue_order_id),
333            Some(self.account_id),
334            Some(price),
335            trigger_price,
336            protection_price,
337        );
338
339        self.send_order_event(OrderEventAny::Updated(event));
340    }
341
342    pub fn generate_order_canceled(
343        &self,
344        strategy_id: StrategyId,
345        instrument_id: InstrumentId,
346        client_order_id: ClientOrderId,
347        venue_order_id: VenueOrderId,
348        ts_event: UnixNanos,
349    ) {
350        let event = OrderCanceled::new(
351            self.trader_id,
352            strategy_id,
353            instrument_id,
354            client_order_id,
355            UUID4::new(),
356            ts_event,
357            self.clock.borrow().timestamp_ns(),
358            false,
359            Some(venue_order_id),
360            Some(self.account_id),
361        );
362
363        self.send_order_event(OrderEventAny::Canceled(event));
364    }
365
366    pub fn generate_order_triggered(
367        &self,
368        strategy_id: StrategyId,
369        instrument_id: InstrumentId,
370        client_order_id: ClientOrderId,
371        venue_order_id: VenueOrderId,
372        ts_event: UnixNanos,
373    ) {
374        let event = OrderTriggered::new(
375            self.trader_id,
376            strategy_id,
377            instrument_id,
378            client_order_id,
379            UUID4::new(),
380            ts_event,
381            self.clock.borrow().timestamp_ns(),
382            false,
383            Some(venue_order_id),
384            Some(self.account_id),
385        );
386
387        self.send_order_event(OrderEventAny::Triggered(event));
388    }
389
390    pub fn generate_order_expired(
391        &self,
392        strategy_id: StrategyId,
393        instrument_id: InstrumentId,
394        client_order_id: ClientOrderId,
395        venue_order_id: VenueOrderId,
396        ts_event: UnixNanos,
397    ) {
398        let event = OrderExpired::new(
399            self.trader_id,
400            strategy_id,
401            instrument_id,
402            client_order_id,
403            UUID4::new(),
404            ts_event,
405            self.clock.borrow().timestamp_ns(),
406            false,
407            Some(venue_order_id),
408            Some(self.account_id),
409        );
410
411        self.send_order_event(OrderEventAny::Expired(event));
412    }
413
414    #[allow(clippy::too_many_arguments)]
415    pub fn generate_order_filled(
416        &self,
417        strategy_id: StrategyId,
418        instrument_id: InstrumentId,
419        client_order_id: ClientOrderId,
420        venue_order_id: VenueOrderId,
421        venue_position_id: PositionId,
422        trade_id: TradeId,
423        order_side: OrderSide,
424        order_type: OrderType,
425        last_qty: Quantity,
426        last_px: Price,
427        quote_currency: Currency,
428        commission: Money,
429        liquidity_side: LiquiditySide,
430        ts_event: UnixNanos,
431    ) {
432        let event = OrderFilled::new(
433            self.trader_id,
434            strategy_id,
435            instrument_id,
436            client_order_id,
437            venue_order_id,
438            self.account_id,
439            trade_id,
440            order_side,
441            order_type,
442            last_qty,
443            last_px,
444            quote_currency,
445            liquidity_side,
446            UUID4::new(),
447            ts_event,
448            self.clock.borrow().timestamp_ns(),
449            false,
450            Some(venue_position_id),
451            Some(commission),
452        );
453
454        self.send_order_event(OrderEventAny::Filled(event));
455    }
456
457    fn send_account_state(&self, account_state: AccountState) {
458        let endpoint = MessagingSwitchboard::portfolio_update_account();
459        msgbus::send_any(endpoint, &account_state as &dyn Any);
460    }
461
462    fn send_order_event(&self, event: OrderEventAny) {
463        let endpoint = MessagingSwitchboard::exec_engine_process();
464        msgbus::send_any(endpoint, &event as &dyn Any);
465    }
466
467    fn send_mass_status_report(&self, report: ExecutionMassStatus) {
468        let endpoint = MessagingSwitchboard::exec_engine_reconcile_execution_report();
469        let report = ExecutionReport::MassStatus(Box::new(report));
470        msgbus::send_any(endpoint, &report as &dyn Any);
471    }
472
473    fn send_order_status_report(&self, report: OrderStatusReport) {
474        let endpoint = MessagingSwitchboard::exec_engine_reconcile_execution_report();
475        let report = ExecutionReport::Order(Box::new(report));
476        msgbus::send_any(endpoint, &report as &dyn Any);
477    }
478
479    fn send_fill_report(&self, report: FillReport) {
480        let endpoint = MessagingSwitchboard::exec_engine_reconcile_execution_report();
481        let report = ExecutionReport::Fill(Box::new(report));
482        msgbus::send_any(endpoint, &report as &dyn Any);
483    }
484
485    fn send_position_report(&self, report: PositionStatusReport) {
486        let endpoint = MessagingSwitchboard::exec_engine_reconcile_execution_report();
487        let report = ExecutionReport::Position(Box::new(report));
488        msgbus::send_any(endpoint, &report as &dyn Any);
489    }
490}