use async_trait::async_trait;
use nautilus_core::UnixNanos;
use nautilus_model::{
accounts::AccountAny,
enums::{LiquiditySide, OmsType},
identifiers::{
AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, Venue, VenueOrderId,
},
instruments::InstrumentAny,
reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
types::{AccountBalance, MarginBalance, Money, Price, Quantity},
};
use super::log_not_implemented;
use crate::messages::execution::{
BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
GenerateOrderStatusReport, GenerateOrderStatusReports, GeneratePositionStatusReports,
ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
};
#[async_trait(?Send)]
pub trait ExecutionClient {
fn is_connected(&self) -> bool;
fn client_id(&self) -> ClientId;
fn account_id(&self) -> AccountId;
fn venue(&self) -> Venue;
fn oms_type(&self) -> OmsType;
fn get_account(&self) -> Option<AccountAny>;
fn handles_order_venue(&self, venue: Venue) -> bool {
self.venue() == venue
}
fn generate_account_state(
&self,
balances: Vec<AccountBalance>,
margins: Vec<MarginBalance>,
reported: bool,
ts_event: UnixNanos,
) -> anyhow::Result<()>;
fn start(&mut self) -> anyhow::Result<()>;
fn stop(&mut self) -> anyhow::Result<()>;
fn reset(&mut self) -> anyhow::Result<()> {
Ok(())
}
fn dispose(&mut self) -> anyhow::Result<()> {
Ok(())
}
async fn connect(&mut self) -> anyhow::Result<()> {
Ok(())
}
async fn disconnect(&mut self) -> anyhow::Result<()> {
Ok(())
}
fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
log_not_implemented(&cmd);
Ok(())
}
fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
log_not_implemented(&cmd);
Ok(())
}
fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
log_not_implemented(&cmd);
Ok(())
}
fn batch_modify_orders(&self, cmd: BatchModifyOrders) -> anyhow::Result<()> {
for modify in cmd.modifies {
self.modify_order(modify)?;
}
Ok(())
}
fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
log_not_implemented(&cmd);
Ok(())
}
fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
log_not_implemented(&cmd);
Ok(())
}
fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
log_not_implemented(&cmd);
Ok(())
}
fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
log_not_implemented(&cmd);
Ok(())
}
fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
log_not_implemented(&cmd);
Ok(())
}
async fn generate_order_status_report(
&self,
cmd: &GenerateOrderStatusReport,
) -> anyhow::Result<Option<OrderStatusReport>> {
log_not_implemented(cmd);
Ok(None)
}
async fn generate_order_status_reports(
&self,
cmd: &GenerateOrderStatusReports,
) -> anyhow::Result<Vec<OrderStatusReport>> {
log_not_implemented(cmd);
Ok(Vec::new())
}
async fn generate_fill_reports(
&self,
cmd: GenerateFillReports,
) -> anyhow::Result<Vec<FillReport>> {
log_not_implemented(&cmd);
Ok(Vec::new())
}
async fn generate_position_status_reports(
&self,
cmd: &GeneratePositionStatusReports,
) -> anyhow::Result<Vec<PositionStatusReport>> {
log_not_implemented(cmd);
Ok(Vec::new())
}
async fn generate_mass_status(
&self,
lookback_mins: Option<u64>,
) -> anyhow::Result<Option<ExecutionMassStatus>> {
log_not_implemented(&lookback_mins);
Ok(None)
}
fn register_external_order(
&self,
_client_order_id: ClientOrderId,
_venue_order_id: VenueOrderId,
_instrument_id: InstrumentId,
_strategy_id: StrategyId,
_ts_init: UnixNanos,
) {
}
fn on_instrument(&mut self, _instrument: InstrumentAny) {
}
#[expect(unused_variables)]
fn calculate_commission(
&self,
instrument: &InstrumentAny,
last_qty: Quantity,
last_px: Price,
liquidity_side: LiquiditySide,
) -> Option<Money> {
None
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, rc::Rc};
use nautilus_core::UUID4;
use nautilus_model::{
enums::OmsType,
identifiers::{TraderId, Venue},
};
use rstest::rstest;
use super::*;
struct RecordingExecutionClient {
modified_order_ids: Rc<RefCell<Vec<ClientOrderId>>>,
}
impl RecordingExecutionClient {
fn new(modified_order_ids: Rc<RefCell<Vec<ClientOrderId>>>) -> Self {
Self { modified_order_ids }
}
}
#[async_trait(?Send)]
impl ExecutionClient for RecordingExecutionClient {
fn is_connected(&self) -> bool {
true
}
fn client_id(&self) -> ClientId {
ClientId::from("TEST")
}
fn account_id(&self) -> AccountId {
AccountId::from("TEST-001")
}
fn venue(&self) -> Venue {
Venue::from("SIM")
}
fn oms_type(&self) -> OmsType {
OmsType::Netting
}
fn get_account(&self) -> Option<AccountAny> {
None
}
fn generate_account_state(
&self,
_balances: Vec<AccountBalance>,
_margins: Vec<MarginBalance>,
_reported: bool,
_ts_event: UnixNanos,
) -> anyhow::Result<()> {
Ok(())
}
fn start(&mut self) -> anyhow::Result<()> {
Ok(())
}
fn stop(&mut self) -> anyhow::Result<()> {
Ok(())
}
fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
self.modified_order_ids
.borrow_mut()
.push(cmd.client_order_id);
Ok(())
}
}
#[rstest]
fn batch_modify_orders_default_fans_out_to_modify_order() {
let modified_order_ids = Rc::new(RefCell::new(Vec::new()));
let client = RecordingExecutionClient::new(modified_order_ids.clone());
let instrument_id = InstrumentId::from("AUD/USD.SIM");
let order1 = ClientOrderId::from("O-DEFAULT-BATCH-001");
let order2 = ClientOrderId::from("O-DEFAULT-BATCH-002");
let command = BatchModifyOrders::new(
TraderId::from("TRADER-001"),
Some(ClientId::from("TEST")),
StrategyId::from("S-001"),
instrument_id,
vec![
ModifyOrder::new(
TraderId::from("TRADER-001"),
Some(ClientId::from("TEST")),
StrategyId::from("S-001"),
instrument_id,
order1,
None,
Some(Quantity::from("10")),
Some(Price::from("1.00010")),
None,
UUID4::new(),
UnixNanos::default(),
None,
None,
),
ModifyOrder::new(
TraderId::from("TRADER-001"),
Some(ClientId::from("TEST")),
StrategyId::from("S-001"),
instrument_id,
order2,
None,
Some(Quantity::from("20")),
Some(Price::from("1.00020")),
None,
UUID4::new(),
UnixNanos::default(),
None,
None,
),
],
UUID4::new(),
UnixNanos::default(),
None,
None,
);
client.batch_modify_orders(command).unwrap();
assert_eq!(modified_order_ids.borrow().as_slice(), &[order1, order2]);
}
}