Skip to main content

nautilus_execution/client/
core.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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
18use std::sync::atomic::{AtomicBool, Ordering};
19
20use nautilus_common::cache::{Cache, CacheView};
21use nautilus_model::{
22    enums::{AccountType, OmsType},
23    identifiers::{AccountId, ClientId, ClientOrderId, TraderId, Venue},
24    orders::{OrderAny, OrderList},
25    types::Currency,
26};
27
28/// Base implementation for execution clients providing identity and connection state.
29///
30/// This struct provides the foundation for all execution clients, holding
31/// client identity, connection state, and read-only cache access. Execution
32/// clients use this as a base and extend it with venue-specific implementations.
33///
34/// For event generation, use `OrderEventFactory` from `nautilus_common::factories`.
35/// For live adapters, use `ExecutionEventEmitter` which combines event generation
36/// with async dispatch. For backtest/sandbox, use `OrderEventFactory` directly
37/// and dispatch via `msgbus::send_order_event()`.
38#[derive(Debug)]
39pub struct ExecutionClientCore {
40    pub trader_id: TraderId,
41    pub client_id: ClientId,
42    pub venue: Venue,
43    pub oms_type: OmsType,
44    pub account_id: AccountId,
45    pub account_type: AccountType,
46    pub base_currency: Option<Currency>,
47    connected: AtomicBool,
48    started: AtomicBool,
49    instruments_initialized: AtomicBool,
50    cache: CacheView,
51}
52
53impl Clone for ExecutionClientCore {
54    fn clone(&self) -> Self {
55        Self {
56            trader_id: self.trader_id,
57            client_id: self.client_id,
58            venue: self.venue,
59            oms_type: self.oms_type,
60            account_id: self.account_id,
61            account_type: self.account_type,
62            base_currency: self.base_currency,
63            connected: AtomicBool::new(self.connected.load(Ordering::Acquire)),
64            started: AtomicBool::new(self.started.load(Ordering::Acquire)),
65            instruments_initialized: AtomicBool::new(
66                self.instruments_initialized.load(Ordering::Acquire),
67            ),
68            cache: self.cache.clone(),
69        }
70    }
71}
72
73impl ExecutionClientCore {
74    /// Creates a new [`ExecutionClientCore`] instance.
75    #[expect(clippy::too_many_arguments)]
76    #[must_use]
77    pub fn new(
78        trader_id: TraderId,
79        client_id: ClientId,
80        venue: Venue,
81        oms_type: OmsType,
82        account_id: AccountId,
83        account_type: AccountType,
84        base_currency: Option<Currency>,
85        cache: impl Into<CacheView>,
86    ) -> Self {
87        Self {
88            trader_id,
89            client_id,
90            venue,
91            oms_type,
92            account_id,
93            account_type,
94            base_currency,
95            connected: AtomicBool::new(false),
96            started: AtomicBool::new(false),
97            instruments_initialized: AtomicBool::new(false),
98            cache: cache.into(),
99        }
100    }
101
102    /// Returns a read-only borrow of the cache.
103    pub fn cache(&self) -> std::cell::Ref<'_, Cache> {
104        self.cache.borrow()
105    }
106
107    /// Returns the order for the given `client_order_id` from the cache.
108    ///
109    /// # Errors
110    ///
111    /// Returns an error if the order is not found in the cache.
112    pub fn get_order(&self, client_order_id: &ClientOrderId) -> anyhow::Result<OrderAny> {
113        Ok(self.cache.borrow().try_order_owned(client_order_id)?)
114    }
115
116    /// Returns all orders for the given order list from the cache.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if any order is not found in the cache.
121    pub fn get_orders_for_list(&self, order_list: &OrderList) -> anyhow::Result<Vec<OrderAny>> {
122        order_list
123            .client_order_ids
124            .iter()
125            .map(|id| self.get_order(id))
126            .collect()
127    }
128
129    /// Returns `true` if the client is connected.
130    #[must_use]
131    pub fn is_connected(&self) -> bool {
132        self.connected.load(Ordering::Acquire)
133    }
134
135    /// Returns `true` if the client is disconnected.
136    #[must_use]
137    pub fn is_disconnected(&self) -> bool {
138        !self.is_connected()
139    }
140
141    /// Sets the client as connected.
142    pub fn set_connected(&self) {
143        self.connected.store(true, Ordering::Release);
144    }
145
146    /// Sets the client as disconnected.
147    pub fn set_disconnected(&self) {
148        self.connected.store(false, Ordering::Release);
149    }
150
151    /// Returns `true` if the client has been started.
152    #[must_use]
153    pub fn is_started(&self) -> bool {
154        self.started.load(Ordering::Acquire)
155    }
156
157    /// Returns `true` if the client has not been started.
158    #[must_use]
159    pub fn is_stopped(&self) -> bool {
160        !self.is_started()
161    }
162
163    /// Sets the client as started.
164    pub fn set_started(&self) {
165        self.started.store(true, Ordering::Release);
166    }
167
168    /// Sets the client as stopped.
169    pub fn set_stopped(&self) {
170        self.started.store(false, Ordering::Release);
171    }
172
173    /// Returns `true` if instruments have been initialized.
174    #[must_use]
175    pub fn instruments_initialized(&self) -> bool {
176        self.instruments_initialized.load(Ordering::Acquire)
177    }
178
179    /// Sets instruments as initialized.
180    pub fn set_instruments_initialized(&self) {
181        self.instruments_initialized.store(true, Ordering::Release);
182    }
183
184    /// Sets the account identifier for the execution client.
185    pub const fn set_account_id(&mut self, account_id: AccountId) {
186        self.account_id = account_id;
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use std::{cell::RefCell, rc::Rc};
193
194    use nautilus_common::cache::OrderLookupError;
195    use nautilus_core::UnixNanos;
196    use nautilus_model::{
197        enums::{OrderSide, OrderType},
198        identifiers::OrderListId,
199        orders::{Order, builder::OrderTestBuilder},
200        types::{Price, Quantity},
201    };
202    use rstest::rstest;
203
204    use super::*;
205
206    #[rstest]
207    fn test_get_orders_for_list_preserves_order_and_cached_fields() {
208        let first = OrderTestBuilder::new(OrderType::Limit)
209            .client_order_id(ClientOrderId::from("O-SECOND"))
210            .instrument_id("AUD/USD.SIM".into())
211            .side(OrderSide::Buy)
212            .price(Price::from("0.65001"))
213            .quantity(Quantity::from(17))
214            .build();
215        let second = OrderTestBuilder::new(OrderType::Limit)
216            .client_order_id(ClientOrderId::from("O-FIRST"))
217            .instrument_id("EUR/USD.SIM".into())
218            .side(OrderSide::Sell)
219            .price(Price::from("1.08002"))
220            .quantity(Quantity::from(29))
221            .build();
222        let cache = Rc::new(RefCell::new(Cache::default()));
223
224        for order in [&second, &first] {
225            cache
226                .borrow_mut()
227                .add_order(order.clone(), None, None, false)
228                .unwrap();
229        }
230
231        let core = core(cache);
232        let list = OrderList::new(
233            OrderListId::from("OL-001"),
234            first.instrument_id(),
235            first.strategy_id(),
236            vec![first.client_order_id(), second.client_order_id()],
237            UnixNanos::new(123),
238        );
239
240        let orders = core.get_orders_for_list(&list).unwrap();
241
242        assert_eq!(orders.len(), 2);
243        for (actual, expected) in orders.iter().zip([&first, &second]) {
244            // OrderAny equality compares only client_order_id
245            assert_eq!(actual.init_event(), expected.init_event());
246            assert_eq!(actual.status(), expected.status());
247            assert_eq!(actual.filled_qty(), expected.filled_qty());
248        }
249    }
250
251    #[rstest]
252    #[case(0)]
253    #[case(1)]
254    fn test_get_orders_for_list_rejects_missing_member(#[case] missing_index: usize) {
255        let order = OrderTestBuilder::new(OrderType::Market)
256            .client_order_id(ClientOrderId::from("O-PRESENT"))
257            .instrument_id("AUD/USD.SIM".into())
258            .side(OrderSide::Buy)
259            .quantity(Quantity::from(17))
260            .build();
261        let cache = Rc::new(RefCell::new(Cache::default()));
262        cache
263            .borrow_mut()
264            .add_order(order.clone(), None, None, false)
265            .unwrap();
266        let core = core(cache);
267        let missing_id = ClientOrderId::from("O-MISSING");
268        let mut ids = vec![order.client_order_id()];
269        ids.insert(missing_index, missing_id);
270        let list = OrderList::new(
271            OrderListId::from("OL-001"),
272            order.instrument_id(),
273            order.strategy_id(),
274            ids,
275            UnixNanos::new(123),
276        );
277
278        let error = core.get_orders_for_list(&list).unwrap_err();
279
280        assert_eq!(
281            error.downcast_ref::<OrderLookupError>(),
282            Some(&OrderLookupError::NotFound {
283                client_order_id: missing_id
284            }),
285        );
286        assert_eq!(
287            core.get_order(&order.client_order_id())
288                .unwrap()
289                .init_event(),
290            order.init_event()
291        );
292    }
293
294    fn core(cache: Rc<RefCell<Cache>>) -> ExecutionClientCore {
295        ExecutionClientCore::new(
296            TraderId::from("TRADER-007"),
297            ClientId::from("CLIENT-003"),
298            Venue::from("SIM"),
299            OmsType::Hedging,
300            AccountId::from("SIM-009"),
301            AccountType::Margin,
302            Some(Currency::USD()),
303            cache,
304        )
305    }
306}