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