use std::{cell::RefCell, collections::HashSet, rc::Rc, str::FromStr};
use ahash::AHashSet;
use nautilus_common::{
cache::Cache,
clients::ExecutionClient,
clock::{self, Clock, TestClock},
messages::execution::{CancelOrder, ModifyOrder, SubmitOrder, SubmitOrderList, TradingCommand},
};
use nautilus_core::{UUID4, UnixNanos, datetime::NANOSECONDS_IN_MINUTE};
use nautilus_execution::engine::{
ExecutionEngine, config::ExecutionEngineConfig, stubs::StubExecutionClient,
};
use nautilus_model::{
accounts::CashAccount,
enums::{
LiquiditySide, OmsType, OrderSide, OrderStatus, OrderType, PositionSide,
PositionSideSpecified, TimeInForce, TriggerType,
},
events::{OrderCanceled, OrderEventAny, OrderFilled, OrderPendingUpdate, OrderUpdated},
identifiers::{
AccountId, ClientId, ClientOrderId, InstrumentId, OrderListId, PositionId, StrategyId,
TradeId, TraderId, Venue, VenueOrderId,
},
instruments::{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::{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_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_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_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_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 cache = execution_engine.cache().borrow();
let accepted_order = cache
.order(&order.client_order_id())
.expect("Order should exist in cache");
assert!(
accepted_order.venue_order_id().is_some(),
"Order should have venue_order_id after acceptance"
);
drop(cache);
let order_canceled_event = TestOrderEventStubs::canceled(
&order,
AccountId::from("TEST-ACCOUNT"),
Some(VenueOrderId::from("V-001")), );
execution_engine.process(&order_canceled_event);
let cache = execution_engine.cache().borrow();
let canceled_order = cache
.order(&order.client_order_id())
.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"
);
drop(cache);
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 cache = execution_engine.cache().borrow();
let canceled_order = cache
.order(&order.client_order_id())
.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"
);
drop(cache);
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 cache = execution_engine.cache().borrow();
let partially_filled_order = cache
.order(&order.client_order_id())
.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 cache = execution_engine.cache().borrow();
let filled_order = cache
.order(&order.client_order_id())
.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"
);
drop(cache);
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_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_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::from("P-1");
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),
"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(),
Some(position_id),
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")),
Some(position_id),
None,
None,
None,
None,
None,
Some(AccountId::test_default()),
);
execution_engine.process(&order2_filled_event);
std::thread::sleep(std::time::Duration::from_millis(1));
{
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. Position state: open={}, closed={}, quantity={:?}, ts_closed={:?}",
position.is_open(),
position.is_closed(),
position.quantity,
position.ts_closed
);
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::from("P-1");
let position2_id = PositionId::from("P-2");
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(position1_id),
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")),
Some(position2_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(&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_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::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.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("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(VenueOrderId::from("V-001")), "Order should retain original venue_order_id as it does not change once assigned"
);
}
#[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 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];
let position_id = original_position.id;
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"
);
drop(cache);
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 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(&ClientOrderId::from("O-MISSING")).is_none());
}
#[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 {
FillReport::new(
AccountId::test_default(),
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,
)
}
#[rstest]
fn test_reconcile_fill_report_order_not_found(mut execution_engine: ExecutionEngine) {
let instrument = audusd_sim();
execution_engine
.cache()
.borrow_mut()
.add_instrument(InstrumentAny::CurrencyPair(instrument.clone()))
.unwrap();
let report = create_fill_report(
instrument.id(),
Some(ClientOrderId::from("O-MISSING")),
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(&ClientOrderId::from("O-MISSING")).is_none());
}
#[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);
}
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);
}
#[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_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.apply(submitted).unwrap();
cache.borrow_mut().update_order(&order).unwrap();
let accepted = TestOrderEventStubs::accepted(&order, account_id, VenueOrderId::new("V-001"));
order.apply(accepted).unwrap();
cache.borrow_mut().update_order(&order).unwrap();
let filled = TestOrderEventStubs::filled(
&order,
&instrument,
Some(TradeId::new("T-1")),
None,
Some(Price::from("1.00000")),
None,
None,
None,
None,
None,
);
order.apply(filled).unwrap();
cache.borrow_mut().update_order(&order).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_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(&ClientOrderId::from("autoclose-999")).is_none());
}