nautilus_execution/client/
base.rs

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