nautilus_execution/client/
base.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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    ) {
169        let event = OrderRejected::new(
170            self.trader_id,
171            strategy_id,
172            instrument_id,
173            client_order_id,
174            self.account_id,
175            reason.into(),
176            UUID4::new(),
177            ts_event,
178            self.clock.borrow().timestamp_ns(),
179            false,
180        );
181        self.send_order_event(OrderEventAny::Rejected(event));
182    }
183
184    pub fn generate_order_accepted(
185        &self,
186        strategy_id: StrategyId,
187        instrument_id: InstrumentId,
188        client_order_id: ClientOrderId,
189        venue_order_id: VenueOrderId,
190        ts_event: UnixNanos,
191    ) {
192        let event = OrderAccepted::new(
193            self.trader_id,
194            strategy_id,
195            instrument_id,
196            client_order_id,
197            venue_order_id,
198            self.account_id,
199            UUID4::new(),
200            ts_event,
201            self.clock.borrow().timestamp_ns(),
202            false,
203        );
204        self.send_order_event(OrderEventAny::Accepted(event));
205    }
206
207    pub fn generate_order_modify_rejected(
208        &self,
209        strategy_id: StrategyId,
210        instrument_id: InstrumentId,
211        client_order_id: ClientOrderId,
212        venue_order_id: VenueOrderId,
213        reason: &str,
214        ts_event: UnixNanos,
215    ) {
216        let event = OrderModifyRejected::new(
217            self.trader_id,
218            strategy_id,
219            instrument_id,
220            client_order_id,
221            reason.into(),
222            UUID4::new(),
223            ts_event,
224            self.clock.borrow().timestamp_ns(),
225            false,
226            Some(venue_order_id),
227            Some(self.account_id),
228        );
229        self.send_order_event(OrderEventAny::ModifyRejected(event));
230    }
231
232    pub fn generate_order_cancel_rejected(
233        &self,
234        strategy_id: StrategyId,
235        instrument_id: InstrumentId,
236        client_order_id: ClientOrderId,
237        venue_order_id: VenueOrderId,
238        reason: &str,
239        ts_event: UnixNanos,
240    ) {
241        let event = OrderCancelRejected::new(
242            self.trader_id,
243            strategy_id,
244            instrument_id,
245            client_order_id,
246            reason.into(),
247            UUID4::new(),
248            ts_event,
249            self.clock.borrow().timestamp_ns(),
250            false,
251            Some(venue_order_id),
252            Some(self.account_id),
253        );
254        self.send_order_event(OrderEventAny::CancelRejected(event));
255    }
256
257    #[allow(clippy::too_many_arguments)]
258    pub fn generate_order_updated(
259        &self,
260        strategy_id: StrategyId,
261        instrument_id: InstrumentId,
262        client_order_id: ClientOrderId,
263        venue_order_id: VenueOrderId,
264        quantity: Quantity,
265        price: Price,
266        trigger_price: Option<Price>,
267        ts_event: UnixNanos,
268        venue_order_id_modified: bool,
269    ) {
270        if !venue_order_id_modified {
271            let cache = self.cache.as_ref().borrow();
272            let existing_order_result = cache.venue_order_id(&client_order_id);
273            if let Some(existing_order) = existing_order_result {
274                if *existing_order != venue_order_id {
275                    log::error!(
276                        "Existing venue order id {existing_order} does not match provided venue order id {venue_order_id}"
277                    );
278                }
279            }
280        }
281
282        let event = OrderUpdated::new(
283            self.trader_id,
284            strategy_id,
285            instrument_id,
286            client_order_id,
287            quantity,
288            UUID4::new(),
289            ts_event,
290            self.clock.borrow().timestamp_ns(),
291            false,
292            Some(venue_order_id),
293            Some(self.account_id),
294            Some(price),
295            trigger_price,
296        );
297
298        self.send_order_event(OrderEventAny::Updated(event));
299    }
300
301    pub fn generate_order_canceled(
302        &self,
303        strategy_id: StrategyId,
304        instrument_id: InstrumentId,
305        client_order_id: ClientOrderId,
306        venue_order_id: VenueOrderId,
307        ts_event: UnixNanos,
308    ) {
309        let event = OrderCanceled::new(
310            self.trader_id,
311            strategy_id,
312            instrument_id,
313            client_order_id,
314            UUID4::new(),
315            ts_event,
316            self.clock.borrow().timestamp_ns(),
317            false,
318            Some(venue_order_id),
319            Some(self.account_id),
320        );
321
322        self.send_order_event(OrderEventAny::Canceled(event));
323    }
324
325    pub fn generate_order_triggered(
326        &self,
327        strategy_id: StrategyId,
328        instrument_id: InstrumentId,
329        client_order_id: ClientOrderId,
330        venue_order_id: VenueOrderId,
331        ts_event: UnixNanos,
332    ) {
333        let event = OrderTriggered::new(
334            self.trader_id,
335            strategy_id,
336            instrument_id,
337            client_order_id,
338            UUID4::new(),
339            ts_event,
340            self.clock.borrow().timestamp_ns(),
341            false,
342            Some(venue_order_id),
343            Some(self.account_id),
344        );
345
346        self.send_order_event(OrderEventAny::Triggered(event));
347    }
348
349    pub fn generate_order_expired(
350        &self,
351        strategy_id: StrategyId,
352        instrument_id: InstrumentId,
353        client_order_id: ClientOrderId,
354        venue_order_id: VenueOrderId,
355        ts_event: UnixNanos,
356    ) {
357        let event = OrderExpired::new(
358            self.trader_id,
359            strategy_id,
360            instrument_id,
361            client_order_id,
362            UUID4::new(),
363            ts_event,
364            self.clock.borrow().timestamp_ns(),
365            false,
366            Some(venue_order_id),
367            Some(self.account_id),
368        );
369
370        self.send_order_event(OrderEventAny::Expired(event));
371    }
372
373    #[allow(clippy::too_many_arguments)]
374    pub fn generate_order_filled(
375        &self,
376        strategy_id: StrategyId,
377        instrument_id: InstrumentId,
378        client_order_id: ClientOrderId,
379        venue_order_id: VenueOrderId,
380        venue_position_id: PositionId,
381        trade_id: TradeId,
382        order_side: OrderSide,
383        order_type: OrderType,
384        last_qty: Quantity,
385        last_px: Price,
386        quote_currency: Currency,
387        commission: Money,
388        liquidity_side: LiquiditySide,
389        ts_event: UnixNanos,
390    ) {
391        let event = OrderFilled::new(
392            self.trader_id,
393            strategy_id,
394            instrument_id,
395            client_order_id,
396            venue_order_id,
397            self.account_id,
398            trade_id,
399            order_side,
400            order_type,
401            last_qty,
402            last_px,
403            quote_currency,
404            liquidity_side,
405            UUID4::new(),
406            ts_event,
407            self.clock.borrow().timestamp_ns(),
408            false,
409            Some(venue_position_id),
410            Some(commission),
411        );
412
413        self.send_order_event(OrderEventAny::Filled(event));
414    }
415
416    fn send_account_state(&self, account_state: AccountState) {
417        let endpoint = "Portfolio.update_account".into();
418        msgbus::send_any(endpoint, &account_state as &dyn Any);
419    }
420
421    fn send_order_event(&self, event: OrderEventAny) {
422        let endpoint = "ExecEngine.process".into();
423        msgbus::send_any(endpoint, &event as &dyn Any);
424    }
425
426    fn send_mass_status_report(&self, report: ExecutionMassStatus) {
427        let endpoint = "ExecEngine.reconcile_mass_status".into();
428        msgbus::send_any(endpoint, &report as &dyn Any);
429    }
430
431    fn send_order_status_report(&self, report: OrderStatusReport) {
432        let endpoint = "ExecEngine.reconcile_report".into();
433        msgbus::send_any(endpoint, &report as &dyn Any);
434    }
435
436    fn send_fill_report(&self, report: FillReport) {
437        let endpoint = "ExecEngine.reconcile_report".into();
438        msgbus::send_any(endpoint, &report as &dyn Any);
439    }
440
441    fn send_position_report(&self, report: PositionStatusReport) {
442        let endpoint = "ExecEngine.reconcile_report".into();
443        msgbus::send_any(endpoint, &report as &dyn Any);
444    }
445}