#[cfg(feature = "event-log")]
use crate::event::Event;
use crate::{
Order, OrderBook, OrderId, OrderStatus, Price, Quantity, Side, TimeInForce, Trade,
error::ValidationError,
result::{
CancelError, CancelResult, ModifyError, ModifyResult, StopSubmitResult, SubmitResult,
},
snapshot::BookSnapshot,
stop::{StopBook, StopOrder, StopStatus, TrailMethod},
};
#[derive(Clone, Debug)]
pub struct Exchange {
pub(crate) book: OrderBook,
pub(crate) trades: Vec<Trade>,
pub(crate) stop_book: StopBook,
pub(crate) last_trade_price: Option<Price>,
#[cfg(feature = "event-log")]
pub(crate) events: Vec<crate::event::Event>,
}
impl Exchange {
pub fn new() -> Self {
Self {
book: OrderBook::new(),
trades: Vec::new(),
stop_book: StopBook::new(),
last_trade_price: None,
#[cfg(feature = "event-log")]
events: Vec::new(),
}
}
pub fn submit_limit(
&mut self,
side: Side,
price: Price,
quantity: Quantity,
tif: TimeInForce,
) -> SubmitResult {
#[cfg(feature = "event-log")]
self.events.push(Event::SubmitLimit {
side,
price,
quantity,
time_in_force: tif,
});
let result = self.submit_limit_internal(side, price, quantity, tif);
if !result.trades.is_empty() {
let last_price = result.trades.last().unwrap().price;
self.last_trade_price = Some(last_price);
self.process_trade_triggers();
}
result
}
pub fn submit_market(&mut self, side: Side, quantity: Quantity) -> SubmitResult {
#[cfg(feature = "event-log")]
self.events.push(Event::SubmitMarket { side, quantity });
let price = match side {
Side::Buy => Price::MAX,
Side::Sell => Price::MIN,
};
let result = self.submit_limit_internal(side, price, quantity, TimeInForce::IOC);
if !result.trades.is_empty() {
let last_price = result.trades.last().unwrap().price;
self.last_trade_price = Some(last_price);
self.process_trade_triggers();
}
result
}
pub fn try_submit_limit(
&mut self,
side: Side,
price: Price,
quantity: Quantity,
tif: TimeInForce,
) -> Result<SubmitResult, ValidationError> {
if quantity == 0 {
return Err(ValidationError::ZeroQuantity);
}
if price.0 <= 0 {
return Err(ValidationError::ZeroPrice);
}
Ok(self.submit_limit(side, price, quantity, tif))
}
pub fn try_submit_market(
&mut self,
side: Side,
quantity: Quantity,
) -> Result<SubmitResult, ValidationError> {
if quantity == 0 {
return Err(ValidationError::ZeroQuantity);
}
Ok(self.submit_market(side, quantity))
}
pub(crate) fn submit_limit_internal(
&mut self,
side: Side,
price: Price,
quantity: Quantity,
tif: TimeInForce,
) -> SubmitResult {
if tif == TimeInForce::FOK && !self.book.can_fully_fill(side, price, quantity) {
let order = self.book.create_order(side, price, quantity, tif);
return SubmitResult {
order_id: order.id,
status: OrderStatus::Cancelled,
trades: Vec::new(),
filled_quantity: 0,
resting_quantity: 0,
cancelled_quantity: quantity,
};
}
let mut order = self.book.create_order(side, price, quantity, tif);
let order_id = order.id;
let match_result = self.book.match_order(&mut order);
self.trades.extend(match_result.trades.iter().cloned());
let filled = order.filled_quantity;
let remaining = order.remaining_quantity;
let (status, resting, cancelled) = if remaining == 0 {
order.status = OrderStatus::Filled;
self.book.orders.insert(order_id, order);
(OrderStatus::Filled, 0, 0)
} else if tif == TimeInForce::GTC {
let status = if filled > 0 {
OrderStatus::PartiallyFilled
} else {
OrderStatus::New
};
order.status = status;
self.book.add_order(order);
(status, remaining, 0)
} else {
let status = if filled > 0 {
OrderStatus::PartiallyFilled
} else {
OrderStatus::Cancelled
};
order.status = status;
self.book.orders.insert(order_id, order);
(status, 0, remaining)
};
SubmitResult {
order_id,
status,
trades: match_result.trades,
filled_quantity: filled,
resting_quantity: resting,
cancelled_quantity: cancelled,
}
}
pub fn cancel(&mut self, order_id: OrderId) -> CancelResult {
#[cfg(feature = "event-log")]
self.events.push(Event::Cancel { order_id });
self.cancel_internal(order_id)
}
pub(crate) fn cancel_internal(&mut self, order_id: OrderId) -> CancelResult {
if self.stop_book.contains_pending(order_id) {
if let Some(stop) = self.stop_book.get(order_id) {
let qty = stop.quantity;
self.stop_book.cancel(order_id);
return CancelResult::success(qty);
}
}
let order = match self.book.get_order(order_id) {
Some(o) => o,
None => return CancelResult::failure(CancelError::OrderNotFound),
};
if !order.is_active() {
return CancelResult::failure(CancelError::OrderNotActive);
}
match self.book.cancel_order(order_id) {
Some(qty) => CancelResult::success(qty),
None => CancelResult::failure(CancelError::OrderNotActive),
}
}
pub fn modify(
&mut self,
order_id: OrderId,
new_price: Price,
new_quantity: Quantity,
) -> ModifyResult {
#[cfg(feature = "event-log")]
self.events.push(Event::Modify {
order_id,
new_price,
new_quantity,
});
self.modify_internal(order_id, new_price, new_quantity)
}
pub(crate) fn modify_internal(
&mut self,
order_id: OrderId,
new_price: Price,
new_quantity: Quantity,
) -> ModifyResult {
if new_quantity == 0 {
return ModifyResult::failure(order_id, ModifyError::InvalidQuantity);
}
let (side, tif) = match self.book.get_order(order_id) {
Some(o) if o.is_active() => (o.side, o.time_in_force),
Some(_) => return ModifyResult::failure(order_id, ModifyError::OrderNotActive),
None => return ModifyResult::failure(order_id, ModifyError::OrderNotFound),
};
let cancelled = match self.book.cancel_order(order_id) {
Some(qty) => qty,
None => return ModifyResult::failure(order_id, ModifyError::OrderNotActive),
};
let result = self.submit_limit_internal(side, new_price, new_quantity, tif);
ModifyResult::success(order_id, result.order_id, cancelled, result.trades)
}
const MAX_CASCADE_DEPTH: usize = 100;
pub fn submit_stop_market(
&mut self,
side: Side,
stop_price: Price,
quantity: Quantity,
) -> StopSubmitResult {
#[cfg(feature = "event-log")]
self.events.push(Event::SubmitStopMarket {
side,
stop_price,
quantity,
});
self.submit_stop_internal(side, stop_price, None, quantity, TimeInForce::GTC)
}
pub fn submit_stop_limit(
&mut self,
side: Side,
stop_price: Price,
limit_price: Price,
quantity: Quantity,
tif: TimeInForce,
) -> StopSubmitResult {
#[cfg(feature = "event-log")]
self.events.push(Event::SubmitStopLimit {
side,
stop_price,
limit_price,
quantity,
time_in_force: tif,
});
self.submit_stop_internal(side, stop_price, Some(limit_price), quantity, tif)
}
pub fn submit_trailing_stop_market(
&mut self,
side: Side,
initial_stop_price: Price,
quantity: Quantity,
trail_method: TrailMethod,
) -> StopSubmitResult {
#[cfg(feature = "event-log")]
self.events.push(Event::SubmitTrailingStopMarket {
side,
stop_price: initial_stop_price,
quantity,
trail_method: trail_method.clone(),
});
self.submit_trailing_stop_internal(
side,
initial_stop_price,
None,
quantity,
TimeInForce::GTC,
trail_method,
)
}
pub fn submit_trailing_stop_limit(
&mut self,
side: Side,
initial_stop_price: Price,
limit_price: Price,
quantity: Quantity,
tif: TimeInForce,
trail_method: TrailMethod,
) -> StopSubmitResult {
#[cfg(feature = "event-log")]
self.events.push(Event::SubmitTrailingStopLimit {
side,
stop_price: initial_stop_price,
limit_price,
quantity,
time_in_force: tif,
trail_method: trail_method.clone(),
});
self.submit_trailing_stop_internal(
side,
initial_stop_price,
Some(limit_price),
quantity,
tif,
trail_method,
)
}
pub(crate) fn submit_trailing_stop_internal(
&mut self,
side: Side,
stop_price: Price,
limit_price: Option<Price>,
quantity: Quantity,
tif: TimeInForce,
trail_method: TrailMethod,
) -> StopSubmitResult {
self.insert_stop_order(
side,
stop_price,
limit_price,
quantity,
tif,
Some(trail_method),
)
}
pub(crate) fn submit_stop_internal(
&mut self,
side: Side,
stop_price: Price,
limit_price: Option<Price>,
quantity: Quantity,
tif: TimeInForce,
) -> StopSubmitResult {
self.insert_stop_order(side, stop_price, limit_price, quantity, tif, None)
}
fn insert_stop_order(
&mut self,
side: Side,
stop_price: Price,
limit_price: Option<Price>,
quantity: Quantity,
tif: TimeInForce,
trail_method: Option<TrailMethod>,
) -> StopSubmitResult {
let id = self.book.next_order_id();
let timestamp = self.book.next_timestamp();
let is_trailing = trail_method.is_some();
let order = StopOrder {
id,
side,
stop_price,
limit_price,
quantity,
time_in_force: tif,
timestamp,
status: StopStatus::Pending,
trail_method,
watermark: None,
};
self.stop_book.insert(order);
if !is_trailing {
if let Some(last_price) = self.last_trade_price {
let should_trigger = match side {
Side::Buy => last_price >= stop_price,
Side::Sell => last_price <= stop_price,
};
if should_trigger {
self.process_trade_triggers();
let status = self
.stop_book
.get(id)
.map(|o| o.status)
.unwrap_or(StopStatus::Triggered);
return StopSubmitResult {
order_id: id,
status,
};
}
}
}
StopSubmitResult {
order_id: id,
status: StopStatus::Pending,
}
}
pub(crate) fn process_trade_triggers(&mut self) {
for _ in 0..Self::MAX_CASCADE_DEPTH {
let trade_price = match self.last_trade_price {
Some(p) => p,
None => return,
};
self.stop_book.update_trailing_stops(trade_price);
let triggered = self.stop_book.collect_triggered(trade_price);
if triggered.is_empty() {
return;
}
let mut new_last_price = None;
for stop in triggered {
let result = match stop.limit_price {
Some(limit) => self.submit_limit_internal(
stop.side,
limit,
stop.quantity,
stop.time_in_force,
),
None => {
let price = match stop.side {
Side::Buy => Price::MAX,
Side::Sell => Price::MIN,
};
self.submit_limit_internal(
stop.side,
price,
stop.quantity,
TimeInForce::IOC,
)
}
};
if let Some(last_trade) = result.trades.last() {
new_last_price = Some(last_trade.price);
}
}
match new_last_price {
Some(p) => self.last_trade_price = Some(p),
None => return, }
}
}
pub fn get_order(&self, order_id: OrderId) -> Option<&Order> {
self.book.get_order(order_id)
}
pub fn best_bid_ask(&self) -> (Option<Price>, Option<Price>) {
self.book.best_bid_ask()
}
pub fn best_bid(&self) -> Option<Price> {
self.book.best_bid()
}
pub fn best_ask(&self) -> Option<Price> {
self.book.best_ask()
}
pub fn spread(&self) -> Option<i64> {
self.book.spread()
}
pub fn depth(&self, levels: usize) -> BookSnapshot {
self.book.snapshot(levels)
}
pub fn full_book(&self) -> BookSnapshot {
self.book.full_snapshot()
}
pub fn trades(&self) -> &[Trade] {
&self.trades
}
pub fn book(&self) -> &OrderBook {
&self.book
}
pub fn book_mut(&mut self) -> &mut OrderBook {
&mut self.book
}
pub fn get_stop_order(&self, order_id: OrderId) -> Option<&StopOrder> {
self.stop_book.get(order_id)
}
pub fn pending_stop_count(&self) -> usize {
self.stop_book.pending_count()
}
pub fn last_trade_price(&self) -> Option<Price> {
self.last_trade_price
}
pub fn stop_book(&self) -> &StopBook {
&self.stop_book
}
pub fn clear_trades(&mut self) {
self.trades.clear();
}
pub fn clear_order_history(&mut self) -> usize {
self.stop_book.clear_history();
self.book.clear_history()
}
pub fn compact(&mut self) {
self.book.compact();
}
}
impl Default for Exchange {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn submit_limit_no_match() {
let mut exchange = Exchange::new();
let result = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
assert_eq!(result.order_id, OrderId(1));
assert_eq!(result.status, OrderStatus::New);
assert!(result.trades.is_empty());
assert_eq!(result.filled_quantity, 0);
assert_eq!(result.resting_quantity, 100);
assert_eq!(result.cancelled_quantity, 0);
assert_eq!(exchange.best_bid(), Some(Price(100_00)));
}
#[test]
fn submit_limit_full_fill() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 100, TimeInForce::GTC);
let result = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
assert_eq!(result.status, OrderStatus::Filled);
assert_eq!(result.trades.len(), 1);
assert_eq!(result.filled_quantity, 100);
assert_eq!(result.resting_quantity, 0);
assert_eq!(result.cancelled_quantity, 0);
assert_eq!(exchange.best_bid(), None);
assert_eq!(exchange.best_ask(), None);
}
#[test]
fn submit_limit_partial_fill_gtc() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 30, TimeInForce::GTC);
let result = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
assert_eq!(result.status, OrderStatus::PartiallyFilled);
assert_eq!(result.filled_quantity, 30);
assert_eq!(result.resting_quantity, 70);
assert_eq!(result.cancelled_quantity, 0);
assert_eq!(exchange.best_bid(), Some(Price(100_00)));
}
#[test]
fn submit_ioc_full_fill() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 100, TimeInForce::GTC);
let result = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::IOC);
assert_eq!(result.status, OrderStatus::Filled);
assert_eq!(result.filled_quantity, 100);
assert_eq!(result.resting_quantity, 0);
}
#[test]
fn submit_ioc_partial_fill() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 30, TimeInForce::GTC);
let result = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::IOC);
assert_eq!(result.status, OrderStatus::PartiallyFilled);
assert_eq!(result.filled_quantity, 30);
assert_eq!(result.resting_quantity, 0); assert_eq!(result.cancelled_quantity, 70);
assert_eq!(exchange.best_bid(), None);
}
#[test]
fn submit_ioc_no_fill() {
let mut exchange = Exchange::new();
let result = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::IOC);
assert_eq!(result.status, OrderStatus::Cancelled);
assert_eq!(result.filled_quantity, 0);
assert_eq!(result.cancelled_quantity, 100);
assert_eq!(exchange.best_bid(), None);
}
#[test]
fn submit_fok_full_fill() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 100, TimeInForce::GTC);
let result = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::FOK);
assert_eq!(result.status, OrderStatus::Filled);
assert_eq!(result.filled_quantity, 100);
assert_eq!(result.trades.len(), 1);
}
#[test]
fn submit_fok_rejected_insufficient_liquidity() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 50, TimeInForce::GTC);
let result = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::FOK);
assert_eq!(result.status, OrderStatus::Cancelled);
assert_eq!(result.filled_quantity, 0);
assert_eq!(result.cancelled_quantity, 100);
assert!(result.trades.is_empty());
assert_eq!(exchange.best_ask(), Some(Price(100_00)));
}
#[test]
fn submit_fok_rejected_no_liquidity() {
let mut exchange = Exchange::new();
let result = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::FOK);
assert_eq!(result.status, OrderStatus::Cancelled);
assert!(result.trades.is_empty());
}
#[test]
fn submit_market_full_fill() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 100, TimeInForce::GTC);
let result = exchange.submit_market(Side::Buy, 100);
assert_eq!(result.status, OrderStatus::Filled);
assert_eq!(result.filled_quantity, 100);
}
#[test]
fn submit_market_partial_fill() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 50, TimeInForce::GTC);
let result = exchange.submit_market(Side::Buy, 100);
assert_eq!(result.status, OrderStatus::PartiallyFilled);
assert_eq!(result.filled_quantity, 50);
assert_eq!(result.cancelled_quantity, 50);
}
#[test]
fn submit_market_no_liquidity() {
let mut exchange = Exchange::new();
let result = exchange.submit_market(Side::Buy, 100);
assert_eq!(result.status, OrderStatus::Cancelled);
assert_eq!(result.filled_quantity, 0);
}
#[test]
fn cancel_order() {
let mut exchange = Exchange::new();
let submit = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
let result = exchange.cancel(submit.order_id);
assert!(result.success);
assert_eq!(result.cancelled_quantity, 100);
assert_eq!(exchange.best_bid(), None);
}
#[test]
fn cancel_nonexistent() {
let mut exchange = Exchange::new();
let result = exchange.cancel(OrderId(999));
assert!(!result.success);
assert_eq!(result.error, Some(CancelError::OrderNotFound));
}
#[test]
fn cancel_already_filled() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 100, TimeInForce::GTC);
let buy = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
let result = exchange.cancel(buy.order_id);
assert!(!result.success);
assert_eq!(result.error, Some(CancelError::OrderNotActive));
}
#[test]
fn modify_order() {
let mut exchange = Exchange::new();
let submit = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
let result = exchange.modify(submit.order_id, Price(99_00), 150);
assert!(result.success);
assert_eq!(result.old_order_id, submit.order_id);
assert!(result.new_order_id.is_some());
assert_ne!(result.new_order_id.unwrap(), submit.order_id);
assert_eq!(result.cancelled_quantity, 100);
assert_eq!(exchange.best_bid(), Some(Price(99_00)));
let new_order = exchange.get_order(result.new_order_id.unwrap()).unwrap();
assert_eq!(new_order.remaining_quantity, 150);
}
#[test]
fn modify_with_immediate_fill() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 50, TimeInForce::GTC);
let submit = exchange.submit_limit(Side::Buy, Price(99_00), 100, TimeInForce::GTC);
let result = exchange.modify(submit.order_id, Price(100_00), 100);
assert!(result.success);
assert_eq!(result.trades.len(), 1);
assert_eq!(result.trades[0].quantity, 50);
}
#[test]
fn modify_nonexistent() {
let mut exchange = Exchange::new();
let result = exchange.modify(OrderId(999), Price(100_00), 100);
assert!(!result.success);
assert_eq!(result.error, Some(ModifyError::OrderNotFound));
}
#[test]
fn modify_zero_quantity() {
let mut exchange = Exchange::new();
let submit = exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
let result = exchange.modify(submit.order_id, Price(100_00), 0);
assert!(!result.success);
assert_eq!(result.error, Some(ModifyError::InvalidQuantity));
}
#[test]
fn try_submit_limit_zero_quantity() {
let mut exchange = Exchange::new();
let result = exchange.try_submit_limit(Side::Buy, Price(100_00), 0, TimeInForce::GTC);
assert_eq!(result.unwrap_err(), ValidationError::ZeroQuantity);
}
#[test]
fn try_submit_limit_zero_price() {
let mut exchange = Exchange::new();
let result = exchange.try_submit_limit(Side::Buy, Price(0), 100, TimeInForce::GTC);
assert_eq!(result.unwrap_err(), ValidationError::ZeroPrice);
}
#[test]
fn try_submit_limit_negative_price() {
let mut exchange = Exchange::new();
let result = exchange.try_submit_limit(Side::Buy, Price(-100), 100, TimeInForce::GTC);
assert_eq!(result.unwrap_err(), ValidationError::ZeroPrice);
}
#[test]
fn try_submit_limit_valid() {
let mut exchange = Exchange::new();
let result = exchange.try_submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
assert!(result.is_ok());
assert_eq!(result.unwrap().order_id, OrderId(1));
}
#[test]
fn try_submit_market_zero_quantity() {
let mut exchange = Exchange::new();
let result = exchange.try_submit_market(Side::Buy, 0);
assert_eq!(result.unwrap_err(), ValidationError::ZeroQuantity);
}
#[test]
fn try_submit_market_valid() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 100, TimeInForce::GTC);
let result = exchange.try_submit_market(Side::Buy, 50);
assert!(result.is_ok());
}
#[test]
fn submit_stop_market_pending() {
let mut exchange = Exchange::new();
let result = exchange.submit_stop_market(Side::Buy, Price(105_00), 100);
assert_eq!(result.status, StopStatus::Pending);
assert_eq!(exchange.pending_stop_count(), 1);
}
#[test]
fn stop_market_triggers_on_trade() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 50, TimeInForce::GTC);
exchange.submit_limit(Side::Sell, Price(105_00), 100, TimeInForce::GTC);
exchange.submit_stop_market(Side::Buy, Price(100_00), 100);
let result = exchange.submit_limit(Side::Buy, Price(100_00), 50, TimeInForce::GTC);
assert_eq!(result.trades.len(), 1);
assert_eq!(exchange.pending_stop_count(), 0);
assert_eq!(exchange.last_trade_price(), Some(Price(105_00)));
}
#[test]
fn stop_limit_triggers_with_limit_price() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 50, TimeInForce::GTC);
exchange.submit_limit(Side::Sell, Price(106_00), 100, TimeInForce::GTC);
exchange.submit_stop_limit(
Side::Buy,
Price(100_00),
Price(105_00),
100,
TimeInForce::GTC,
);
exchange.submit_limit(Side::Buy, Price(100_00), 50, TimeInForce::GTC);
assert_eq!(exchange.pending_stop_count(), 0);
assert_eq!(exchange.best_bid(), Some(Price(105_00)));
}
#[test]
fn cancel_stop_order() {
let mut exchange = Exchange::new();
let stop = exchange.submit_stop_market(Side::Buy, Price(105_00), 100);
assert_eq!(exchange.pending_stop_count(), 1);
let result = exchange.cancel(stop.order_id);
assert!(result.success);
assert_eq!(result.cancelled_quantity, 100);
assert_eq!(exchange.pending_stop_count(), 0);
}
#[test]
fn sell_stop_triggers_on_price_drop() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Buy, Price(100_00), 50, TimeInForce::GTC);
exchange.submit_limit(Side::Buy, Price(95_00), 100, TimeInForce::GTC);
exchange.submit_stop_market(Side::Sell, Price(100_00), 100);
exchange.submit_limit(Side::Sell, Price(100_00), 50, TimeInForce::GTC);
assert_eq!(exchange.pending_stop_count(), 0);
}
#[test]
fn immediate_trigger_if_price_already_past() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 50, TimeInForce::GTC);
exchange.submit_limit(Side::Buy, Price(100_00), 50, TimeInForce::GTC);
assert_eq!(exchange.last_trade_price(), Some(Price(100_00)));
exchange.submit_limit(Side::Sell, Price(105_00), 100, TimeInForce::GTC);
let result = exchange.submit_stop_market(Side::Buy, Price(99_00), 100);
assert_eq!(result.status, StopStatus::Triggered);
assert_eq!(exchange.pending_stop_count(), 0);
}
#[test]
fn stop_cascade() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 50, TimeInForce::GTC);
exchange.submit_limit(Side::Sell, Price(102_00), 50, TimeInForce::GTC);
exchange.submit_limit(Side::Sell, Price(104_00), 50, TimeInForce::GTC);
exchange.submit_stop_market(Side::Buy, Price(100_00), 50);
exchange.submit_stop_market(Side::Buy, Price(102_00), 50);
exchange.submit_limit(Side::Buy, Price(100_00), 50, TimeInForce::GTC);
assert_eq!(exchange.pending_stop_count(), 0);
}
#[test]
fn trades_are_recorded() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 100, TimeInForce::GTC);
exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
assert_eq!(exchange.trades().len(), 1);
assert_eq!(exchange.trades()[0].quantity, 100);
}
#[test]
fn depth_snapshot() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
exchange.submit_limit(Side::Buy, Price(99_00), 200, TimeInForce::GTC);
exchange.submit_limit(Side::Sell, Price(101_00), 150, TimeInForce::GTC);
let snap = exchange.depth(10);
assert_eq!(snap.bids.len(), 2);
assert_eq!(snap.asks.len(), 1);
assert_eq!(snap.best_bid(), Some(Price(100_00)));
assert_eq!(snap.best_ask(), Some(Price(101_00)));
}
#[test]
fn trailing_stop_market_sell() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 100, TimeInForce::GTC);
exchange.submit_limit(Side::Buy, Price(90_00), 200, TimeInForce::GTC);
let result = exchange.submit_trailing_stop_market(
Side::Sell,
Price(95_00),
100,
TrailMethod::Fixed(3_00),
);
assert_eq!(result.status, StopStatus::Pending);
exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
let stop = exchange.get_stop_order(result.order_id).unwrap();
assert_eq!(stop.watermark, Some(Price(100_00)));
assert_eq!(stop.stop_price, Price(97_00));
}
#[test]
fn trailing_stop_triggers_on_reversal() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 50, TimeInForce::GTC);
exchange.submit_limit(Side::Sell, Price(105_00), 50, TimeInForce::GTC);
exchange.submit_limit(Side::Buy, Price(90_00), 200, TimeInForce::GTC);
exchange.submit_trailing_stop_market(
Side::Sell,
Price(98_00),
50,
TrailMethod::Fixed(2_00),
);
exchange.submit_limit(Side::Buy, Price(100_00), 50, TimeInForce::GTC);
assert_eq!(exchange.pending_stop_count(), 1);
exchange.submit_limit(Side::Buy, Price(105_00), 50, TimeInForce::GTC);
exchange.submit_limit(Side::Buy, Price(103_00), 50, TimeInForce::GTC);
exchange.submit_limit(Side::Sell, Price(103_00), 50, TimeInForce::GTC);
assert_eq!(exchange.pending_stop_count(), 0);
}
#[test]
fn trailing_stop_percentage_method() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(100_00), 100, TimeInForce::GTC);
exchange.submit_limit(Side::Buy, Price(80_00), 200, TimeInForce::GTC);
let result = exchange.submit_trailing_stop_market(
Side::Sell,
Price(90_00),
50,
TrailMethod::Percentage(0.05),
);
assert_eq!(result.status, StopStatus::Pending);
exchange.submit_limit(Side::Buy, Price(100_00), 100, TimeInForce::GTC);
let stop = exchange.get_stop_order(result.order_id).unwrap();
assert_eq!(stop.watermark, Some(Price(100_00)));
assert_eq!(stop.stop_price, Price(95_00));
}
#[test]
fn trailing_stop_does_not_trigger_immediately() {
let mut exchange = Exchange::new();
exchange.submit_limit(Side::Sell, Price(90_00), 50, TimeInForce::GTC);
exchange.submit_limit(Side::Buy, Price(90_00), 50, TimeInForce::GTC);
assert_eq!(exchange.last_trade_price(), Some(Price(90_00)));
let result = exchange.submit_trailing_stop_market(
Side::Sell,
Price(95_00),
50,
TrailMethod::Fixed(3_00),
);
assert_eq!(result.status, StopStatus::Pending);
assert_eq!(exchange.pending_stop_count(), 1);
}
#[test]
fn cancel_trailing_stop() {
let mut exchange = Exchange::new();
let result = exchange.submit_trailing_stop_market(
Side::Sell,
Price(95_00),
100,
TrailMethod::Fixed(3_00),
);
let cancel = exchange.cancel(result.order_id);
assert!(cancel.success);
assert_eq!(exchange.pending_stop_count(), 0);
}
}