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