nautilus_execution/client/
core.rs1use 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#[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 #[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 pub fn cache(&self) -> std::cell::Ref<'_, Cache> {
104 self.cache.borrow()
105 }
106
107 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 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 #[must_use]
131 pub fn is_connected(&self) -> bool {
132 self.connected.load(Ordering::Acquire)
133 }
134
135 #[must_use]
137 pub fn is_disconnected(&self) -> bool {
138 !self.is_connected()
139 }
140
141 pub fn set_connected(&self) {
143 self.connected.store(true, Ordering::Release);
144 }
145
146 pub fn set_disconnected(&self) {
148 self.connected.store(false, Ordering::Release);
149 }
150
151 #[must_use]
153 pub fn is_started(&self) -> bool {
154 self.started.load(Ordering::Acquire)
155 }
156
157 #[must_use]
159 pub fn is_stopped(&self) -> bool {
160 !self.is_started()
161 }
162
163 pub fn set_started(&self) {
165 self.started.store(true, Ordering::Release);
166 }
167
168 pub fn set_stopped(&self) {
170 self.started.store(false, Ordering::Release);
171 }
172
173 #[must_use]
175 pub fn instruments_initialized(&self) -> bool {
176 self.instruments_initialized.load(Ordering::Acquire)
177 }
178
179 pub fn set_instruments_initialized(&self) {
181 self.instruments_initialized.store(true, Ordering::Release);
182 }
183
184 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 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}