pub mod any;
pub mod limit;
pub mod limit_if_touched;
pub mod list;
pub mod market;
pub mod market_if_touched;
pub mod market_to_limit;
pub mod stop_limit;
pub mod stop_market;
pub mod trailing_stop_limit;
pub mod trailing_stop_market;
#[cfg(any(test, feature = "stubs"))]
pub mod builder;
#[cfg(any(test, feature = "stubs"))]
pub mod stubs;
use ahash::AHashSet;
use enum_dispatch::enum_dispatch;
use indexmap::IndexMap;
use nautilus_core::{
UUID4, UnixNanos,
correctness::{CorrectnessError, check_predicate_false},
};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use ustr::Ustr;
#[cfg(any(test, feature = "stubs"))]
pub use crate::orders::builder::OrderTestBuilder;
pub use crate::orders::{
any::{LimitOrderAny, OrderAny, OrderReplayError, PassiveOrderAny, StopOrderAny},
limit::LimitOrder,
limit_if_touched::LimitIfTouchedOrder,
list::{OrderList, OrderListValidationError},
market::MarketOrder,
market_if_touched::MarketIfTouchedOrder,
market_to_limit::MarketToLimitOrder,
stop_limit::StopLimitOrder,
stop_market::StopMarketOrder,
trailing_stop_limit::TrailingStopLimitOrder,
trailing_stop_market::TrailingStopMarketOrder,
};
use crate::{
enums::{
ContingencyType, LiquiditySide, OrderSide, OrderSideSpecified, OrderStatus, OrderType,
PositionSide, TimeInForce, TrailingOffsetType, TriggerType,
},
events::{
OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderEmulated,
OrderEventAny, OrderExpired, OrderFillVoided, OrderFilled, OrderInitialized,
OrderModifyRejected, OrderPendingCancel, OrderPendingUpdate, OrderRejected, OrderReleased,
OrderSubmitted, OrderTriggered, OrderUpdated,
},
identifiers::{
AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
},
orderbook::OwnBookOrder,
reports::OrderStatusReport,
types::{Currency, Money, Price, Quantity},
};
pub const STOP_ORDER_TYPES: &[OrderType] = &[
OrderType::StopMarket,
OrderType::StopLimit,
OrderType::MarketIfTouched,
OrderType::LimitIfTouched,
];
pub const LIMIT_ORDER_TYPES: &[OrderType] = &[
OrderType::Limit,
OrderType::StopLimit,
OrderType::LimitIfTouched,
OrderType::TrailingStopLimit,
];
pub const TRIGGERABLE_ORDER_TYPES: &[OrderType] = &[
OrderType::StopLimit,
OrderType::TrailingStopLimit,
OrderType::LimitIfTouched,
];
pub const LOCAL_ACTIVE_ORDER_STATUSES: &[OrderStatus] = &[
OrderStatus::Initialized,
OrderStatus::Emulated,
OrderStatus::Released,
];
#[derive(thiserror::Error, Debug)]
pub enum OrderError {
#[error("Order not found: {0}")]
NotFound(ClientOrderId),
#[error("Order invariant failed: must have a side for this operation")]
NoOrderSide,
#[error("Invalid event for order type")]
InvalidOrderEvent,
#[error("Invalid order state transition")]
InvalidStateTransition,
#[error("Order was already initialized")]
AlreadyInitialized,
#[error("Order had no previous state")]
NoPreviousState,
#[error("Duplicate fill: trade_id {0} already applied to order")]
DuplicateFill(TradeId),
#[error("Duplicate fill void: trade_id {0} already has this cumulative correction")]
DuplicateFillVoid(TradeId),
#[error("Fill void for trade_id {0} is older than the applied correction")]
StaleFillVoid(TradeId),
#[error("Fill void quantity exceeds the original fill for trade_id {0}")]
OverVoid(TradeId),
#[error("Fill void commission is missing or conflicts with trade_id {0}")]
InvalidFillVoidCommission(TradeId),
#[error("{0}")]
Invariant(#[from] CorrectnessError),
}
#[must_use]
pub fn ustr_indexmap_to_str(h: IndexMap<Ustr, Ustr>) -> IndexMap<String, String> {
h.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[must_use]
pub fn str_indexmap_to_ustr(h: IndexMap<String, String>) -> IndexMap<Ustr, Ustr> {
h.into_iter()
.map(|(k, v)| (Ustr::from(&k), Ustr::from(&v)))
.collect()
}
#[inline]
pub(crate) fn check_display_qty(
display_qty: Option<Quantity>,
quantity: Quantity,
) -> Result<(), OrderError> {
if let Some(q) = display_qty {
check_predicate_false(q > quantity, "`display_qty` may not exceed `quantity`")?;
}
Ok(())
}
#[inline]
pub(crate) fn check_time_in_force(
time_in_force: TimeInForce,
expire_time: Option<UnixNanos>,
) -> Result<(), OrderError> {
check_predicate_false(
time_in_force == TimeInForce::Gtd && expire_time.unwrap_or_default() == 0,
"`expire_time` is required for `GTD` order",
)?;
Ok(())
}
impl OrderStatus {
#[rustfmt::skip]
pub fn transition(&mut self, event: &OrderEventAny) -> Result<Self, OrderError> {
let new_state = match (self, event) {
(Self::Initialized, OrderEventAny::Denied(_)) => Self::Denied,
(Self::Initialized, OrderEventAny::Emulated(_)) => Self::Emulated, (Self::Initialized, OrderEventAny::Released(_)) => Self::Released, (Self::Initialized, OrderEventAny::Submitted(_)) => Self::Submitted,
(Self::Initialized, OrderEventAny::Rejected(_)) => Self::Rejected, (Self::Initialized, OrderEventAny::Accepted(_)) => Self::Accepted, (Self::Initialized, OrderEventAny::Canceled(_)) => Self::Canceled, (Self::Initialized, OrderEventAny::Expired(_)) => Self::Expired, (Self::Initialized, OrderEventAny::Triggered(_)) => Self::Triggered, (Self::Initialized, OrderEventAny::Updated(_)) => Self::Initialized, (Self::Emulated, OrderEventAny::Canceled(_)) => Self::Canceled, (Self::Emulated, OrderEventAny::Expired(_)) => Self::Expired, (Self::Emulated, OrderEventAny::Updated(_)) => Self::Emulated, (Self::Emulated, OrderEventAny::Released(_)) => Self::Released, (Self::Released, OrderEventAny::Submitted(_)) => Self::Submitted, (Self::Released, OrderEventAny::Denied(_)) => Self::Denied, (Self::Released, OrderEventAny::Canceled(_)) => Self::Canceled, (Self::Released, OrderEventAny::Updated(_)) => Self::Released, (Self::Submitted, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
(Self::Submitted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
(Self::Submitted, OrderEventAny::Rejected(_)) => Self::Rejected,
(Self::Submitted, OrderEventAny::Canceled(_)) => Self::Canceled, (Self::Submitted, OrderEventAny::Accepted(_)) => Self::Accepted,
(Self::Submitted, OrderEventAny::Updated(_)) => Self::Submitted,
(Self::Submitted, OrderEventAny::Filled(_)) => Self::Filled,
(Self::Accepted, OrderEventAny::Rejected(_)) => Self::Rejected, (Self::Accepted, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
(Self::Accepted, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
(Self::Accepted, OrderEventAny::Canceled(_)) => Self::Canceled,
(Self::Accepted, OrderEventAny::Triggered(_)) => Self::Triggered,
(Self::Accepted, OrderEventAny::Updated(_)) => Self::Accepted, (Self::Accepted, OrderEventAny::Expired(_)) => Self::Expired,
(Self::Accepted, OrderEventAny::Filled(_)) => Self::Filled,
(Self::Accepted, OrderEventAny::FillVoided(_)) => Self::Accepted,
(Self::Canceled, OrderEventAny::Filled(_)) => Self::Filled, (Self::Canceled, OrderEventAny::FillVoided(_)) => Self::Canceled,
(Self::Canceled, OrderEventAny::Updated(_)) => Self::Canceled,
(Self::PendingUpdate, OrderEventAny::Rejected(_)) => Self::Rejected,
(Self::PendingUpdate, OrderEventAny::Accepted(_)) => Self::Accepted,
(Self::PendingUpdate, OrderEventAny::Canceled(_)) => Self::Canceled,
(Self::PendingUpdate, OrderEventAny::Expired(_)) => Self::Expired,
(Self::PendingUpdate, OrderEventAny::Triggered(_)) => Self::Triggered,
(Self::PendingUpdate, OrderEventAny::Submitted(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
(Self::PendingUpdate, OrderEventAny::ModifyRejected(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::Updated(_)) => Self::PendingUpdate, (Self::PendingUpdate, OrderEventAny::Filled(_)) => Self::Filled,
(Self::PendingUpdate, OrderEventAny::FillVoided(_)) => Self::PendingUpdate,
(Self::PendingCancel, OrderEventAny::Rejected(_)) => Self::Rejected,
(Self::PendingCancel, OrderEventAny::PendingCancel(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::CancelRejected(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::Canceled(_)) => Self::Canceled,
(Self::PendingCancel, OrderEventAny::Expired(_)) => Self::Expired,
(Self::PendingCancel, OrderEventAny::Accepted(_)) => Self::Accepted, (Self::PendingCancel, OrderEventAny::Updated(_)) => Self::PendingCancel, (Self::PendingCancel, OrderEventAny::Filled(_)) => Self::Filled,
(Self::PendingCancel, OrderEventAny::FillVoided(_)) => Self::PendingCancel,
(Self::Triggered, OrderEventAny::Rejected(_)) => Self::Rejected,
(Self::Triggered, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
(Self::Triggered, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
(Self::Triggered, OrderEventAny::Canceled(_)) => Self::Canceled,
(Self::Triggered, OrderEventAny::Expired(_)) => Self::Expired,
(Self::Triggered, OrderEventAny::Filled(_)) => Self::Filled,
(Self::Triggered, OrderEventAny::Updated(_)) => Self::Triggered,
(Self::Triggered, OrderEventAny::FillVoided(_)) => Self::Triggered,
(Self::PartiallyFilled, OrderEventAny::PendingUpdate(_)) => Self::PendingUpdate,
(Self::PartiallyFilled, OrderEventAny::PendingCancel(_)) => Self::PendingCancel,
(Self::PartiallyFilled, OrderEventAny::Canceled(_)) => Self::Canceled,
(Self::PartiallyFilled, OrderEventAny::Expired(_)) => Self::Expired,
(Self::PartiallyFilled, OrderEventAny::Filled(_)) => Self::Filled,
(Self::PartiallyFilled, OrderEventAny::Accepted(_)) => Self::Accepted,
(Self::PartiallyFilled, OrderEventAny::Updated(_)) => Self::PartiallyFilled,
(Self::PartiallyFilled, OrderEventAny::FillVoided(_)) => Self::PartiallyFilled,
(Self::Filled, OrderEventAny::FillVoided(_)) => Self::Voided,
(Self::Filled, OrderEventAny::Updated(_)) => Self::Filled,
(Self::Expired, OrderEventAny::FillVoided(_)) => Self::Expired,
(Self::Expired, OrderEventAny::Updated(_)) => Self::Expired,
(Self::Voided, OrderEventAny::FillVoided(_)) => Self::Voided,
_ => return Err(OrderError::InvalidStateTransition),
};
Ok(new_state)
}
}
#[enum_dispatch]
pub trait Order: 'static + Send {
fn into_any(self) -> OrderAny;
fn status(&self) -> OrderStatus;
fn trader_id(&self) -> TraderId;
fn strategy_id(&self) -> StrategyId;
fn instrument_id(&self) -> InstrumentId;
fn symbol(&self) -> Symbol;
fn venue(&self) -> Venue;
fn client_order_id(&self) -> ClientOrderId;
fn venue_order_id(&self) -> Option<VenueOrderId>;
fn position_id(&self) -> Option<PositionId>;
fn account_id(&self) -> Option<AccountId>;
fn last_trade_id(&self) -> Option<TradeId>;
fn order_side(&self) -> OrderSide;
fn order_type(&self) -> OrderType;
fn quantity(&self) -> Quantity;
fn time_in_force(&self) -> TimeInForce;
fn expire_time(&self) -> Option<UnixNanos>;
fn price(&self) -> Option<Price>;
fn activation_price(&self) -> Option<Price> {
None
}
fn trigger_price(&self) -> Option<Price>;
fn trigger_type(&self) -> Option<TriggerType>;
fn liquidity_side(&self) -> Option<LiquiditySide>;
fn is_post_only(&self) -> bool;
fn is_reduce_only(&self) -> bool;
fn is_quote_quantity(&self) -> bool;
fn display_qty(&self) -> Option<Quantity>;
fn limit_offset(&self) -> Option<Decimal>;
fn trailing_offset(&self) -> Option<Decimal>;
fn trailing_offset_type(&self) -> Option<TrailingOffsetType>;
fn emulation_trigger(&self) -> Option<TriggerType>;
fn trigger_instrument_id(&self) -> Option<InstrumentId>;
fn contingency_type(&self) -> Option<ContingencyType>;
fn order_list_id(&self) -> Option<OrderListId>;
fn linked_order_ids(&self) -> Option<&[ClientOrderId]>;
fn parent_order_id(&self) -> Option<ClientOrderId>;
fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId>;
fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>>;
fn exec_spawn_id(&self) -> Option<ClientOrderId>;
fn tags(&self) -> Option<&[Ustr]>;
fn filled_qty(&self) -> Quantity;
fn voided_qty(&self) -> Quantity;
fn leaves_qty(&self) -> Quantity;
fn overfill_qty(&self) -> Quantity;
fn calculate_overfill(&self, fill_qty: Quantity) -> Quantity {
let potential_filled = self.filled_qty() + fill_qty;
let quantity = self.quantity();
if potential_filled > quantity {
potential_filled - quantity
} else {
Quantity::zero(fill_qty.precision)
}
}
fn avg_px(&self) -> Option<Decimal>;
fn slippage(&self) -> Option<Decimal>;
fn init_id(&self) -> UUID4;
fn ts_init(&self) -> UnixNanos;
fn ts_submitted(&self) -> Option<UnixNanos>;
fn ts_accepted(&self) -> Option<UnixNanos>;
fn ts_closed(&self) -> Option<UnixNanos>;
fn ts_last(&self) -> UnixNanos;
fn order_side_specified(&self) -> OrderSideSpecified {
self.order_side().as_specified()
}
fn commissions(&self) -> &IndexMap<Currency, Money>;
fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError>;
fn update(&mut self, event: &OrderUpdated);
fn events(&self) -> Vec<&OrderEventAny>;
fn last_event(&self) -> &OrderEventAny {
self.events()
.last()
.expect("Order invariant violated: no events")
}
fn event_count(&self) -> usize {
self.events().len()
}
fn venue_order_ids(&self) -> Vec<&VenueOrderId>;
fn trade_ids(&self) -> Vec<&TradeId>;
fn has_price(&self) -> bool;
fn is_duplicate_fill(&self, fill: &OrderFilled) -> bool {
self.events().iter().any(|event| {
if let OrderEventAny::Filled(existing) = event {
existing.trade_id == fill.trade_id
&& existing.order_side == fill.order_side
&& existing.last_qty == fill.last_qty
&& existing.last_px == fill.last_px
} else {
false
}
})
}
fn is_buy(&self) -> bool {
self.order_side() == OrderSide::Buy
}
fn is_sell(&self) -> bool {
self.order_side() == OrderSide::Sell
}
fn is_passive(&self) -> bool {
self.order_type() != OrderType::Market
}
fn is_aggressive(&self) -> bool {
self.order_type() == OrderType::Market
}
fn is_emulated(&self) -> bool {
self.status() == OrderStatus::Emulated
}
fn is_active_local(&self) -> bool {
matches!(
self.status(),
OrderStatus::Initialized | OrderStatus::Emulated | OrderStatus::Released
)
}
fn is_primary(&self) -> bool {
self.exec_algorithm_id().is_some()
&& self
.exec_spawn_id()
.is_some_and(|spawn_id| self.client_order_id() == spawn_id)
}
fn is_spawned(&self) -> bool {
self.exec_algorithm_id().is_some()
&& self
.exec_spawn_id()
.is_some_and(|spawn_id| self.client_order_id() != spawn_id)
}
fn is_contingency(&self) -> bool {
self.contingency_type().is_some()
}
fn is_parent_order(&self) -> bool {
match self.contingency_type() {
Some(c) => c == ContingencyType::Oto,
None => false,
}
}
fn is_child_order(&self) -> bool {
self.parent_order_id().is_some()
}
fn is_open(&self) -> bool {
if let Some(emulation_trigger) = self.emulation_trigger()
&& emulation_trigger != TriggerType::NoTrigger
{
return false;
}
matches!(
self.status(),
OrderStatus::Accepted
| OrderStatus::Triggered
| OrderStatus::PendingCancel
| OrderStatus::PendingUpdate
| OrderStatus::PartiallyFilled
)
}
fn is_canceled(&self) -> bool {
self.status() == OrderStatus::Canceled
}
fn is_closed(&self) -> bool {
matches!(
self.status(),
OrderStatus::Denied
| OrderStatus::Rejected
| OrderStatus::Canceled
| OrderStatus::Expired
| OrderStatus::Filled
| OrderStatus::Voided
)
}
fn is_inflight(&self) -> bool {
if let Some(emulation_trigger) = self.emulation_trigger()
&& emulation_trigger != TriggerType::NoTrigger
{
return false;
}
matches!(
self.status(),
OrderStatus::Submitted | OrderStatus::PendingCancel | OrderStatus::PendingUpdate
)
}
fn is_pending_update(&self) -> bool {
self.status() == OrderStatus::PendingUpdate
}
fn is_pending_cancel(&self) -> bool {
self.status() == OrderStatus::PendingCancel
}
fn to_own_book_order(&self) -> OwnBookOrder {
OwnBookOrder::new(
self.trader_id(),
self.client_order_id(),
self.venue_order_id(),
self.order_side().as_specified(),
self.price().expect("`OwnBookOrder` must have a price"),
self.leaves_qty(),
self.order_type(),
self.time_in_force(),
self.status(),
self.ts_last(),
self.ts_accepted().unwrap_or_default(),
self.ts_submitted().unwrap_or_default(),
self.ts_init(),
)
}
fn to_order_status_report(&self, report_id: Option<UUID4>) -> Option<OrderStatusReport> {
let account_id = self.account_id()?;
let venue_order_id = self.venue_order_id()?;
let mut report = OrderStatusReport::new(
account_id,
self.instrument_id(),
Some(self.client_order_id()),
venue_order_id,
self.order_side(),
self.order_type(),
self.time_in_force(),
self.status(),
self.quantity(),
self.filled_qty(),
self.ts_accepted().unwrap_or_else(|| self.ts_last()),
self.ts_last(),
self.ts_init(),
report_id,
)
.with_post_only(self.is_post_only())
.with_reduce_only(self.is_reduce_only());
if let Some(price) = self.price() {
report = report.with_price(price);
}
if let Some(activation_price) = self.activation_price() {
report = report.with_activation_price(activation_price);
}
if let Some(trigger_price) = self.trigger_price() {
report = report.with_trigger_price(trigger_price);
}
if let Some(trigger_type) = self.trigger_type() {
report = report.with_trigger_type(trigger_type);
}
if let Some(limit_offset) = self.limit_offset() {
report = report.with_limit_offset(limit_offset);
}
if let Some(trailing_offset) = self.trailing_offset() {
report = report.with_trailing_offset(trailing_offset);
}
if let Some(trailing_offset_type) = self.trailing_offset_type() {
report = report.with_trailing_offset_type(trailing_offset_type);
}
if let Some(display_qty) = self.display_qty() {
report = report.with_display_qty(display_qty);
}
if let Some(expire_time) = self.expire_time() {
report = report.with_expire_time(expire_time);
}
if let Some(contingency_type) = self.contingency_type() {
report = report.with_contingency_type(contingency_type);
}
if let Some(order_list_id) = self.order_list_id() {
report = report.with_order_list_id(order_list_id);
}
if let Some(linked_order_ids) = self.linked_order_ids() {
report = report.with_linked_order_ids(linked_order_ids.iter().copied());
}
if let Some(parent_order_id) = self.parent_order_id() {
report = report.with_parent_order_id(parent_order_id);
}
if let Some(position_id) = self.position_id() {
report = report.with_venue_position_id(position_id);
}
let mut ts_triggered = None;
let mut rejected_reason = None;
for event in self.events() {
match event {
OrderEventAny::Triggered(triggered) => ts_triggered = Some(triggered.ts_event),
OrderEventAny::Rejected(rejected) => rejected_reason = Some(rejected.reason),
_ => {}
}
}
if let Some(ts_triggered) = ts_triggered {
report = report.with_ts_triggered(ts_triggered);
}
if let Some(reason) = rejected_reason {
report = report.with_cancel_reason(reason.to_string());
}
if let Some(avg_px) = self.avg_px() {
report = report.with_avg_px(avg_px);
}
Some(report)
}
fn is_triggered(&self) -> Option<bool>; fn set_position_id(&mut self, position_id: Option<PositionId>);
fn set_quantity(&mut self, quantity: Quantity);
fn set_leaves_qty(&mut self, leaves_qty: Quantity);
fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>);
fn set_is_quote_quantity(&mut self, is_quote_quantity: bool);
fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide);
fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool;
fn previous_status(&self) -> Option<OrderStatus>;
}
impl<T> From<&T> for OrderInitialized
where
T: Order,
{
fn from(order: &T) -> Self {
Self {
trader_id: order.trader_id(),
strategy_id: order.strategy_id(),
instrument_id: order.instrument_id(),
client_order_id: order.client_order_id(),
order_side: order.order_side(),
order_type: order.order_type(),
quantity: order.quantity(),
price: order.price(),
activation_price: order.activation_price(),
trigger_price: order.trigger_price(),
trigger_type: order.trigger_type(),
time_in_force: order.time_in_force(),
expire_time: order.expire_time(),
post_only: order.is_post_only(),
reduce_only: order.is_reduce_only(),
quote_quantity: order.is_quote_quantity(),
display_qty: order.display_qty(),
limit_offset: order.limit_offset(),
trailing_offset: order.trailing_offset(),
trailing_offset_type: order.trailing_offset_type(),
emulation_trigger: order.emulation_trigger(),
trigger_instrument_id: order.trigger_instrument_id(),
contingency_type: order.contingency_type(),
order_list_id: order.order_list_id(),
linked_order_ids: order.linked_order_ids().map(|x| x.to_vec()),
parent_order_id: order.parent_order_id(),
exec_algorithm_id: order.exec_algorithm_id(),
exec_algorithm_params: order.exec_algorithm_params().map(|x| x.to_owned()),
exec_spawn_id: order.exec_spawn_id(),
tags: order.tags().map(|x| x.to_vec()),
event_id: order.init_id(),
ts_event: order.ts_init(),
ts_init: order.ts_init(),
reconciliation: false,
causation_id: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OrderCore {
pub events: Vec<OrderEventAny>,
pub commissions: IndexMap<Currency, Money>,
pub venue_order_ids: Vec<VenueOrderId>,
pub trade_ids: Vec<TradeId>,
pub previous_status: Option<OrderStatus>,
pub status: OrderStatus,
pub trader_id: TraderId,
pub strategy_id: StrategyId,
pub instrument_id: InstrumentId,
pub client_order_id: ClientOrderId,
pub venue_order_id: Option<VenueOrderId>,
pub position_id: Option<PositionId>,
pub account_id: Option<AccountId>,
pub last_trade_id: Option<TradeId>,
pub side: OrderSide,
pub order_type: OrderType,
pub quantity: Quantity,
pub time_in_force: TimeInForce,
pub liquidity_side: Option<LiquiditySide>,
pub is_reduce_only: bool,
pub is_quote_quantity: bool,
pub emulation_trigger: Option<TriggerType>,
pub contingency_type: Option<ContingencyType>,
pub order_list_id: Option<OrderListId>,
pub linked_order_ids: Option<Vec<ClientOrderId>>,
pub parent_order_id: Option<ClientOrderId>,
pub exec_algorithm_id: Option<ExecAlgorithmId>,
pub exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
pub exec_spawn_id: Option<ClientOrderId>,
pub tags: Option<Vec<Ustr>>,
pub filled_qty: Quantity,
#[serde(default)]
pub voided_qty: Quantity,
pub leaves_qty: Quantity,
pub overfill_qty: Quantity,
pub avg_px: Option<Decimal>,
pub slippage: Option<Decimal>,
pub init_id: UUID4,
pub ts_init: UnixNanos,
pub ts_submitted: Option<UnixNanos>,
pub ts_accepted: Option<UnixNanos>,
pub ts_closed: Option<UnixNanos>,
pub ts_last: UnixNanos,
}
impl OrderCore {
#[must_use]
pub fn new(init: OrderInitialized) -> Self {
let events: Vec<OrderEventAny> = vec![OrderEventAny::Initialized(init.clone())];
Self {
events,
commissions: IndexMap::new(),
venue_order_ids: Vec::new(),
trade_ids: Vec::new(),
previous_status: None,
status: OrderStatus::Initialized,
trader_id: init.trader_id,
strategy_id: init.strategy_id,
instrument_id: init.instrument_id,
client_order_id: init.client_order_id,
venue_order_id: None,
position_id: None,
account_id: None,
last_trade_id: None,
side: init.order_side,
order_type: init.order_type,
quantity: init.quantity,
time_in_force: init.time_in_force,
liquidity_side: Some(LiquiditySide::NoLiquiditySide),
is_reduce_only: init.reduce_only,
is_quote_quantity: init.quote_quantity,
emulation_trigger: init.emulation_trigger.or(Some(TriggerType::NoTrigger)),
contingency_type: init
.contingency_type
.or(Some(ContingencyType::NoContingency)),
order_list_id: init.order_list_id,
linked_order_ids: init.linked_order_ids,
parent_order_id: init.parent_order_id,
exec_algorithm_id: init.exec_algorithm_id,
exec_algorithm_params: init.exec_algorithm_params,
exec_spawn_id: init.exec_spawn_id,
tags: init.tags,
filled_qty: Quantity::zero(init.quantity.precision),
voided_qty: Quantity::zero(init.quantity.precision),
leaves_qty: init.quantity,
overfill_qty: Quantity::zero(init.quantity.precision),
avg_px: None,
slippage: None,
init_id: init.event_id,
ts_init: init.ts_event,
ts_submitted: None,
ts_accepted: None,
ts_closed: None,
ts_last: init.ts_event,
}
}
pub fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
if self.client_order_id != event.client_order_id() {
return Err(CorrectnessError::PredicateViolation {
message: format!(
"Event client_order_id {} does not match order client_order_id {}",
event.client_order_id(),
self.client_order_id
),
}
.into());
}
if self.strategy_id != event.strategy_id() {
return Err(CorrectnessError::PredicateViolation {
message: format!(
"Event strategy_id {} does not match order strategy_id {}",
event.strategy_id(),
self.strategy_id
),
}
.into());
}
if let OrderEventAny::FillVoided(event) = &event {
self.validate_fill_void(event)?;
}
if let OrderEventAny::Filled(fill) = &event
&& self.events.iter().any(
|event| matches!(event, OrderEventAny::Filled(existing) if existing.trade_id == fill.trade_id),
)
{
return Err(OrderError::DuplicateFill(fill.trade_id));
}
if matches!(event, OrderEventAny::Triggered(_))
&& !TRIGGERABLE_ORDER_TYPES.contains(&self.order_type)
{
return Err(OrderError::InvalidOrderEvent);
}
if matches!(event, OrderEventAny::Initialized(_)) {
return Err(OrderError::AlreadyInitialized);
}
let new_status = self.status.transition(&event)?;
let rejection_status = if matches!(
event,
OrderEventAny::ModifyRejected(_) | OrderEventAny::CancelRejected(_)
) {
self.previous_status.ok_or(OrderError::NoPreviousState)?
} else {
self.status
};
if let OrderEventAny::Updated(update) = &event {
let invalid_fields = match self.order_type {
OrderType::Market => update.price.is_some() || update.trigger_price.is_some(),
OrderType::StopMarket
| OrderType::MarketIfTouched
| OrderType::TrailingStopMarket => update.price.is_some(),
OrderType::Limit | OrderType::MarketToLimit => update.trigger_price.is_some(),
OrderType::StopLimit | OrderType::LimitIfTouched | OrderType::TrailingStopLimit => {
false
}
};
if invalid_fields {
return Err(OrderError::InvalidOrderEvent);
}
}
let source_status = self.status;
if !matches!(
event,
OrderEventAny::Initialized(_)
| OrderEventAny::ModifyRejected(_)
| OrderEventAny::CancelRejected(_)
) && !matches!(
self.status,
OrderStatus::PendingUpdate | OrderStatus::PendingCancel
) {
self.previous_status = Some(self.status);
}
self.status = new_status;
match &event {
OrderEventAny::Initialized(_) => return Err(OrderError::AlreadyInitialized),
OrderEventAny::Denied(event) => self.denied(event),
OrderEventAny::Emulated(event) => self.emulated(event),
OrderEventAny::Released(event) => self.released(event),
OrderEventAny::Submitted(event) => self.submitted(event),
OrderEventAny::Rejected(event) => self.rejected(event),
OrderEventAny::Accepted(event) => self.accepted(event),
OrderEventAny::PendingUpdate(event) => self.pending_update(event),
OrderEventAny::PendingCancel(event) => self.pending_cancel(event),
OrderEventAny::ModifyRejected(event) => self.modify_rejected(event, rejection_status),
OrderEventAny::CancelRejected(event) => self.cancel_rejected(event, rejection_status),
OrderEventAny::Updated(event) => self.updated(event),
OrderEventAny::Triggered(event) => self.triggered(event),
OrderEventAny::Canceled(event) => self.canceled(event),
OrderEventAny::Expired(event) => self.expired(event),
OrderEventAny::Filled(event) => self.filled(event),
OrderEventAny::FillVoided(event) => self.fill_voided(event, source_status),
}
self.ts_last = event.ts_event();
self.events.push(event);
Ok(())
}
fn denied(&mut self, event: &OrderDenied) {
self.ts_closed = Some(event.ts_event);
}
fn emulated(&self, _event: &OrderEmulated) {
}
fn released(&mut self, _event: &OrderReleased) {
self.emulation_trigger = None;
}
fn submitted(&mut self, event: &OrderSubmitted) {
self.account_id = Some(event.account_id);
self.ts_submitted = Some(event.ts_event);
}
fn accepted(&mut self, event: &OrderAccepted) {
self.account_id = Some(event.account_id);
self.venue_order_id = Some(event.venue_order_id);
self.venue_order_ids.push(event.venue_order_id);
self.ts_accepted = Some(event.ts_event);
}
fn rejected(&mut self, event: &OrderRejected) {
self.ts_closed = Some(event.ts_event);
}
fn pending_update(&self, _event: &OrderPendingUpdate) {
}
fn pending_cancel(&self, _event: &OrderPendingCancel) {
}
fn modify_rejected(&mut self, _event: &OrderModifyRejected, previous_status: OrderStatus) {
self.status = previous_status;
}
fn cancel_rejected(&mut self, _event: &OrderCancelRejected, previous_status: OrderStatus) {
self.status = previous_status;
}
fn triggered(&self, _event: &OrderTriggered) {}
fn canceled(&mut self, event: &OrderCanceled) {
self.ts_closed = Some(event.ts_event);
}
fn expired(&mut self, event: &OrderExpired) {
self.ts_closed = Some(event.ts_event);
}
fn validate_fill_void(&self, event: &OrderFillVoided) -> Result<(), OrderError> {
let fill = self.events.iter().find_map(|candidate| match candidate {
OrderEventAny::Filled(fill) if fill.trade_id == event.trade_id => Some(fill),
_ => None,
});
if event.voided_qty.is_zero() {
return Err(OrderError::OverVoid(event.trade_id));
}
let previous = self.latest_fill_void(event.trade_id);
if let Some(previous) = previous {
if event.voided_qty < previous.voided_qty {
return Err(OrderError::StaleFillVoid(event.trade_id));
}
if event.voided_qty == previous.voided_qty
&& event.commission_voided == previous.commission_voided
&& event.is_reopened == previous.is_reopened
{
return Err(OrderError::DuplicateFillVoid(event.trade_id));
}
}
let Some(fill) = fill else {
if event.is_reopened {
return Err(OrderError::InvalidOrderEvent);
}
if !self.matches_fill_void_identity(event) {
return Err(OrderError::InvalidOrderEvent);
}
if event.voided_qty > self.quantity
|| event
.commission_voided
.is_some_and(|commission| !commission.is_zero())
{
return Err(OrderError::OverVoid(event.trade_id));
}
return Ok(());
};
if fill.venue_order_id != event.venue_order_id
|| fill.account_id != event.account_id
|| fill.order_side != event.order_side
|| fill.order_type != event.order_type
|| fill.last_px != event.last_px
|| fill.currency != event.currency
|| fill.liquidity_side != event.liquidity_side
|| fill.position_id != event.position_id
{
return Err(OrderError::InvalidOrderEvent);
}
if event.voided_qty > fill.last_qty {
return Err(OrderError::OverVoid(event.trade_id));
}
self.validate_fill_void_commission(fill, event, previous)
}
fn matches_fill_void_identity(&self, event: &OrderFillVoided) -> bool {
event.trader_id == self.trader_id
&& event.instrument_id == self.instrument_id
&& event.order_side == self.side
&& event.order_type == self.order_type
&& (self.venue_order_id == Some(event.venue_order_id)
|| self.venue_order_ids.contains(&event.venue_order_id))
&& self.account_id == Some(event.account_id)
}
fn validate_fill_void_commission(
&self,
fill: &OrderFilled,
event: &OrderFillVoided,
previous: Option<&OrderFillVoided>,
) -> Result<(), OrderError> {
match (fill.commission, event.commission_voided) {
(None, None) => Ok(()),
(None, Some(commission)) if commission.is_zero() => Ok(()),
(Some(_), None) if previous.and_then(|prior| prior.commission_voided).is_none() => {
Ok(())
}
(Some(commission), Some(voided))
if commission.currency == voided.currency
&& commission.raw.signum() == voided.raw.signum()
&& voided.raw.abs() <= commission.raw.abs()
&& previous
.and_then(|prior| prior.commission_voided)
.is_none_or(|prior| voided.raw.abs() >= prior.raw.abs()) =>
{
Ok(())
}
_ => Err(OrderError::InvalidFillVoidCommission(event.trade_id)),
}
}
fn latest_fill_void(&self, trade_id: TradeId) -> Option<&OrderFillVoided> {
self.events
.iter()
.rev()
.find_map(|candidate| match candidate {
OrderEventAny::FillVoided(event) if event.trade_id == trade_id => Some(event),
_ => None,
})
}
fn fill_voided(&mut self, event: &OrderFillVoided, source_status: OrderStatus) {
let has_local_fill = self.events.iter().any(
|candidate| matches!(candidate, OrderEventAny::Filled(fill) if fill.trade_id == event.trade_id),
);
let non_reopened_voided_qty = self.recompute_fill_state(Some(event));
let unfilled_qty = self.quantity.saturating_sub(self.filled_qty);
let working_leaves = unfilled_qty.saturating_sub(non_reopened_voided_qty);
match source_status {
OrderStatus::Voided => {
self.status = OrderStatus::Voided;
self.leaves_qty = Quantity::zero(self.quantity.precision);
}
_ if !has_local_fill && !event.is_reopened => {
self.status = OrderStatus::Voided;
self.leaves_qty = Quantity::zero(self.quantity.precision);
}
OrderStatus::Canceled | OrderStatus::Expired => {
self.status = source_status;
self.leaves_qty = unfilled_qty;
}
_ if working_leaves.is_zero() => {
self.status = OrderStatus::Voided;
self.leaves_qty = working_leaves;
}
OrderStatus::PendingUpdate | OrderStatus::PendingCancel | OrderStatus::Triggered => {
self.status = source_status;
self.leaves_qty = working_leaves;
}
_ => {
self.status = if self.filled_qty.is_zero() {
OrderStatus::Accepted
} else {
OrderStatus::PartiallyFilled
};
self.leaves_qty = working_leaves;
}
}
if self.status.is_open() {
self.ts_closed = None;
} else if self.status.is_closed() {
self.ts_closed = self.ts_closed.or(Some(event.ts_event));
}
}
fn recompute_fill_state(&mut self, additional: Option<&OrderFillVoided>) -> Quantity {
let corrections = self.fill_corrections(additional);
let mut filled_raw = Quantity::zero(self.quantity.precision).raw;
let mut voided_raw = Quantity::zero(self.quantity.precision).raw;
let mut non_reopened_voided_raw = Quantity::zero(self.quantity.precision).raw;
let mut commissions = IndexMap::<Currency, Money>::new();
let mut trade_ids = Vec::new();
let mut last_trade_id = None;
let mut position_id = None;
let mut liquidity_side = Some(LiquiditySide::NoLiquiditySide);
let mut matched_corrections = AHashSet::new();
for candidate in &self.events {
let OrderEventAny::Filled(fill) = candidate else {
continue;
};
let correction = corrections.get(&fill.trade_id).copied();
if correction.is_some() {
matched_corrections.insert(fill.trade_id);
}
let removed = Self::removed_fill_qty(fill, correction);
let effective = fill.last_qty - removed;
voided_raw = voided_raw.saturating_add(removed.raw);
if correction.is_some_and(|event| !event.is_reopened) {
non_reopened_voided_raw = non_reopened_voided_raw.saturating_add(removed.raw);
}
if !effective.is_zero() {
filled_raw = filled_raw.saturating_add(effective.raw);
trade_ids.push(fill.trade_id);
last_trade_id = Some(fill.trade_id);
position_id = fill.position_id;
liquidity_side = Some(fill.liquidity_side);
}
if let Some(commission) = fill.commission {
let surviving = correction
.and_then(|event| event.commission_voided)
.filter(|voided| !voided.is_zero())
.map_or(commission, |voided| commission - voided);
if !surviving.is_zero() {
commissions
.entry(surviving.currency)
.and_modify(|total| *total = *total + surviving)
.or_insert(surviving);
}
}
}
for (trade_id, correction) in corrections {
if !matched_corrections.contains(&trade_id) {
voided_raw = voided_raw.saturating_add(correction.voided_qty.raw);
}
}
self.filled_qty = Quantity::from_raw(filled_raw, self.quantity.precision);
self.voided_qty = Quantity::from_raw(voided_raw, self.quantity.precision);
self.overfill_qty = self.filled_qty.saturating_sub(self.quantity);
self.avg_px = self.avg_px_from_fills(additional, None);
self.commissions = commissions;
self.trade_ids = trade_ids;
self.last_trade_id = last_trade_id;
self.position_id = position_id;
self.liquidity_side = liquidity_side;
Quantity::from_raw(non_reopened_voided_raw, self.quantity.precision)
}
fn updated(&mut self, event: &OrderUpdated) {
if matches!(
self.status,
OrderStatus::PendingUpdate | OrderStatus::PendingCancel,
) && let Some(previous) = self.previous_status
{
self.status = previous;
}
if event.reconciliation && !self.filled_qty.is_zero() && event.quantity == self.filled_qty {
self.status = OrderStatus::Filled;
self.ts_closed = Some(event.ts_event);
}
if let Some(venue_order_id) = &event.venue_order_id
&& (self.venue_order_id.is_none()
|| venue_order_id != self.venue_order_id.as_ref().unwrap())
{
self.venue_order_id = Some(*venue_order_id);
self.venue_order_ids.push(*venue_order_id);
}
self.is_quote_quantity = event.is_quote_quantity;
}
fn filled(&mut self, event: &OrderFilled) {
let new_filled_qty = Quantity::from_raw(
self.filled_qty.raw.saturating_add(event.last_qty.raw),
self.filled_qty.precision,
);
if new_filled_qty > self.quantity {
let overfill_raw = new_filled_qty.raw - self.quantity.raw;
self.overfill_qty = Quantity::from_raw(
self.overfill_qty.raw.saturating_add(overfill_raw),
self.filled_qty.precision,
);
}
let new_leaves_qty = self.leaves_qty.saturating_sub(event.last_qty);
if new_filled_qty >= self.quantity {
self.status = OrderStatus::Filled;
self.ts_closed = Some(event.ts_event);
} else if new_leaves_qty.is_zero() && !self.voided_qty.is_zero() {
self.status = OrderStatus::Voided;
self.ts_closed = Some(event.ts_event);
} else {
self.status = OrderStatus::PartiallyFilled;
}
self.venue_order_id = Some(event.venue_order_id);
self.position_id = event.position_id;
self.trade_ids.push(event.trade_id);
self.last_trade_id = Some(event.trade_id);
self.liquidity_side = Some(event.liquidity_side);
self.filled_qty = new_filled_qty;
self.leaves_qty = new_leaves_qty;
self.ts_last = event.ts_event;
if let Some(commission) = event.commission {
let commission_currency = commission.currency;
if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) {
*existing_commission = *existing_commission + commission;
} else {
self.commissions.insert(commission_currency, commission);
}
}
if self.ts_accepted.is_none() {
self.ts_accepted = Some(event.ts_event);
}
self.avg_px = self.avg_px_from_fills(None, Some(event));
debug_assert!(
matches!(
self.status,
OrderStatus::PartiallyFilled | OrderStatus::Filled | OrderStatus::Voided
),
"Invariant: status must be PartiallyFilled, Filled, or Voided after fill handler (status={:?})",
self.status
);
debug_assert!(
self.venue_order_id.is_some()
&& self.last_trade_id.is_some()
&& !self.trade_ids.is_empty(),
"Invariant: venue_order_id, last_trade_id, and trade_ids must be set after fill"
);
debug_assert!(
self.filled_qty
.raw
.saturating_add(self.voided_qty.raw)
.saturating_add(self.leaves_qty.raw)
>= self.quantity.raw,
"Invariant: filled_qty + voided_qty + leaves_qty >= quantity (filled={}, voided={}, leaves={}, quantity={})",
self.filled_qty,
self.voided_qty,
self.leaves_qty,
self.quantity
);
}
fn avg_px_from_fills(
&self,
additional: Option<&OrderFillVoided>,
pending: Option<&OrderFilled>,
) -> Option<Decimal> {
let corrections = self.fill_corrections(additional);
let mut notional = Decimal::ZERO;
let mut quantity = Decimal::ZERO;
for candidate in &self.events {
let OrderEventAny::Filled(fill) = candidate else {
continue;
};
let removed = Self::removed_fill_qty(fill, corrections.get(&fill.trade_id).copied());
let effective = (fill.last_qty - removed).as_decimal();
if effective.is_zero() {
continue;
}
notional = notional.saturating_add(effective.saturating_mul(fill.last_px.as_decimal()));
quantity = quantity.saturating_add(effective);
}
if let Some(fill) = pending {
let last_qty = fill.last_qty.as_decimal();
notional = notional.saturating_add(last_qty.saturating_mul(fill.last_px.as_decimal()));
quantity = quantity.saturating_add(last_qty);
}
notional.checked_div(quantity)
}
fn fill_corrections<'a>(
&'a self,
additional: Option<&'a OrderFillVoided>,
) -> IndexMap<TradeId, &'a OrderFillVoided> {
let mut corrections = IndexMap::new();
for candidate in &self.events {
if let OrderEventAny::FillVoided(event) = candidate {
corrections.insert(event.trade_id, event);
}
}
if let Some(event) = additional {
corrections.insert(event.trade_id, event);
}
corrections
}
fn removed_fill_qty(fill: &OrderFilled, correction: Option<&OrderFillVoided>) -> Quantity {
correction.map_or_else(
|| Quantity::zero(fill.last_qty.precision),
|event| event.voided_qty.min(fill.last_qty),
)
}
pub fn set_slippage(&mut self, price: Price) {
self.slippage = self.avg_px.and_then(|avg_px| {
let current_price = price.as_decimal();
match self.side {
OrderSide::Buy if avg_px > current_price => avg_px.checked_sub(current_price),
OrderSide::Sell if avg_px < current_price => current_price.checked_sub(avg_px),
_ => None,
}
});
}
#[must_use]
pub fn opposite_side(side: OrderSide) -> OrderSide {
match side {
OrderSide::Buy => OrderSide::Sell,
OrderSide::Sell => OrderSide::Buy,
OrderSide::NoOrderSide => OrderSide::NoOrderSide,
}
}
#[must_use]
pub fn closing_side(side: PositionSide) -> OrderSide {
match side {
PositionSide::Long => OrderSide::Sell,
PositionSide::Short => OrderSide::Buy,
PositionSide::Flat => OrderSide::NoOrderSide,
PositionSide::NoPositionSide => OrderSide::NoOrderSide,
}
}
#[must_use]
pub fn signed_decimal_qty(&self) -> Decimal {
match self.side {
OrderSide::Buy => self.quantity.as_decimal(),
OrderSide::Sell => -self.quantity.as_decimal(),
OrderSide::NoOrderSide => panic!("Invalid order side"),
}
}
#[must_use]
pub fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
if side == PositionSide::Flat {
return false;
}
match (self.side, side) {
(OrderSide::Buy, PositionSide::Long) => false,
(OrderSide::Buy, PositionSide::Short) => self.leaves_qty <= position_qty,
(OrderSide::Sell, PositionSide::Short) => false,
(OrderSide::Sell, PositionSide::Long) => self.leaves_qty <= position_qty,
_ => true,
}
}
#[must_use]
pub fn commission(&self, currency: &Currency) -> Option<Money> {
self.commissions.get(currency).copied()
}
#[must_use]
pub fn commissions(&self) -> IndexMap<Currency, Money> {
self.commissions.clone()
}
#[must_use]
pub fn commissions_vec(&self) -> Vec<Money> {
self.commissions.values().copied().collect()
}
#[must_use]
pub fn init_event(&self) -> Option<OrderEventAny> {
self.events.first().cloned()
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use rstest::rstest;
use rust_decimal_macros::dec;
use super::*;
use crate::{
enums::{LiquiditySide, OrderSide, OrderStatus, PositionSide, TriggerType},
events::order::spec::{
OrderAcceptedSpec, OrderCancelRejectedSpec, OrderCanceledSpec, OrderDeniedSpec,
OrderExpiredSpec, OrderFillVoidedSpec, OrderFilledSpec, OrderInitializedSpec,
OrderModifyRejectedSpec, OrderPendingCancelSpec, OrderPendingUpdateSpec,
OrderRejectedSpec, OrderSubmittedSpec, OrderTriggeredSpec, OrderUpdatedSpec,
},
identifiers::InstrumentId,
instruments::{CurrencyPair, Instrument, InstrumentAny, stubs::audusd_sim},
orders::{MarketOrder, builder::OrderTestBuilder, stubs::TestOrderStubs},
types::{Price, Quantity},
};
#[rstest]
#[case(OrderSide::Buy, OrderSide::Sell)]
#[case(OrderSide::Sell, OrderSide::Buy)]
#[case(OrderSide::NoOrderSide, OrderSide::NoOrderSide)]
fn test_order_opposite_side(#[case] order_side: OrderSide, #[case] expected_side: OrderSide) {
let result = OrderCore::opposite_side(order_side);
assert_eq!(result, expected_side);
}
#[rstest]
#[case(PositionSide::Long, OrderSide::Sell)]
#[case(PositionSide::Short, OrderSide::Buy)]
#[case(PositionSide::NoPositionSide, OrderSide::NoOrderSide)]
fn test_closing_side(#[case] position_side: PositionSide, #[case] expected_side: OrderSide) {
let result = OrderCore::closing_side(position_side);
assert_eq!(result, expected_side);
}
#[rstest]
#[case(OrderSide::Buy, dec!(10_000))]
#[case(OrderSide::Sell, dec!(-10_000))]
fn test_signed_decimal_qty(#[case] order_side: OrderSide, #[case] expected: Decimal) {
let order: MarketOrder = OrderInitializedSpec::builder()
.order_side(order_side)
.quantity(Quantity::from(10_000))
.build()
.try_into()
.unwrap();
let result = order.signed_decimal_qty();
assert_eq!(result, expected);
}
#[rustfmt::skip]
#[rstest]
#[case(OrderSide::Buy, Quantity::from(100), PositionSide::Long, Quantity::from(50), false)]
#[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(50), true)]
#[case(OrderSide::Buy, Quantity::from(50), PositionSide::Short, Quantity::from(100), true)]
#[case(OrderSide::Buy, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)]
#[case(OrderSide::Sell, Quantity::from(50), PositionSide::Flat, Quantity::from(0), false)]
#[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(50), true)]
#[case(OrderSide::Sell, Quantity::from(50), PositionSide::Long, Quantity::from(100), true)]
#[case(OrderSide::Sell, Quantity::from(100), PositionSide::Short, Quantity::from(50), false)]
fn test_would_reduce_only(
#[case] order_side: OrderSide,
#[case] order_qty: Quantity,
#[case] position_side: PositionSide,
#[case] position_qty: Quantity,
#[case] expected: bool,
) {
let order: MarketOrder = OrderInitializedSpec::builder()
.order_side(order_side)
.quantity(order_qty)
.build()
.try_into()
.unwrap();
assert_eq!(
order.would_reduce_only(position_side, position_qty),
expected
);
}
#[rstest]
fn test_order_state_transition_denied() {
let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
let denied = OrderDeniedSpec::builder().build();
let event = OrderEventAny::Denied(denied);
order.apply(event.clone()).unwrap();
assert_eq!(order.status, OrderStatus::Denied);
assert!(order.is_closed());
assert!(!order.is_open());
assert_eq!(order.event_count(), 2);
assert_eq!(order.last_event(), &event);
}
#[rstest]
fn test_order_life_cycle_to_filled() {
let init = OrderInitializedSpec::builder().build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let filled = OrderFilledSpec::builder().build();
let mut order: MarketOrder = init.clone().try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(filled)).unwrap();
assert_eq!(order.client_order_id, init.client_order_id);
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.filled_qty(), Quantity::from(100_000));
assert_eq!(order.leaves_qty(), Quantity::from(0));
assert_eq!(order.avg_px(), Some(dec!(1.0)));
assert!(!order.is_open());
assert!(order.is_closed());
assert_eq!(order.commission(&Currency::USD()), None);
assert_eq!(order.commissions(), &IndexMap::new());
}
#[rstest]
fn test_order_life_cycle_fills_with_negative_prices() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let fill1 = OrderFilledSpec::builder()
.last_qty(Quantity::from(50_000))
.last_px(Price::from("-5.00000"))
.trade_id(TradeId::from("TRADE-1"))
.build();
let fill2 = OrderFilledSpec::builder()
.last_qty(Quantity::from(50_000))
.last_px(Price::from("-7.00000"))
.trade_id(TradeId::from("TRADE-2"))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(fill1)).unwrap();
order.apply(OrderEventAny::Filled(fill2)).unwrap();
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.filled_qty(), Quantity::from(100_000));
assert_eq!(order.leaves_qty(), Quantity::from(0));
assert_eq!(order.avg_px(), Some(dec!(-6.0)));
}
fn fill(trade_id: &str, last_qty: &str, last_px: &str) -> (TradeId, Quantity, Price) {
(
TradeId::from(trade_id),
Quantity::from(last_qty),
Price::from(last_px),
)
}
fn market_order_with_fills(
order_side: OrderSide,
quantity: Quantity,
fills: &[(TradeId, Quantity, Price)],
) -> MarketOrder {
let mut order: MarketOrder = OrderInitializedSpec::builder()
.order_side(order_side)
.quantity(quantity)
.build()
.try_into()
.unwrap();
order
.apply(OrderEventAny::Accepted(
OrderAcceptedSpec::builder().build(),
))
.unwrap();
for (trade_id, last_qty, last_px) in fills {
order
.apply(OrderEventAny::Filled(
OrderFilledSpec::builder()
.order_side(order_side)
.trade_id(*trade_id)
.last_qty(*last_qty)
.last_px(*last_px)
.build(),
))
.unwrap();
}
order
}
#[rstest]
#[case(OrderSide::Buy)]
#[case(OrderSide::Sell)]
fn test_avg_px_weighted_average_is_exact(#[case] order_side: OrderSide) {
let order = market_order_with_fills(
order_side,
Quantity::from(6),
&[
fill("TRADE-1", "1", "0.07"),
fill("TRADE-2", "2", "0.13"),
fill("TRADE-3", "3", "0.29"),
],
);
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.filled_qty(), Quantity::from(6));
assert_eq!(order.leaves_qty(), Quantity::from(0));
assert_eq!(order.voided_qty(), Quantity::from(0));
assert_eq!(order.avg_px(), Some(dec!(0.2)));
}
#[rstest]
fn test_avg_px_keeps_a_quotient_no_f64_can_hold() {
let order = market_order_with_fills(
OrderSide::Buy,
Quantity::from(3),
&[fill("TRADE-1", "1", "1.00"), fill("TRADE-2", "2", "2.00")],
);
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.filled_qty(), Quantity::from(3));
assert_eq!(order.avg_px(), Some(dec!(1.6666666666666666666666666667)));
assert_ne!(
order.avg_px(),
Some(Decimal::from_f64_retain(5.0_f64 / 3.0).unwrap())
);
}
#[rstest]
#[case(OrderSide::Buy, "1.07", "1.00", Some(dec!(0.07)))]
#[case(OrderSide::Sell, "0.93", "1.00", Some(dec!(0.07)))]
#[case(OrderSide::Buy, "0.93", "1.00", None)]
#[case(OrderSide::Sell, "1.07", "1.00", None)]
fn test_set_slippage_by_side(
#[case] order_side: OrderSide,
#[case] fill_px: &str,
#[case] reference_px: &str,
#[case] expected: Option<Decimal>,
) {
let mut order =
market_order_with_fills(order_side, Quantity::from(1), &[fill("T-1", "1", fill_px)]);
order.set_slippage(Price::from(reference_px));
assert_eq!(order.avg_px(), Some(Decimal::from_str(fill_px).unwrap()));
assert_eq!(order.slippage(), expected);
}
#[rstest]
fn test_avg_px_invariant_to_fill_arrival_order() {
let mut fills = [
fill("TRADE-1", "50000", "1.00001"),
fill("TRADE-2", "30000", "1.00002"),
fill("TRADE-3", "20000", "1.00003"),
];
let ascending = market_order_with_fills(OrderSide::Buy, Quantity::from(100_000), &fills);
fills.reverse();
let descending = market_order_with_fills(OrderSide::Buy, Quantity::from(100_000), &fills);
assert_eq!(ascending.avg_px(), Some(dec!(1.000_017)));
assert_eq!(descending.avg_px(), ascending.avg_px());
assert_eq!(descending.filled_qty(), ascending.filled_qty());
}
#[rstest]
fn test_avg_px_after_fill_void_matches_order_without_voided_fill() {
let surviving = [fill("TRADE-1", "1", "0.05"), fill("TRADE-2", "3", "0.15")];
let voided = fill("TRADE-VOIDED", "2", "0.13");
let expected = market_order_with_fills(OrderSide::Buy, Quantity::from(6), &surviving);
let mut corrected = market_order_with_fills(
OrderSide::Buy,
Quantity::from(6),
&[surviving[0], surviving[1], voided],
);
corrected
.apply(OrderEventAny::FillVoided(
OrderFillVoidedSpec::builder()
.trade_id(voided.0)
.voided_qty(voided.1)
.last_px(voided.2)
.build(),
))
.unwrap();
let replayed =
OrderAny::from_events(corrected.events().into_iter().cloned().collect()).unwrap();
assert_eq!(expected.avg_px(), Some(dec!(0.125)));
assert_eq!(corrected.avg_px(), expected.avg_px());
assert_eq!(replayed.avg_px(), expected.avg_px());
assert_eq!(corrected.status(), OrderStatus::Voided);
assert_eq!(corrected.filled_qty(), Quantity::from(4));
assert_eq!(corrected.voided_qty(), Quantity::from(2));
assert_eq!(corrected.leaves_qty(), Quantity::from(0));
assert_eq!(replayed.filled_qty(), corrected.filled_qty());
assert_eq!(replayed.voided_qty(), corrected.voided_qty());
}
#[rstest]
fn test_avg_px_recomputed_from_partially_voided_fill() {
let mut order = market_order_with_fills(
OrderSide::Buy,
Quantity::from(6),
&[fill("TRADE-1", "4", "1.00"), fill("TRADE-2", "2", "1.14")],
);
order
.apply(OrderEventAny::FillVoided(
OrderFillVoidedSpec::builder()
.trade_id(TradeId::from("TRADE-1"))
.voided_qty(Quantity::from(2))
.last_px(Price::from("1.00"))
.build(),
))
.unwrap();
assert_eq!(order.status(), OrderStatus::Voided);
assert_eq!(order.filled_qty(), Quantity::from(4));
assert_eq!(order.voided_qty(), Quantity::from(2));
assert_eq!(order.leaves_qty(), Quantity::from(0));
assert_eq!(order.avg_px(), Some(dec!(1.07)));
}
#[rstest]
fn test_avg_px_over_reopened_fill_void_and_replacement_fill() {
let mut order = market_order_with_fills(
OrderSide::Buy,
Quantity::from(6),
&[fill("TRADE-1", "4", "1.00")],
);
order
.apply(OrderEventAny::FillVoided(
OrderFillVoidedSpec::builder()
.trade_id(TradeId::from("TRADE-1"))
.voided_qty(Quantity::from(2))
.last_px(Price::from("1.00"))
.is_reopened(true)
.build(),
))
.unwrap();
order
.apply(OrderEventAny::Filled(
OrderFilledSpec::builder()
.trade_id(TradeId::from("TRADE-2"))
.last_qty(Quantity::from(2))
.last_px(Price::from("1.14"))
.build(),
))
.unwrap();
order.set_slippage(Price::from("1.00"));
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
assert_eq!(order.filled_qty(), Quantity::from(4));
assert_eq!(order.voided_qty(), Quantity::from(2));
assert_eq!(order.leaves_qty(), Quantity::from(2));
assert_eq!(order.avg_px(), Some(dec!(1.07)));
assert_eq!(order.slippage(), Some(dec!(0.07)));
}
#[rstest]
fn test_avg_px_cleared_when_every_fill_is_voided() {
let mut order = market_order_with_fills(
OrderSide::Buy,
Quantity::from(6),
&[fill("TRADE-1", "4", "1.50")],
);
order
.apply(OrderEventAny::FillVoided(
OrderFillVoidedSpec::builder()
.trade_id(TradeId::from("TRADE-1"))
.voided_qty(Quantity::from(4))
.last_px(Price::from("1.50"))
.build(),
))
.unwrap();
assert_eq!(order.status(), OrderStatus::Accepted);
assert_eq!(order.filled_qty(), Quantity::from(0));
assert_eq!(order.voided_qty(), Quantity::from(4));
assert_eq!(order.leaves_qty(), Quantity::from(2));
assert_eq!(order.avg_px(), None);
}
#[cfg(feature = "high-precision")]
#[rstest]
fn test_avg_px_exact_over_high_precision_quantities() {
let order = market_order_with_fills(
OrderSide::Buy,
Quantity::from("0.000000000010"),
&[
fill("TRADE-1", "0.000000000005", "1.00001"),
fill("TRADE-2", "0.000000000003", "1.00002"),
fill("TRADE-3", "0.000000000002", "1.00003"),
],
);
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.filled_qty(), Quantity::from("0.000000000010"));
assert_eq!(order.avg_px(), Some(dec!(1.000_017)));
}
#[cfg(feature = "high-precision")]
#[rstest]
fn test_avg_px_exact_at_the_widest_representable_fill_scale() {
let order = market_order_with_fills(
OrderSide::Buy,
Quantity::from("0.0000000000000008"),
&[
fill("TRADE-1", "0.0000000000000002", "0.000000000001"),
fill("TRADE-2", "0.0000000000000006", "0.000000000003"),
],
);
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.filled_qty(), Quantity::from("0.0000000000000008"));
assert_eq!(order.avg_px(), Some(dec!(2.5e-12)));
}
#[rstest]
fn test_order_life_cycle_accumulates_fill_commissions() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let fill1 = OrderFilledSpec::builder()
.last_qty(Quantity::from(50_000))
.trade_id(TradeId::from("TRADE-1"))
.commission(Money::from("1.25 USD"))
.build();
let fill2 = OrderFilledSpec::builder()
.last_qty(Quantity::from(50_000))
.trade_id(TradeId::from("TRADE-2"))
.commission(Money::from("1.35 USD"))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(fill1)).unwrap();
order.apply(OrderEventAny::Filled(fill2)).unwrap();
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(
order.commission(&Currency::USD()),
Some(Money::from("2.60 USD"))
);
assert_eq!(order.commissions_vec(), vec![Money::from("2.60 USD")]);
}
#[rstest]
fn test_fill_void_reopens_only_when_explicitly_marked() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let filled = OrderFilledSpec::builder()
.trade_id(TradeId::from("TRADE-VOID"))
.last_qty(Quantity::from(100_000))
.commission(Money::from("1.00 USD"))
.build();
let fill_voided = OrderFillVoidedSpec::builder()
.trade_id(filled.trade_id)
.voided_qty(Quantity::from(40_000))
.commission_voided(Money::from("0.40 USD"))
.build();
let later_fill = OrderFilledSpec::builder()
.trade_id(TradeId::from("TRADE-AFTER-REOPEN"))
.last_qty(Quantity::from(40_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order
.apply(OrderEventAny::Accepted(
OrderAcceptedSpec::builder().build(),
))
.unwrap();
order.apply(OrderEventAny::Filled(filled)).unwrap();
let mut terminal = order.clone();
terminal
.apply(OrderEventAny::FillVoided(fill_voided.clone()))
.unwrap();
let fill_while_terminal = terminal.apply(OrderEventAny::Filled(later_fill.clone()));
order
.apply(OrderEventAny::FillVoided(OrderFillVoided {
is_reopened: true,
..fill_voided
}))
.unwrap();
let status_after_correction = order.status();
order.apply(OrderEventAny::Filled(later_fill)).unwrap();
assert!(matches!(
fill_while_terminal,
Err(OrderError::InvalidStateTransition)
));
assert_eq!(terminal.status(), OrderStatus::Voided);
assert_eq!(status_after_correction, OrderStatus::PartiallyFilled);
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.filled_qty(), Quantity::from(100_000));
assert_eq!(order.voided_qty(), Quantity::from(40_000));
assert_eq!(order.leaves_qty(), Quantity::from(0));
assert_eq!(
order.commission(&Currency::USD()),
Some(Money::from("0.60 USD"))
);
}
#[rstest]
fn test_fill_void_preserves_existing_working_remainder() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let filled = OrderFilledSpec::builder()
.trade_id(TradeId::from("TRADE-VOID"))
.last_qty(Quantity::from(40_000))
.build();
let later_fill = OrderFilledSpec::builder()
.trade_id(TradeId::from("TRADE-REMAINDER"))
.last_qty(Quantity::from(60_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order
.apply(OrderEventAny::Accepted(
OrderAcceptedSpec::builder().build(),
))
.unwrap();
order.apply(OrderEventAny::Filled(filled.clone())).unwrap();
order
.apply(OrderEventAny::FillVoided(
OrderFillVoidedSpec::builder()
.trade_id(filled.trade_id)
.voided_qty(filled.last_qty)
.build(),
))
.unwrap();
assert_eq!(order.status(), OrderStatus::Accepted);
assert_eq!(order.filled_qty(), Quantity::from(0));
assert_eq!(order.voided_qty(), Quantity::from(40_000));
assert_eq!(order.leaves_qty(), Quantity::from(60_000));
assert!(order.is_open());
order.apply(OrderEventAny::Filled(later_fill)).unwrap();
assert_eq!(order.status(), OrderStatus::Voided);
assert_eq!(order.filled_qty(), Quantity::from(60_000));
assert_eq!(order.voided_qty(), Quantity::from(40_000));
assert_eq!(order.leaves_qty(), Quantity::from(0));
assert!(order.is_closed());
}
#[rstest]
fn test_reopened_fill_void_requires_existing_fill() {
let trade_id = TradeId::from("TRADE-OUT-OF-ORDER");
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let early_void = OrderFillVoidedSpec::builder()
.trade_id(trade_id)
.voided_qty(Quantity::from(10_000))
.is_reopened(true)
.build();
let late_fill = OrderFilledSpec::builder()
.trade_id(trade_id)
.last_qty(Quantity::from(40_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order
.apply(OrderEventAny::Accepted(
OrderAcceptedSpec::builder().build(),
))
.unwrap();
let result = order.apply(OrderEventAny::FillVoided(early_void));
order.apply(OrderEventAny::Filled(late_fill)).unwrap();
assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
assert_eq!(order.filled_qty(), Quantity::from(40_000));
assert_eq!(order.voided_qty(), Quantity::from(0));
assert_eq!(order.leaves_qty(), Quantity::from(60_000));
}
#[rstest]
#[case(OrderStatus::Accepted)]
#[case(OrderStatus::Canceled)]
#[case(OrderStatus::Expired)]
fn test_terminal_fill_void_rejects_later_order_events(#[case] status_before_void: OrderStatus) {
let trade_id = TradeId::from("TRADE-TERMINAL-VOID");
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let terminal_void = OrderFillVoidedSpec::builder()
.trade_id(trade_id)
.voided_qty(Quantity::from(40_000))
.commission_voided(Money::from("0 USD"))
.build();
let late_fill = OrderFilledSpec::builder()
.trade_id(trade_id)
.last_qty(Quantity::from(40_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order
.apply(OrderEventAny::Accepted(
OrderAcceptedSpec::builder().build(),
))
.unwrap();
match status_before_void {
OrderStatus::Accepted => {}
OrderStatus::Canceled => order
.apply(OrderEventAny::Canceled(
OrderCanceledSpec::builder().build(),
))
.unwrap(),
OrderStatus::Expired => order
.apply(OrderEventAny::Expired(OrderExpiredSpec::builder().build()))
.unwrap(),
_ => unreachable!(),
}
order
.apply(OrderEventAny::FillVoided(terminal_void))
.unwrap();
let update_result = order.apply(OrderEventAny::Updated(
OrderUpdatedSpec::builder()
.quantity(Quantity::from(120_000))
.build(),
));
let cancel_result = order.apply(OrderEventAny::Canceled(
OrderCanceledSpec::builder().build(),
));
let fill_result = order.apply(OrderEventAny::Filled(late_fill));
assert!(matches!(
update_result,
Err(OrderError::InvalidStateTransition)
));
assert!(matches!(
cancel_result,
Err(OrderError::InvalidStateTransition)
));
assert!(matches!(
fill_result,
Err(OrderError::InvalidStateTransition)
));
assert_eq!(order.status(), OrderStatus::Voided);
assert_eq!(order.filled_qty(), Quantity::from(0));
assert_eq!(order.voided_qty(), Quantity::from(40_000));
assert_eq!(order.leaves_qty(), Quantity::from(0));
}
#[rstest]
#[case(Quantity::from(100_001), None)]
#[case(Quantity::from(100_000), Some(Money::from("1 USD")))]
fn test_terminal_fill_void_rejects_invalid_economic_values(
#[case] voided_qty: Quantity,
#[case] commission_voided: Option<Money>,
) {
let trade_id = TradeId::from("TRADE-TERMINAL-VOID");
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order
.apply(OrderEventAny::Accepted(
OrderAcceptedSpec::builder().build(),
))
.unwrap();
let mut invalid = OrderFillVoidedSpec::builder()
.trade_id(trade_id)
.voided_qty(voided_qty)
.build();
invalid.commission_voided = commission_voided;
let result = order.apply(OrderEventAny::FillVoided(invalid));
assert!(matches!(
result,
Err(OrderError::OverVoid(error_trade_id)) if error_trade_id == trade_id
));
assert_eq!(order.status(), OrderStatus::Accepted);
assert_eq!(order.voided_qty(), Quantity::from(0));
}
#[rstest]
fn test_terminal_fill_void_accepts_current_venue_order_id_after_fill_before_accept() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let filled = OrderFilledSpec::builder()
.account_id(submitted.account_id)
.trade_id(TradeId::from("TRADE-APPLIED"))
.last_qty(Quantity::from(40_000))
.build();
let terminal_void = OrderFillVoidedSpec::builder()
.venue_order_id(filled.venue_order_id)
.account_id(filled.account_id)
.trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
.voided_qty(Quantity::from(10_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Filled(filled)).unwrap();
order
.apply(OrderEventAny::FillVoided(terminal_void))
.unwrap();
assert_eq!(order.status(), OrderStatus::Voided);
assert_eq!(order.filled_qty(), Quantity::from(40_000));
assert_eq!(order.voided_qty(), Quantity::from(10_000));
assert_eq!(order.leaves_qty(), Quantity::from(0));
}
#[rstest]
fn test_terminal_fill_void_requires_matching_order_identity() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order
.apply(OrderEventAny::Accepted(
OrderAcceptedSpec::builder().build(),
))
.unwrap();
let mismatched = OrderFillVoidedSpec::builder()
.instrument_id(InstrumentId::from("OTHER.SIM"))
.trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
.voided_qty(Quantity::from(100_000))
.build();
let result = order.apply(OrderEventAny::FillVoided(mismatched));
assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
assert_eq!(order.status(), OrderStatus::Accepted);
assert_eq!(order.voided_qty(), Quantity::from(0));
}
#[rstest]
#[case(OrderStatus::Canceled)]
#[case(OrderStatus::Expired)]
fn test_fill_void_does_not_reopen_canceled_or_expired_order(
#[case] terminal_status: OrderStatus,
) {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let filled = OrderFilledSpec::builder()
.trade_id(TradeId::from("TRADE-TERMINAL-VOID"))
.last_qty(Quantity::from(60_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order
.apply(OrderEventAny::Accepted(
OrderAcceptedSpec::builder().build(),
))
.unwrap();
order.apply(OrderEventAny::Filled(filled.clone())).unwrap();
match terminal_status {
OrderStatus::Canceled => order
.apply(OrderEventAny::Canceled(
OrderCanceledSpec::builder().build(),
))
.unwrap(),
OrderStatus::Expired => order
.apply(OrderEventAny::Expired(OrderExpiredSpec::builder().build()))
.unwrap(),
_ => unreachable!(),
}
let voided = OrderFillVoidedSpec::builder()
.trade_id(filled.trade_id)
.voided_qty(Quantity::from(20_000))
.is_reopened(true)
.build();
order.apply(OrderEventAny::FillVoided(voided)).unwrap();
assert_eq!(order.status(), terminal_status);
assert_eq!(order.filled_qty(), Quantity::from(40_000));
assert_eq!(order.voided_qty(), Quantity::from(20_000));
assert!(order.is_closed());
assert!(order.ts_closed().is_some());
}
#[rstest]
fn test_fill_void_rejects_duplicate_stale_and_over_void() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let filled = OrderFilledSpec::builder()
.trade_id(TradeId::from("TRADE-VOID"))
.last_qty(Quantity::from(100_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order
.apply(OrderEventAny::Accepted(
OrderAcceptedSpec::builder().build(),
))
.unwrap();
order.apply(OrderEventAny::Filled(filled)).unwrap();
order
.apply(OrderEventAny::FillVoided(
OrderFillVoidedSpec::builder()
.trade_id(TradeId::from("TRADE-VOID"))
.voided_qty(Quantity::from(40_000))
.build(),
))
.unwrap();
let duplicate = order.apply(OrderEventAny::FillVoided(
OrderFillVoidedSpec::builder()
.trade_id(TradeId::from("TRADE-VOID"))
.voided_qty(Quantity::from(40_000))
.build(),
));
let stale = order.apply(OrderEventAny::FillVoided(
OrderFillVoidedSpec::builder()
.trade_id(TradeId::from("TRADE-VOID"))
.voided_qty(Quantity::from(30_000))
.build(),
));
let over_void = order.apply(OrderEventAny::FillVoided(
OrderFillVoidedSpec::builder()
.trade_id(TradeId::from("TRADE-VOID"))
.voided_qty(Quantity::from(100_001))
.build(),
));
assert!(matches!(
duplicate,
Err(OrderError::DuplicateFillVoid(trade_id))
if trade_id == TradeId::from("TRADE-VOID")
));
assert!(matches!(
stale,
Err(OrderError::StaleFillVoid(trade_id))
if trade_id == TradeId::from("TRADE-VOID")
));
assert!(matches!(
over_void,
Err(OrderError::OverVoid(trade_id))
if trade_id == TradeId::from("TRADE-VOID")
));
assert_eq!(order.filled_qty(), Quantity::from(60_000));
assert_eq!(order.voided_qty(), Quantity::from(40_000));
}
#[rstest]
fn test_unknown_fill_void_does_not_reverse_surviving_fill() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let filled = OrderFilledSpec::builder()
.trade_id(TradeId::from("TRADE-APPLIED"))
.last_qty(Quantity::from(60_000))
.build();
let voided = OrderFillVoidedSpec::builder()
.trade_id(TradeId::from("TRADE-NEVER-APPLIED"))
.voided_qty(Quantity::from(40_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order
.apply(OrderEventAny::Accepted(
OrderAcceptedSpec::builder().build(),
))
.unwrap();
order.apply(OrderEventAny::Filled(filled)).unwrap();
order.apply(OrderEventAny::FillVoided(voided)).unwrap();
order
.apply(OrderEventAny::FillVoided(
OrderFillVoidedSpec::builder()
.trade_id(TradeId::from("TRADE-APPLIED"))
.voided_qty(Quantity::from(10_000))
.is_reopened(true)
.build(),
))
.unwrap();
let late_fill = order.apply(OrderEventAny::Filled(
OrderFilledSpec::builder()
.trade_id(TradeId::from("TRADE-LATE"))
.last_qty(Quantity::from(10_000))
.build(),
));
assert_eq!(order.status(), OrderStatus::Voided);
assert_eq!(order.filled_qty(), Quantity::from(50_000));
assert_eq!(order.voided_qty(), Quantity::from(50_000));
assert_eq!(order.leaves_qty(), Quantity::zero(0));
assert!(order.is_closed());
assert!(matches!(late_fill, Err(OrderError::InvalidStateTransition)));
}
#[rstest]
fn test_order_state_transition_to_canceled() {
let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
let submitted = OrderSubmittedSpec::builder().build();
let canceled = OrderCanceledSpec::builder().build();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Canceled(canceled)).unwrap();
assert_eq!(order.status(), OrderStatus::Canceled);
assert!(order.is_closed());
assert!(!order.is_open());
}
#[rstest]
fn test_order_life_cycle_to_partially_filled() {
let init = OrderInitializedSpec::builder().build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let filled = OrderFilledSpec::builder()
.last_qty(Quantity::from(50_000))
.build();
let mut order: MarketOrder = init.clone().try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(filled)).unwrap();
assert_eq!(order.client_order_id, init.client_order_id);
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
assert_eq!(order.filled_qty(), Quantity::from(50_000));
assert_eq!(order.leaves_qty(), Quantity::from(50_000));
assert!(order.is_open());
assert!(!order.is_closed());
}
#[rstest]
fn test_order_commission_calculation() {
let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
order
.commissions
.insert(Currency::USD(), Money::new(10.0, Currency::USD()));
assert_eq!(
order.commission(&Currency::USD()),
Some(Money::new(10.0, Currency::USD()))
);
assert_eq!(
order.commissions_vec(),
vec![Money::new(10.0, Currency::USD())]
);
}
#[rstest]
fn test_order_is_primary() {
let order: MarketOrder = OrderInitializedSpec::builder()
.exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
.exec_spawn_id(ClientOrderId::from("O-001"))
.client_order_id(ClientOrderId::from("O-001"))
.build()
.try_into()
.unwrap();
assert!(order.is_primary());
assert!(!order.is_spawned());
}
#[rstest]
fn test_order_is_spawned() {
let order: MarketOrder = OrderInitializedSpec::builder()
.exec_algorithm_id(ExecAlgorithmId::from("ALGO-001"))
.exec_spawn_id(ClientOrderId::from("O-002"))
.client_order_id(ClientOrderId::from("O-001"))
.build()
.try_into()
.unwrap();
assert!(!order.is_primary());
assert!(order.is_spawned());
}
#[rstest]
fn test_order_is_contingency() {
let order: MarketOrder = OrderInitializedSpec::builder()
.contingency_type(ContingencyType::Oto)
.linked_order_ids(vec![ClientOrderId::from("O-LINKED")])
.build()
.try_into()
.unwrap();
assert!(order.is_contingency());
assert!(order.is_parent_order());
assert!(!order.is_child_order());
}
#[rstest]
fn test_order_is_child_order() {
let order: MarketOrder = OrderInitializedSpec::builder()
.parent_order_id(ClientOrderId::from("PARENT-001"))
.build()
.try_into()
.unwrap();
assert!(order.is_child_order());
assert!(!order.is_parent_order());
}
#[rstest]
fn test_to_own_book_order_timestamp_ordering() {
use crate::orders::limit::LimitOrder;
let init = OrderInitializedSpec::builder()
.price(Price::from("100.00"))
.build();
let submitted = OrderSubmittedSpec::builder()
.ts_event(UnixNanos::from(1_000_000))
.build();
let accepted = OrderAcceptedSpec::builder()
.ts_event(UnixNanos::from(2_000_000))
.build();
let mut order: LimitOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
let own_book_order = order.to_own_book_order();
assert_eq!(own_book_order.ts_submitted, UnixNanos::from(1_000_000));
assert_eq!(own_book_order.ts_accepted, UnixNanos::from(2_000_000));
assert_eq!(own_book_order.ts_last, UnixNanos::from(2_000_000));
}
#[rstest]
fn test_to_own_book_order_partial_fill_uses_leaves_qty() {
use crate::orders::limit::LimitOrder;
let init = OrderInitializedSpec::builder()
.price(Price::from("100.00"))
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let filled = OrderFilledSpec::builder()
.order_type(OrderType::Limit)
.last_qty(Quantity::from(40_000))
.last_px(Price::from("100.00"))
.build();
let mut order: LimitOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(filled)).unwrap();
let own_book_order = order.to_own_book_order();
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
assert_eq!(order.leaves_qty(), Quantity::from(60_000));
assert_eq!(own_book_order.size, Quantity::from(60_000));
}
#[rstest]
fn test_order_accepted_without_submitted_sets_account_id() {
let init = OrderInitializedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder()
.account_id(AccountId::from("EXTERNAL-001"))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
assert_eq!(order.account_id(), None);
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
assert_eq!(order.account_id(), Some(AccountId::from("EXTERNAL-001")));
assert_eq!(order.status(), OrderStatus::Accepted);
}
#[rstest]
fn test_order_accepted_after_submitted_preserves_account_id() {
let init = OrderInitializedSpec::builder().build();
let submitted = OrderSubmittedSpec::builder()
.account_id(AccountId::from("SUBMITTED-001"))
.build();
let accepted = OrderAcceptedSpec::builder()
.account_id(AccountId::from("ACCEPTED-001"))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
assert_eq!(order.account_id(), Some(AccountId::from("SUBMITTED-001")));
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
assert_eq!(order.account_id(), Some(AccountId::from("ACCEPTED-001")));
assert_eq!(order.status(), OrderStatus::Accepted);
}
#[rstest]
fn test_overfill_tracks_overfill_qty() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let overfill = OrderFilledSpec::builder()
.last_qty(Quantity::from(110_000)) .build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(overfill)).unwrap();
assert_eq!(order.overfill_qty(), Quantity::from(10_000));
assert_eq!(order.filled_qty(), Quantity::from(110_000));
assert_eq!(order.leaves_qty(), Quantity::from(0));
assert_eq!(order.status(), OrderStatus::Filled);
}
#[rstest]
fn test_partial_fill_then_overfill() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let fill1 = OrderFilledSpec::builder()
.last_qty(Quantity::from(80_000))
.trade_id(TradeId::from("TRADE-1"))
.build();
let fill2 = OrderFilledSpec::builder()
.last_qty(Quantity::from(30_000)) .trade_id(TradeId::from("TRADE-2"))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(fill1)).unwrap();
assert_eq!(order.overfill_qty(), Quantity::from(0));
assert_eq!(order.filled_qty(), Quantity::from(80_000));
assert_eq!(order.leaves_qty(), Quantity::from(20_000));
order.apply(OrderEventAny::Filled(fill2)).unwrap();
assert_eq!(order.overfill_qty(), Quantity::from(10_000));
assert_eq!(order.filled_qty(), Quantity::from(110_000));
assert_eq!(order.leaves_qty(), Quantity::from(0));
assert_eq!(order.status(), OrderStatus::Filled);
}
#[rstest]
fn test_exact_fill_no_overfill() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let filled = OrderFilledSpec::builder()
.last_qty(Quantity::from(100_000)) .build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(filled)).unwrap();
assert_eq!(order.overfill_qty(), Quantity::from(0));
assert_eq!(order.filled_qty(), Quantity::from(100_000));
assert_eq!(order.leaves_qty(), Quantity::from(0));
}
#[rstest]
fn test_partial_fill_then_overfill_with_fractional_quantities() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from("2450.5"))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let fill1 = OrderFilledSpec::builder()
.last_qty(Quantity::from("1202.5"))
.trade_id(TradeId::from("TRADE-1"))
.build();
let fill2 = OrderFilledSpec::builder()
.last_qty(Quantity::from("1285.5")) .trade_id(TradeId::from("TRADE-2"))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(fill1)).unwrap();
assert_eq!(order.overfill_qty(), Quantity::from(0));
assert_eq!(order.filled_qty(), Quantity::from("1202.5"));
assert_eq!(order.leaves_qty(), Quantity::from("1248.0"));
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
order.apply(OrderEventAny::Filled(fill2)).unwrap();
assert_eq!(order.overfill_qty(), Quantity::from("37.5"));
assert_eq!(order.filled_qty(), Quantity::from("2488.0"));
assert_eq!(order.leaves_qty(), Quantity::from(0));
assert_eq!(order.status(), OrderStatus::Filled);
}
#[rstest]
fn test_calculate_overfill_returns_zero_when_no_overfill() {
let order: MarketOrder = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build()
.try_into()
.unwrap();
let overfill = order.calculate_overfill(Quantity::from(50_000));
assert_eq!(overfill, Quantity::from(0));
let overfill = order.calculate_overfill(Quantity::from(100_000));
assert_eq!(overfill, Quantity::from(0));
}
#[rstest]
fn test_calculate_overfill_returns_overfill_amount() {
let order: MarketOrder = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build()
.try_into()
.unwrap();
let overfill = order.calculate_overfill(Quantity::from(110_000));
assert_eq!(overfill, Quantity::from(10_000));
}
#[rstest]
fn test_calculate_overfill_accounts_for_existing_fills() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let partial_fill = OrderFilledSpec::builder()
.last_qty(Quantity::from(60_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
let overfill = order.calculate_overfill(Quantity::from(50_000));
assert_eq!(overfill, Quantity::from(10_000));
let overfill = order.calculate_overfill(Quantity::from(40_000));
assert_eq!(overfill, Quantity::from(0));
}
#[rstest]
fn test_calculate_overfill_with_fractional_quantities() {
let order: MarketOrder = OrderInitializedSpec::builder()
.quantity(Quantity::from("2450.5"))
.build()
.try_into()
.unwrap();
let overfill = order.calculate_overfill(Quantity::from("2488.0"));
assert_eq!(overfill, Quantity::from("37.5"));
}
#[rstest]
fn test_calculate_overfill_zero_after_fractional_partial_fill() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from("1.000"))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let partial_fill = OrderFilledSpec::builder()
.last_qty(Quantity::from("0.072"))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
let overfill = order.calculate_overfill(Quantity::from("0.072"));
assert_eq!(overfill, Quantity::from("0.000"));
}
#[rstest]
fn test_duplicate_fill_rejected() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let fill1 = OrderFilledSpec::builder()
.last_qty(Quantity::from(50_000))
.trade_id(TradeId::from("TRADE-001"))
.build();
let fill2_duplicate = OrderFilledSpec::builder()
.last_qty(Quantity::from(50_000))
.trade_id(TradeId::from("TRADE-001")) .build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(fill1)).unwrap();
assert_eq!(order.filled_qty(), Quantity::from(50_000));
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
let result = order.apply(OrderEventAny::Filled(fill2_duplicate));
assert!(result.is_err());
match result.unwrap_err() {
OrderError::DuplicateFill(trade_id) => {
assert_eq!(trade_id, TradeId::from("TRADE-001"));
}
e => panic!("Expected DuplicateFill error, was: {e:?}"),
}
assert_eq!(order.filled_qty(), Quantity::from(50_000));
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
}
#[rstest]
fn test_check_display_qty_returns_typed_invariant_with_stable_display() {
let error = check_display_qty(Some(Quantity::from(2)), Quantity::from(1)).unwrap_err();
match error {
OrderError::Invariant(CorrectnessError::PredicateViolation { ref message }) => {
assert_eq!(message, "`display_qty` may not exceed `quantity`");
}
other => panic!("Expected typed invariant error, was: {other:?}"),
}
assert_eq!(error.to_string(), "`display_qty` may not exceed `quantity`");
}
#[rstest]
fn test_check_time_in_force_returns_typed_invariant_with_stable_display() {
let error = check_time_in_force(TimeInForce::Gtd, None).unwrap_err();
match error {
OrderError::Invariant(CorrectnessError::PredicateViolation { ref message }) => {
assert_eq!(message, "`expire_time` is required for `GTD` order");
}
other => panic!("Expected typed invariant error, was: {other:?}"),
}
assert_eq!(
error.to_string(),
"`expire_time` is required for `GTD` order"
);
}
#[rstest]
fn test_different_trade_ids_allowed() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let fill1 = OrderFilledSpec::builder()
.last_qty(Quantity::from(50_000))
.trade_id(TradeId::from("TRADE-001"))
.build();
let fill2 = OrderFilledSpec::builder()
.last_qty(Quantity::from(50_000))
.trade_id(TradeId::from("TRADE-002")) .build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(fill1)).unwrap();
order.apply(OrderEventAny::Filled(fill2)).unwrap();
assert_eq!(order.filled_qty(), Quantity::from(100_000));
assert_eq!(order.status(), OrderStatus::Filled);
assert_eq!(order.trade_ids.len(), 2);
}
#[rstest]
fn test_pending_update_order_restores_status_on_updated() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let pending_update = OrderPendingUpdateSpec::builder().build();
let updated = OrderUpdatedSpec::builder()
.quantity(Quantity::from(50_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
assert_eq!(order.status(), OrderStatus::Accepted);
order
.apply(OrderEventAny::PendingUpdate(pending_update))
.unwrap();
assert_eq!(order.status(), OrderStatus::PendingUpdate);
order.apply(OrderEventAny::Updated(updated)).unwrap();
assert_eq!(order.status(), OrderStatus::Accepted);
assert_eq!(order.quantity(), Quantity::from(50_000));
}
#[rstest]
fn test_pending_update_accepts_delayed_submitted() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder()
.ts_event(UnixNanos::from(1_000))
.build();
let accepted = OrderAcceptedSpec::builder().build();
let pending_update = OrderPendingUpdateSpec::builder().build();
let delayed_submitted = OrderSubmittedSpec::builder()
.account_id(AccountId::from("SIM-002"))
.ts_event(UnixNanos::from(3_000))
.build();
let updated = OrderUpdatedSpec::builder()
.quantity(Quantity::from(50_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order
.apply(OrderEventAny::PendingUpdate(pending_update))
.unwrap();
assert_eq!(order.status(), OrderStatus::PendingUpdate);
assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
assert_eq!(order.account_id(), Some(AccountId::from("SIM-001")));
assert_eq!(order.ts_submitted(), Some(UnixNanos::from(1_000)));
order
.apply(OrderEventAny::Submitted(delayed_submitted))
.unwrap();
assert_eq!(order.status(), OrderStatus::PendingUpdate);
assert_eq!(order.previous_status(), Some(OrderStatus::Accepted));
assert_eq!(order.account_id(), Some(AccountId::from("SIM-002")));
assert_eq!(order.ts_submitted(), Some(UnixNanos::from(3_000)));
order.apply(OrderEventAny::Updated(updated)).unwrap();
assert_eq!(order.status(), OrderStatus::Accepted);
assert_eq!(order.quantity(), Quantity::from(50_000));
}
#[rstest]
fn test_rejected_event_preserves_status_restoration() {
let init = OrderInitializedSpec::builder().build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let invalid = OrderSubmittedSpec::builder().build();
let pending_update = OrderPendingUpdateSpec::builder().build();
let modify_rejected = OrderModifyRejectedSpec::builder().build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
let previous_status = order.previous_status();
let result = order.apply(OrderEventAny::Submitted(invalid));
assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
assert_eq!(order.previous_status(), previous_status);
order
.apply(OrderEventAny::PendingUpdate(pending_update))
.unwrap();
order
.apply(OrderEventAny::ModifyRejected(modify_rejected))
.unwrap();
assert_eq!(order.status(), OrderStatus::Accepted);
}
#[rstest]
fn test_rejected_event_preserves_cancel_rejected_restoration() {
let init = OrderInitializedSpec::builder().build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let invalid = OrderSubmittedSpec::builder().build();
let pending_cancel = OrderPendingCancelSpec::builder().build();
let cancel_rejected = OrderCancelRejectedSpec::builder().build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
let previous_status = order.previous_status();
let result = order.apply(OrderEventAny::Submitted(invalid));
assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
assert_eq!(order.previous_status(), previous_status);
order
.apply(OrderEventAny::PendingCancel(pending_cancel))
.unwrap();
order
.apply(OrderEventAny::CancelRejected(cancel_rejected))
.unwrap();
assert_eq!(order.status(), OrderStatus::Accepted);
}
#[rstest]
#[case(true, false, "client_order_id")]
#[case(false, true, "strategy_id")]
fn test_update_identity_mismatch_precedes_field_validation(
#[case] wrong_client: bool,
#[case] wrong_strategy: bool,
#[case] expected_field: &str,
) {
let init = OrderInitializedSpec::builder().build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
let state = (
order.status(),
order.previous_status(),
order.ts_last(),
order.events().len(),
);
let client_order_id = if wrong_client {
ClientOrderId::from("O-INVALID")
} else {
order.client_order_id()
};
let strategy_id = if wrong_strategy {
StrategyId::from("OTHER-001")
} else {
order.strategy_id()
};
let updated = OrderUpdatedSpec::builder()
.client_order_id(client_order_id)
.strategy_id(strategy_id)
.price(Price::new(95.0, 2))
.build();
let result = order.apply(OrderEventAny::Updated(updated));
let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
else {
panic!("expected an identity predicate violation, was {result:?}");
};
assert!(
message.contains(expected_field),
"message did not name {expected_field}: {message}"
);
assert_eq!(order.status(), state.0);
assert_eq!(order.previous_status(), state.1);
assert_eq!(order.ts_last(), state.2);
assert_eq!(order.events().len(), state.3);
}
#[rstest]
fn test_update_invalid_transition_precedes_field_validation() {
let init = OrderInitializedSpec::builder().build();
let denied = OrderDeniedSpec::builder().build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Denied(denied)).unwrap();
let state = (
order.status(),
order.previous_status(),
order.ts_last(),
order.events().len(),
);
let updated = OrderUpdatedSpec::builder()
.price(Price::new(95.0, 2))
.build();
let result = order.apply(OrderEventAny::Updated(updated));
assert!(matches!(result, Err(OrderError::InvalidStateTransition)));
assert_eq!(order.status(), state.0);
assert_eq!(order.previous_status(), state.1);
assert_eq!(order.ts_last(), state.2);
assert_eq!(order.events().len(), state.3);
}
#[rstest]
fn test_direct_update_panics_on_invalid_field_before_mutation() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
let original_quantity = order.quantity();
let updated = OrderUpdatedSpec::builder()
.quantity(Quantity::from(50_000))
.price(Price::new(95.0, 2))
.build();
let result =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| order.update(&updated)));
assert!(result.is_err());
assert_eq!(order.quantity(), original_quantity);
}
#[rstest]
fn test_partially_filled_order_can_be_updated() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::from(100_000))
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let partial_fill = OrderFilledSpec::builder()
.last_qty(Quantity::from(40_000))
.build();
let updated = OrderUpdatedSpec::builder()
.quantity(Quantity::from(80_000)) .build();
let mut order: MarketOrder = init.try_into().unwrap();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(partial_fill)).unwrap();
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
assert_eq!(order.filled_qty(), Quantity::from(40_000));
order.apply(OrderEventAny::Updated(updated)).unwrap();
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
assert_eq!(order.quantity(), Quantity::from(80_000));
assert_eq!(order.leaves_qty(), Quantity::from(40_000)); }
#[rstest]
fn test_triggered_order_can_be_updated() {
let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let triggered = OrderTriggeredSpec::builder().build();
let updated = OrderUpdatedSpec::builder()
.quantity(Quantity::from(80_000))
.build();
let mut order = OrderTestBuilder::new(OrderType::StopLimit)
.instrument_id(instrument_id)
.quantity(Quantity::from(100_000))
.price(Price::from("0.99500"))
.trigger_price(Price::from("1.00000"))
.trigger_type(TriggerType::LastPrice)
.build();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Triggered(triggered)).unwrap();
assert_eq!(order.status(), OrderStatus::Triggered);
order.apply(OrderEventAny::Updated(updated)).unwrap();
assert_eq!(order.status(), OrderStatus::Triggered);
assert_eq!(order.quantity(), Quantity::from(80_000));
}
#[rstest]
fn test_order_updated_with_is_quote_quantity_clears_flag() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::new(10.0, 6))
.quote_quantity(true)
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let updated = OrderUpdatedSpec::builder()
.quantity(Quantity::new(47.393_365, 6))
.is_quote_quantity(false)
.build();
let mut order: MarketOrder = init.try_into().unwrap();
assert!(order.is_quote_quantity());
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Updated(updated)).unwrap();
assert!(!order.is_quote_quantity());
assert_eq!(order.quantity(), Quantity::new(47.393_365, 6));
assert_eq!(order.leaves_qty(), Quantity::new(47.393_365, 6));
}
#[rstest]
fn test_order_updated_default_is_quote_quantity_clears_flag() {
let init = OrderInitializedSpec::builder()
.quantity(Quantity::new(10.0, 6))
.quote_quantity(true)
.build();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let updated = OrderUpdatedSpec::builder()
.quantity(Quantity::new(8.0, 6))
.build();
let mut order: MarketOrder = init.try_into().unwrap();
assert!(order.is_quote_quantity());
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Updated(updated)).unwrap();
assert!(!order.is_quote_quantity());
assert_eq!(order.quantity(), Quantity::new(8.0, 6));
}
#[rstest]
fn test_canceled_then_partial_fill_then_canceled() {
let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let canceled1 = OrderCanceledSpec::builder().build();
let fill = OrderFilledSpec::builder()
.last_qty(Quantity::from(50_000))
.trade_id(TradeId::from("FILL-1"))
.build();
let canceled2 = OrderCanceledSpec::builder().build();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Canceled(canceled1)).unwrap();
assert_eq!(order.status(), OrderStatus::Canceled);
assert!(order.is_closed());
order.apply(OrderEventAny::Filled(fill)).unwrap();
assert_eq!(order.status(), OrderStatus::PartiallyFilled);
assert_eq!(order.filled_qty(), Quantity::from(50_000));
assert!(order.is_open());
order.apply(OrderEventAny::Canceled(canceled2)).unwrap();
assert_eq!(order.status(), OrderStatus::Canceled);
assert!(order.is_closed());
}
#[rstest]
fn test_updated_restores_status_before_pending_cancel() {
let mut order: MarketOrder = OrderInitializedSpec::builder().build().try_into().unwrap();
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let pending_cancel = OrderPendingCancelSpec::builder().build();
let updated = OrderUpdatedSpec::builder()
.quantity(Quantity::from(150_000))
.build();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
let ts_accepted = order.ts_accepted();
let venue_order_ids: Vec<VenueOrderId> =
order.venue_order_ids().into_iter().copied().collect();
order
.apply(OrderEventAny::PendingCancel(pending_cancel))
.unwrap();
order.apply(OrderEventAny::Updated(updated)).unwrap();
assert_eq!(order.status(), OrderStatus::Accepted);
assert_eq!(order.quantity(), Quantity::from(150_000));
assert_eq!(order.ts_accepted(), ts_accepted);
assert_eq!(
order
.venue_order_ids()
.into_iter()
.copied()
.collect::<Vec<_>>(),
venue_order_ids,
);
}
#[rstest]
fn test_apply_triggered_to_stop_market_order_returns_error() {
let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let triggered = OrderTriggeredSpec::builder().build();
let mut order = OrderTestBuilder::new(OrderType::StopMarket)
.instrument_id(instrument_id)
.quantity(Quantity::from(1))
.trigger_price(Price::from("1.00000"))
.trigger_type(TriggerType::LastPrice)
.build();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
let result = order.apply(OrderEventAny::Triggered(triggered));
assert!(result.is_err());
assert_eq!(order.status(), OrderStatus::Accepted);
}
#[rstest]
fn test_apply_triggered_to_stop_limit_order_succeeds() {
let instrument_id = InstrumentId::from("ETHUSDT-LINEAR.BYBIT");
let submitted = OrderSubmittedSpec::builder().build();
let accepted = OrderAcceptedSpec::builder().build();
let triggered = OrderTriggeredSpec::builder().build();
let mut order = OrderTestBuilder::new(OrderType::StopLimit)
.instrument_id(instrument_id)
.quantity(Quantity::from(1))
.price(Price::from("0.99500"))
.trigger_price(Price::from("1.00000"))
.trigger_type(TriggerType::LastPrice)
.build();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Triggered(triggered)).unwrap();
assert_eq!(order.status(), OrderStatus::Triggered);
}
#[rstest]
fn test_to_order_status_report_for_accepted_limit_order() {
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("AUDUSD.SIM"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
let accepted = TestOrderStubs::make_accepted_order(&order);
let report = accepted.to_order_status_report(None).unwrap();
assert_eq!(report.account_id, AccountId::from("SIM-001"));
assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
assert_eq!(report.client_order_id, Some(accepted.client_order_id()));
assert_eq!(report.venue_order_id, VenueOrderId::from("V-001"));
assert_eq!(report.order_side, OrderSide::Buy);
assert_eq!(report.order_type, OrderType::Limit);
assert_eq!(report.time_in_force, accepted.time_in_force());
assert_eq!(report.order_status, OrderStatus::Accepted);
assert_eq!(report.quantity, Quantity::from(100_000));
assert_eq!(report.filled_qty, Quantity::from(0));
assert_eq!(report.price, Some(Price::from("1.00000")));
assert_eq!(report.avg_px, None);
assert_eq!(report.ts_accepted, accepted.ts_accepted().unwrap());
assert_eq!(report.ts_last, accepted.ts_last());
assert_eq!(report.ts_init, accepted.ts_init());
}
#[rstest]
fn test_to_order_status_report_for_filled_market_order(audusd_sim: CurrencyPair) {
let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
let order = OrderTestBuilder::new(OrderType::Market)
.instrument_id(audusd_sim.id())
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.build();
let filled = TestOrderStubs::make_filled_order(&order, &audusd_sim, LiquiditySide::Maker);
let report_id = UUID4::new();
let report = filled.to_order_status_report(Some(report_id)).unwrap();
assert_eq!(report.report_id, report_id);
assert_eq!(report.order_status, OrderStatus::Filled);
assert_eq!(report.quantity, Quantity::from(100_000));
assert_eq!(report.filled_qty, Quantity::from(100_000));
assert_eq!(report.price, None);
assert_eq!(report.avg_px, Some(dec!(1)));
assert_eq!(report.venue_position_id, Some(PositionId::from("1")));
assert_eq!(report.ts_accepted, filled.ts_accepted().unwrap());
assert_eq!(report.ts_last, filled.ts_last());
}
#[rstest]
fn test_to_order_status_report_maps_optional_fields() {
let order = OrderTestBuilder::new(OrderType::StopLimit)
.instrument_id(InstrumentId::from("AUDUSD.SIM"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.price(Price::from("0.99500"))
.trigger_price(Price::from("1.00000"))
.trigger_type(TriggerType::LastPrice)
.time_in_force(TimeInForce::Gtd)
.expire_time(UnixNanos::from(5_000_000_000))
.display_qty(Quantity::from(10_000))
.post_only(true)
.reduce_only(true)
.contingency_type(ContingencyType::Oto)
.order_list_id(OrderListId::from("OL-001"))
.linked_order_ids(vec![ClientOrderId::from("O-CHILD")])
.parent_order_id(ClientOrderId::from("O-PARENT"))
.build();
let accepted = TestOrderStubs::make_accepted_order(&order);
let report = accepted.to_order_status_report(None).unwrap();
assert_eq!(report.price, Some(Price::from("0.99500")));
assert_eq!(report.trigger_price, Some(Price::from("1.00000")));
assert_eq!(report.trigger_type, Some(TriggerType::LastPrice));
assert_eq!(report.expire_time, Some(UnixNanos::from(5_000_000_000)));
assert_eq!(report.display_qty, Some(Quantity::from(10_000)));
assert!(report.post_only);
assert!(report.reduce_only);
assert_eq!(report.contingency_type, ContingencyType::Oto);
assert_eq!(report.order_list_id, Some(OrderListId::from("OL-001")));
assert_eq!(
report.linked_order_ids,
Some(vec![ClientOrderId::from("O-CHILD")])
);
assert_eq!(
report.parent_order_id,
Some(ClientOrderId::from("O-PARENT"))
);
}
#[rstest]
fn test_to_order_status_report_maps_ts_triggered() {
let order = OrderTestBuilder::new(OrderType::StopLimit)
.instrument_id(InstrumentId::from("AUDUSD.SIM"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("0.99500"))
.trigger_price(Price::from("1.00000"))
.trigger_type(TriggerType::LastPrice)
.build();
let mut order = TestOrderStubs::make_accepted_order(&order);
let triggered = OrderTriggeredSpec::builder()
.ts_event(UnixNanos::from(1_500_000_000))
.build();
order.apply(OrderEventAny::Triggered(triggered)).unwrap();
let report = order.to_order_status_report(None).unwrap();
assert_eq!(report.order_status, OrderStatus::Triggered);
assert_eq!(report.ts_triggered, Some(UnixNanos::from(1_500_000_000)));
}
#[rstest]
fn test_to_order_status_report_maps_rejection_reason() {
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("AUDUSD.SIM"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
let mut order = TestOrderStubs::make_accepted_order(&order);
let rejected = OrderRejectedSpec::builder()
.reason(Ustr::from("INSUFFICIENT_MARGIN"))
.build();
order.apply(OrderEventAny::Rejected(rejected)).unwrap();
let report = order.to_order_status_report(None).unwrap();
assert_eq!(report.order_status, OrderStatus::Rejected);
assert_eq!(
report.cancel_reason,
Some("INSUFFICIENT_MARGIN".to_string())
);
}
#[rstest]
fn test_to_order_status_report_maps_distinct_timestamps() {
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("AUDUSD.SIM"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.ts_init(UnixNanos::from(1_000))
.build();
let submitted = OrderSubmittedSpec::builder()
.ts_event(UnixNanos::from(2_000))
.build();
let accepted = OrderAcceptedSpec::builder()
.ts_event(UnixNanos::from(3_000))
.build();
let filled = OrderFilledSpec::builder()
.last_qty(Quantity::from(50_000))
.ts_event(UnixNanos::from(4_000))
.build();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Accepted(accepted)).unwrap();
order.apply(OrderEventAny::Filled(filled)).unwrap();
let report = order.to_order_status_report(None).unwrap();
assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
assert_eq!(report.filled_qty, Quantity::from(50_000));
assert_eq!(report.ts_accepted, UnixNanos::from(3_000));
assert_eq!(report.ts_last, UnixNanos::from(4_000));
assert_eq!(report.ts_init, UnixNanos::from(1_000));
}
#[rstest]
fn test_to_order_status_report_ts_accepted_falls_back_to_ts_last() {
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("AUDUSD.SIM"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
let submitted = OrderSubmittedSpec::builder()
.ts_event(UnixNanos::from(2_000))
.build();
let updated = OrderUpdatedSpec::builder()
.venue_order_id(VenueOrderId::from("V-001"))
.ts_event(UnixNanos::from(3_000))
.build();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
order.apply(OrderEventAny::Updated(updated)).unwrap();
let report = order.to_order_status_report(None).unwrap();
assert_eq!(report.order_status, OrderStatus::Submitted);
assert_eq!(report.venue_order_id, VenueOrderId::from("V-001"));
assert_eq!(report.ts_accepted, UnixNanos::from(3_000));
assert_eq!(report.ts_last, UnixNanos::from(3_000));
}
#[rstest]
fn test_to_order_status_report_maps_trailing_offsets() {
let order = OrderTestBuilder::new(OrderType::TrailingStopLimit)
.instrument_id(InstrumentId::from("AUDUSD.SIM"))
.side(OrderSide::Sell)
.quantity(Quantity::from(100_000))
.price(Price::from("0.99500"))
.activation_price(Price::from("1.00500"))
.trigger_price(Price::from("1.00000"))
.trigger_type(TriggerType::LastPrice)
.limit_offset(dec!(0.0001))
.trailing_offset(dec!(0.0002))
.trailing_offset_type(TrailingOffsetType::Price)
.build();
let accepted = TestOrderStubs::make_accepted_order(&order);
let report = accepted.to_order_status_report(None).unwrap();
assert_eq!(report.activation_price, Some(Price::from("1.00500")));
assert_eq!(report.limit_offset, Some(dec!(0.0001)));
assert_eq!(report.trailing_offset, Some(dec!(0.0002)));
assert_eq!(report.trailing_offset_type, TrailingOffsetType::Price);
}
#[rstest]
fn test_to_order_status_report_returns_none_before_venue_ack() {
let order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("AUDUSD.SIM"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
assert!(order.to_order_status_report(None).is_none());
}
#[rstest]
fn test_to_order_status_report_returns_none_for_submitted_order() {
let mut order = OrderTestBuilder::new(OrderType::Limit)
.instrument_id(InstrumentId::from("AUDUSD.SIM"))
.side(OrderSide::Buy)
.quantity(Quantity::from(100_000))
.price(Price::from("1.00000"))
.build();
let submitted = OrderSubmittedSpec::builder().build();
order.apply(OrderEventAny::Submitted(submitted)).unwrap();
assert!(order.account_id().is_some());
assert!(order.to_order_status_report(None).is_none());
}
}