use std::sync::atomic::{AtomicBool, Ordering};
use nautilus_common::cache::{Cache, CacheView};
use nautilus_model::{
enums::{AccountType, OmsType},
identifiers::{AccountId, ClientId, ClientOrderId, TraderId, Venue},
orders::{OrderAny, OrderList},
types::Currency,
};
#[derive(Debug)]
pub struct ExecutionClientCore {
pub trader_id: TraderId,
pub client_id: ClientId,
pub venue: Venue,
pub oms_type: OmsType,
pub account_id: AccountId,
pub account_type: AccountType,
pub base_currency: Option<Currency>,
connected: AtomicBool,
started: AtomicBool,
instruments_initialized: AtomicBool,
cache: CacheView,
}
impl Clone for ExecutionClientCore {
fn clone(&self) -> Self {
Self {
trader_id: self.trader_id,
client_id: self.client_id,
venue: self.venue,
oms_type: self.oms_type,
account_id: self.account_id,
account_type: self.account_type,
base_currency: self.base_currency,
connected: AtomicBool::new(self.connected.load(Ordering::Acquire)),
started: AtomicBool::new(self.started.load(Ordering::Acquire)),
instruments_initialized: AtomicBool::new(
self.instruments_initialized.load(Ordering::Acquire),
),
cache: self.cache.clone(),
}
}
}
impl ExecutionClientCore {
#[expect(clippy::too_many_arguments)]
#[must_use]
pub fn new(
trader_id: TraderId,
client_id: ClientId,
venue: Venue,
oms_type: OmsType,
account_id: AccountId,
account_type: AccountType,
base_currency: Option<Currency>,
cache: impl Into<CacheView>,
) -> Self {
Self {
trader_id,
client_id,
venue,
oms_type,
account_id,
account_type,
base_currency,
connected: AtomicBool::new(false),
started: AtomicBool::new(false),
instruments_initialized: AtomicBool::new(false),
cache: cache.into(),
}
}
pub fn cache(&self) -> std::cell::Ref<'_, Cache> {
self.cache.borrow()
}
pub fn get_order(&self, client_order_id: &ClientOrderId) -> anyhow::Result<OrderAny> {
Ok(self.cache.borrow().try_order_owned(client_order_id)?)
}
pub fn get_orders_for_list(&self, order_list: &OrderList) -> anyhow::Result<Vec<OrderAny>> {
order_list
.client_order_ids
.iter()
.map(|id| self.get_order(id))
.collect()
}
#[must_use]
pub fn is_connected(&self) -> bool {
self.connected.load(Ordering::Acquire)
}
#[must_use]
pub fn is_disconnected(&self) -> bool {
!self.is_connected()
}
pub fn set_connected(&self) {
self.connected.store(true, Ordering::Release);
}
pub fn set_disconnected(&self) {
self.connected.store(false, Ordering::Release);
}
#[must_use]
pub fn is_started(&self) -> bool {
self.started.load(Ordering::Acquire)
}
#[must_use]
pub fn is_stopped(&self) -> bool {
!self.is_started()
}
pub fn set_started(&self) {
self.started.store(true, Ordering::Release);
}
pub fn set_stopped(&self) {
self.started.store(false, Ordering::Release);
}
#[must_use]
pub fn instruments_initialized(&self) -> bool {
self.instruments_initialized.load(Ordering::Acquire)
}
pub fn set_instruments_initialized(&self) {
self.instruments_initialized.store(true, Ordering::Release);
}
pub const fn set_account_id(&mut self, account_id: AccountId) {
self.account_id = account_id;
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, rc::Rc};
use nautilus_common::cache::OrderLookupError;
use nautilus_core::UnixNanos;
use nautilus_model::{
enums::{OrderSide, OrderType},
identifiers::OrderListId,
orders::{Order, builder::OrderTestBuilder},
types::{Price, Quantity},
};
use rstest::rstest;
use super::*;
#[rstest]
fn test_get_orders_for_list_preserves_order_and_cached_fields() {
let first = OrderTestBuilder::new(OrderType::Limit)
.client_order_id(ClientOrderId::from("O-SECOND"))
.instrument_id("AUD/USD.SIM".into())
.side(OrderSide::Buy)
.price(Price::from("0.65001"))
.quantity(Quantity::from(17))
.build();
let second = OrderTestBuilder::new(OrderType::Limit)
.client_order_id(ClientOrderId::from("O-FIRST"))
.instrument_id("EUR/USD.SIM".into())
.side(OrderSide::Sell)
.price(Price::from("1.08002"))
.quantity(Quantity::from(29))
.build();
let cache = Rc::new(RefCell::new(Cache::default()));
for order in [&second, &first] {
cache
.borrow_mut()
.add_order(order.clone(), None, None, false)
.unwrap();
}
let core = core(cache);
let list = OrderList::new(
OrderListId::from("OL-001"),
first.instrument_id(),
first.strategy_id(),
vec![first.client_order_id(), second.client_order_id()],
UnixNanos::new(123),
);
let orders = core.get_orders_for_list(&list).unwrap();
assert_eq!(orders.len(), 2);
for (actual, expected) in orders.iter().zip([&first, &second]) {
assert_eq!(actual.init_event(), expected.init_event());
assert_eq!(actual.status(), expected.status());
assert_eq!(actual.filled_qty(), expected.filled_qty());
}
}
#[rstest]
#[case(0)]
#[case(1)]
fn test_get_orders_for_list_rejects_missing_member(#[case] missing_index: usize) {
let order = OrderTestBuilder::new(OrderType::Market)
.client_order_id(ClientOrderId::from("O-PRESENT"))
.instrument_id("AUD/USD.SIM".into())
.side(OrderSide::Buy)
.quantity(Quantity::from(17))
.build();
let cache = Rc::new(RefCell::new(Cache::default()));
cache
.borrow_mut()
.add_order(order.clone(), None, None, false)
.unwrap();
let core = core(cache);
let missing_id = ClientOrderId::from("O-MISSING");
let mut ids = vec![order.client_order_id()];
ids.insert(missing_index, missing_id);
let list = OrderList::new(
OrderListId::from("OL-001"),
order.instrument_id(),
order.strategy_id(),
ids,
UnixNanos::new(123),
);
let error = core.get_orders_for_list(&list).unwrap_err();
assert_eq!(
error.downcast_ref::<OrderLookupError>(),
Some(&OrderLookupError::NotFound {
client_order_id: missing_id
}),
);
assert_eq!(
core.get_order(&order.client_order_id())
.unwrap()
.init_event(),
order.init_event()
);
}
fn core(cache: Rc<RefCell<Cache>>) -> ExecutionClientCore {
ExecutionClientCore::new(
TraderId::from("TRADER-007"),
ClientId::from("CLIENT-003"),
Venue::from("SIM"),
OmsType::Hedging,
AccountId::from("SIM-009"),
AccountType::Margin,
Some(Currency::USD()),
cache,
)
}
}