use std::{
cell::RefCell,
collections::HashSet,
future::Future,
pin::pin,
rc::Rc,
str::FromStr,
task::{Context, Poll, Waker},
};
use ahash::AHashSet;
use nautilus_common::{
cache::{Cache, CacheSnapshotRef},
clients::ExecutionClient,
clock::{self, Clock, TestClock},
messages::{
ExecutionReport,
execution::{
CancelAllOrders, CancelOrder, ModifyOrder, SubmitOrder, SubmitOrderList, TradingCommand,
},
},
msgbus::{
self, MessageBus, MessagingSwitchboard, TypedHandler, stubs::get_any_saving_handler,
switchboard,
},
};
use nautilus_core::{UUID4, UnixNanos, datetime::NANOSECONDS_IN_MINUTE};
use nautilus_execution::engine::{
ExecutionEngine, config::ExecutionEngineConfig, stubs::StubExecutionClient,
};
use nautilus_model::{
accounts::CashAccount,
enums::{
AccountType, ContingencyType, LiquiditySide, OmsType, OrderSide, OrderStatus, OrderType,
PositionSide, PositionSideSpecified, TimeInForce, TriggerType,
},
events::{
AccountState, OrderCanceled, OrderEventAny, OrderFilled, OrderPendingUpdate, OrderUpdated,
},
identifiers::{
AccountId, ClientId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
StrategyId, TradeId, TraderId, Venue, VenueOrderId,
},
instruments::{CurrencyPair, Instrument, InstrumentAny, stubs::audusd_sim},
orders::{Order, OrderAny, OrderList, builder::OrderTestBuilder, stubs::TestOrderEventStubs},
position::Position,
reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
stubs::{TestDefault, stub_position_long},
types::{AccountBalance, Currency, Money, Price, Quantity},
};
use rstest::*;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
#[fixture]
fn test_clock() -> Rc<RefCell<dyn clock::Clock>> {
Rc::new(RefCell::new(TestClock::new()))
}
#[fixture]
fn test_cache() -> Rc<RefCell<Cache>> {
Rc::new(RefCell::new(Cache::default()))
}
#[fixture]
fn execution_engine() -> ExecutionEngine {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
ExecutionEngine::new(clock, cache, None)
}
#[fixture]
fn execution_engine_with_config() -> ExecutionEngine {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: true,
snapshot_orders: true,
snapshot_positions: true,
manage_own_order_books: false,
..Default::default()
};
ExecutionEngine::new(clock, cache, Some(config))
}
#[fixture]
fn stub_client() -> StubExecutionClient {
StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
)
}
#[rstest]
fn test_register_client_success(
mut execution_engine: ExecutionEngine,
stub_client: StubExecutionClient,
) {
let client_id = stub_client.client_id();
let result = execution_engine.register_client(Box::new(stub_client));
assert!(
result.is_ok(),
"Failed to register client: {:?}",
result.err()
);
assert!(
execution_engine.get_client(&client_id).is_some(),
"Client should be registered and retrievable"
);
}
#[rstest]
fn test_client_ids_preserve_registration_order(mut execution_engine: ExecutionEngine) {
for (id, venue) in [
("ZULU", Venue::from("ZULU")),
("ALPHA", Venue::from("ALPHA")),
("MIKE", Venue::from("MIKE")),
] {
let client = StubExecutionClient::new(
ClientId::from(id),
AccountId::from("TEST-ACCOUNT"),
venue,
OmsType::Netting,
None,
);
execution_engine.register_client(Box::new(client)).unwrap();
}
let ids = execution_engine.client_ids();
assert_eq!(
ids,
vec![
ClientId::from("ZULU"),
ClientId::from("ALPHA"),
ClientId::from("MIKE"),
],
);
}
#[rstest]
fn test_register_venue_routing_success(
mut execution_engine: ExecutionEngine,
stub_client: StubExecutionClient,
) {
let client_id = stub_client.client_id();
let venue = Venue::test_default();
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let result = execution_engine.register_venue_routing(client_id, venue);
assert!(
result.is_ok(),
"Failed to register venue routing: {:?}",
result.err()
);
assert!(
execution_engine.get_client(&client_id).is_some(),
"Client should still be registered after venue routing"
);
}
#[rstest]
fn test_subscribe_venue_instruments_delivers_to_client_adapter(
mut execution_engine: ExecutionEngine,
stub_client: StubExecutionClient,
) {
*msgbus::get_message_bus().borrow_mut() = MessageBus::default();
let venue = Venue::test_default();
let received = stub_client.received_instruments();
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let engine_rc = Rc::new(RefCell::new(execution_engine));
ExecutionEngine::subscribe_venue_instruments(&engine_rc, venue);
let instrument = InstrumentAny::CurrencyPair(audusd_sim());
let topic = switchboard::get_instrument_topic(instrument.id());
msgbus::publish_instrument(topic, &instrument);
let received = received.borrow();
assert_eq!(received.len(), 1);
assert_eq!(received[0].id(), instrument.id());
}
#[rstest]
fn test_deregister_client_removes_client(
mut execution_engine: ExecutionEngine,
stub_client: StubExecutionClient,
) {
let client_id = stub_client.client_id();
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
assert!(
execution_engine.get_client(&client_id).is_some(),
"Client should be registered initially"
);
let result = execution_engine.deregister_client(client_id);
assert!(
result.is_ok(),
"Failed to deregister client: {:?}",
result.err()
);
assert!(
execution_engine.get_client(&client_id).is_none(),
"Client should be removed after deregistration"
);
}
#[rstest]
fn test_check_connected_when_client_connected_returns_true(mut execution_engine: ExecutionEngine) {
let mut stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
stub_client.start().unwrap();
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let is_connected = execution_engine.check_connected();
assert!(is_connected, "Should return true when client is connected");
}
#[rstest]
fn test_check_connected_when_client_disconnected_returns_false(
mut execution_engine: ExecutionEngine,
) {
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let is_connected = execution_engine.check_connected();
assert!(
!is_connected,
"Should return false when client is disconnected"
);
}
#[rstest]
fn test_check_disconnected_when_client_disconnected_returns_true(
mut execution_engine: ExecutionEngine,
) {
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let is_disconnected = execution_engine.check_disconnected();
assert!(
is_disconnected,
"Should return true when client is disconnected"
);
}
#[rstest]
fn test_check_integrity_returns_true(execution_engine: ExecutionEngine) {
let integrity_check = execution_engine.check_integrity();
assert!(
integrity_check,
"Integrity check should pass for new execution engine"
);
}
#[rstest]
fn test_execution_engine_with_config_initializes_correctly(
execution_engine_with_config: ExecutionEngine,
) {
let integrity_check = execution_engine_with_config.check_integrity();
assert!(integrity_check);
}
#[rstest]
fn test_set_position_id_counts_updates_correctly(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
let position = stub_position_long(instrument);
let strategy_id = position.strategy_id;
execution_engine
.cache()
.borrow_mut()
.add_position(&position, OmsType::Netting)
.unwrap();
execution_engine.set_position_id_counts();
let actual_count = execution_engine.position_id_count(strategy_id);
assert_eq!(
actual_count, 1,
"Expected position ID count to be 1 for strategy_id {strategy_id:?}, but got {actual_count}"
);
}
#[rstest]
fn test_execution_engine_default_config_initializes_correctly(execution_engine: ExecutionEngine) {
let integrity_check = execution_engine.check_integrity();
assert!(integrity_check);
}
#[rstest]
fn test_counters_increment_and_reset(mut execution_engine: ExecutionEngine) {
assert_eq!(execution_engine.command_count(), 0);
assert_eq!(execution_engine.event_count(), 0);
assert_eq!(execution_engine.report_count(), 0);
let instrument_id = audusd_sim().id();
let command = CancelAllOrders::new(
TraderId::test_default(),
None,
StrategyId::test_default(),
instrument_id,
OrderSide::NoOrderSide,
UUID4::new(),
UnixNanos::default(),
None,
);
execution_engine.execute(TradingCommand::CancelAllOrders(command));
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(1))
.build();
let event = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&event);
let report = create_order_status_report(
Some(ClientOrderId::from("O-001")),
VenueOrderId::from("V-001"),
instrument_id,
OrderStatus::Accepted,
Quantity::from(1),
Quantity::from(0),
);
execution_engine.reconcile_execution_report(&ExecutionReport::Order(Box::new(report)));
assert_eq!(execution_engine.command_count(), 1);
assert_eq!(execution_engine.event_count(), 1);
assert_eq!(execution_engine.report_count(), 1);
execution_engine.reset();
assert_eq!(execution_engine.command_count(), 0);
assert_eq!(execution_engine.event_count(), 0);
assert_eq!(execution_engine.report_count(), 0);
}
#[rstest]
fn test_submit_order_denied_with_custom_position_id_under_netting(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
let custom_position_id = PositionId::new(format!("{}-GRID", instrument.id));
execution_engine
.cache()
.borrow_mut()
.add_order(
order.clone(),
Some(custom_position_id),
Some(ClientId::from("STUB")),
true,
)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
position_id: Some(custom_position_id),
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
let cache = execution_engine.cache().borrow();
let cached_order = cache
.order(&order.client_order_id())
.expect("Order should be cached");
assert_eq!(cached_order.status(), OrderStatus::Denied);
}
#[rstest]
fn test_submit_order_list_denied_with_custom_position_id_under_netting(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let entry = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-002-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let stop_loss = OrderTestBuilder::new(OrderType::StopMarket)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-002-002-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from_str("0.50000").unwrap())
.build();
let take_profit = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-002-003-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.00000").unwrap())
.build();
let orders = [entry.clone(), stop_loss.clone(), take_profit.clone()];
let custom_position_id = PositionId::new(format!("{}-GRID", instrument.id));
for order in &orders {
execution_engine
.cache()
.borrow_mut()
.add_order(
order.clone(),
Some(custom_position_id),
Some(ClientId::from("STUB")),
true,
)
.unwrap();
}
let order_list = OrderList::new(
OrderListId::from("L-001"),
instrument.id,
strategy_id,
vec![
entry.client_order_id(),
stop_loss.client_order_id(),
take_profit.client_order_id(),
],
UnixNanos::default(),
);
let submit_order_list = SubmitOrderList {
trader_id,
client_id: Some(ClientId::from("STUB")),
strategy_id,
instrument_id: instrument.id,
order_list,
order_inits: orders.iter().map(|o| o.init_event().clone()).collect(),
exec_algorithm_id: None,
position_id: Some(custom_position_id),
params: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrderList(submit_order_list));
let cache = execution_engine.cache().borrow();
for order in &orders {
let cached = cache
.order(&order.client_order_id())
.expect("Order should be cached");
assert_eq!(cached.status(), OrderStatus::Denied);
}
}
#[rstest]
fn test_submit_order_denied_with_unspecified_strategy_oms_and_netting_client(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine.register_oms_type(strategy_id, OmsType::Unspecified);
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
let custom_position_id = PositionId::new(format!("{}-GRID", instrument.id));
execution_engine
.cache()
.borrow_mut()
.add_order(
order.clone(),
Some(custom_position_id),
Some(ClientId::from("STUB")),
true,
)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
position_id: Some(custom_position_id),
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
let cache = execution_engine.cache().borrow();
let cached_order = cache
.order(&order.client_order_id())
.expect("Order should be cached");
assert_eq!(cached_order.status(), OrderStatus::Denied);
}
#[rstest]
fn test_submit_order_with_duplicate_client_order_id_handles_gracefully(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order.clone()));
let order_submitted_event = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&order_submitted_event);
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
assert!(
execution_engine
.cache()
.borrow()
.order_exists(&order.client_order_id()),
"Order should exist in cache"
);
let cache = execution_engine.cache().borrow();
let cached_order = cache
.order(&order.client_order_id())
.expect("Order should be cached");
assert_eq!(
cached_order.status(),
OrderStatus::Submitted,
"Order should be in Submitted status"
);
assert_eq!(
cached_order.client_order_id(),
order.client_order_id(),
"Cached order should have correct client order ID"
);
}
#[rstest]
fn test_submit_order_for_random_venue_logs(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(), OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(10))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(
order.clone(),
None,
Some(ClientId::from("RANDOM_VENUE")),
true,
)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("RANDOM_VENUE")), exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
let cache = execution_engine.cache().borrow();
assert!(
cache.order_exists(&order.client_order_id()),
"Order should be added to cache when client routing is available"
);
assert_eq!(
order.status(),
OrderStatus::Initialized,
"Order status should remain INITIALIZED with stub client"
);
}
#[rstest]
fn test_order_filled_with_unrecognized_strategy_id(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&order_submitted_event);
let different_strategy_id = StrategyId::from("RANDOM-001");
let order_filled_event = OrderFilled::new(
trader_id,
different_strategy_id, instrument.id,
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::test_default(),
TradeId::new("T-001"),
order.order_side(),
order.order_type(),
order.quantity(),
Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false,
None,
Some(Money::from("2 USD")),
);
execution_engine.process(&OrderEventAny::Filled(order_filled_event));
let cache = execution_engine.cache().borrow();
let updated_order = cache.order(&order.client_order_id()).unwrap();
assert_eq!(
updated_order.status(),
OrderStatus::Submitted,
"Order should remain SUBMITTED when filled event has mismatched strategy_id"
);
}
#[rstest]
fn test_process_filled_order_publishes_order_fills_topic(mut execution_engine: ExecutionEngine) {
let (instrument, order) = prepare_accepted_order(&mut execution_engine);
let fills_topic = switchboard::get_order_fills_topic(instrument.id());
let order_topic = switchboard::get_event_orders_topic(order.strategy_id());
let received = Rc::new(RefCell::new(Vec::<OrderEventAny>::new()));
let topics = Rc::new(RefCell::new(Vec::<&'static str>::new()));
let fills_handler = TypedHandler::from({
let received = received.clone();
let topics = topics.clone();
move |event: &OrderEventAny| {
topics.borrow_mut().push("fills");
received.borrow_mut().push(event.clone());
}
});
let order_handler = TypedHandler::from({
let topics = topics.clone();
move |_event: &OrderEventAny| {
topics.borrow_mut().push("orders");
}
});
msgbus::subscribe_order_events(fills_topic.into(), fills_handler.clone(), None);
msgbus::subscribe_order_events(order_topic.into(), order_handler.clone(), None);
let event = OrderEventAny::Filled(OrderFilled::new(
order.trader_id(),
order.strategy_id(),
instrument.id(),
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::test_default(),
TradeId::new("T-001"),
order.order_side(),
order.order_type(),
order.quantity(),
Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false,
None,
Some(Money::from("2 USD")),
));
execution_engine.process(&event);
msgbus::unsubscribe_order_events(fills_topic.into(), &fills_handler);
msgbus::unsubscribe_order_events(order_topic.into(), &order_handler);
let received = received.borrow();
assert_eq!(received.len(), 1);
assert!(matches!(received[0], OrderEventAny::Filled(_)));
assert_eq!(received[0].client_order_id(), order.client_order_id());
assert_eq!(received[0].instrument_id(), instrument.id());
assert_eq!(topics.borrow().as_slice(), ["fills", "orders"]);
}
#[rstest]
fn test_process_canceled_order_publishes_order_cancels_topic(
mut execution_engine: ExecutionEngine,
) {
let (instrument, order) = prepare_accepted_order(&mut execution_engine);
let cancels_topic = switchboard::get_order_cancels_topic(instrument.id());
let order_topic = switchboard::get_event_orders_topic(order.strategy_id());
let received = Rc::new(RefCell::new(Vec::<OrderEventAny>::new()));
let topics = Rc::new(RefCell::new(Vec::<&'static str>::new()));
let cancels_handler = TypedHandler::from({
let received = received.clone();
let topics = topics.clone();
move |event: &OrderEventAny| {
topics.borrow_mut().push("cancels");
received.borrow_mut().push(event.clone());
}
});
let order_handler = TypedHandler::from({
let topics = topics.clone();
move |_event: &OrderEventAny| {
topics.borrow_mut().push("orders");
}
});
msgbus::subscribe_order_events(cancels_topic.into(), cancels_handler.clone(), None);
msgbus::subscribe_order_events(order_topic.into(), order_handler.clone(), None);
let event = TestOrderEventStubs::canceled(
&order,
AccountId::test_default(),
Some(VenueOrderId::from("V-001")),
);
execution_engine.process(&event);
msgbus::unsubscribe_order_events(cancels_topic.into(), &cancels_handler);
msgbus::unsubscribe_order_events(order_topic.into(), &order_handler);
let received = received.borrow();
assert_eq!(received.len(), 1);
assert!(matches!(received[0], OrderEventAny::Canceled(_)));
assert_eq!(received[0].client_order_id(), order.client_order_id());
assert_eq!(received[0].instrument_id(), instrument.id());
assert_eq!(topics.borrow().as_slice(), ["orders", "cancels"]);
}
fn prepare_accepted_order(execution_engine: &mut ExecutionEngine) -> (InstrumentAny, OrderAny) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = InstrumentAny::from(audusd_sim());
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone())
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_account(CashAccount::default().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id())
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let accepted = TestOrderEventStubs::accepted(
&order,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&accepted);
(instrument, order)
}
#[rstest]
fn test_submit_bracket_order_list_with_all_duplicate_client_order_id_logs_does_not_submit(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let entry = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let stop_loss = OrderTestBuilder::new(OrderType::StopMarket)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-002-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from_str("0.50000").unwrap())
.build();
let take_profit = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-003-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.00000").unwrap())
.build();
let orders = [entry.clone(), stop_loss.clone(), take_profit.clone()];
let order_list = OrderList::new(
OrderListId::from("1"),
instrument.id,
strategy_id,
vec![
entry.client_order_id(),
stop_loss.client_order_id(),
take_profit.client_order_id(),
],
UnixNanos::default(),
);
let submit_order_list = SubmitOrderList {
trader_id,
client_id: Some(ClientId::from("STUB")),
strategy_id,
instrument_id: instrument.id,
order_list,
order_inits: orders.iter().map(|o| o.init_event().clone()).collect(),
exec_algorithm_id: None,
position_id: None,
params: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
for order in &orders {
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
}
execution_engine.execute(TradingCommand::SubmitOrderList(submit_order_list.clone()));
let entry_submitted = TestOrderEventStubs::submitted(&entry, AccountId::test_default());
execution_engine.process(&entry_submitted);
let stop_loss_submitted = TestOrderEventStubs::submitted(&stop_loss, AccountId::test_default());
execution_engine.process(&stop_loss_submitted);
let take_profit_submitted =
TestOrderEventStubs::submitted(&take_profit, AccountId::test_default());
execution_engine.process(&take_profit_submitted);
let cache = execution_engine.cache().borrow();
let entry_updated = cache
.order(&entry.client_order_id())
.expect("Entry order should exist")
.clone();
let stop_loss_updated = cache
.order(&stop_loss.client_order_id())
.expect("Stop loss order should exist")
.clone();
let take_profit_updated = cache
.order(&take_profit.client_order_id())
.expect("Take profit order should exist")
.clone();
drop(cache);
execution_engine.execute(TradingCommand::SubmitOrderList(submit_order_list));
assert_eq!(
entry_updated.status(),
OrderStatus::Submitted,
"Entry order should remain SUBMITTED (not invalidated by duplicate)"
);
assert_eq!(
stop_loss_updated.status(),
OrderStatus::Submitted,
"Stop loss order should remain SUBMITTED (not invalidated by duplicate)"
);
assert_eq!(
take_profit_updated.status(),
OrderStatus::Submitted,
"Take profit order should remain SUBMITTED (not invalidated by duplicate)"
);
let cache = execution_engine.cache().borrow();
assert!(
cache.order_exists(&entry.client_order_id()),
"Entry order should exist in cache"
);
assert!(
cache.order_exists(&stop_loss.client_order_id()),
"Stop loss order should exist in cache"
);
assert!(
cache.order_exists(&take_profit.client_order_id()),
"Take profit order should exist in cache"
);
}
#[rstest]
fn test_submit_order_successfully_processes_and_caches_order(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
position_id: None,
params: None,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
client_id: Some(ClientId::from("STUB")),
instrument_id: instrument.id,
exec_algorithm_id: None,
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
let cache = execution_engine.cache().borrow();
assert!(
cache.order_exists(&order.client_order_id()),
"Order should exist in cache after submission"
);
let cached_order = cache
.order(&order.client_order_id())
.expect("Order should be retrievable from cache");
assert_eq!(
cached_order.trader_id(),
trader_id,
"Cached order should have correct trader ID"
);
assert_eq!(
cached_order.strategy_id(),
strategy_id,
"Cached order should have correct strategy ID"
);
assert_eq!(
cached_order.instrument_id(),
instrument.id,
"Cached order should have correct instrument ID"
);
assert_eq!(
cached_order.quantity(),
Quantity::from(100_000),
"Cached order should have correct quantity"
);
assert_eq!(
cached_order.status(),
OrderStatus::Initialized,
"Order should be in Initialized status after submission"
);
}
#[rstest]
fn test_submit_order_with_cleared_cache_logs_error(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
client_id: Some(ClientId::from("STUB")),
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
exec_algorithm_id: None,
position_id: None,
params: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
assert!(
execution_engine
.cache()
.borrow()
.order_exists(&order.client_order_id()),
"Order should be added to cache after submission"
);
execution_engine.cache().borrow_mut().reset();
assert!(
!execution_engine
.cache()
.borrow()
.order_exists(&order.client_order_id()),
"Order should no longer exist in cache after clearing"
);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
assert_eq!(
order.status(),
OrderStatus::Initialized,
"Order status should remain INITIALIZED when cache is cleared"
);
assert!(
!execution_engine
.cache()
.borrow()
.order_exists(&order.client_order_id()),
"Order should remain absent from cache"
);
}
#[rstest]
fn test_when_applying_event_to_order_with_invalid_state_trigger_logs(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
client_id: Some(ClientId::from("STUB")),
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
exec_algorithm_id: None,
position_id: None,
params: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
assert!(
execution_engine
.cache()
.borrow()
.order_exists(&order.client_order_id()),
"Order should be added to cache after submission"
);
let order_filled_event = TestOrderEventStubs::filled(
&order,
&instrument.into(),
Some(TradeId::new("E-19700101-000000-001-001")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order_filled_event);
assert_eq!(
order.status(),
OrderStatus::Initialized,
"Order status should remain INITIALIZED when invalid event is applied"
);
}
#[rstest]
fn test_order_filled_event_when_order_not_found_in_cache_logs(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
assert!(
!execution_engine
.cache()
.borrow()
.order_exists(&order.client_order_id()),
"Order should not exist in cache"
);
let order_filled_event = TestOrderEventStubs::filled(
&order,
&instrument.into(),
Some(TradeId::new("E-19700101-000000-001-001")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order_filled_event);
assert_eq!(
order.status(),
OrderStatus::Initialized,
"Order status should remain INITIALIZED when order not found in cache"
);
assert!(
!execution_engine
.cache()
.borrow()
.order_exists(&order.client_order_id()),
"Order should still not exist in cache"
);
}
#[rstest]
fn test_cancel_order_for_already_closed_order_logs_and_does_nothing(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), false)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
client_id: Some(ClientId::from("STUB")),
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
exec_algorithm_id: None,
position_id: None,
params: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
let order_submitted_event = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let order_filled_event = OrderEventAny::Filled(OrderFilled::new(
order.trader_id(),
order.strategy_id(),
order.instrument_id(),
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::test_default(),
TradeId::new("E-19700101-000000-001-001"),
order.order_side(),
order.order_type(),
order.quantity(), Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false,
None, Some(Money::from("2 USD")), ));
execution_engine.process(&order_filled_event);
let order_status = {
let cache = execution_engine.cache().borrow();
let cached_order = cache
.order(&order.client_order_id())
.expect("Order should exist in cache");
cached_order.status()
};
assert_eq!(
order_status,
OrderStatus::Filled,
"Order should be FILLED before cancel attempt"
);
let cancel_order = CancelOrder {
trader_id,
client_id: Some(ClientId::from("STUB")),
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
venue_order_id: Some(VenueOrderId::from("V-001")),
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
params: None,
};
execution_engine.execute(TradingCommand::CancelOrder(cancel_order));
let order_status_after_cancel = {
let cache = execution_engine.cache().borrow();
let cached_order_after_cancel = cache
.order(&order.client_order_id())
.expect("Order should still exist in cache");
cached_order_after_cancel.status()
};
assert_eq!(
order_status_after_cancel,
OrderStatus::Filled,
"Order status should remain FILLED after cancel attempt"
);
}
#[rstest]
fn test_canceled_order_receiving_fill_event_reopens_and_completes_order(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event =
TestOrderEventStubs::submitted(&order, AccountId::from("TEST-ACCOUNT"));
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::from("TEST-ACCOUNT"),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let accepted_order = execution_engine
.cache()
.borrow()
.order(&order.client_order_id())
.map(|o| o.clone())
.expect("Order should exist in cache");
assert!(
accepted_order.venue_order_id().is_some(),
"Order should have venue_order_id after acceptance"
);
let order_canceled_event = TestOrderEventStubs::canceled(
&order,
AccountId::from("TEST-ACCOUNT"),
Some(VenueOrderId::from("V-001")), );
execution_engine.process(&order_canceled_event);
let canceled_order = execution_engine
.cache()
.borrow()
.order(&order.client_order_id())
.map(|o| o.clone())
.expect("Order should exist in cache");
assert_eq!(
canceled_order.status(),
OrderStatus::Canceled,
"Order should be in Canceled status before fill event"
);
assert!(
canceled_order.is_closed(),
"Order should be closed before fill event"
);
let order_filled_event = OrderEventAny::Filled(OrderFilled::new(
order.trader_id(),
order.strategy_id(),
instrument.id(),
order.client_order_id(),
VenueOrderId::from("V-001"), AccountId::from("TEST-ACCOUNT"),
TradeId::new("E-19700101-000000-001-001-1"),
order.order_side(),
order.order_type(),
order.quantity(), Price::from_str("1.0").unwrap(), instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, Some(PositionId::new("1")),
Some(Money::from("2 USD")),
));
execution_engine.process(&order_filled_event);
let cache = execution_engine.cache().borrow();
let filled_order = cache
.order(&order.client_order_id())
.expect("Order should still exist in cache after fill");
assert_eq!(
filled_order.status(),
OrderStatus::Filled,
"Canceled order should transition to Filled status when receiving fill event"
);
assert!(
filled_order.is_closed(),
"Order should be closed after being filled"
);
assert_eq!(
filled_order.quantity(),
Quantity::from(100_000),
"Order quantity should remain unchanged"
);
assert_eq!(
filled_order.client_order_id(),
order.client_order_id(),
"Order client order ID should remain unchanged"
);
}
#[rstest]
fn test_canceled_order_receiving_partial_fill_event_reopens_and_becomes_partially_filled(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event =
TestOrderEventStubs::submitted(&order, AccountId::from("TEST-ACCOUNT"));
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::from("TEST-ACCOUNT"),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let order_canceled_event = TestOrderEventStubs::canceled(
&order,
AccountId::from("TEST-ACCOUNT"),
Some(VenueOrderId::from("V-001")), );
execution_engine.process(&order_canceled_event);
let canceled_order = execution_engine
.cache()
.borrow()
.order(&order.client_order_id())
.map(|o| o.clone())
.expect("Order should exist in cache");
assert_eq!(
canceled_order.status(),
OrderStatus::Canceled,
"Order should be in Canceled status before partial fill event"
);
assert!(
canceled_order.is_closed(),
"Order should be closed before partial fill event"
);
let partial_fill_qty = Quantity::from(50_000); let order_partially_filled_event = OrderEventAny::Filled(OrderFilled::new(
order.trader_id(),
order.strategy_id(),
instrument.id(),
order.client_order_id(),
VenueOrderId::from("V-001"), AccountId::from("TEST-ACCOUNT"),
TradeId::new("E-19700101-000000-001-001-1"),
order.order_side(),
order.order_type(),
partial_fill_qty, Price::from_str("1.0").unwrap(), instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, Some(PositionId::new("1")),
Some(Money::from("2 USD")),
));
execution_engine.process(&order_partially_filled_event);
let partially_filled_order = execution_engine
.cache()
.borrow()
.order(&order.client_order_id())
.map(|o| o.clone())
.expect("Order should still exist in cache after partial fill");
assert_eq!(
partially_filled_order.status(),
OrderStatus::PartiallyFilled,
"Canceled order should transition to PartiallyFilled status when receiving partial fill event"
);
assert!(
!partially_filled_order.is_closed(),
"Order should be reopened (not closed) after partial fill"
);
assert!(
partially_filled_order.is_open(),
"Order should be open after partial fill"
);
assert_eq!(
partially_filled_order.filled_qty(),
partial_fill_qty,
"Order filled quantity should match the partial fill"
);
assert_eq!(
partially_filled_order.leaves_qty(),
Quantity::from(50_000), "Order leaves quantity should be correct after partial fill"
);
assert_eq!(
partially_filled_order.quantity(),
Quantity::from(100_000),
"Order total quantity should remain unchanged"
);
assert_eq!(
partially_filled_order.client_order_id(),
order.client_order_id(),
"Order client order ID should remain unchanged"
);
}
#[rstest]
fn test_process_event_with_no_venue_order_id_logs_and_does_nothing(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.0").unwrap())
.emulation_trigger(TriggerType::BidAsk)
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event =
TestOrderEventStubs::submitted(&order, AccountId::from("TEST-ACCOUNT"));
execution_engine.process(&order_submitted_event);
let different_client_order_id = ClientOrderId::from("DIFFERENT-ORDER-ID");
let order_canceled_event = OrderEventAny::Canceled(OrderCanceled::new(
order.trader_id(),
order.strategy_id(),
order.instrument_id(),
different_client_order_id, UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, None, None, ));
execution_engine.process(&order_canceled_event);
let cache = execution_engine.cache().borrow();
let order_after = cache
.order(&order.client_order_id())
.expect("Order should exist in cache");
assert_eq!(
order_after.status(),
OrderStatus::Submitted,
"Order should remain in Submitted status when event has no venue_order_id"
);
}
#[rstest]
fn test_modify_order_for_already_closed_order_logs_and_does_nothing(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::StopMarket)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from_str("0.85101").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event =
TestOrderEventStubs::submitted(&order, AccountId::from("TEST-ACCOUNT"));
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::from("TEST-ACCOUNT"),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let order_filled_event = OrderEventAny::Filled(OrderFilled::new(
order.trader_id(),
order.strategy_id(),
instrument.id(),
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::from("TEST-ACCOUNT"),
TradeId::new("E-19700101-000000-001-001-1"),
order.order_side(),
order.order_type(),
order.quantity(), Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, Some(PositionId::new("1")),
Some(Money::from("2 USD")),
));
execution_engine.process(&order_filled_event);
let filled_order = execution_engine
.cache()
.borrow()
.order(&order.client_order_id())
.map(|o| o.clone())
.expect("Order should exist in cache");
assert_eq!(
filled_order.status(),
OrderStatus::Filled,
"Order should be in Filled status before modify attempt"
);
assert!(
filled_order.is_closed(),
"Order should be closed before modify attempt"
);
let modify_order = ModifyOrder {
trader_id,
client_id: Some(ClientId::from("STUB")),
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
venue_order_id: order.venue_order_id(),
quantity: Some(Quantity::from(200_000)), price: None, trigger_price: order.trigger_price(), command_id: UUID4::new(),
ts_init: UnixNanos::default(),
params: None,
};
execution_engine.execute(TradingCommand::ModifyOrder(modify_order));
let cache = execution_engine.cache().borrow();
let order_after = cache
.order(&order.client_order_id())
.expect("Order should still exist in cache");
assert_eq!(
order_after.status(),
OrderStatus::Filled,
"Order should remain in Filled status after modify attempt"
);
assert_eq!(
order_after.quantity(),
Quantity::from(100_000),
"Order quantity should remain unchanged after modify attempt"
);
}
#[rstest]
fn test_handle_order_event_with_different_client_order_id_but_matching_venue_order_id_fails_to_apply(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event =
TestOrderEventStubs::submitted(&order, AccountId::from("TEST-ACCOUNT"));
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::from("TEST-ACCOUNT"),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let different_venue_order_id = VenueOrderId::from("DIFFERENT-V-001"); let order_canceled_event = OrderEventAny::Canceled(OrderCanceled::new(
order.trader_id(),
order.strategy_id(),
order.instrument_id(),
order.client_order_id(), UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, Some(different_venue_order_id), Some(AccountId::from("TEST-ACCOUNT")),
));
execution_engine.process(&order_canceled_event);
let cache = execution_engine.cache().borrow();
let order_after = cache
.order(&order.client_order_id())
.expect("Order should exist in cache");
assert_eq!(
order_after.status(),
OrderStatus::Canceled,
"Order should be canceled when client_order_id matches"
);
}
#[rstest]
fn test_handle_order_event_with_random_client_order_id_and_order_id_not_cached(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event =
TestOrderEventStubs::submitted(&order, AccountId::from("TEST-ACCOUNT"));
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::from("TEST-ACCOUNT"),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let random_client_order_id = ClientOrderId::from("web_001"); let random_venue_order_id = VenueOrderId::from("RANDOM_001"); let order_canceled_event = OrderEventAny::Canceled(OrderCanceled::new(
order.trader_id(),
order.strategy_id(),
order.instrument_id(),
random_client_order_id, UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, Some(random_venue_order_id), Some(AccountId::from("TEST-ACCOUNT")),
));
execution_engine.process(&order_canceled_event);
let cache = execution_engine.cache().borrow();
let order_after = cache
.order(&order.client_order_id())
.expect("Order should exist in cache");
assert_eq!(
order_after.status(),
OrderStatus::Accepted,
"Order should remain in Accepted status when event has random IDs"
);
}
#[rstest]
fn test_handle_duplicate_order_events_logs_error_and_does_not_apply(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::from("TEST-ACCOUNT"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event =
TestOrderEventStubs::submitted(&order, AccountId::from("TEST-ACCOUNT"));
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::from("TEST-ACCOUNT"),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let venue_order_id = VenueOrderId::from("V-001"); let order_canceled_event = OrderEventAny::Canceled(OrderCanceled::new(
order.trader_id(),
order.strategy_id(),
order.instrument_id(),
order.client_order_id(), UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, Some(venue_order_id), Some(AccountId::from("TEST-ACCOUNT")),
));
execution_engine.process(&order_canceled_event);
execution_engine.process(&order_canceled_event); let cache = execution_engine.cache().borrow();
let order_after = cache
.order(&order.client_order_id())
.expect("Order should exist in cache");
assert_eq!(
order_after.status(),
OrderStatus::Canceled,
"Order should be canceled when found by venue_order_id"
);
assert_eq!(
order_after.event_count(),
4, "Order should have correct event count"
);
}
#[rstest]
fn test_handle_order_fill_event_with_no_position_id_correctly_handles_fill(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&order_submitted_event);
let order_filled_event = TestOrderEventStubs::filled(
&order,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001")), None, None, None, None, None, None, Some(AccountId::test_default()), );
execution_engine.process(&order_filled_event);
let cache = execution_engine.cache().borrow();
let position_ids = cache.position_ids(None, None, None, None);
assert_eq!(position_ids.len(), 1, "Should have exactly one position");
let actual_position_id = position_ids
.iter()
.next()
.expect("Should have one position ID");
let position = cache
.position(actual_position_id)
.expect("Position should exist");
assert!(position.is_open(), "Position should be open");
assert!(!position.is_closed(), "Position should not be closed");
assert_eq!(
position.strategy_id, strategy_id,
"Position should have correct strategy ID"
);
assert_eq!(
position.instrument_id, instrument.id,
"Position should have correct instrument ID"
);
assert_eq!(
cache.positions_total_count(None, None, None, None, None),
1,
"Total position count should be 1"
);
assert_eq!(
cache.positions_open_count(None, None, None, None, None),
1,
"Open position count should be 1"
);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
0,
"Closed position count should be 0"
);
}
#[rstest]
fn test_handle_hedging_fill_generates_position_id_from_runtime_strategy_tag(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::from("ExampleStrategy-XNAS-T01");
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Hedging,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-T01-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let order_filled_event = OrderEventAny::Filled(OrderFilled::new(
trader_id,
strategy_id,
instrument.id,
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::test_default(),
TradeId::new("E-19700101-000000-001-T01-1"),
order.order_side(),
order.order_type(),
order.quantity(),
Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false,
None,
Some(Money::from("2 USD")),
));
execution_engine.process(&order_filled_event);
let expected_position_id = PositionId::from("P-19700101-000000-001-T01-1");
let cache = execution_engine.cache().borrow();
let position_ids = cache.position_ids(None, None, None, None);
assert!(
position_ids.contains(&expected_position_id),
"Expected {expected_position_id}, found {position_ids:?}"
);
let position = cache
.position(&expected_position_id)
.expect("Position should use the final runtime strategy tag");
assert_eq!(position.strategy_id, strategy_id);
assert_eq!(position.id, expected_position_id);
}
#[rstest]
fn test_handle_order_fill_event(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
{
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
}
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let expected_position_id = PositionId::from(format!("{}-{}", instrument.id, strategy_id));
let position = Position::new(
&instrument.clone().into(),
OrderFilled::new(
order.trader_id(),
order.strategy_id(),
instrument.id(),
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::test_default(),
TradeId::new("E-19700101-000000-001-001-0"), order.order_side(),
order.order_type(),
order.quantity(),
Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false,
Some(expected_position_id),
Some(Money::from("2 USD")),
),
);
execution_engine
.cache()
.borrow_mut()
.add_position(&position, OmsType::Netting)
.unwrap();
let order_filled_event = OrderEventAny::Filled(OrderFilled::new(
order.trader_id(),
order.strategy_id(),
instrument.id(),
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::test_default(),
TradeId::new("E-19700101-000000-001-001-1"),
order.order_side(),
order.order_type(),
Quantity::from(50_000),
Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false,
Some(expected_position_id), Some(Money::from("2 USD")),
));
execution_engine.process(&order_filled_event);
let cache = execution_engine.cache().borrow();
println!("Filtering parameters:");
println!(" Venue: {:?}", Venue::test_default());
println!(" Instrument ID: {:?}", instrument.id);
println!(" Strategy ID: {strategy_id:?}");
println!(" Expected Position ID: {expected_position_id:?}");
let position = cache
.position(&expected_position_id)
.expect("Position should exist");
println!(" Position venue: {:?}", position.instrument_id.venue);
println!(" Position instrument: {:?}", position.instrument_id);
println!(" Position strategy: {:?}", position.strategy_id);
assert!(
cache
.position_open_ids(None, None, None, None)
.contains(&expected_position_id),
"Position should be in open IDs (unfiltered)"
);
assert!(
cache.is_position_open(&expected_position_id),
"Position should be open"
);
assert!(
!cache.is_position_closed(&expected_position_id),
"Position should not be closed"
);
let position = cache
.position(&expected_position_id)
.expect("Position should be retrievable");
assert_eq!(
position.id, expected_position_id,
"Position should have correct ID"
);
assert!(
cache
.position_ids(None, None, None, None)
.contains(&expected_position_id),
"Position ID should be in position IDs list"
);
assert!(
!cache
.position_closed_ids(
Some(&Venue::test_default()),
Some(&instrument.id),
Some(&strategy_id),
None
)
.contains(&expected_position_id),
"Position should not be in closed IDs for strategy"
);
assert!(
!cache
.position_closed_ids(None, None, None, None)
.contains(&expected_position_id),
"Position should not be in closed IDs"
);
assert!(
cache
.position_open_ids(
Some(&Venue::test_default()),
Some(&instrument.id),
Some(&strategy_id),
None
)
.contains(&expected_position_id),
"Position should be in open IDs for strategy"
);
assert!(
cache
.position_open_ids(None, None, None, None)
.contains(&expected_position_id),
"Position should be in open IDs"
);
assert_eq!(
cache.positions_total_count(None, None, None, None, None),
1,
"Total position count should be 1"
);
assert_eq!(
cache.positions_open_count(None, None, None, None, None),
1,
"Open position count should be 1"
);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
0,
"Closed position count should be 0"
);
}
#[rstest]
fn test_handle_order_fill_event_skips_duplicate_fill(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_account(CashAccount::default().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
let client_order_id = order.client_order_id();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let fill = OrderEventAny::Filled(OrderFilled::new(
order.trader_id(),
order.strategy_id(),
instrument.id(),
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::test_default(),
TradeId::new("E-19700101-000000-001-001-1"),
order.order_side(),
order.order_type(),
Quantity::from(50_000),
Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false,
None,
Some(Money::from("2 USD")),
));
execution_engine.process(&fill);
let (event_count, filled_qty, trade_ids, position_count) = {
let cache = execution_engine.cache().borrow();
let order = cache.order(&client_order_id).unwrap();
(
order.event_count(),
order.filled_qty(),
order.trade_ids().len(),
cache.positions_total_count(None, None, None, None, None),
)
};
execution_engine.process(&fill);
let cache = execution_engine.cache().borrow();
let order = cache.order(&client_order_id).unwrap();
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
assert_eq!(order.filled_qty(), filled_qty);
assert_eq!(order.event_count(), event_count);
assert_eq!(order.trade_ids().len(), trade_ids);
assert_eq!(
cache.positions_total_count(None, None, None, None, None),
position_count
);
}
#[rstest]
fn test_handle_multiple_partial_fill_events(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let expected_position_id = PositionId::from(format!("{}-{}", instrument.id, strategy_id));
let position = Position::new(
&instrument.clone().into(),
OrderFilled::new(
order.trader_id(),
order.strategy_id(),
instrument.id(),
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::test_default(),
TradeId::new("E-19700101-000000-001-001-0"), order.order_side(),
order.order_type(),
order.quantity(),
Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false,
Some(expected_position_id),
Some(Money::from("2 USD")),
),
);
execution_engine
.cache()
.borrow_mut()
.add_position(&position, OmsType::Netting)
.unwrap();
let fill_event_1 = OrderEventAny::Filled(OrderFilled::new(
order.trader_id(),
order.strategy_id(),
instrument.id(),
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::test_default(),
TradeId::new("E-19700101-000000-001-001-1"),
order.order_side(),
order.order_type(),
Quantity::from(20_100), Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, None, Some(Money::from("2 USD")),
));
execution_engine.process(&fill_event_1);
let fill_event_2 = OrderEventAny::Filled(OrderFilled::new(
order.trader_id(),
order.strategy_id(),
instrument.id(),
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::test_default(),
TradeId::new("E-19700101-000000-001-001-2"),
order.order_side(),
order.order_type(),
Quantity::from(19_900), Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, None, Some(Money::from("2 USD")),
));
execution_engine.process(&fill_event_2);
let fill_event_3 = OrderEventAny::Filled(OrderFilled::new(
order.trader_id(),
order.strategy_id(),
instrument.id(),
order.client_order_id(),
VenueOrderId::from("V-001"),
AccountId::test_default(),
TradeId::new("E-19700101-000000-001-001-3"),
order.order_side(),
order.order_type(),
Quantity::from(60_000), Price::from_str("1.0").unwrap(),
instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, None, Some(Money::from("2 USD")),
));
execution_engine.process(&fill_event_3);
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&expected_position_id),
"Position should exist with generated ID"
);
assert!(
cache.is_position_open(&expected_position_id),
"Position should be open"
);
assert!(
!cache.is_position_closed(&expected_position_id),
"Position should not be closed"
);
let position = cache
.position(&expected_position_id)
.expect("Position should be retrievable");
assert_eq!(
position.id, expected_position_id,
"Position should have correct ID"
);
assert!(
cache
.position_ids(None, None, None, None)
.contains(&expected_position_id),
"Position ID should be in position IDs list"
);
assert!(
!cache
.position_closed_ids(
Some(&Venue::test_default()),
Some(&instrument.id),
Some(&strategy_id),
None
)
.contains(&expected_position_id),
"Position should not be in closed IDs for strategy"
);
assert!(
!cache
.position_closed_ids(None, None, None, None)
.contains(&expected_position_id),
"Position should not be in closed IDs"
);
assert!(
cache
.position_open_ids(
Some(&Venue::test_default()),
Some(&instrument.id),
Some(&strategy_id),
None
)
.contains(&expected_position_id),
"Position should be in open IDs for strategy"
);
assert!(
cache
.position_open_ids(None, None, None, None)
.contains(&expected_position_id),
"Position should be in open IDs"
);
assert_eq!(
cache.positions_total_count(None, None, None, None, None),
1,
"Total position count should be 1"
);
assert_eq!(
cache.positions_open_count(None, None, None, None, None),
1,
"Open position count should be 1"
);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
0,
"Closed position count should be 0"
);
}
#[rstest]
fn test_handle_position_opening_with_position_id_none(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let order_filled_event = TestOrderEventStubs::filled(
&order,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001")),
None, None, None, None, None, None, Some(AccountId::test_default()), );
execution_engine.process(&order_filled_event);
let cache = execution_engine.cache().borrow();
let position_ids = cache.position_ids(None, None, None, None);
assert_eq!(position_ids.len(), 1, "Should have exactly one position");
let generated_position_id = position_ids
.iter()
.next()
.expect("Should have one position ID");
let position = cache
.position(generated_position_id)
.expect("Position should exist");
assert!(position.is_open(), "Position should be open");
assert!(!position.is_closed(), "Position should not be closed");
assert_eq!(
position.strategy_id, strategy_id,
"Position should have correct strategy ID"
);
assert_eq!(
position.instrument_id, instrument.id,
"Position should have correct instrument ID"
);
assert!(
cache.position_exists(generated_position_id),
"Position should exist with generated ID"
);
assert!(
cache.is_position_open(generated_position_id),
"Position should be open"
);
assert!(
!cache.is_position_closed(generated_position_id),
"Position should not be closed"
);
assert!(
cache
.position_ids(None, None, None, None)
.contains(generated_position_id),
"Position ID should be in position IDs list"
);
assert!(
cache
.position_open_ids(None, None, None, None)
.contains(generated_position_id),
"Position should be in open IDs"
);
assert!(
!cache
.position_closed_ids(None, None, None, None)
.contains(generated_position_id),
"Position should not be in closed IDs"
);
assert_eq!(
cache.positions_total_count(None, None, None, None, None),
1,
"Total position count should be 1"
);
assert_eq!(
cache.positions_open_count(None, None, None, None, None),
1,
"Open position count should be 1"
);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
0,
"Closed position count should be 0"
);
}
#[rstest]
fn test_add_to_existing_position_on_order_fill(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order1 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order1.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order1_submitted_event = TestOrderEventStubs::submitted(&order1, AccountId::test_default());
execution_engine.process(&order1_submitted_event);
let order1_accepted_event = TestOrderEventStubs::accepted(
&order1,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order1_accepted_event);
let order1_filled_event = TestOrderEventStubs::filled(
&order1,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
None, None, None, None, None, None, Some(AccountId::test_default()), );
execution_engine.process(&order1_filled_event);
let cache = execution_engine.cache().borrow();
let position_ids = cache.position_ids(None, None, None, None);
assert_eq!(
position_ids.len(),
1,
"Should have exactly one position after first fill"
);
let expected_position_id = *position_ids.iter().next().unwrap();
println!("Expected position ID: {expected_position_id:?}");
drop(cache);
let order2 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order2.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order2_submitted_event = TestOrderEventStubs::submitted(&order2, AccountId::test_default());
execution_engine.process(&order2_submitted_event);
let order2_accepted_event = TestOrderEventStubs::accepted(
&order2,
AccountId::test_default(),
VenueOrderId::from("V-002"),
);
execution_engine.process(&order2_accepted_event);
let order2_filled_event = TestOrderEventStubs::filled(
&order2,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001-2")),
Some(expected_position_id), None, None, None, None, None, Some(AccountId::test_default()), );
execution_engine.process(&order2_filled_event);
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&expected_position_id),
"Position should exist after second fill"
);
assert!(
cache.is_position_open(&expected_position_id),
"Position should be open after second fill"
);
assert!(
!cache.is_position_closed(&expected_position_id),
"Position should not be closed after second fill"
);
let position = cache
.position(&expected_position_id)
.expect("Position should be retrievable");
assert_eq!(
position.buy_qty,
Quantity::from(200_000), "Position should have combined quantity from both orders"
);
assert_eq!(
position.id, expected_position_id,
"Position should have the expected ID"
);
assert_eq!(
position.strategy_id, strategy_id,
"Position should have correct strategy ID"
);
assert_eq!(
position.instrument_id, instrument.id,
"Position should have correct instrument ID"
);
assert_eq!(
cache.positions_total_count(None, None, None, None, None),
1,
"Total position count should be 1"
);
assert_eq!(
cache.positions_open_count(None, None, None, None, None),
1,
"Open position count should be 1"
);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
0,
"Closed position count should be 0"
);
assert!(
cache
.position_open_ids(None, None, None, None)
.contains(&expected_position_id),
"Position should be in open IDs"
);
assert!(
!cache
.position_closed_ids(None, None, None, None)
.contains(&expected_position_id),
"Position should not be in closed IDs"
);
assert_eq!(
cache
.position_open_ids(
Some(&Venue::test_default()),
Some(&instrument.id),
Some(&strategy_id),
None
)
.len(),
1,
"Should have 1 open position for strategy"
);
assert_eq!(
cache
.position_closed_ids(
Some(&Venue::test_default()),
Some(&instrument.id),
Some(&strategy_id),
None
)
.len(),
0,
"Should have 0 closed positions for strategy"
);
}
#[rstest]
fn test_close_position_on_order_fill(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order1 = OrderTestBuilder::new(OrderType::StopMarket)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from_str("1.00000").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order1.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order1_submitted_event = TestOrderEventStubs::submitted(&order1, AccountId::test_default());
execution_engine.process(&order1_submitted_event);
let order1_accepted_event = TestOrderEventStubs::accepted(
&order1,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order1_accepted_event);
let position_id = PositionId::new(format!("{}-{}", instrument.id, strategy_id));
let order1_filled_event = TestOrderEventStubs::filled(
&order1,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order1_filled_event);
{
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&position_id),
"Position should exist after first fill"
);
assert!(
cache.is_position_open(&position_id),
"Position should be open after first fill"
);
assert!(
!cache.is_position_closed(&position_id),
"Position should not be closed after first fill"
);
}
let order2 = OrderTestBuilder::new(OrderType::StopMarket)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from_str("1.00000").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order2.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order2_submitted_event = TestOrderEventStubs::submitted(&order2, AccountId::test_default());
execution_engine.process(&order2_submitted_event);
let order2_accepted_event = TestOrderEventStubs::accepted(
&order2,
AccountId::test_default(),
VenueOrderId::from("V-002"),
);
execution_engine.process(&order2_accepted_event);
let order2_filled_event = TestOrderEventStubs::filled(
&order2,
&instrument.into(),
Some(TradeId::new("E-19700101-000000-001-001-2")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order2_filled_event);
{
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&position_id),
"Position should still exist after closing"
);
let position = cache.position(&position_id).expect("Position should exist");
assert!(
position.is_closed(),
"Position should be closed after second fill"
);
assert!(
!position.is_open(),
"Position should not be open after second fill"
);
assert!(
cache.is_position_closed(&position_id),
"Cache should report position as closed"
);
assert!(
!cache.is_position_open(&position_id),
"Cache should not report position as open"
);
assert_eq!(
cache.positions_total_count(None, None, None, None, None),
1,
"Total position count should be 1"
);
assert_eq!(
cache.positions_open_count(None, None, None, None, None),
0,
"Open position count should be 0"
);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
1,
"Closed position count should be 1"
);
assert!(
!cache
.position_open_ids(None, None, None, None)
.contains(&position_id),
"Position should not be in open IDs"
);
assert!(
cache
.position_closed_ids(None, None, None, None)
.contains(&position_id),
"Position should be in closed IDs"
);
} }
#[rstest]
fn test_multiple_strategy_positions_opened(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy1_id = StrategyId::from("TEST-STRATEGY-001");
let strategy2_id = StrategyId::from("TEST-STRATEGY-002");
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order1 = OrderTestBuilder::new(OrderType::StopMarket)
.trader_id(trader_id)
.strategy_id(strategy1_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from_str("1.00000").unwrap())
.build();
let order2 = OrderTestBuilder::new(OrderType::StopMarket)
.trader_id(trader_id)
.strategy_id(strategy2_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-002-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from_str("1.00000").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order1.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order2.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let position1_id = PositionId::new(format!("{}-{}", instrument.id, strategy1_id));
let position2_id = PositionId::new(format!("{}-{}", instrument.id, strategy2_id));
let order1_submitted_event = TestOrderEventStubs::submitted(&order1, AccountId::test_default());
execution_engine.process(&order1_submitted_event);
let order1_accepted_event = TestOrderEventStubs::accepted(
&order1,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order1_accepted_event);
let order1_filled_event = TestOrderEventStubs::filled(
&order1,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order1_filled_event);
let order2_submitted_event = TestOrderEventStubs::submitted(&order2, AccountId::test_default());
execution_engine.process(&order2_submitted_event);
let order2_accepted_event = TestOrderEventStubs::accepted(
&order2,
AccountId::test_default(),
VenueOrderId::from("V-002"),
);
execution_engine.process(&order2_accepted_event);
let order2_filled_event = TestOrderEventStubs::filled(
&order2,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001-2")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order2_filled_event);
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&position1_id),
"Position 1 should exist"
);
assert!(
cache.position_exists(&position2_id),
"Position 2 should exist"
);
assert!(
cache.is_position_open(&position1_id),
"Position 1 should be open"
);
assert!(
cache.is_position_open(&position2_id),
"Position 2 should be open"
);
assert!(
!cache.is_position_closed(&position1_id),
"Position 1 should not be closed"
);
assert!(
!cache.is_position_closed(&position2_id),
"Position 2 should not be closed"
);
assert!(
cache.position(&position1_id).is_some(),
"Position 1 should be retrievable"
);
assert!(
cache.position(&position2_id).is_some(),
"Position 2 should be retrievable"
);
let venue = Venue::test_default();
assert!(
cache
.position_ids(
Some(&venue),
Some(&instrument.id),
Some(&strategy1_id),
None
)
.contains(&position1_id),
"Position 1 should be in strategy1's position IDs"
);
assert!(
cache
.position_ids(
Some(&venue),
Some(&instrument.id),
Some(&strategy2_id),
None
)
.contains(&position2_id),
"Position 2 should be in strategy2's position IDs"
);
assert!(
cache
.position_ids(None, None, None, None)
.contains(&position1_id),
"Position 1 should be in global position IDs"
);
assert!(
cache
.position_ids(None, None, None, None)
.contains(&position2_id),
"Position 2 should be in global position IDs"
);
assert_eq!(
cache.position_open_ids(None, None, None, None).len(),
2,
"Should have 2 open positions globally"
);
assert_eq!(
cache
.position_open_ids(
Some(&venue),
Some(&instrument.id),
Some(&strategy1_id),
None
)
.len(),
1,
"Strategy1 should have 1 open position"
);
assert_eq!(
cache
.position_open_ids(
Some(&venue),
Some(&instrument.id),
Some(&strategy2_id),
None
)
.len(),
1,
"Strategy2 should have 1 open position"
);
assert!(
cache
.position_open_ids(
Some(&venue),
Some(&instrument.id),
Some(&strategy1_id),
None
)
.contains(&position1_id),
"Position 1 should be in strategy1's open IDs"
);
assert!(
cache
.position_open_ids(
Some(&venue),
Some(&instrument.id),
Some(&strategy2_id),
None
)
.contains(&position2_id),
"Position 2 should be in strategy2's open IDs"
);
assert!(
cache
.position_open_ids(None, None, None, None)
.contains(&position1_id),
"Position 1 should be in global open IDs"
);
assert!(
cache
.position_open_ids(None, None, None, None)
.contains(&position2_id),
"Position 2 should be in global open IDs"
);
assert!(
!cache
.position_closed_ids(
Some(&venue),
Some(&instrument.id),
Some(&strategy1_id),
None
)
.contains(&position1_id),
"Position 1 should not be in strategy1's closed IDs"
);
assert!(
!cache
.position_closed_ids(
Some(&venue),
Some(&instrument.id),
Some(&strategy2_id),
None
)
.contains(&position2_id),
"Position 2 should not be in strategy2's closed IDs"
);
assert!(
!cache
.position_closed_ids(None, None, None, None)
.contains(&position1_id),
"Position 1 should not be in global closed IDs"
);
assert!(
!cache
.position_closed_ids(None, None, None, None)
.contains(&position2_id),
"Position 2 should not be in global closed IDs"
);
assert_eq!(
cache.positions_total_count(None, None, None, None, None),
2,
"Total position count should be 2"
);
assert_eq!(
cache.positions_open_count(None, None, None, None, None),
2,
"Open position count should be 2"
);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
0,
"Closed position count should be 0"
);
}
#[rstest]
fn test_flip_position_on_opposite_filled_same_position_sell(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Hedging,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order1 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let order2 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-002-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(150_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order1.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order2.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let position_id = PositionId::from("P-19700101-000000-000-000-1");
let order1_submitted_event = TestOrderEventStubs::submitted(&order1, AccountId::test_default());
execution_engine.process(&order1_submitted_event);
let order1_accepted_event = TestOrderEventStubs::accepted(
&order1,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order1_accepted_event);
let order1_filled_event = TestOrderEventStubs::filled(
&order1,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
Some(position_id),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order1_filled_event);
{
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&position_id),
"Initial position should exist"
);
assert!(
cache.is_position_open(&position_id),
"Initial position should be open"
);
let position = cache.position(&position_id).expect("Position should exist");
assert_eq!(position.side, PositionSide::Long, "Position should be long");
assert_eq!(
position.quantity,
Quantity::from(100_000),
"Position quantity should be 100,000"
);
}
let order2_submitted_event = TestOrderEventStubs::submitted(&order2, AccountId::test_default());
execution_engine.process(&order2_submitted_event);
let order2_accepted_event = TestOrderEventStubs::accepted(
&order2,
AccountId::test_default(),
VenueOrderId::from("V-002"),
);
execution_engine.process(&order2_accepted_event);
let order2_filled_event = TestOrderEventStubs::filled(
&order2,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001-2")),
Some(position_id), None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order2_filled_event);
let cache = execution_engine.cache().borrow();
let all_position_ids = cache.position_ids(None, None, None, None);
println!("All position IDs after flip: {all_position_ids:?}");
assert!(
!all_position_ids.is_empty(),
"Should have at least one position after flip"
);
if cache.position_exists(&position_id) {
let original_position = cache
.position(&position_id)
.expect("Original position should exist");
println!(
"Original position after flip: is_closed={}, quantity={:?}, side={:?}",
original_position.is_closed(),
original_position.quantity,
original_position.side
);
assert!(
original_position.is_closed(),
"Original position should be closed after flip"
);
}
let open_positions = cache.position_open_ids(None, None, None, None);
println!("Open position IDs after flip: {open_positions:?}");
if open_positions.is_empty() {
panic!(
"Expected to find at least one open position after flip, but found none. All positions: {all_position_ids:?}"
);
} else {
let flipped_position_id = open_positions
.iter()
.next()
.expect("Should have at least one open position");
let flipped_position = cache
.position(flipped_position_id)
.expect("Flipped position should exist");
println!(
"Flipped position: id={:?}, side={:?}, quantity={:?}",
flipped_position_id, flipped_position.side, flipped_position.quantity
);
assert_eq!(
flipped_position.side,
PositionSide::Short,
"Flipped position should be short"
);
assert_eq!(
flipped_position.quantity,
Quantity::from(50_000), "Flipped position quantity should be 50,000 (150,000 - 100,000)"
);
assert_eq!(
flipped_position.strategy_id, strategy_id,
"Flipped position should have same strategy ID"
);
assert_eq!(
flipped_position.instrument_id, instrument.id,
"Flipped position should have same instrument ID"
);
assert_eq!(
cache.positions_open_count(None, None, None, None, None),
1,
"Should have 1 open position (flipped position)"
);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
1,
"Should have 1 closed position (original position)"
);
assert_eq!(
cache.positions_total_count(None, None, None, None, None),
2,
"Total position count should be 2 (original + flipped)"
);
}
}
#[rstest]
fn test_flip_position_on_opposite_filled_same_position_buy(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Hedging,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order1 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.build();
let order2 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-002-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(150_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order1.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order2.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let position_id = PositionId::from("P-19700101-000000-000-None-1");
let order1_submitted_event = TestOrderEventStubs::submitted(&order1, AccountId::test_default());
execution_engine.process(&order1_submitted_event);
let order1_accepted_event = TestOrderEventStubs::accepted(
&order1,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order1_accepted_event);
let order1_filled_event = TestOrderEventStubs::filled(
&order1,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
Some(position_id),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order1_filled_event);
{
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&position_id),
"Initial position should exist"
);
assert!(
cache.is_position_open(&position_id),
"Initial position should be open"
);
let position = cache.position(&position_id).expect("Position should exist");
assert_eq!(
position.side,
PositionSide::Short,
"Position should be short"
);
assert_eq!(
position.quantity,
Quantity::from(100_000),
"Position quantity should be 100,000"
);
}
let order2_submitted_event = TestOrderEventStubs::submitted(&order2, AccountId::test_default());
execution_engine.process(&order2_submitted_event);
let order2_accepted_event = TestOrderEventStubs::accepted(
&order2,
AccountId::test_default(),
VenueOrderId::from("V-002"),
);
execution_engine.process(&order2_accepted_event);
let order2_filled_event = TestOrderEventStubs::filled(
&order2,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001-2")),
Some(position_id), None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order2_filled_event);
let cache = execution_engine.cache().borrow();
let all_position_ids = cache.position_ids(None, None, None, None);
println!("All position IDs after flip: {all_position_ids:?}");
assert!(
!all_position_ids.is_empty(),
"Should have at least one position after flip"
);
if cache.position_exists(&position_id) {
let original_position = cache
.position(&position_id)
.expect("Original position should exist");
println!(
"Original position after flip: is_closed={}, quantity={:?}, side={:?}",
original_position.is_closed(),
original_position.quantity,
original_position.side
);
assert!(
original_position.is_closed(),
"Original position should be closed after flip"
);
}
let open_positions = cache.position_open_ids(None, None, None, None);
println!("Open position IDs after flip: {open_positions:?}");
if open_positions.is_empty() {
panic!(
"Expected to find at least one open position after flip, but found none. All positions: {all_position_ids:?}"
);
} else {
let flipped_position_id = open_positions
.iter()
.next()
.expect("Should have at least one open position");
let flipped_position = cache
.position(flipped_position_id)
.expect("Flipped position should exist");
println!(
"Flipped position: id={:?}, side={:?}, quantity={:?}",
flipped_position_id, flipped_position.side, flipped_position.quantity
);
assert_eq!(
flipped_position.side,
PositionSide::Long,
"Flipped position should be long"
);
assert_eq!(
flipped_position.quantity,
Quantity::from(50_000), "Flipped position quantity should be 50,000 (150,000 - 100,000)"
);
assert_eq!(
flipped_position.strategy_id, strategy_id,
"Flipped position should have same strategy ID"
);
assert_eq!(
flipped_position.instrument_id, instrument.id,
"Flipped position should have same instrument ID"
);
assert_eq!(
cache.positions_open_count(None, None, None, None, None),
1,
"Should have 1 open position (flipped position)"
);
assert_eq!(
cache.positions_closed_count(None, None, None, None, None),
1,
"Should have 1 closed position (original position)"
);
assert_eq!(
cache.positions_total_count(None, None, None, None, None),
2,
"Total position count should be 2 (original + flipped)"
);
}
}
#[rstest]
fn test_flip_position_on_flat_position_then_filled_reusing_position_id(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Hedging,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order1 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.build();
let order2 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-002-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let order3 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-003-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order1.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order2.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order3.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let position_id = PositionId::from("P-19700101-000000-000-001-1");
let order1_submitted_event = TestOrderEventStubs::submitted(&order1, AccountId::test_default());
execution_engine.process(&order1_submitted_event);
let order1_accepted_event = TestOrderEventStubs::accepted(
&order1,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order1_accepted_event);
let order1_filled_event = TestOrderEventStubs::filled(
&order1,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
Some(position_id),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order1_filled_event);
{
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&position_id),
"Initial position should exist"
);
assert!(
cache.is_position_open(&position_id),
"Initial position should be open"
);
let position = cache.position(&position_id).expect("Position should exist");
assert_eq!(
position.side,
PositionSide::Short,
"Position should be short"
);
assert_eq!(
position.quantity,
Quantity::from(100_000),
"Position quantity should be 100,000"
);
}
let order2_submitted_event = TestOrderEventStubs::submitted(&order2, AccountId::test_default());
execution_engine.process(&order2_submitted_event);
let order2_accepted_event = TestOrderEventStubs::accepted(
&order2,
AccountId::test_default(),
VenueOrderId::from("V-002"),
);
execution_engine.process(&order2_accepted_event);
let order2_filled_event = TestOrderEventStubs::filled(
&order2,
&instrument.into(),
Some(TradeId::new("E-19700101-000000-001-001-2")),
Some(position_id), None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order2_filled_event);
{
let cache = execution_engine.cache().borrow();
let position = cache.position(&position_id).expect("Position should exist");
assert!(position.is_closed(), "Position should be closed (flat)");
assert_eq!(
position.signed_qty, 0.0,
"Position signed quantity should be 0"
);
}
execution_engine
.cache()
.borrow_mut()
.add_order(
order3.clone(),
Some(position_id),
Some(ClientId::from("STUB")),
true,
)
.unwrap();
{
let cache = execution_engine.cache().borrow();
let cached_order3 = cache
.order(&order3.client_order_id())
.expect("Order 3 should be in cache");
assert_eq!(
cached_order3.status(),
OrderStatus::Initialized,
"Order 3 should remain initialized when using closed position ID"
);
}
}
#[rstest]
fn test_cached_position_id_used_for_netting_fill(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let user_position_id = PositionId::from("MY-CUSTOM-POS-1");
execution_engine
.cache()
.borrow_mut()
.add_order(
order.clone(),
Some(user_position_id),
Some(ClientId::from("STUB")),
true,
)
.unwrap();
let order_submitted_event = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let netting_position_id = PositionId::new(format!("{}-{}", instrument.id, strategy_id));
let order_filled_event = TestOrderEventStubs::filled(
&order,
&instrument.into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order_filled_event);
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&user_position_id),
"Position should exist with user-supplied ID"
);
assert!(
cache.is_position_open(&user_position_id),
"Position should be open"
);
assert!(
!cache.position_exists(&netting_position_id),
"No position should exist with the netting-generated ID"
);
assert_eq!(
cache.positions_total_count(None, None, None, None, None),
1,
"Total position count should be 1"
);
}
#[rstest]
fn test_cached_position_id_used_for_hedging_fill(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Hedging,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let user_position_id = PositionId::from("MY-HEDGE-POS-1");
execution_engine
.cache()
.borrow_mut()
.add_order(
order.clone(),
Some(user_position_id),
Some(ClientId::from("STUB")),
true,
)
.unwrap();
let order_submitted_event = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let order_filled_event = TestOrderEventStubs::filled(
&order,
&instrument.into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order_filled_event);
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&user_position_id),
"Position should exist with user-supplied ID"
);
assert!(
cache.is_position_open(&user_position_id),
"Position should be open"
);
let position = cache
.position(&user_position_id)
.expect("Position should exist");
assert_eq!(position.side, PositionSide::Long, "Position should be long");
assert_eq!(
position.quantity,
Quantity::from(100_000),
"Position quantity should be 100,000"
);
}
#[rstest]
fn test_cached_position_id_overrides_fill_position_id(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Hedging,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let cached_position_id = PositionId::from("CACHED-POS");
execution_engine
.cache()
.borrow_mut()
.add_order(
order.clone(),
Some(cached_position_id),
Some(ClientId::from("STUB")),
true,
)
.unwrap();
let order_submitted_event = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&order_submitted_event);
let order_accepted_event = TestOrderEventStubs::accepted(
&order,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order_accepted_event);
let venue_position_id = PositionId::from("VENUE-POS");
let order_filled_event = TestOrderEventStubs::filled(
&order,
&instrument.into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
Some(venue_position_id),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order_filled_event);
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&cached_position_id),
"Position should exist with cached ID"
);
assert!(
!cache.position_exists(&venue_position_id),
"No position should exist with venue-supplied ID"
);
assert!(
cache.is_position_open(&cached_position_id),
"Position should be open"
);
}
#[rstest]
fn test_exec_algorithm_position_id_propagates_to_primary_order(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Hedging,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let exec_algo_id = ExecAlgorithmId::new("TWAP");
let primary_client_order_id = ClientOrderId::from("O-PRIMARY-001");
let child_client_order_id = ClientOrderId::from("O-CHILD-001");
let primary_order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(primary_client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(200_000))
.exec_algorithm_id(exec_algo_id)
.exec_spawn_id(primary_client_order_id)
.build();
let child_order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(child_client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.exec_algorithm_id(exec_algo_id)
.exec_spawn_id(primary_client_order_id)
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(primary_order, None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(
child_order.clone(),
None,
Some(ClientId::from("STUB")),
true,
)
.unwrap();
let child_submitted_event =
TestOrderEventStubs::submitted(&child_order, AccountId::test_default());
execution_engine.process(&child_submitted_event);
let child_accepted_event = TestOrderEventStubs::accepted(
&child_order,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&child_accepted_event);
let child_filled_event = TestOrderEventStubs::filled(
&child_order,
&instrument.into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&child_filled_event);
let cache = execution_engine.cache().borrow();
let primary = cache
.order(&primary_client_order_id)
.expect("Primary order should be in cache");
assert!(
primary.position_id().is_some(),
"Primary order should have position_id assigned"
);
let primary_cached_pos_id = cache.position_id(&primary_client_order_id);
assert!(
primary_cached_pos_id.is_some(),
"Cache should index position_id for primary order"
);
assert_eq!(
primary.position_id().unwrap(),
*primary_cached_pos_id.unwrap(),
"Primary order position_id and cache index should match"
);
let position_id = primary.position_id().unwrap();
assert!(
cache.position_exists(&position_id),
"Position should exist with generated ID"
);
assert!(
cache.is_position_open(&position_id),
"Position should be open"
);
}
#[rstest]
fn test_exec_algorithm_does_not_overwrite_primary_cached_position_id(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Hedging,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let exec_algo_id = ExecAlgorithmId::new("TWAP");
let primary_client_order_id = ClientOrderId::from("O-PRIMARY-002");
let child_client_order_id = ClientOrderId::from("O-CHILD-002");
let user_position_id = PositionId::from("USER-ALGO-POS");
let primary_order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(primary_client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(200_000))
.exec_algorithm_id(exec_algo_id)
.exec_spawn_id(primary_client_order_id)
.build();
let child_order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(child_client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.exec_algorithm_id(exec_algo_id)
.exec_spawn_id(primary_client_order_id)
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(
primary_order,
Some(user_position_id),
Some(ClientId::from("STUB")),
true,
)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(
child_order.clone(),
None,
Some(ClientId::from("STUB")),
true,
)
.unwrap();
let child_submitted_event =
TestOrderEventStubs::submitted(&child_order, AccountId::test_default());
execution_engine.process(&child_submitted_event);
let child_accepted_event = TestOrderEventStubs::accepted(
&child_order,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&child_accepted_event);
let child_filled_event = TestOrderEventStubs::filled(
&child_order,
&instrument.into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&child_filled_event);
let cache = execution_engine.cache().borrow();
let primary_cached_pos_id = cache
.position_id(&primary_client_order_id)
.expect("Primary should have cached position_id");
assert_eq!(
*primary_cached_pos_id, user_position_id,
"Primary order's cached position_id should remain the user-supplied value"
);
}
#[rstest]
fn test_flip_position_when_netting_oms(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order1 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.build();
let order2 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-002-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(200_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order1.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order2.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let position_id = PositionId::new(format!("{}-{}", instrument.id, strategy_id));
let order1_submitted_event = TestOrderEventStubs::submitted(&order1, AccountId::test_default());
execution_engine.process(&order1_submitted_event);
let order1_accepted_event = TestOrderEventStubs::accepted(
&order1,
AccountId::test_default(),
VenueOrderId::from("V-001"),
);
execution_engine.process(&order1_accepted_event);
let order1_filled_event = TestOrderEventStubs::filled(
&order1,
&instrument.clone().into(),
Some(TradeId::new("E-19700101-000000-001-001-1")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order1_filled_event);
{
let cache = execution_engine.cache().borrow();
assert!(
cache.position_exists(&position_id),
"Initial position should exist"
);
assert!(
cache.is_position_open(&position_id),
"Initial position should be open"
);
let position = cache.position(&position_id).expect("Position should exist");
assert_eq!(
position.side,
PositionSide::Short,
"Position should be short"
);
assert_eq!(
position.quantity,
Quantity::from(100_000),
"Position quantity should be 100,000"
);
}
let order2_submitted_event = TestOrderEventStubs::submitted(&order2, AccountId::test_default());
execution_engine.process(&order2_submitted_event);
let order2_accepted_event = TestOrderEventStubs::accepted(
&order2,
AccountId::test_default(),
VenueOrderId::from("V-002"),
);
execution_engine.process(&order2_accepted_event);
let order2_filled_event = TestOrderEventStubs::filled(
&order2,
&instrument.into(),
Some(TradeId::new("E-19700101-000000-001-001-2")),
None,
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order2_filled_event);
let cache = execution_engine.cache().borrow();
let position = cache
.position(&position_id)
.expect("flipped position should exist");
assert_eq!(
position.id, position_id,
"flipped position should have correct ID"
);
assert_eq!(
position.quantity,
Quantity::from(100_000),
"flipped position should have quantity 100,000 after flip"
);
assert_eq!(
position.side,
PositionSide::Long,
"flipped position should be long"
);
}
#[rstest]
fn test_handle_updated_order_event(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
let order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(10_000))
.price(Price::from_str("10.0").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let order_submitted_event = TestOrderEventStubs::submitted(&order, account_id);
execution_engine.process(&order_submitted_event);
let order_accepted_event =
TestOrderEventStubs::accepted(&order, account_id, VenueOrderId::from("V-001"));
execution_engine.process(&order_accepted_event);
let order_pending_update_event = OrderEventAny::PendingUpdate(OrderPendingUpdate::new(
trader_id,
strategy_id,
instrument.id,
order.client_order_id(),
account_id,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false,
Some(VenueOrderId::from("V-001")),
));
execution_engine.process(&order_pending_update_event);
{
let cache = execution_engine.cache().borrow();
let cached_order = cache
.order(&order.client_order_id())
.expect("Order should exist in cache");
assert_eq!(
cached_order.venue_order_id(),
Some(VenueOrderId::from("V-001")),
"Order should have correct venue_order_id"
);
}
let new_venue_id = VenueOrderId::from("1");
let order_updated_event = OrderEventAny::Updated(OrderUpdated::new(
trader_id,
strategy_id,
instrument.id,
order.client_order_id(),
order.quantity(),
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false,
Some(new_venue_id),
Some(account_id),
order.price(),
None, None, false, ));
execution_engine.process(&order_updated_event);
let cache = execution_engine.cache().borrow();
let cached_order = cache
.order(&order.client_order_id())
.expect("Order should exist in cache");
assert_eq!(
cached_order.venue_order_id(),
Some(new_venue_id),
"Order should update venue_order_id from the OrderUpdated event"
);
}
#[rstest]
fn test_submit_market_should_not_add_to_own_book() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: true,
snapshot_orders: true,
snapshot_positions: true,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
let cache = execution_engine.cache().borrow();
let own_order_book = cache.own_order_book(&order.instrument_id());
assert!(
own_order_book.is_none(),
"Market orders should not be added to own order book even when order book management is enabled"
);
}
#[rstest]
#[case(TimeInForce::Fok)]
#[case(TimeInForce::Ioc)]
fn test_submit_ioc_fok_should_not_add_to_own_book(#[case] time_in_force: TimeInForce) {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: true,
snapshot_orders: true,
snapshot_positions: true,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("10.0").unwrap())
.time_in_force(time_in_force)
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
let cache = execution_engine.cache().borrow();
let own_order_book = cache.own_order_book(&order.instrument_id());
assert!(
own_order_book.is_none(),
"Orders with {time_in_force} time in force should not be added to own order book even when order book management is enabled"
);
}
#[rstest]
fn test_submit_order_adds_to_own_book_bid() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: true,
snapshot_orders: true,
snapshot_positions: true,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("10.0").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&order.instrument_id())
.expect("Own order book should exist");
assert_eq!(
own_book.update_count, 1,
"Own order book should have update count of 1"
);
let asks = own_book.asks_as_map(None, None, None);
assert_eq!(asks.len(), 0, "Own order book should have no ask orders");
let bids = own_book.bids_as_map(None, None, None);
assert_eq!(
bids.len(),
1,
"Own order book should have exactly one bid price level"
);
let price_key = dec!(10.0);
assert!(
bids.contains_key(&price_key),
"Own order book should contain bid orders at price 10.0"
);
let bid_orders = &bids[&price_key];
assert_eq!(
bid_orders.len(),
1,
"Should have exactly one order at price level 10.0"
);
let own_order = &bid_orders[0];
assert_eq!(
own_order.client_order_id,
order.client_order_id(),
"Own order should have the same client order ID"
);
assert_eq!(
own_order.price.as_decimal(),
dec!(10.0),
"Own order should have price 10.0"
);
assert_eq!(
own_order.size.as_decimal(),
Decimal::from(100_000),
"Own order should have size 100,000"
);
assert_eq!(
own_order.status,
OrderStatus::Initialized,
"Own order should have status Initialized"
);
assert!(
own_book.is_order_in_book(&order.client_order_id()),
"Order should be in the own order book"
);
let bid_client_order_ids = own_book.bid_client_order_ids();
assert_eq!(
bid_client_order_ids.len(),
1,
"Should have exactly one bid client order ID"
);
assert_eq!(
bid_client_order_ids[0],
order.client_order_id(),
"Bid client order ID should match the submitted order"
);
let ask_client_order_ids = own_book.ask_client_order_ids();
assert_eq!(
ask_client_order_ids.len(),
0,
"Should have no ask client order IDs"
);
}
#[rstest]
fn test_submit_order_adds_to_own_book_ask() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: true,
snapshot_orders: true,
snapshot_positions: true,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.price(Price::from_str("11.0").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&order.instrument_id())
.expect("Own order book should exist");
assert_eq!(
own_book.update_count, 1,
"Own order book should have update count of 1"
);
let bids = own_book.bids_as_map(None, None, None);
assert_eq!(bids.len(), 0, "Own order book should have no bid orders");
let asks = own_book.asks_as_map(None, None, None);
assert_eq!(
asks.len(),
1,
"Own order book should have exactly one ask price level"
);
let price_key = dec!(11.0);
assert!(
asks.contains_key(&price_key),
"Own order book should contain ask orders at price 11.0"
);
let ask_orders = &asks[&price_key];
assert_eq!(
ask_orders.len(),
1,
"Should have exactly one order at price level 11.0"
);
let own_order = &ask_orders[0];
assert_eq!(
own_order.client_order_id,
order.client_order_id(),
"Own order should have the same client order ID"
);
assert_eq!(
own_order.price.as_decimal(),
dec!(11.0),
"Own order should have price 11.0"
);
assert_eq!(
own_order.size.as_decimal(),
Decimal::from(100_000),
"Own order should have size 100,000"
);
assert_eq!(
own_order.status,
OrderStatus::Initialized,
"Own order should have status Initialized"
);
assert!(
own_book.is_order_in_book(&order.client_order_id()),
"Order should be in the own order book"
);
let ask_client_order_ids = own_book.ask_client_order_ids();
assert_eq!(
ask_client_order_ids.len(),
1,
"Should have exactly one ask client order ID"
);
assert_eq!(
ask_client_order_ids[0],
order.client_order_id(),
"Ask client order ID should match the submitted order"
);
let bid_client_order_ids = own_book.bid_client_order_ids();
assert_eq!(
bid_client_order_ids.len(),
0,
"Should have no bid client order IDs"
);
}
#[rstest]
fn test_cancel_order_removes_from_own_book() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: true,
snapshot_orders: true,
snapshot_positions: true,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let order_bid = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("10.0").unwrap())
.build();
let order_ask = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-2"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.price(Price::from_str("11.0").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order_bid.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order_ask.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order_bid = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order_bid.client_order_id(),
order_init: order_bid.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
let submit_order_ask = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order_ask.client_order_id(),
order_init: order_ask.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order_bid));
execution_engine.execute(TradingCommand::SubmitOrder(submit_order_ask));
let order_submitted_bid = TestOrderEventStubs::submitted(&order_bid, account_id);
let order_submitted_ask = TestOrderEventStubs::submitted(&order_ask, account_id);
execution_engine.process(&order_submitted_bid);
execution_engine.process(&order_submitted_ask);
let order_accepted_bid =
TestOrderEventStubs::accepted(&order_bid, account_id, VenueOrderId::from("V-001"));
let order_accepted_ask =
TestOrderEventStubs::accepted(&order_ask, account_id, VenueOrderId::from("V-002"));
execution_engine.process(&order_accepted_bid);
execution_engine.process(&order_accepted_ask);
let cancel_order_bid = CancelOrder {
trader_id,
client_id: Some(ClientId::from("STUB")),
strategy_id,
instrument_id: instrument.id,
client_order_id: order_bid.client_order_id(),
venue_order_id: Some(VenueOrderId::from("V-001")),
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
params: None,
};
let cancel_order_ask = CancelOrder {
trader_id,
client_id: Some(ClientId::from("STUB")),
strategy_id,
instrument_id: instrument.id,
client_order_id: order_ask.client_order_id(),
venue_order_id: Some(VenueOrderId::from("V-002")),
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
params: None,
};
execution_engine.execute(TradingCommand::CancelOrder(cancel_order_bid));
execution_engine.execute(TradingCommand::CancelOrder(cancel_order_ask));
let order_canceled_bid =
TestOrderEventStubs::canceled(&order_bid, account_id, Some(VenueOrderId::from("V-001")));
let order_canceled_ask =
TestOrderEventStubs::canceled(&order_ask, account_id, Some(VenueOrderId::from("V-002")));
execution_engine.process(&order_canceled_bid);
execution_engine.process(&order_canceled_ask);
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
assert_eq!(own_book.update_count, 8, "Expected update count to be 8");
let bid_client_order_ids = own_book.bid_client_order_ids();
let ask_client_order_ids = own_book.ask_client_order_ids();
assert!(bid_client_order_ids.is_empty(), "Expected no bid orders");
assert!(ask_client_order_ids.is_empty(), "Expected no ask orders");
let bids = own_book.bids_as_map(None, None, None);
let asks = own_book.asks_as_map(None, None, None);
assert!(bids.is_empty(), "Expected no bid orders in own book");
assert!(asks.is_empty(), "Expected no ask orders in own book");
}
#[rstest]
fn test_own_book_status_filtering() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: true,
snapshot_orders: true,
snapshot_positions: true,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let order_bid = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("10.0").unwrap())
.build();
let order_ask = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-2"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.price(Price::from_str("11.0").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order_bid.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order_ask.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order_bid = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order_bid.client_order_id(),
order_init: order_bid.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
let submit_order_ask = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order_ask.client_order_id(),
order_init: order_ask.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order_bid));
execution_engine.execute(TradingCommand::SubmitOrder(submit_order_ask));
let order_submitted_bid = TestOrderEventStubs::submitted(&order_bid, account_id);
let order_submitted_ask = TestOrderEventStubs::submitted(&order_ask, account_id);
execution_engine.process(&order_submitted_bid);
execution_engine.process(&order_submitted_ask);
let order_accepted_bid =
TestOrderEventStubs::accepted(&order_bid, account_id, VenueOrderId::from("V-001"));
let order_accepted_ask =
TestOrderEventStubs::accepted(&order_ask, account_id, VenueOrderId::from("V-002"));
execution_engine.process(&order_accepted_bid);
execution_engine.process(&order_accepted_ask);
let cancel_order_bid = CancelOrder {
trader_id,
client_id: Some(ClientId::from("STUB")),
strategy_id,
instrument_id: instrument.id,
client_order_id: order_bid.client_order_id(),
venue_order_id: Some(VenueOrderId::from("V-001")),
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
params: None,
};
let cancel_order_ask = CancelOrder {
trader_id,
client_id: Some(ClientId::from("STUB")),
strategy_id,
instrument_id: instrument.id,
client_order_id: order_ask.client_order_id(),
venue_order_id: Some(VenueOrderId::from("V-002")),
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
params: None,
};
execution_engine.execute(TradingCommand::CancelOrder(cancel_order_bid));
execution_engine.execute(TradingCommand::CancelOrder(cancel_order_ask));
let order_canceled_bid =
TestOrderEventStubs::canceled(&order_bid, account_id, Some(VenueOrderId::from("V-001")));
let order_canceled_ask =
TestOrderEventStubs::canceled(&order_ask, account_id, Some(VenueOrderId::from("V-002")));
execution_engine.process(&order_canceled_bid);
execution_engine.process(&order_canceled_ask);
let cache = execution_engine.cache().borrow();
let bid_order = cache
.order(&order_bid.client_order_id())
.expect("Bid order should exist in cache");
let ask_order = cache
.order(&order_ask.client_order_id())
.expect("Ask order should exist in cache");
assert_eq!(
bid_order.status(),
OrderStatus::Canceled,
"Bid order should be in Canceled status"
);
assert_eq!(
ask_order.status(),
OrderStatus::Canceled,
"Ask order should be in Canceled status"
);
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
assert_eq!(own_book.update_count, 8, "Expected update count to be 8");
let bids = own_book.bids_as_map(None, None, None);
let asks = own_book.asks_as_map(None, None, None);
assert_eq!(
bids.len(),
0,
"Expected 0 bid orders in own book after cancellation"
);
assert_eq!(
asks.len(),
0,
"Expected 0 ask orders in own book after cancellation"
);
let accepted_statuses: AHashSet<OrderStatus> =
AHashSet::from_iter([OrderStatus::Accepted, OrderStatus::PartiallyFilled]);
let filtered_bids = own_book.bids_as_map(Some(&accepted_statuses), None, None);
let filtered_asks = own_book.asks_as_map(Some(&accepted_statuses), None, None);
assert!(
filtered_bids.is_empty(),
"Expected no bid orders after cancellation"
);
assert!(
filtered_asks.is_empty(),
"Expected no ask orders after cancellation"
);
}
#[rstest]
fn test_filled_order_removes_from_own_book() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: true,
snapshot_orders: true,
snapshot_positions: true,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let order_bid = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("10.0").unwrap())
.build();
let order_ask = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-2"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.price(Price::from_str("11.0").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order_bid.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order_ask.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order_bid = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order_bid.client_order_id(),
order_init: order_bid.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
let submit_order_ask = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order_ask.client_order_id(),
order_init: order_ask.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order_bid));
execution_engine.execute(TradingCommand::SubmitOrder(submit_order_ask));
let order_submitted_bid = TestOrderEventStubs::submitted(&order_bid, account_id);
let order_submitted_ask = TestOrderEventStubs::submitted(&order_ask, account_id);
execution_engine.process(&order_submitted_bid);
execution_engine.process(&order_submitted_ask);
let order_accepted_bid =
TestOrderEventStubs::accepted(&order_bid, account_id, VenueOrderId::from("V-001"));
let order_accepted_ask =
TestOrderEventStubs::accepted(&order_ask, account_id, VenueOrderId::from("V-002"));
execution_engine.process(&order_accepted_bid);
execution_engine.process(&order_accepted_ask);
let order_filled_bid = OrderEventAny::Filled(OrderFilled::new(
order_bid.trader_id(),
order_bid.strategy_id(),
instrument.id(),
order_bid.client_order_id(),
VenueOrderId::from("V-001"), account_id,
TradeId::new("E-19700101-000000-001-001"),
order_bid.order_side(),
order_bid.order_type(),
order_bid.quantity(), Price::from_str("10.0").unwrap(), instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, None, None, ));
let order_filled_ask = OrderEventAny::Filled(OrderFilled::new(
order_ask.trader_id(),
order_ask.strategy_id(),
instrument.id(),
order_ask.client_order_id(),
VenueOrderId::from("V-002"), account_id,
TradeId::new("E-19700101-000000-001-002"),
order_ask.order_side(),
order_ask.order_type(),
order_ask.quantity(), Price::from_str("11.0").unwrap(), instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, None, None, ));
execution_engine.process(&order_filled_bid);
execution_engine.process(&order_filled_ask);
let cache = execution_engine.cache().borrow();
let bid_order = cache
.order(&order_bid.client_order_id())
.expect("Bid order should exist in cache");
let ask_order = cache
.order(&order_ask.client_order_id())
.expect("Ask order should exist in cache");
assert_eq!(
bid_order.status(),
OrderStatus::Filled,
"Bid order should be in Filled status"
);
assert_eq!(
ask_order.status(),
OrderStatus::Filled,
"Ask order should be in Filled status"
);
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
assert_eq!(own_book.update_count, 8, "Expected update count to be 8");
let bids = own_book.bids_as_map(None, None, None);
let asks = own_book.asks_as_map(None, None, None);
assert_eq!(
bids.len(),
0,
"Expected 0 bid orders in own book after fill"
);
assert_eq!(
asks.len(),
0,
"Expected 0 ask orders in own book after fill"
);
let bids = own_book.bids_as_map(None, None, None);
let asks = own_book.asks_as_map(None, None, None);
assert!(bids.is_empty(), "Expected no bid orders after fill");
assert!(asks.is_empty(), "Expected no ask orders after fill");
}
#[rstest]
fn test_order_updates_in_own_book() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: true,
snapshot_orders: true,
snapshot_positions: true,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let order_bid = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("10.0").unwrap())
.build();
let order_ask = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-2"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.price(Price::from_str("11.0").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order_bid.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order_ask.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order_bid = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order_bid.client_order_id(),
order_init: order_bid.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
let submit_order_ask = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order_ask.client_order_id(),
order_init: order_ask.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order_bid));
execution_engine.execute(TradingCommand::SubmitOrder(submit_order_ask));
let order_submitted_bid = TestOrderEventStubs::submitted(&order_bid, account_id);
let order_submitted_ask = TestOrderEventStubs::submitted(&order_ask, account_id);
execution_engine.process(&order_submitted_bid);
execution_engine.process(&order_submitted_ask);
let order_accepted_bid =
TestOrderEventStubs::accepted(&order_bid, account_id, VenueOrderId::from("V-001"));
let order_accepted_ask =
TestOrderEventStubs::accepted(&order_ask, account_id, VenueOrderId::from("V-002"));
execution_engine.process(&order_accepted_bid);
execution_engine.process(&order_accepted_ask);
let new_bid_price = Price::from_str("9.0").unwrap();
let new_ask_price = Price::from_str("12.0").unwrap();
let order_updated_bid = OrderEventAny::Updated(OrderUpdated::new(
order_bid.trader_id(),
order_bid.strategy_id(),
instrument.id(),
order_bid.client_order_id(),
order_bid.quantity(),
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, Some(VenueOrderId::from("V-001")), Some(account_id), Some(new_bid_price), None, None, false, ));
let order_updated_ask = OrderEventAny::Updated(OrderUpdated::new(
order_ask.trader_id(),
order_ask.strategy_id(),
instrument.id(),
order_ask.client_order_id(),
order_ask.quantity(),
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, Some(VenueOrderId::from("V-002")), Some(account_id), Some(new_ask_price), None, None, false, ));
execution_engine.process(&order_updated_bid);
execution_engine.process(&order_updated_ask);
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
assert_eq!(own_book.update_count, 8, "Expected update count to be 8");
let bids = own_book.bids_as_map(None, None, None);
let asks = own_book.asks_as_map(None, None, None);
assert_eq!(bids.len(), 1, "Expected 1 bid order in own book");
assert_eq!(asks.len(), 1, "Expected 1 ask order in own book");
let bid_orders = bids
.get(&new_bid_price.as_decimal())
.expect("Should have bid orders at new price");
assert_eq!(
bid_orders.len(),
1,
"Should have exactly 1 bid order at new price"
);
let own_order_bid = &bid_orders[0];
assert_eq!(own_order_bid.client_order_id, order_bid.client_order_id());
assert_eq!(own_order_bid.price, new_bid_price);
assert_eq!(own_order_bid.status, OrderStatus::Accepted);
let ask_orders = asks
.get(&new_ask_price.as_decimal())
.expect("Should have ask orders at new price");
assert_eq!(
ask_orders.len(),
1,
"Should have exactly 1 ask order at new price"
);
let own_order_ask = &ask_orders[0];
assert_eq!(own_order_ask.client_order_id, order_ask.client_order_id());
assert_eq!(own_order_ask.price, new_ask_price);
assert_eq!(own_order_ask.status, OrderStatus::Accepted);
let accepted_statuses: AHashSet<OrderStatus> = AHashSet::from_iter([OrderStatus::Accepted]);
let filtered_bids = own_book.bids_as_map(Some(&accepted_statuses), None, None);
let filtered_asks = own_book.asks_as_map(Some(&accepted_statuses), None, None);
assert_eq!(
filtered_bids.len(),
1,
"Should have 1 bid order with ACCEPTED status"
);
assert_eq!(
filtered_asks.len(),
1,
"Should have 1 ask order with ACCEPTED status"
);
let filtered_bid_orders = filtered_bids
.get(&new_bid_price.as_decimal())
.expect("Should have filtered bid orders");
let filtered_ask_orders = filtered_asks
.get(&new_ask_price.as_decimal())
.expect("Should have filtered ask orders");
assert_eq!(
filtered_bid_orders.len(),
1,
"Should have exactly 1 filtered bid order"
);
assert_eq!(
filtered_ask_orders.len(),
1,
"Should have exactly 1 filtered ask order"
);
assert_eq!(
filtered_bid_orders[0].client_order_id,
order_bid.client_order_id()
);
assert_eq!(
filtered_ask_orders[0].client_order_id,
order_ask.client_order_id()
);
}
#[rstest]
fn test_position_flip_with_own_order_book() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: true,
snapshot_orders: true,
snapshot_positions: true,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let buy_order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.0").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(buy_order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_buy_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: buy_order.client_order_id(),
order_init: buy_order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_buy_order));
let order_submitted_buy = TestOrderEventStubs::submitted(&buy_order, account_id);
let order_accepted_buy =
TestOrderEventStubs::accepted(&buy_order, account_id, VenueOrderId::from("V-001"));
execution_engine.process(&order_submitted_buy);
execution_engine.process(&order_accepted_buy);
let order_filled_buy = OrderEventAny::Filled(OrderFilled::new(
buy_order.trader_id(),
buy_order.strategy_id(),
instrument.id(),
buy_order.client_order_id(),
VenueOrderId::from("V-001"),
account_id,
TradeId::new("E-19700101-000000-001-001"),
buy_order.order_side(),
buy_order.order_type(),
buy_order.quantity(), Price::from_str("1.0").unwrap(), instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, None, None, ));
execution_engine.process(&order_filled_buy);
let position_id = {
let cache = execution_engine.cache().borrow();
let positions = cache.positions(None, None, None, None, None);
assert_eq!(
positions.len(),
1,
"Should have exactly 1 position after buy order fill"
);
let original_position = &positions[0];
assert!(
original_position.is_open(),
"Original position should be open"
);
assert_eq!(
original_position.side,
PositionSide::Long,
"Original position should be long"
);
assert_eq!(
original_position.quantity,
Quantity::from(100_000),
"Original position should have correct quantity"
);
original_position.id
};
let sell_order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-2"))
.side(OrderSide::Sell)
.quantity(Quantity::from(200_000)) .price(Price::from_str("1.1").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(sell_order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_sell_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: sell_order.client_order_id(),
order_init: sell_order.init_event().clone(),
position_id: Some(position_id),
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_sell_order));
let order_submitted_sell = TestOrderEventStubs::submitted(&sell_order, account_id);
let order_accepted_sell =
TestOrderEventStubs::accepted(&sell_order, account_id, VenueOrderId::from("V-002"));
execution_engine.process(&order_submitted_sell);
execution_engine.process(&order_accepted_sell);
let order_filled_sell = OrderEventAny::Filled(OrderFilled::new(
sell_order.trader_id(),
sell_order.strategy_id(),
instrument.id(),
sell_order.client_order_id(),
VenueOrderId::from("V-002"),
account_id,
TradeId::new("E-19700101-000000-001-002"),
sell_order.order_side(),
sell_order.order_type(),
sell_order.quantity(), Price::from_str("1.1").unwrap(), instrument.quote_currency(),
LiquiditySide::Maker,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
false, Some(position_id), None, ));
execution_engine.process(&order_filled_sell);
let cache = execution_engine.cache().borrow();
let positions = cache.positions(None, None, None, None, None);
assert_eq!(
positions.len(),
1,
"Expected 1 position after position flip (position replacement)"
);
let flipped_position = &positions[0];
assert!(
flipped_position.is_open(),
"Flipped position should be open"
);
assert_eq!(
flipped_position.side,
PositionSide::Short,
"Flipped position should be short"
);
assert_eq!(
flipped_position.quantity,
Quantity::from(100_000),
"Flipped position should have correct quantity"
);
assert_eq!(
flipped_position.id, position_id,
"Position ID should remain the same after flip"
);
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
assert!(
own_book.update_count > 0,
"Own order book should have been updated"
);
let bids = own_book.bids_as_map(None, None, None);
let asks = own_book.asks_as_map(None, None, None);
assert_eq!(
bids.len(),
0,
"Expected 0 bid orders in own book after fill"
);
assert_eq!(asks.len(), 0, "Expected 0 ask orders after fill");
}
#[rstest]
fn test_own_book_with_crossed_orders() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: true,
snapshot_orders: true,
snapshot_positions: true,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let buy_order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.05").unwrap()) .build();
let sell_order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-2"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.04").unwrap()) .build();
execution_engine
.cache()
.borrow_mut()
.add_order(buy_order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(sell_order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_buy_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: buy_order.client_order_id(),
order_init: buy_order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
let submit_sell_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: sell_order.client_order_id(),
order_init: sell_order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_buy_order));
execution_engine.execute(TradingCommand::SubmitOrder(submit_sell_order));
let order_submitted_buy = TestOrderEventStubs::submitted(&buy_order, account_id);
let order_submitted_sell = TestOrderEventStubs::submitted(&sell_order, account_id);
execution_engine.process(&order_submitted_buy);
execution_engine.process(&order_submitted_sell);
let order_accepted_buy =
TestOrderEventStubs::accepted(&buy_order, account_id, VenueOrderId::from("V-001"));
let order_accepted_sell =
TestOrderEventStubs::accepted(&sell_order, account_id, VenueOrderId::from("V-002"));
execution_engine.process(&order_accepted_buy);
execution_engine.process(&order_accepted_sell);
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
assert!(
own_book.update_count > 0,
"Expected update count to be greater than 0"
);
let bids = own_book.bids_as_map(None, None, None);
let asks = own_book.asks_as_map(None, None, None);
assert_eq!(bids.len(), 1, "Expected 1 bid order in own book");
assert_eq!(asks.len(), 1, "Expected 1 ask order in own book");
let bid_price = dec!(1.05);
let ask_price = dec!(1.04);
assert!(
bids.contains_key(&bid_price),
"Expected bid order at price 1.05"
);
assert!(
asks.contains_key(&ask_price),
"Expected ask order at price 1.04"
);
let accepted_statuses: AHashSet<OrderStatus> = AHashSet::from_iter([OrderStatus::Accepted]);
let active_bid_orders = own_book.bids_as_map(Some(&accepted_statuses), None, None);
let active_ask_orders = own_book.asks_as_map(Some(&accepted_statuses), None, None);
assert_eq!(active_bid_orders.len(), 1, "Expected 1 active bid order");
assert!(
active_bid_orders.contains_key(&bid_price),
"Expected active bid order at price 1.05"
);
assert_eq!(active_ask_orders.len(), 1, "Expected 1 active ask order");
assert!(
active_ask_orders.contains_key(&ask_price),
"Expected active ask order at price 1.04"
);
}
#[rstest]
fn test_own_book_with_contingent_orders() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: false,
snapshot_orders: false,
snapshot_positions: false,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let entry_order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.00").unwrap()) .build();
let tp_order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-002-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.10").unwrap()) .build();
let sl_order = OrderTestBuilder::new(OrderType::StopMarket)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-003-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from_str("0.90").unwrap()) .build();
let submit_entry_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: entry_order.client_order_id(),
order_init: entry_order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
let submit_tp_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: tp_order.client_order_id(),
order_init: tp_order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
let submit_sl_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: sl_order.client_order_id(),
order_init: sl_order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine
.cache()
.borrow_mut()
.add_order(
entry_order.clone(),
None,
Some(ClientId::from("STUB")),
true,
)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(tp_order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(sl_order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine.execute(TradingCommand::SubmitOrder(submit_entry_order));
execution_engine.execute(TradingCommand::SubmitOrder(submit_tp_order));
let entry_submitted_event = TestOrderEventStubs::submitted(&entry_order, account_id);
execution_engine.process(&entry_submitted_event);
let entry_accepted_event =
TestOrderEventStubs::accepted(&entry_order, account_id, VenueOrderId::from("1"));
execution_engine.process(&entry_accepted_event);
let tp_submitted_event = TestOrderEventStubs::submitted(&tp_order, account_id);
execution_engine.process(&tp_submitted_event);
let tp_accepted_event =
TestOrderEventStubs::accepted(&tp_order, account_id, VenueOrderId::from("2"));
execution_engine.process(&tp_accepted_event);
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
assert!(
own_book.update_count > 1,
"Own order book should have been updated"
);
let bids = own_book.bids_as_map(None, None, None);
assert_eq!(bids.len(), 1, "Expected 1 bid order in own book");
let bid_price = dec!(1.00);
assert!(bids.contains_key(&bid_price), "Expected bid at price 1.00");
assert_eq!(
bids[&bid_price].len(),
1,
"Expected 1 order at bid price 1.00"
);
let asks = own_book.asks_as_map(None, None, None);
assert_eq!(asks.len(), 1, "Expected 1 ask order in own book");
let ask_price = dec!(1.10);
assert!(asks.contains_key(&ask_price), "Expected ask at price 1.10");
assert_eq!(
asks[&ask_price].len(),
1,
"Expected 1 order at ask price 1.10"
);
drop(cache);
let cached_entry_order = {
let cache = execution_engine.cache().borrow();
cache
.order(&entry_order.client_order_id())
.expect("Entry order should exist in cache")
.clone()
};
let entry_filled_event = TestOrderEventStubs::filled(
&cached_entry_order,
&InstrumentAny::CurrencyPair(instrument.clone()),
None, None, None, None, None, None, None, None, );
execution_engine.process(&entry_filled_event);
execution_engine.execute(TradingCommand::SubmitOrder(submit_sl_order));
let sl_submitted_event = TestOrderEventStubs::submitted(&sl_order, account_id);
execution_engine.process(&sl_submitted_event);
let sl_accepted_event =
TestOrderEventStubs::accepted(&sl_order, account_id, VenueOrderId::from("3"));
execution_engine.process(&sl_accepted_event);
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
let bids = own_book.bids_as_map(None, None, None);
assert_eq!(
bids.len(),
0,
"Expected 0 bid orders in own book after entry fill"
);
let asks = own_book.asks_as_map(None, None, None);
assert_eq!(asks.len(), 1, "Expected 1 ask order in own book");
let tp_price = dec!(1.10);
assert!(
asks.contains_key(&tp_price),
"Expected TP order at price 1.10"
);
let entry_position_id = cache.position_id(&entry_order.client_order_id());
let tp_position_id = cache.position_id(&tp_order.client_order_id());
let sl_position_id = cache.position_id(&sl_order.client_order_id());
if let (Some(entry_pos_id), Some(tp_pos_id), Some(sl_pos_id)) =
(entry_position_id, tp_position_id, sl_position_id)
{
assert_eq!(
entry_pos_id, tp_pos_id,
"TP order should have same position ID as entry order"
);
assert_eq!(
entry_pos_id, sl_pos_id,
"SL order should have same position ID as entry order"
);
}
}
#[rstest]
#[case(OrderStatus::Initialized, "1.00", vec![], true)]
#[case(OrderStatus::Submitted, "1.01", vec![OrderStatus::Submitted], true)]
#[case(OrderStatus::Accepted, "1.02", vec![OrderStatus::Submitted, OrderStatus::Accepted], true)]
#[case(OrderStatus::PartiallyFilled, "1.03", vec![OrderStatus::Submitted, OrderStatus::Accepted, OrderStatus::PartiallyFilled], true)]
#[case(OrderStatus::Filled, "1.04", vec![OrderStatus::Submitted, OrderStatus::Accepted, OrderStatus::Filled], false)]
#[case(OrderStatus::Canceled, "1.05", vec![OrderStatus::Submitted, OrderStatus::Accepted, OrderStatus::Canceled], false)]
fn test_own_book_order_status_filtering_parameterized(
#[case] final_status: OrderStatus,
#[case] price_str: &str,
#[case] process_steps: Vec<OrderStatus>,
#[case] expected_in_book: bool,
) {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: false,
snapshot_orders: false,
snapshot_positions: false,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str(price_str).unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
for step in process_steps {
match step {
OrderStatus::Submitted => {
let event = TestOrderEventStubs::submitted(&order, account_id);
execution_engine.process(&event);
}
OrderStatus::Accepted => {
let event =
TestOrderEventStubs::accepted(&order, account_id, VenueOrderId::from("1"));
execution_engine.process(&event);
}
OrderStatus::PartiallyFilled => {
let cached_order = {
let cache = execution_engine.cache().borrow();
cache
.order(&order.client_order_id())
.expect("Order should exist in cache")
.clone()
};
let event = TestOrderEventStubs::filled(
&cached_order,
&InstrumentAny::CurrencyPair(instrument.clone()),
None, None, None, Some(Quantity::from(50_000)), None, None, None, None, );
execution_engine.process(&event);
}
OrderStatus::Filled => {
let cached_order = {
let cache = execution_engine.cache().borrow();
cache
.order(&order.client_order_id())
.expect("Order should exist in cache")
.clone()
};
let event = TestOrderEventStubs::filled(
&cached_order,
&InstrumentAny::CurrencyPair(instrument.clone()),
None, None, None, None, None, None, None, None, );
execution_engine.process(&event);
}
OrderStatus::Canceled => {
let event = TestOrderEventStubs::canceled(
&order,
account_id,
Some(VenueOrderId::from("1")),
);
execution_engine.process(&event);
}
_ => {} }
}
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
let price_decimal = Decimal::from_str(price_str).unwrap();
if expected_in_book {
let bids = own_book.bids_as_map(None, None, None);
assert!(!bids.is_empty(), "Expected orders in own book");
assert!(
bids.contains_key(&price_decimal),
"Expected order at price {price_str}"
);
let status_filter: AHashSet<OrderStatus> = AHashSet::from_iter([final_status]);
let filtered_bids = own_book.bids_as_map(Some(&status_filter), None, None);
assert!(
filtered_bids.contains_key(&price_decimal),
"Expected order at price {price_str} with status {final_status:?}"
);
} else {
let bids = own_book.bids_as_map(None, None, None);
if bids.contains_key(&price_decimal) {
assert_eq!(
bids[&price_decimal].len(),
0,
"Expected no orders at price {} after status {}",
price_str,
final_status as u8
);
}
}
}
#[rstest]
fn test_own_book_combined_status_filtering() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: false,
snapshot_orders: false,
snapshot_positions: false,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let initialized_order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.00").unwrap())
.build();
let submitted_order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-002-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.01").unwrap())
.build();
let accepted_order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-003-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.02").unwrap())
.build();
let partially_filled_order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-004-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str("1.03").unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(
initialized_order.clone(),
None,
Some(ClientId::from("STUB")),
true,
)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(
submitted_order.clone(),
None,
Some(ClientId::from("STUB")),
true,
)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(
accepted_order.clone(),
None,
Some(ClientId::from("STUB")),
true,
)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(
partially_filled_order.clone(),
None,
Some(ClientId::from("STUB")),
true,
)
.unwrap();
let submit_initialized = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: initialized_order.client_order_id(),
order_init: initialized_order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_initialized));
let submit_submitted = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: submitted_order.client_order_id(),
order_init: submitted_order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_submitted));
let submitted_event = TestOrderEventStubs::submitted(&submitted_order, account_id);
execution_engine.process(&submitted_event);
let submit_accepted = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: accepted_order.client_order_id(),
order_init: accepted_order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_accepted));
let accepted_submitted_event = TestOrderEventStubs::submitted(&accepted_order, account_id);
execution_engine.process(&accepted_submitted_event);
let accepted_accepted_event =
TestOrderEventStubs::accepted(&accepted_order, account_id, VenueOrderId::from("V-003"));
execution_engine.process(&accepted_accepted_event);
let submit_partial = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: partially_filled_order.client_order_id(),
order_init: partially_filled_order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_partial));
let partial_submitted_event =
TestOrderEventStubs::submitted(&partially_filled_order, account_id);
execution_engine.process(&partial_submitted_event);
let partial_accepted_event = TestOrderEventStubs::accepted(
&partially_filled_order,
account_id,
VenueOrderId::from("V-004"),
);
execution_engine.process(&partial_accepted_event);
let cached_partial_order = {
let cache = execution_engine.cache().borrow();
cache
.order(&partially_filled_order.client_order_id())
.expect("Order should exist in cache")
.clone()
};
let partial_filled_event = TestOrderEventStubs::filled(
&cached_partial_order,
&InstrumentAny::CurrencyPair(instrument.clone()),
None, None, None, Some(Quantity::from(50_000)), None, None, None, None, );
execution_engine.process(&partial_filled_event);
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
let early_statuses: AHashSet<OrderStatus> =
AHashSet::from_iter([OrderStatus::Initialized, OrderStatus::Submitted]);
let early_orders = own_book.bids_as_map(Some(&early_statuses), None, None);
let early_order_count: usize = early_orders.values().map(std::vec::Vec::len).sum();
assert_eq!(
early_order_count, 2,
"Expected 2 orders with INITIALIZED or SUBMITTED status"
);
let price_100 = dec!(1.00);
let price_101 = dec!(1.01);
assert!(
early_orders.contains_key(&price_100),
"Expected order at price 1.00 in early statuses"
);
assert!(
early_orders.contains_key(&price_101),
"Expected order at price 1.01 in early statuses"
);
let active_statuses: AHashSet<OrderStatus> =
AHashSet::from_iter([OrderStatus::Accepted, OrderStatus::PartiallyFilled]);
let active_orders = own_book.bids_as_map(Some(&active_statuses), None, None);
let active_order_count: usize = active_orders.values().map(std::vec::Vec::len).sum();
assert_eq!(
active_order_count, 2,
"Expected 2 orders with ACCEPTED or PARTIALLY_FILLED status"
);
let price_102 = dec!(1.02);
let price_103 = dec!(1.03);
assert!(
active_orders.contains_key(&price_102),
"Expected order at price 1.02 in active statuses"
);
assert!(
active_orders.contains_key(&price_103),
"Expected order at price 1.03 in active statuses"
);
let all_orders = own_book.bids_as_map(None, None, None);
let all_order_count: usize = all_orders.values().map(std::vec::Vec::len).sum();
assert_eq!(all_order_count, 4, "Expected 4 total orders in own book");
assert!(
all_orders.contains_key(&price_100),
"Expected order at price 1.00 in all orders"
);
assert!(
all_orders.contains_key(&price_101),
"Expected order at price 1.01 in all orders"
);
assert!(
all_orders.contains_key(&price_102),
"Expected order at price 1.02 in all orders"
);
assert!(
all_orders.contains_key(&price_103),
"Expected order at price 1.03 in all orders"
);
}
#[rstest]
fn test_own_book_status_integrity_during_transitions() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
debug: false,
snapshot_orders: false,
snapshot_positions: false,
manage_own_order_books: true, ..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let prices = ["1.00", "1.01", "1.02"];
let mut orders = Vec::new();
for (i, price) in prices.iter().enumerate() {
let order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from(format!(
"O-20240101-000000-001-00{}-1",
i + 1
)))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from_str(price).unwrap())
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
position_id: None,
params: None,
client_id: Some(ClientId::from("STUB")),
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
let submitted_event = TestOrderEventStubs::submitted(&order, account_id);
execution_engine.process(&submitted_event);
let accepted_event = TestOrderEventStubs::accepted(
&order,
account_id,
VenueOrderId::from(format!("V-00{}", i + 1)),
);
execution_engine.process(&accepted_event);
orders.push(order);
}
{
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
let accepted_statuses: AHashSet<OrderStatus> = AHashSet::from_iter([OrderStatus::Accepted]);
let accepted_orders = own_book.bids_as_map(Some(&accepted_statuses), None, None);
assert_eq!(accepted_orders.len(), 3, "Expected 3 accepted orders");
for (price, order) in prices.iter().zip(&orders) {
let price_decimal = Decimal::from_str(price).unwrap();
assert!(
accepted_orders.contains_key(&price_decimal),
"Expected order at price {price}"
);
let orders_at_price = &accepted_orders[&price_decimal];
assert!(
orders_at_price
.iter()
.any(|o| o.client_order_id == order.client_order_id()),
"Expected order {} at price {}",
order.client_order_id(),
price
);
}
}
let cached_order_1 = {
let cache = execution_engine.cache().borrow();
cache
.order(&orders[1].client_order_id())
.expect("Order should exist in cache")
.clone()
};
let partial_fill_event = TestOrderEventStubs::filled(
&cached_order_1,
&InstrumentAny::CurrencyPair(instrument.clone()),
None, Some(PositionId::new("1")), None, Some(Quantity::from(50_000)), None, None, None, None, );
execution_engine.process(&partial_fill_event);
{
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
let partially_filled_statuses: AHashSet<OrderStatus> =
AHashSet::from_iter([OrderStatus::PartiallyFilled]);
let partially_filled_orders =
own_book.bids_as_map(Some(&partially_filled_statuses), None, None);
assert_eq!(
partially_filled_orders.len(),
1,
"Expected 1 partially filled order"
);
let price_101 = dec!(1.01);
assert!(
partially_filled_orders.contains_key(&price_101),
"Expected partially filled order at price 1.01"
);
let accepted_statuses: AHashSet<OrderStatus> = AHashSet::from_iter([OrderStatus::Accepted]);
let accepted_after_partial = own_book.bids_as_map(Some(&accepted_statuses), None, None);
assert_eq!(
accepted_after_partial.len(),
2,
"Expected 2 accepted orders after partial fill"
);
assert!(
!accepted_after_partial.contains_key(&price_101),
"Order at 1.01 should not be in accepted status"
);
}
let cancel_event =
TestOrderEventStubs::canceled(&orders[2], account_id, Some(VenueOrderId::from("V-003")));
execution_engine.process(&cancel_event);
{
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
let canceled_statuses: AHashSet<OrderStatus> = AHashSet::from_iter([OrderStatus::Canceled]);
let canceled_orders = own_book.bids_as_map(Some(&canceled_statuses), None, None);
assert_eq!(
canceled_orders.len(),
0,
"CANCELED orders should not appear in the book"
);
let accepted_statuses: AHashSet<OrderStatus> = AHashSet::from_iter([OrderStatus::Accepted]);
let accepted_after_cancel = own_book.bids_as_map(Some(&accepted_statuses), None, None);
assert_eq!(
accepted_after_cancel.len(),
1,
"Expected 1 accepted order after cancellation"
);
let price_102 = dec!(1.02);
assert!(
!accepted_after_cancel.contains_key(&price_102),
"Canceled order should not be in accepted status"
);
}
let cached_order_0 = {
let cache = execution_engine.cache().borrow();
cache
.order(&orders[0].client_order_id())
.expect("Order should exist in cache")
.clone()
};
let first_partial_fill = TestOrderEventStubs::filled(
&cached_order_0,
&InstrumentAny::CurrencyPair(instrument.clone()),
Some(TradeId::new("001")), None, None, Some(Quantity::from(50_000)), None, None, None, None, );
execution_engine.process(&first_partial_fill);
{
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
let partially_filled_statuses: AHashSet<OrderStatus> =
AHashSet::from_iter([OrderStatus::PartiallyFilled]);
let partially_after_first =
own_book.bids_as_map(Some(&partially_filled_statuses), None, None);
assert_eq!(
partially_after_first.len(),
2,
"Expected 2 partially filled orders"
);
let price_100 = dec!(1.00);
assert!(
partially_after_first.contains_key(&price_100),
"Expected partially filled order at price 1.00"
);
}
let cached_order_0_updated = {
let cache = execution_engine.cache().borrow();
cache
.order(&orders[0].client_order_id())
.expect("Order should exist in cache")
.clone()
};
let complete_fill = TestOrderEventStubs::filled(
&cached_order_0_updated,
&InstrumentAny::CurrencyPair(instrument.clone()),
Some(TradeId::new("002")), None, None, Some(Quantity::from(50_000)), None, None, None, None, );
execution_engine.process(&complete_fill);
{
let cache = execution_engine.cache().borrow();
let own_book = cache
.own_order_book(&instrument.id)
.expect("Own order book should exist");
let partially_filled_statuses: AHashSet<OrderStatus> =
AHashSet::from_iter([OrderStatus::PartiallyFilled]);
let partially_after_complete =
own_book.bids_as_map(Some(&partially_filled_statuses), None, None);
assert_eq!(
partially_after_complete.len(),
1,
"Expected 1 partially filled order after complete fill"
);
let order_0_cached = cache
.order(&orders[0].client_order_id())
.expect("Order 0 should exist");
let order_1_cached = cache
.order(&orders[1].client_order_id())
.expect("Order 1 should exist");
let order_2_cached = cache
.order(&orders[2].client_order_id())
.expect("Order 2 should exist");
assert_eq!(
order_0_cached.status(),
OrderStatus::Filled,
"Order 0 should be FILLED"
);
assert_eq!(
order_1_cached.status(),
OrderStatus::PartiallyFilled,
"Order 1 should be PARTIALLY_FILLED"
);
assert_eq!(
order_2_cached.status(),
OrderStatus::Canceled,
"Order 2 should be CANCELED"
);
let all_orders = own_book.bids_as_map(None, None, None);
let price_101 = dec!(1.01);
assert!(
all_orders.contains_key(&price_101),
"Order at price 1.01 should exist in own book"
);
let filled_statuses: AHashSet<OrderStatus> = AHashSet::from_iter([OrderStatus::Filled]);
let filled_orders = own_book.bids_as_map(Some(&filled_statuses), None, None);
assert_eq!(
filled_orders.len(),
0,
"FILLED orders should not appear in the own book"
);
}
#[rstest]
fn test_get_external_client_ids_when_none_configured(execution_engine: ExecutionEngine) {
let external_ids = execution_engine.get_external_client_ids();
assert!(external_ids.is_empty());
}
#[rstest]
fn test_get_external_client_ids_returns_configured_ids() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
external_clients: Some(vec![
ClientId::from("EXTERNAL-1"),
ClientId::from("EXTERNAL-2"),
]),
..Default::default()
};
let engine = ExecutionEngine::new(clock, cache, Some(config));
let external_ids = engine.get_external_client_ids();
assert_eq!(external_ids.len(), 2);
assert!(external_ids.contains(&ClientId::from("EXTERNAL-1")));
assert!(external_ids.contains(&ClientId::from("EXTERNAL-2")));
}
#[rstest]
fn test_get_external_order_claim_when_none_registered(execution_engine: ExecutionEngine) {
let instrument_id = InstrumentId::from("EUR/USD.SIM");
let claim = execution_engine.get_external_order_claim(&instrument_id);
assert!(claim.is_none());
}
#[rstest]
fn test_register_and_get_external_order_claim(mut execution_engine: ExecutionEngine) {
let strategy_id = StrategyId::from("TEST-001");
let instrument_id = InstrumentId::from("EUR/USD.SIM");
let mut claims = HashSet::new();
claims.insert(instrument_id);
execution_engine
.register_external_order_claims(strategy_id, &claims)
.unwrap();
let claim = execution_engine.get_external_order_claim(&instrument_id);
assert_eq!(claim, Some(strategy_id));
}
#[rstest]
fn test_register_external_order_claims_duplicate_fails(mut execution_engine: ExecutionEngine) {
let strategy_id_1 = StrategyId::from("TEST-001");
let strategy_id_2 = StrategyId::from("TEST-002");
let instrument_id = InstrumentId::from("EUR/USD.SIM");
let mut claims = HashSet::new();
claims.insert(instrument_id);
execution_engine
.register_external_order_claims(strategy_id_1, &claims)
.unwrap();
let result = execution_engine.register_external_order_claims(strategy_id_2, &claims);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("already exists for")
);
}
#[rstest]
fn test_register_external_order_claims_is_atomic(mut execution_engine: ExecutionEngine) {
let strategy_id_1 = StrategyId::from("TEST-001");
let strategy_id_2 = StrategyId::from("TEST-002");
let instrument_id_1 = InstrumentId::from("EUR/USD.SIM");
let instrument_id_2 = InstrumentId::from("GBP/USD.SIM");
let instrument_id_3 = InstrumentId::from("AUD/USD.SIM");
let mut initial_claims = HashSet::new();
initial_claims.insert(instrument_id_2);
execution_engine
.register_external_order_claims(strategy_id_1, &initial_claims)
.unwrap();
let mut conflicting_claims = HashSet::new();
conflicting_claims.insert(instrument_id_1);
conflicting_claims.insert(instrument_id_2); conflicting_claims.insert(instrument_id_3);
let result =
execution_engine.register_external_order_claims(strategy_id_2, &conflicting_claims);
assert!(result.is_err());
assert_eq!(
execution_engine.get_external_order_claim(&instrument_id_1),
None
);
assert_eq!(
execution_engine.get_external_order_claim(&instrument_id_2),
Some(strategy_id_1)
);
assert_eq!(
execution_engine.get_external_order_claim(&instrument_id_3),
None
);
}
#[rstest]
fn test_get_external_order_claims_instruments(mut execution_engine: ExecutionEngine) {
let strategy_id = StrategyId::from("TEST-001");
let instrument_id_1 = InstrumentId::from("EUR/USD.SIM");
let instrument_id_2 = InstrumentId::from("GBP/USD.SIM");
let mut claims = HashSet::new();
claims.insert(instrument_id_1);
claims.insert(instrument_id_2);
execution_engine
.register_external_order_claims(strategy_id, &claims)
.unwrap();
let instruments = execution_engine.get_external_order_claims_instruments();
assert_eq!(instruments.len(), 2);
assert!(instruments.contains(&instrument_id_1));
assert!(instruments.contains(&instrument_id_2));
}
#[rstest]
fn test_register_oms_type_for_strategy(mut execution_engine: ExecutionEngine) {
let strategy_id = StrategyId::from("TEST-001");
execution_engine.register_oms_type(strategy_id, OmsType::Hedging);
}
#[rstest]
fn test_register_oms_type_allows_override(mut execution_engine: ExecutionEngine) {
let strategy_id = StrategyId::from("TEST-001");
execution_engine.register_oms_type(strategy_id, OmsType::Hedging);
execution_engine.register_oms_type(strategy_id, OmsType::Netting);
}
#[rstest]
fn test_get_clients_for_orders_empty_list(execution_engine: ExecutionEngine) {
let orders: Vec<OrderAny> = vec![];
let clients = execution_engine.get_clients_for_orders(&orders);
assert!(clients.is_empty());
}
#[rstest]
fn test_get_clients_for_orders_with_default_client(
mut execution_engine: ExecutionEngine,
stub_client: StubExecutionClient,
) {
let client_id = stub_client.client_id();
execution_engine.register_default_client(Box::new(stub_client));
let instrument = InstrumentAny::CurrencyPair(audusd_sim());
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from("100"))
.build();
let clients = execution_engine.get_clients_for_orders(&[order]);
assert_eq!(clients.len(), 1);
assert_eq!(clients[0].client_id(), client_id);
}
#[rstest]
fn test_set_manage_own_order_books(mut execution_engine: ExecutionEngine) {
assert!(!execution_engine.config().manage_own_order_books);
execution_engine.set_manage_own_order_books(true);
assert!(execution_engine.config().manage_own_order_books);
execution_engine.set_manage_own_order_books(false);
assert!(!execution_engine.config().manage_own_order_books);
}
#[rstest]
fn test_netting_flip_creates_snapshot(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order1 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order1.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let position_id = PositionId::from("P-1");
execution_engine.process(&TestOrderEventStubs::submitted(
&order1,
AccountId::test_default(),
));
execution_engine.process(&TestOrderEventStubs::accepted(
&order1,
AccountId::test_default(),
VenueOrderId::from("V-1"),
));
execution_engine.process(&TestOrderEventStubs::filled(
&order1,
&instrument.clone().into(),
Some(TradeId::new("T-1")),
Some(position_id),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
));
{
let cache = execution_engine.cache().borrow();
assert!(
cache.position_snapshot_bytes(&position_id).is_none(),
"Should have no snapshots before flip"
);
}
let order2 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-2"))
.side(OrderSide::Sell)
.quantity(Quantity::from(150_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order2.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine.process(&TestOrderEventStubs::submitted(
&order2,
AccountId::test_default(),
));
execution_engine.process(&TestOrderEventStubs::accepted(
&order2,
AccountId::test_default(),
VenueOrderId::from("V-2"),
));
execution_engine.process(&TestOrderEventStubs::filled(
&order2,
&instrument.into(),
Some(TradeId::new("T-2")),
Some(position_id),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
));
let cache = execution_engine.cache().borrow();
assert!(
cache.position_snapshot_bytes(&position_id).is_some(),
"Should have snapshot after NETTING flip"
);
let position = cache.position(&position_id).unwrap();
assert_eq!(position.side, PositionSide::Short);
assert!(!position.is_closed());
assert_eq!(position.quantity, Quantity::from(50_000));
assert!(cache.is_position_open(&position_id));
assert!(!cache.is_position_closed(&position_id));
}
#[rstest]
fn test_hedging_flip_no_snapshot(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Hedging,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let order1 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order1.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let position_id1 = PositionId::from("P-19700101-000000-000-001-1");
execution_engine.process(&TestOrderEventStubs::submitted(
&order1,
AccountId::test_default(),
));
execution_engine.process(&TestOrderEventStubs::accepted(
&order1,
AccountId::test_default(),
VenueOrderId::from("V-1"),
));
execution_engine.process(&TestOrderEventStubs::filled(
&order1,
&instrument.clone().into(),
Some(TradeId::new("T-1")),
Some(position_id1),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
));
let order2 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-2"))
.side(OrderSide::Sell)
.quantity(Quantity::from(150_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order2.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine.process(&TestOrderEventStubs::submitted(
&order2,
AccountId::test_default(),
));
execution_engine.process(&TestOrderEventStubs::accepted(
&order2,
AccountId::test_default(),
VenueOrderId::from("V-2"),
));
execution_engine.process(&TestOrderEventStubs::filled(
&order2,
&instrument.into(),
Some(TradeId::new("T-2")),
Some(position_id1),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
));
let cache = execution_engine.cache().borrow();
assert!(
cache.position_snapshot_bytes(&position_id1).is_none(),
"HEDGING mode should not create snapshots (new position ID used)"
);
let position1 = cache.position(&position_id1).unwrap();
assert!(position1.is_closed());
let positions = cache.positions_open(None, None, None, None, None);
assert_eq!(positions.len(), 1, "Should have 1 open position");
assert_ne!(
positions[0].id, position_id1,
"Flipped position should have new ID"
);
assert_eq!(positions[0].side, PositionSide::Short);
assert_eq!(positions[0].quantity, Quantity::from(50_000));
}
#[rstest]
fn test_netting_reopen_creates_snapshot(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let account = CashAccount::default();
execution_engine
.cache()
.borrow_mut()
.add_account(account.into())
.unwrap();
let position_id = PositionId::from("P-1");
let order1 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order1.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine.process(&TestOrderEventStubs::submitted(
&order1,
AccountId::test_default(),
));
execution_engine.process(&TestOrderEventStubs::accepted(
&order1,
AccountId::test_default(),
VenueOrderId::from("V-1"),
));
execution_engine.process(&TestOrderEventStubs::filled(
&order1,
&instrument.clone().into(),
Some(TradeId::new("T-1")),
Some(position_id),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
));
let order2 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-2"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order2.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine.process(&TestOrderEventStubs::submitted(
&order2,
AccountId::test_default(),
));
execution_engine.process(&TestOrderEventStubs::accepted(
&order2,
AccountId::test_default(),
VenueOrderId::from("V-2"),
));
execution_engine.process(&TestOrderEventStubs::filled(
&order2,
&instrument.clone().into(),
Some(TradeId::new("T-2")),
Some(position_id),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
));
{
let cache = execution_engine.cache().borrow();
let position = cache.position(&position_id).unwrap();
assert!(position.is_closed());
assert!(cache.is_position_closed(&position_id));
}
let order3 = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-3"))
.side(OrderSide::Buy)
.quantity(Quantity::from(50_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order3.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine.process(&TestOrderEventStubs::submitted(
&order3,
AccountId::test_default(),
));
execution_engine.process(&TestOrderEventStubs::accepted(
&order3,
AccountId::test_default(),
VenueOrderId::from("V-3"),
));
execution_engine.process(&TestOrderEventStubs::filled(
&order3,
&instrument.into(),
Some(TradeId::new("T-3")),
Some(position_id),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
));
let cache = execution_engine.cache().borrow();
assert!(
cache.position_snapshot_bytes(&position_id).is_some(),
"Should have snapshot after NETTING reopen"
);
let position = cache.position(&position_id).unwrap();
assert_eq!(position.side, PositionSide::Long);
assert!(!position.is_closed());
assert_eq!(position.quantity, Quantity::from(50_000));
assert!(cache.is_position_open(&position_id));
assert!(!cache.is_position_closed(&position_id));
}
}
fn setup_netting_snapshot_engine(
execution_engine: &mut ExecutionEngine,
instrument: &CurrencyPair,
) {
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
AccountId::test_default(),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_account(CashAccount::default().into())
.unwrap();
}
#[expect(clippy::too_many_arguments)]
fn process_filled_order(
execution_engine: &mut ExecutionEngine,
trader_id: TraderId,
strategy_id: StrategyId,
instrument: &CurrencyPair,
client_order_id: &str,
venue_order_id: &str,
trade_id: &str,
side: OrderSide,
quantity: u64,
position_id: PositionId,
) {
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from(client_order_id))
.side(side)
.quantity(Quantity::from(quantity))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine.process(&TestOrderEventStubs::submitted(
&order,
AccountId::test_default(),
));
execution_engine.process(&TestOrderEventStubs::accepted(
&order,
AccountId::test_default(),
VenueOrderId::from(venue_order_id),
));
let instrument_any: InstrumentAny = instrument.clone().into();
execution_engine.process(&TestOrderEventStubs::filled(
&order,
&instrument_any,
Some(TradeId::new(trade_id)),
Some(position_id),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
));
}
#[rstest]
fn test_snapshot_anchorer_runs_on_netting_flip(mut execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let position_id = PositionId::new(format!("{}-{strategy_id}", instrument.id));
setup_netting_snapshot_engine(&mut execution_engine, &instrument);
process_filled_order(
&mut execution_engine,
trader_id,
strategy_id,
&instrument,
"O-ANCHOR-FLIP-1",
"V-ANCHOR-FLIP-1",
"T-ANCHOR-FLIP-1",
OrderSide::Buy,
100_000,
position_id,
);
let anchors = Rc::new(RefCell::new(Vec::<CacheSnapshotRef>::new()));
let anchors_for_cb = Rc::clone(&anchors);
execution_engine.set_snapshot_anchorer(Some(Rc::new(move |snapshot_ref| {
anchors_for_cb.borrow_mut().push(snapshot_ref);
Ok(())
})));
process_filled_order(
&mut execution_engine,
trader_id,
strategy_id,
&instrument,
"O-ANCHOR-FLIP-2",
"V-ANCHOR-FLIP-2",
"T-ANCHOR-FLIP-2",
OrderSide::Sell,
150_000,
position_id,
);
let cache = execution_engine.cache().borrow();
let frames = cache
.position_snapshot_bytes(&position_id)
.expect("position snapshot");
let anchors = anchors.borrow();
let position = cache.position(&position_id).expect("position");
assert_eq!(anchors.len(), 1);
assert_eq!(
anchors[0].blob_ref,
format!("cache://position-snapshots/{}/0", position_id.as_str()),
);
assert_eq!(anchors[0].blob.as_ref(), frames[0].as_slice());
assert_eq!(position.side, PositionSide::Short);
assert!(!position.is_closed());
}
#[rstest]
fn test_snapshot_anchorer_error_does_not_stop_netting_reopen(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
let position_id = PositionId::new(format!("{}-{strategy_id}", instrument.id));
setup_netting_snapshot_engine(&mut execution_engine, &instrument);
process_filled_order(
&mut execution_engine,
trader_id,
strategy_id,
&instrument,
"O-ANCHOR-REOPEN-1",
"V-ANCHOR-REOPEN-1",
"T-ANCHOR-REOPEN-1",
OrderSide::Buy,
100_000,
position_id,
);
process_filled_order(
&mut execution_engine,
trader_id,
strategy_id,
&instrument,
"O-ANCHOR-REOPEN-2",
"V-ANCHOR-REOPEN-2",
"T-ANCHOR-REOPEN-2",
OrderSide::Sell,
100_000,
position_id,
);
let anchors = Rc::new(RefCell::new(Vec::<CacheSnapshotRef>::new()));
let anchors_for_cb = Rc::clone(&anchors);
execution_engine.set_snapshot_anchorer(Some(Rc::new(move |snapshot_ref| {
anchors_for_cb.borrow_mut().push(snapshot_ref);
Err(anyhow::anyhow!("anchor write failed"))
})));
process_filled_order(
&mut execution_engine,
trader_id,
strategy_id,
&instrument,
"O-ANCHOR-REOPEN-3",
"V-ANCHOR-REOPEN-3",
"T-ANCHOR-REOPEN-3",
OrderSide::Buy,
50_000,
position_id,
);
let cache = execution_engine.cache().borrow();
let frames = cache
.position_snapshot_bytes(&position_id)
.expect("position snapshot");
let anchors = anchors.borrow();
let position = cache.position(&position_id).expect("position");
assert_eq!(anchors.len(), 1);
assert_eq!(
anchors[0].blob_ref,
format!("cache://position-snapshots/{}/0", position_id.as_str()),
);
assert_eq!(anchors[0].blob.as_ref(), frames[0].as_slice());
assert_eq!(position.side, PositionSide::Long);
assert!(!position.is_closed());
}
fn create_order_status_report(
client_order_id: Option<ClientOrderId>,
venue_order_id: VenueOrderId,
instrument_id: InstrumentId,
status: OrderStatus,
quantity: Quantity,
filled_qty: Quantity,
) -> OrderStatusReport {
OrderStatusReport::new(
AccountId::test_default(),
instrument_id,
client_order_id,
venue_order_id,
OrderSide::Buy,
OrderType::Limit,
TimeInForce::Gtc,
status,
quantity,
filled_qty,
UnixNanos::from(1_000_000),
UnixNanos::from(1_000_000),
UnixNanos::from(1_000_000),
None,
)
.with_price(Price::from("1.00000"))
}
#[rstest]
fn test_reconcile_order_status_report_order_not_in_cache(mut execution_engine: ExecutionEngine) {
let report = create_order_status_report(
Some(ClientOrderId::from("O-MISSING")),
VenueOrderId::from("V-001"),
audusd_sim().id(),
OrderStatus::Accepted,
Quantity::from(100_000),
Quantity::from(0),
);
execution_engine.reconcile_order_status_report(&report);
let cache = execution_engine.cache().borrow();
assert!(!cache.order_exists(&ClientOrderId::from("O-MISSING")));
}
#[rstest]
fn test_reconcile_order_status_report_generates_canceled_event(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
let client_order_id = order.client_order_id();
let venue_order_id = VenueOrderId::from("V-001");
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, None, true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let accepted = TestOrderEventStubs::accepted(&order, AccountId::test_default(), venue_order_id);
execution_engine.process(&accepted);
let report = create_order_status_report(
Some(client_order_id),
venue_order_id,
instrument.id(),
OrderStatus::Canceled,
Quantity::from(100_000),
Quantity::from(0),
);
execution_engine.reconcile_order_status_report(&report);
let cache = execution_engine.cache().borrow();
let order = cache.order(&client_order_id).unwrap();
assert_eq!(order.status(), OrderStatus::Canceled);
}
#[rstest]
fn test_reconcile_order_status_report_no_event_when_in_sync(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
let client_order_id = order.client_order_id();
let venue_order_id = VenueOrderId::from("V-001");
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, None, true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let accepted = TestOrderEventStubs::accepted(&order, AccountId::test_default(), venue_order_id);
execution_engine.process(&accepted);
let report = create_order_status_report(
Some(client_order_id),
venue_order_id,
instrument.id(),
OrderStatus::Accepted,
Quantity::from(100_000),
Quantity::from(0),
);
execution_engine.reconcile_order_status_report(&report);
let cache = execution_engine.cache().borrow();
let order = cache.order(&client_order_id).unwrap();
assert_eq!(order.status(), OrderStatus::Accepted);
}
#[rstest]
fn test_reconcile_order_status_report_finds_order_by_venue_order_id(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
let client_order_id = order.client_order_id();
let venue_order_id = VenueOrderId::from("V-001");
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, None, true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let accepted = TestOrderEventStubs::accepted(&order, AccountId::test_default(), venue_order_id);
execution_engine.process(&accepted);
let report = create_order_status_report(
None,
venue_order_id,
instrument.id(),
OrderStatus::Canceled,
Quantity::from(100_000),
Quantity::from(0),
);
execution_engine.reconcile_order_status_report(&report);
let cache = execution_engine.cache().borrow();
let order = cache.order(&client_order_id).unwrap();
assert_eq!(order.status(), OrderStatus::Canceled);
}
fn create_fill_report(
instrument_id: InstrumentId,
client_order_id: Option<ClientOrderId>,
venue_order_id: VenueOrderId,
trade_id: TradeId,
last_qty: Quantity,
last_px: Price,
) -> FillReport {
create_fill_report_with_account(
AccountId::test_default(),
instrument_id,
client_order_id,
venue_order_id,
trade_id,
last_qty,
last_px,
)
}
fn create_fill_report_with_account(
account_id: AccountId,
instrument_id: InstrumentId,
client_order_id: Option<ClientOrderId>,
venue_order_id: VenueOrderId,
trade_id: TradeId,
last_qty: Quantity,
last_px: Price,
) -> FillReport {
FillReport::new(
account_id,
instrument_id,
venue_order_id,
trade_id,
OrderSide::Buy,
last_qty,
last_px,
Money::new(0.10, Currency::USD()),
LiquiditySide::Taker,
client_order_id,
None,
UnixNanos::from(1_000_000),
UnixNanos::from(1_000_000),
None,
)
}
fn cash_account_for(account_id: AccountId) -> CashAccount {
let state = AccountState::new(
account_id,
AccountType::Cash,
vec![AccountBalance::new(
Money::from("1000000 USD"),
Money::from("0 USD"),
Money::from("1000000 USD"),
)],
vec![],
true,
UUID4::new(),
UnixNanos::default(),
UnixNanos::default(),
Some(Currency::USD()),
);
CashAccount::new(state, true, false)
}
#[rstest]
fn test_reconcile_fill_report_bootstraps_external_order(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
let client_order_id = ClientOrderId::from("O-MISSING");
let venue_order_id = VenueOrderId::from("V-001");
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let report = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
TradeId::from("T-001"),
Quantity::from(50_000),
Price::from("1.00000"),
);
execution_engine.reconcile_fill_report(&report);
let cache = execution_engine.cache().borrow();
let order = cache
.order(&client_order_id)
.expect("external order should be created from fill report");
assert_eq!(order.order_type(), OrderType::Market);
assert_eq!(order.quantity(), Quantity::from(50_000));
assert_eq!(order.filled_qty(), Quantity::from(50_000));
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.trade_ids().len(), 1);
assert_eq!(*order.trade_ids()[0], TradeId::from("T-001"));
assert_eq!(order.time_in_force(), TimeInForce::Ioc);
assert!(order.is_reduce_only());
assert_eq!(order.strategy_id(), StrategyId::from("EXTERNAL"));
}
#[rstest]
fn test_reconcile_fill_report_bootstrap_uses_claimed_strategy(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
let client_order_id = ClientOrderId::from("O-CLAIMED");
let venue_order_id = VenueOrderId::from("V-CLAIMED-001");
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let strategy_id = StrategyId::from("MyStrategy-001");
let mut instruments = HashSet::new();
instruments.insert(instrument.id());
execution_engine
.register_external_order_claims(strategy_id, &instruments)
.unwrap();
let report = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
TradeId::from("T-CLAIMED-001"),
Quantity::from(50_000),
Price::from("1.00000"),
);
execution_engine.reconcile_fill_report(&report);
let cache = execution_engine.cache().borrow();
let order = cache
.order(&client_order_id)
.expect("external order should be created from fill report");
assert_eq!(order.strategy_id(), strategy_id);
}
#[rstest]
fn test_reconcile_fill_report_skips_when_instrument_missing(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
let client_order_id = ClientOrderId::from("O-MISSING");
let report = create_fill_report(
instrument.id(),
Some(client_order_id),
VenueOrderId::from("V-001"),
TradeId::from("T-001"),
Quantity::from(50_000),
Price::from("1.00000"),
);
execution_engine.reconcile_fill_report(&report);
let cache = execution_engine.cache().borrow();
assert!(!cache.order_exists(&client_order_id));
}
#[rstest]
fn test_reconcile_fill_report_applies_fill_event(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
let client_order_id = ClientOrderId::from("O-001");
let venue_order_id = VenueOrderId::from("V-001");
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.client_order_id(client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let accepted = TestOrderEventStubs::accepted(&order, AccountId::test_default(), venue_order_id);
execution_engine.process(&accepted);
let report = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
TradeId::from("T-001"),
Quantity::from(50_000),
Price::from("1.00000"),
);
execution_engine.reconcile_fill_report(&report);
let cache = execution_engine.cache().borrow();
let order = cache.order(&client_order_id).unwrap();
assert_eq!(order.filled_qty(), Quantity::from(50_000));
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
}
#[rstest]
fn test_reconcile_fill_report_finds_order_by_venue_order_id(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
let client_order_id = ClientOrderId::from("O-001");
let venue_order_id = VenueOrderId::from("V-001");
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.client_order_id(client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let accepted = TestOrderEventStubs::accepted(&order, AccountId::test_default(), venue_order_id);
execution_engine.process(&accepted);
let report = create_fill_report(
instrument.id(),
None,
venue_order_id,
TradeId::from("T-001"),
Quantity::from(100_000),
Price::from("1.00000"),
);
execution_engine.reconcile_fill_report(&report);
let cache = execution_engine.cache().borrow();
let order = cache.order(&client_order_id).unwrap();
assert_eq!(order.filled_qty(), Quantity::from(100_000));
assert_eq!(order.status(), OrderStatus::Filled);
}
#[rstest]
fn test_reconcile_fill_report_uses_report_account_for_position(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
let client_order_id = ClientOrderId::from("O-ACCOUNT-001");
let venue_order_id = VenueOrderId::from("V-ACCOUNT-001");
let report_account_id = AccountId::from("A-001");
let order_account_id = AccountId::from("B-001");
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_account(cash_account_for(report_account_id).into())
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_account(cash_account_for(order_account_id).into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.client_order_id(client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine.process(&TestOrderEventStubs::submitted(&order, order_account_id));
execution_engine.process(&TestOrderEventStubs::accepted(
&order,
order_account_id,
venue_order_id,
));
let report = create_fill_report_with_account(
report_account_id,
instrument.id(),
Some(client_order_id),
venue_order_id,
TradeId::from("T-ACCOUNT-001"),
Quantity::from(50_000),
Price::from("1.00000"),
);
execution_engine.reconcile_fill_report(&report);
let cache = execution_engine.cache().borrow();
let report_account_positions = cache.positions(
None,
Some(&instrument.id()),
None,
Some(&report_account_id),
None,
);
let order_account_positions = cache.positions(
None,
Some(&instrument.id()),
None,
Some(&order_account_id),
None,
);
assert_eq!(report_account_positions.len(), 1);
assert_eq!(report_account_positions[0].account_id, report_account_id);
assert_eq!(report_account_positions[0].instrument_id, instrument.id());
assert!(order_account_positions.is_empty());
}
#[rstest]
fn test_handle_fill_skips_duplicate_trade_id_already_on_position(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
let instrument_any = InstrumentAny::CurrencyPair(instrument.clone());
let account_id = AccountId::test_default();
let trade_id = TradeId::from("T-POS-DUP");
let order_1_id = ClientOrderId::from("O-POS-DUP-1");
let order_2_id = ClientOrderId::from("O-POS-DUP-2");
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument_any.clone())
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_account(CashAccount::default().into())
.unwrap();
let order_1 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.client_order_id(order_1_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let order_2 = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.client_order_id(order_2_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order_1.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(order_2.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
execution_engine.process(&TestOrderEventStubs::submitted(&order_1, account_id));
execution_engine.process(&TestOrderEventStubs::accepted(
&order_1,
account_id,
VenueOrderId::from("V-POS-DUP-1"),
));
execution_engine.process(&TestOrderEventStubs::submitted(&order_2, account_id));
execution_engine.process(&TestOrderEventStubs::accepted(
&order_2,
account_id,
VenueOrderId::from("V-POS-DUP-2"),
));
let order_1 = execution_engine
.cache()
.borrow()
.order(&order_1_id)
.unwrap()
.clone();
let fill_1 = TestOrderEventStubs::filled(
&order_1,
&instrument_any,
Some(trade_id),
None,
Some(Price::from("1.00000")),
Some(Quantity::from(50_000)),
Some(LiquiditySide::Taker),
Some(Money::from("0 USD")),
None,
Some(account_id),
);
execution_engine.process(&fill_1);
let order_2 = execution_engine
.cache()
.borrow()
.order(&order_2_id)
.unwrap()
.clone();
let fill_2 = TestOrderEventStubs::filled(
&order_2,
&instrument_any,
Some(trade_id),
None,
Some(Price::from("1.00000")),
Some(Quantity::from(50_000)),
Some(LiquiditySide::Taker),
Some(Money::from("0 USD")),
None,
Some(account_id),
);
execution_engine.process(&fill_2);
let cache = execution_engine.cache().borrow();
let order_2 = cache.order(&order_2_id).unwrap();
let positions = cache.positions(None, Some(&instrument.id()), None, Some(&account_id), None);
assert_eq!(order_2.status(), OrderStatus::Accepted);
assert_eq!(order_2.filled_qty(), Quantity::from(0));
assert_eq!(positions.len(), 1);
assert_eq!(positions[0].quantity, Quantity::from(50_000));
assert!(positions[0].trade_ids.contains(&trade_id));
}
fn create_position_report(
instrument_id: InstrumentId,
position_side: PositionSideSpecified,
quantity: Quantity,
venue_position_id: Option<PositionId>,
) -> PositionStatusReport {
PositionStatusReport::new(
AccountId::test_default(),
instrument_id,
position_side,
quantity,
UnixNanos::from(1_000_000),
UnixNanos::from(1_000_000),
None, venue_position_id,
None, )
}
#[rstest]
fn test_reconcile_position_report_netting_mode(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let report = create_position_report(
instrument.id(),
PositionSideSpecified::Long,
Quantity::from(100_000),
None,
);
execution_engine.reconcile_position_report(&report);
}
#[rstest]
fn test_reconcile_position_report_hedging_mode_position_not_found(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
let venue_position_id = PositionId::from("P-001");
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let report = create_position_report(
instrument.id(),
PositionSideSpecified::Long,
Quantity::from(100_000),
Some(venue_position_id),
);
execution_engine.reconcile_position_report(&report);
}
#[rstest]
fn test_reconcile_execution_mass_status_empty(mut execution_engine: ExecutionEngine) {
let mass_status = ExecutionMassStatus::new(
ClientId::from("SIM"),
AccountId::test_default(),
Venue::from("SIM"),
UnixNanos::from(1_000_000),
None,
);
execution_engine.reconcile_execution_mass_status(&mass_status);
assert_eq!(execution_engine.report_count(), 1);
execution_engine.reset();
execution_engine
.reconcile_execution_report(&ExecutionReport::MassStatus(Box::new(mass_status)));
assert_eq!(execution_engine.report_count(), 1);
}
#[rstest]
fn test_reconcile_execution_mass_status_with_order_reports(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
let client_order_id = ClientOrderId::from("O-001");
let venue_order_id = VenueOrderId::from("V-001");
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.client_order_id(client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let accepted = TestOrderEventStubs::accepted(&order, AccountId::test_default(), venue_order_id);
execution_engine.process(&accepted);
let order_report = create_order_status_report(
Some(client_order_id),
venue_order_id,
instrument.id(),
OrderStatus::Canceled,
Quantity::from(100_000),
Quantity::from(0),
);
let mut mass_status = ExecutionMassStatus::new(
ClientId::from("SIM"),
AccountId::test_default(),
Venue::from("SIM"),
UnixNanos::from(1_000_000),
None,
);
mass_status.add_order_reports(vec![order_report]);
execution_engine.reconcile_execution_mass_status(&mass_status);
let cache = execution_engine.cache().borrow();
let order = cache.order(&client_order_id).unwrap();
assert_eq!(order.status(), OrderStatus::Canceled);
}
#[rstest]
fn test_reconcile_execution_mass_status_publishes_skipped_external_fill(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let venue_order_id = VenueOrderId::from("V-EXT-FILL");
let order_report = create_order_status_report(
None, venue_order_id,
instrument.id(),
OrderStatus::Filled,
Quantity::from(100_000),
Quantity::from(100_000),
);
let fill_report = FillReport::new(
AccountId::test_default(),
instrument.id(),
venue_order_id,
TradeId::from("T-EXT-FILL"),
OrderSide::Buy,
Quantity::from(100_000),
Price::from("1.00000"),
Money::new(0.0, Currency::USD()),
LiquiditySide::Taker,
None,
None,
UnixNanos::from(1_000_000),
UnixNanos::from(1_000_000),
None,
);
let mut mass_status = ExecutionMassStatus::new(
ClientId::from("SIM"),
AccountId::test_default(),
Venue::from("SIM"),
UnixNanos::from(1_000_000),
None,
);
mass_status.add_order_reports(vec![order_report]);
mass_status.add_fill_reports(vec![fill_report.clone()]);
let raw_fill_topic = MessagingSwitchboard::reconciliation_raw_fill_report_topic();
let pattern: msgbus::MStr<msgbus::Pattern> = raw_fill_topic.into();
let (handler, saver) = get_any_saving_handler::<FillReport>(None);
msgbus::subscribe_any(pattern, handler.clone(), None);
execution_engine.reconcile_execution_mass_status(&mass_status);
msgbus::unsubscribe_any(pattern, &handler);
let captured = saver.get_messages();
assert_eq!(
captured.len(),
1,
"skipped external-order fill must still publish on the raw topic",
);
assert_eq!(captured[0], fill_report);
}
#[rstest]
fn test_reconcile_order_status_report_publishes_raw_topic(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
let client_order_id = ClientOrderId::from("O-RAW-STATUS");
let venue_order_id = VenueOrderId::from("V-RAW-STATUS");
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.client_order_id(client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let report = create_order_status_report(
Some(client_order_id),
venue_order_id,
instrument.id(),
OrderStatus::Accepted,
Quantity::from(100_000),
Quantity::from(0),
);
let topic = MessagingSwitchboard::reconciliation_raw_order_status_report_topic();
let pattern: msgbus::MStr<msgbus::Pattern> = topic.into();
let (handler, saver) = get_any_saving_handler::<OrderStatusReport>(None);
msgbus::subscribe_any(pattern, handler.clone(), None);
execution_engine.reconcile_order_status_report(&report);
msgbus::unsubscribe_any(pattern, &handler);
let captured = saver.get_messages();
assert_eq!(
captured.len(),
1,
"raw OrderStatusReport must be published once"
);
assert_eq!(captured[0], report);
}
#[rstest]
fn test_reconcile_fill_report_publishes_raw_topic(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
let client_order_id = ClientOrderId::from("O-RAW-FILL");
let venue_order_id = VenueOrderId::from("V-RAW-FILL");
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.client_order_id(client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let accepted = TestOrderEventStubs::accepted(&order, AccountId::test_default(), venue_order_id);
execution_engine.process(&accepted);
let report = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
TradeId::from("T-RAW-FILL"),
Quantity::from(50_000),
Price::from("1.00000"),
);
let topic = MessagingSwitchboard::reconciliation_raw_fill_report_topic();
let pattern: msgbus::MStr<msgbus::Pattern> = topic.into();
let (handler, saver) = get_any_saving_handler::<FillReport>(None);
msgbus::subscribe_any(pattern, handler.clone(), None);
execution_engine.reconcile_fill_report(&report);
msgbus::unsubscribe_any(pattern, &handler);
let captured = saver.get_messages();
assert_eq!(captured.len(), 1, "raw FillReport must be published once");
assert_eq!(captured[0], report);
}
#[rstest]
fn test_reconcile_order_with_fills_publishes_order_and_each_fill(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
let client_order_id = ClientOrderId::from("O-RAW-BUNDLE");
let venue_order_id = VenueOrderId::from("V-RAW-BUNDLE");
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.client_order_id(client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let accepted = TestOrderEventStubs::accepted(&order, AccountId::test_default(), venue_order_id);
execution_engine.process(&accepted);
let order_report = create_order_status_report(
Some(client_order_id),
venue_order_id,
instrument.id(),
OrderStatus::Filled,
Quantity::from(100_000),
Quantity::from(100_000),
);
let fill1 = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
TradeId::from("T-RAW-BUNDLE-1"),
Quantity::from(30_000),
Price::from("1.00000"),
);
let fill2 = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
TradeId::from("T-RAW-BUNDLE-2"),
Quantity::from(40_000),
Price::from("1.00001"),
);
let fill3 = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
TradeId::from("T-RAW-BUNDLE-3"),
Quantity::from(30_000),
Price::from("1.00002"),
);
let order_topic = MessagingSwitchboard::reconciliation_raw_order_status_report_topic();
let fill_topic = MessagingSwitchboard::reconciliation_raw_fill_report_topic();
let order_pattern: msgbus::MStr<msgbus::Pattern> = order_topic.into();
let fill_pattern: msgbus::MStr<msgbus::Pattern> = fill_topic.into();
let (order_handler, order_saver) = get_any_saving_handler::<OrderStatusReport>(None);
let (fill_handler, fill_saver) = get_any_saving_handler::<FillReport>(None);
msgbus::subscribe_any(order_pattern, order_handler.clone(), None);
msgbus::subscribe_any(fill_pattern, fill_handler.clone(), None);
execution_engine.reconcile_order_with_fills(
&order_report,
&[fill1.clone(), fill2.clone(), fill3.clone()],
);
msgbus::unsubscribe_any(order_pattern, &order_handler);
msgbus::unsubscribe_any(fill_pattern, &fill_handler);
let orders = order_saver.get_messages();
assert_eq!(orders.len(), 1, "exactly one OrderStatusReport published");
assert_eq!(orders[0], order_report);
let fills = fill_saver.get_messages();
assert_eq!(fills.len(), 3, "one publish per fill in the input slice");
assert_eq!(fills[0], fill1);
assert_eq!(fills[1], fill2);
assert_eq!(fills[2], fill3);
}
#[rstest]
fn test_reconcile_position_report_publishes_raw_topic(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let report = create_position_report(
instrument.id(),
PositionSideSpecified::Long,
Quantity::from(100_000),
None,
);
let topic = MessagingSwitchboard::reconciliation_raw_position_status_report_topic();
let pattern: msgbus::MStr<msgbus::Pattern> = topic.into();
let (handler, saver) = get_any_saving_handler::<PositionStatusReport>(None);
msgbus::subscribe_any(pattern, handler.clone(), None);
execution_engine.reconcile_position_report(&report);
msgbus::unsubscribe_any(pattern, &handler);
let captured = saver.get_messages();
assert_eq!(
captured.len(),
1,
"raw PositionStatusReport must be published once"
);
assert_eq!(captured[0], report);
}
#[rstest]
fn test_get_all_clients_empty(execution_engine: ExecutionEngine) {
let clients = execution_engine.get_all_clients();
assert!(clients.is_empty());
}
#[rstest]
fn test_get_all_clients_with_registered_client(
mut execution_engine: ExecutionEngine,
stub_client: StubExecutionClient,
) {
let client_id = stub_client.client_id();
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let clients = execution_engine.get_all_clients();
assert_eq!(clients.len(), 1);
assert_eq!(clients[0].client_id(), client_id);
}
#[rstest]
fn test_get_all_clients_includes_default_client(mut execution_engine: ExecutionEngine) {
let default_client = StubExecutionClient::new(
ClientId::from("DEFAULT"),
AccountId::from("DEFAULT-ACCOUNT"),
Venue::from("DEFAULT"),
OmsType::Netting,
None,
);
let default_id = default_client.client_id();
execution_engine.register_default_client(Box::new(default_client));
let clients = execution_engine.get_all_clients();
assert_eq!(clients.len(), 1);
assert_eq!(clients[0].client_id(), default_id);
}
#[rstest]
fn test_get_all_clients_with_registered_and_default(
mut execution_engine: ExecutionEngine,
stub_client: StubExecutionClient,
) {
let registered_id = stub_client.client_id();
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
let default_client = StubExecutionClient::new(
ClientId::from("DEFAULT"),
AccountId::from("DEFAULT-ACCOUNT"),
Venue::from("DEFAULT"),
OmsType::Netting,
None,
);
let default_id = default_client.client_id();
execution_engine.register_default_client(Box::new(default_client));
let clients = execution_engine.get_all_clients();
let client_ids: HashSet<ClientId> = clients.iter().map(|c| c.client_id()).collect();
assert_eq!(clients.len(), 2);
assert!(client_ids.contains(®istered_id));
assert!(client_ids.contains(&default_id));
}
#[rstest]
fn test_submit_order_with_no_client_denies_order(execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(10))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, None, true)
.unwrap();
let submit_order = SubmitOrder {
trader_id,
strategy_id,
instrument_id: instrument.id,
client_order_id: order.client_order_id(),
order_init: order.init_event().clone(),
position_id: None,
params: None,
client_id: None,
exec_algorithm_id: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrder(submit_order));
let cache = execution_engine.cache().borrow();
let cached_order = cache.order(&order.client_order_id()).unwrap();
assert_eq!(cached_order.status(), OrderStatus::Denied);
}
#[rstest]
fn test_register_client_errors_on_duplicate_venue(mut execution_engine: ExecutionEngine) {
let client_a = StubExecutionClient::new(
ClientId::from("CLIENT_A"),
AccountId::from("ACCOUNT-A"),
Venue::test_default(),
OmsType::Netting,
None,
);
let client_b = StubExecutionClient::new(
ClientId::from("CLIENT_B"),
AccountId::from("ACCOUNT-B"),
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(client_a))
.unwrap();
let result = execution_engine.register_client(Box::new(client_b));
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("already routed to CLIENT_A"),
);
}
#[rstest]
fn test_submit_order_list_with_no_client_denies_all_orders(execution_engine: ExecutionEngine) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let entry = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-001-1"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let stop_loss = OrderTestBuilder::new(OrderType::StopMarket)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-19700101-000000-001-002-1"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from_str("0.50000").unwrap())
.build();
let orders = [entry.clone(), stop_loss.clone()];
for order in &orders {
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, None, true)
.unwrap();
}
let order_list = OrderList::new(
OrderListId::from("1"),
instrument.id,
strategy_id,
vec![entry.client_order_id(), stop_loss.client_order_id()],
UnixNanos::default(),
);
let submit_order_list = SubmitOrderList {
trader_id,
client_id: None,
strategy_id,
instrument_id: instrument.id,
order_list,
order_inits: orders.iter().map(|o| o.init_event().clone()).collect(),
exec_algorithm_id: None,
position_id: None,
params: None,
command_id: UUID4::new(),
ts_init: UnixNanos::default(),
};
execution_engine.execute(TradingCommand::SubmitOrderList(submit_order_list));
let cache = execution_engine.cache().borrow();
let cached_entry = cache.order(&entry.client_order_id()).unwrap();
let cached_stop = cache.order(&stop_loss.client_order_id()).unwrap();
assert_eq!(cached_entry.status(), OrderStatus::Denied);
assert_eq!(cached_stop.status(), OrderStatus::Denied);
}
#[rstest]
fn test_start_purge_timers_registers_when_configured() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
purge_closed_orders_interval_mins: Some(5),
purge_closed_positions_interval_mins: Some(10),
purge_account_events_interval_mins: Some(15),
..Default::default()
};
let mut engine = ExecutionEngine::new(clock.clone(), cache, Some(config));
engine.start();
let names = clock
.borrow()
.timer_names()
.into_iter()
.map(String::from)
.collect::<Vec<_>>();
assert!(names.contains(&"ExecEngine_PURGE_CLOSED_ORDERS".to_string()));
assert!(names.contains(&"ExecEngine_PURGE_CLOSED_POSITIONS".to_string()));
assert!(names.contains(&"ExecEngine_PURGE_ACCOUNT_EVENTS".to_string()));
assert_eq!(clock.borrow().timer_count(), 3);
}
#[rstest]
fn test_start_purge_timers_not_registered_when_unconfigured() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let mut engine = ExecutionEngine::new(clock.clone(), cache, None);
engine.start();
assert_eq!(clock.borrow().timer_count(), 0);
}
#[rstest]
fn test_start_purge_timers_zero_interval_skipped() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
purge_closed_orders_interval_mins: Some(0),
purge_closed_positions_interval_mins: Some(0),
purge_account_events_interval_mins: Some(0),
..Default::default()
};
let mut engine = ExecutionEngine::new(clock.clone(), cache, Some(config));
engine.start();
assert_eq!(clock.borrow().timer_count(), 0);
}
#[rstest]
fn test_stop_purge_timers_cancels_timers() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
purge_closed_orders_interval_mins: Some(5),
purge_closed_positions_interval_mins: Some(10),
purge_account_events_interval_mins: Some(15),
..Default::default()
};
let mut engine = ExecutionEngine::new(clock.clone(), cache, Some(config));
engine.start();
assert_eq!(clock.borrow().timer_count(), 3);
engine.stop();
assert_eq!(clock.borrow().timer_count(), 0);
}
#[rstest]
fn test_purge_closed_orders_timer_fires_callback() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let instrument = InstrumentAny::CurrencyPair(audusd_sim());
cache
.borrow_mut()
.add_instrument(instrument.clone())
.unwrap();
let account_id = AccountId::new("SIM-001");
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(instrument.id())
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
cache
.borrow_mut()
.add_order(order.clone(), None, None, false)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, account_id);
order = cache.borrow_mut().update_order(&submitted).unwrap();
let accepted = TestOrderEventStubs::accepted(&order, account_id, VenueOrderId::new("V-001"));
order = cache.borrow_mut().update_order(&accepted).unwrap();
let filled = TestOrderEventStubs::filled(
&order,
&instrument,
Some(TradeId::new("T-1")),
None,
Some(Price::from("1.00000")),
None,
None,
None,
None,
None,
);
order = cache.borrow_mut().update_order(&filled).unwrap();
assert!(order.is_closed());
let config = ExecutionEngineConfig {
purge_closed_orders_interval_mins: Some(1),
purge_closed_orders_buffer_mins: Some(0),
..Default::default()
};
let mut engine = ExecutionEngine::new(clock.clone(), cache.clone(), Some(config));
engine.start();
assert_eq!(
cache
.borrow()
.orders_closed(None, None, None, None, None)
.len(),
1
);
let interval_ns = NANOSECONDS_IN_MINUTE;
let events = clock
.borrow_mut()
.advance_time(UnixNanos::from(interval_ns + 1), true);
let handlers = clock.borrow().match_handlers(events);
for handler in handlers {
handler.callback.call(handler.event);
}
assert_eq!(
cache
.borrow()
.orders_closed(None, None, None, None, None)
.len(),
0
);
}
#[rstest]
fn test_reconcile_order_status_report_creates_external_order_when_filled(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let report = create_order_status_report(
Some(ClientOrderId::from("autoclose-001")),
VenueOrderId::from("V-EXT-001"),
instrument.id(),
OrderStatus::Filled,
Quantity::from(100_000),
Quantity::from(100_000),
);
execution_engine.reconcile_order_status_report(&report);
let cache = execution_engine.cache().borrow();
let order = cache
.order(&ClientOrderId::from("autoclose-001"))
.expect("external order should be in cache");
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.strategy_id(), StrategyId::from("EXTERNAL"));
}
#[rstest]
fn test_reconcile_order_status_report_creates_external_order_accepted(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let report = create_order_status_report(
None,
VenueOrderId::from("V-EXT-002"),
instrument.id(),
OrderStatus::Accepted,
Quantity::from(50_000),
Quantity::from(0),
);
execution_engine.reconcile_order_status_report(&report);
let cache = execution_engine.cache().borrow();
let client_order_id = ClientOrderId::from("V-EXT-002");
let order = cache
.order(&client_order_id)
.expect("external order should be in cache");
assert_eq!(order.status(), OrderStatus::Accepted);
assert_eq!(order.quantity(), Quantity::from(50_000));
}
#[rstest]
fn test_reconcile_order_status_report_external_order_bootstraps_own_book() {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
manage_own_order_books: true,
..Default::default()
};
let mut execution_engine = ExecutionEngine::new(clock, cache, Some(config));
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let report = create_order_status_report(
Some(ClientOrderId::from("external-open-001")),
VenueOrderId::from("V-EXT-OWN-001"),
instrument.id(),
OrderStatus::Accepted,
Quantity::from(50_000),
Quantity::from(0),
);
execution_engine.reconcile_order_status_report(&report);
let cache = execution_engine.cache().borrow();
let order = cache
.order(&ClientOrderId::from("external-open-001"))
.expect("external order should be in cache");
let own_book = cache
.own_order_book(&instrument.id())
.expect("own book should be initialized for external open order");
assert_eq!(order.status(), OrderStatus::Accepted);
assert!(own_book.is_order_in_book(&order.client_order_id()));
assert_eq!(
own_book.bid_client_order_ids(),
vec![order.client_order_id()]
);
assert!(own_book.ask_client_order_ids().is_empty());
}
#[rstest]
fn test_reconcile_order_status_report_external_order_uses_claimed_strategy(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let strategy_id = StrategyId::from("MyStrategy-001");
let mut instruments = HashSet::new();
instruments.insert(instrument.id());
execution_engine
.register_external_order_claims(strategy_id, &instruments)
.unwrap();
let report = create_order_status_report(
Some(ClientOrderId::from("adl_autoclose-001")),
VenueOrderId::from("V-EXT-003"),
instrument.id(),
OrderStatus::Filled,
Quantity::from(100_000),
Quantity::from(100_000),
);
execution_engine.reconcile_order_status_report(&report);
let cache = execution_engine.cache().borrow();
let order = cache
.order(&ClientOrderId::from("adl_autoclose-001"))
.expect("external order should be in cache");
assert_eq!(order.strategy_id(), strategy_id);
assert_eq!(order.status(), OrderStatus::Filled);
}
#[rstest]
fn test_reconcile_order_status_report_external_order_adds_venue_order_id_index(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let report = create_order_status_report(
Some(ClientOrderId::from("settlement-001")),
VenueOrderId::from("V-EXT-004"),
instrument.id(),
OrderStatus::Filled,
Quantity::from(100_000),
Quantity::from(100_000),
);
execution_engine.reconcile_order_status_report(&report);
let cache = execution_engine.cache().borrow();
let resolved = cache.client_order_id(&VenueOrderId::from("V-EXT-004"));
assert_eq!(resolved, Some(&ClientOrderId::from("settlement-001")));
}
#[rstest]
fn test_reconcile_order_status_report_external_order_skipped_without_instrument(
mut execution_engine: ExecutionEngine,
) {
let report = create_order_status_report(
Some(ClientOrderId::from("autoclose-999")),
VenueOrderId::from("V-EXT-999"),
audusd_sim().id(),
OrderStatus::Filled,
Quantity::from(100_000),
Quantity::from(100_000),
);
execution_engine.reconcile_order_status_report(&report);
let cache = execution_engine.cache().borrow();
assert!(!cache.order_exists(&ClientOrderId::from("autoclose-999")));
}
#[rstest]
fn test_reconcile_order_with_fills_creates_external_with_real_fill(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let client_order_id = ClientOrderId::from("adl_autoclose-001");
let venue_order_id = VenueOrderId::from("V-BUNDLED-001");
let trade_id = TradeId::from("T-REAL-001");
let order_report = create_order_status_report(
Some(client_order_id),
venue_order_id,
instrument.id(),
OrderStatus::Filled,
Quantity::from(100_000),
Quantity::from(100_000),
);
let fill_report = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
trade_id,
Quantity::from(100_000),
Price::from("1.00000"),
);
execution_engine.reconcile_order_with_fills(&order_report, &[fill_report]);
let cache = execution_engine.cache().borrow();
let order = cache
.order(&client_order_id)
.expect("external order should be in cache");
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.trade_ids().len(), 1);
assert_eq!(*order.trade_ids()[0], trade_id);
}
#[rstest]
fn test_reconcile_order_with_fills_infers_residual_gap(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let client_order_id = ClientOrderId::from("adl_autoclose-002");
let venue_order_id = VenueOrderId::from("V-BUNDLED-002");
let real_trade_id = TradeId::from("T-REAL-002");
let order_report = create_order_status_report(
Some(client_order_id),
venue_order_id,
instrument.id(),
OrderStatus::Filled,
Quantity::from(100_000),
Quantity::from(100_000),
);
let real_fill = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
real_trade_id,
Quantity::from(70_000),
Price::from("1.00000"),
);
execution_engine.reconcile_order_with_fills(&order_report, &[real_fill]);
let cache = execution_engine.cache().borrow();
let order = cache
.order(&client_order_id)
.expect("external order should be in cache");
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.filled_qty(), Quantity::from(100_000));
assert_eq!(order.trade_ids().len(), 2);
assert!(order.trade_ids().iter().any(|id| **id == real_trade_id));
}
#[rstest]
fn test_reconcile_order_with_fills_applies_to_cached_order(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
let client_order_id = ClientOrderId::from("O-LOCAL-001");
let venue_order_id = VenueOrderId::from("V-LOCAL-001");
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(instrument.id())
.client_order_id(client_order_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(order.clone(), None, Some(ClientId::from("STUB")), true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&order, AccountId::test_default());
execution_engine.process(&submitted);
let accepted = TestOrderEventStubs::accepted(&order, AccountId::test_default(), venue_order_id);
execution_engine.process(&accepted);
let order_report = create_order_status_report(
Some(client_order_id),
venue_order_id,
instrument.id(),
OrderStatus::Filled,
Quantity::from(100_000),
Quantity::from(100_000),
);
let fill = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
TradeId::from("T-LOCAL-001"),
Quantity::from(100_000),
Price::from("1.00000"),
);
execution_engine.reconcile_order_with_fills(&order_report, &[fill]);
let cache = execution_engine.cache().borrow();
let order = cache.order(&client_order_id).unwrap();
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.filled_qty(), Quantity::from(100_000));
assert_eq!(order.trade_ids().len(), 1);
assert_eq!(*order.trade_ids()[0], TradeId::from("T-LOCAL-001"));
}
#[rstest]
#[case(OrderStatus::Canceled)]
#[case(OrderStatus::Expired)]
fn test_reconcile_order_with_fills_emits_terminal_event_for_external(
mut execution_engine: ExecutionEngine,
#[case] terminal_status: OrderStatus,
) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let client_order_id = ClientOrderId::from("autoclose-terminal-001");
let venue_order_id = VenueOrderId::from("V-TERMINAL-001");
let order_report = create_order_status_report(
Some(client_order_id),
venue_order_id,
instrument.id(),
terminal_status,
Quantity::from(100_000),
Quantity::from(0),
);
execution_engine.reconcile_order_with_fills(&order_report, &[]);
let cache = execution_engine.cache().borrow();
let order = cache
.order(&client_order_id)
.expect("external order should be in cache");
assert_eq!(order.status(), terminal_status);
assert!(order.is_closed());
assert_eq!(order.filled_qty(), Quantity::from(0));
}
#[rstest]
fn test_reconcile_order_with_fills_applies_multiple_fills(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let client_order_id = ClientOrderId::from("adl_autoclose-multi");
let venue_order_id = VenueOrderId::from("V-MULTI-001");
let trade_id_a = TradeId::from("T-MULTI-A");
let trade_id_b = TradeId::from("T-MULTI-B");
let order_report = create_order_status_report(
Some(client_order_id),
venue_order_id,
instrument.id(),
OrderStatus::Filled,
Quantity::from(100_000),
Quantity::from(100_000),
);
let fill_a = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
trade_id_a,
Quantity::from(30_000),
Price::from("1.00000"),
);
let fill_b = create_fill_report(
instrument.id(),
Some(client_order_id),
venue_order_id,
trade_id_b,
Quantity::from(70_000),
Price::from("1.00000"),
);
execution_engine.reconcile_order_with_fills(&order_report, &[fill_a, fill_b]);
let cache = execution_engine.cache().borrow();
let order = cache
.order(&client_order_id)
.expect("external order should be in cache");
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.filled_qty(), Quantity::from(100_000));
assert_eq!(order.trade_ids().len(), 2);
assert!(order.trade_ids().iter().any(|id| **id == trade_id_a));
assert!(order.trade_ids().iter().any(|id| **id == trade_id_b));
}
#[rstest]
fn test_reconcile_order_with_fills_empty_fills_infers_full_quantity(
mut execution_engine: ExecutionEngine,
) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let client_order_id = ClientOrderId::from("adl_autoclose-empty");
let venue_order_id = VenueOrderId::from("V-EMPTY-001");
let order_report = create_order_status_report(
Some(client_order_id),
venue_order_id,
instrument.id(),
OrderStatus::Filled,
Quantity::from(100_000),
Quantity::from(100_000),
);
execution_engine.reconcile_order_with_fills(&order_report, &[]);
let cache = execution_engine.cache().borrow();
let order = cache
.order(&client_order_id)
.expect("external order should be in cache");
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.filled_qty(), Quantity::from(100_000));
assert_eq!(order.trade_ids().len(), 1);
}
fn poll_to_completion<F: Future>(fut: F) -> F::Output {
let mut fut = pin!(fut);
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
loop {
if let Poll::Ready(out) = fut.as_mut().poll(&mut cx) {
return out;
}
}
}
#[rstest]
#[case::own_books_enabled(true)]
#[case::own_books_disabled(false)]
fn test_load_cache_no_reentrant_panic(#[case] manage_own_order_books: bool) {
let clock = Rc::new(RefCell::new(TestClock::new()));
let cache = Rc::new(RefCell::new(Cache::default()));
let config = ExecutionEngineConfig {
manage_own_order_books,
..Default::default()
};
let mut engine = ExecutionEngine::new(clock, cache, Some(config));
let instrument = audusd_sim();
engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
let order = OrderTestBuilder::new(OrderType::Limit)
.trader_id(TraderId::test_default())
.strategy_id(StrategyId::test_default())
.instrument_id(instrument.id)
.client_order_id(ClientOrderId::from("O-20240101-000000-001-001-1"))
.side(OrderSide::Buy)
.price(Price::from("1.00000"))
.quantity(Quantity::from(100_000))
.build();
engine
.cache()
.borrow_mut()
.add_order(order, None, None, true)
.unwrap();
poll_to_completion(engine.load_cache()).expect("load_cache should not panic");
let cache = engine.cache().borrow();
assert!(cache.orders(None, None, None, None, None).is_empty());
assert!(cache.own_order_book(&instrument.id).is_none());
}
#[rstest]
fn test_handle_order_fill_oto_links_position_to_contingent_children(
mut execution_engine: ExecutionEngine,
) {
let trader_id = TraderId::test_default();
let strategy_id = StrategyId::test_default();
let account_id = AccountId::test_default();
let instrument = audusd_sim();
let parent_id = ClientOrderId::from("O-20240101-000000-001-001-1");
let child_id = ClientOrderId::from("O-20240101-000000-001-001-2");
let stub_client = StubExecutionClient::new(
ClientId::from("STUB"),
account_id,
Venue::test_default(),
OmsType::Netting,
None,
);
execution_engine
.register_client(Box::new(stub_client))
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_instrument(instrument.clone().into())
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_account(CashAccount::default().into())
.unwrap();
let parent_order = OrderTestBuilder::new(OrderType::Market)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(parent_id)
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.contingency_type(ContingencyType::Oto)
.linked_order_ids(vec![child_id])
.build();
let child_order = OrderTestBuilder::new(OrderType::StopMarket)
.trader_id(trader_id)
.strategy_id(strategy_id)
.instrument_id(instrument.id)
.client_order_id(child_id)
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.trigger_price(Price::from("0.95"))
.contingency_type(ContingencyType::Oto)
.parent_order_id(parent_id)
.build();
execution_engine
.cache()
.borrow_mut()
.add_order(
parent_order.clone(),
None,
Some(ClientId::from("STUB")),
true,
)
.unwrap();
execution_engine
.cache()
.borrow_mut()
.add_order(child_order, None, Some(ClientId::from("STUB")), true)
.unwrap();
let submitted = TestOrderEventStubs::submitted(&parent_order, account_id);
execution_engine.process(&submitted);
let filled = TestOrderEventStubs::filled(
&parent_order,
&instrument.into(),
Some(TradeId::new("E-19700101-000000-001-001-0")),
None,
None,
None,
None,
None,
None,
Some(account_id),
);
execution_engine.process(&filled);
let cache = execution_engine.cache().borrow();
let position_ids = cache.position_ids(None, None, None, None);
assert_eq!(position_ids.len(), 1);
let position_id = *position_ids.iter().next().unwrap();
let cached_child = cache.order(&child_id).expect("child order in cache");
assert_eq!(
cached_child.position_id(),
Some(position_id),
"OTO child order must inherit the parent's position id",
);
assert_eq!(
cache.position_id(&child_id),
Some(&position_id),
"Cache index must map the contingent child to the new position id",
);
}