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