use crate::{
AccountEventKind, InstrumentAccountSnapshot, UnindexedAccountEvent, UnindexedAccountSnapshot,
balance::AssetBalance,
client::mock::MockExecutionConfig,
error::{ApiError, UnindexedApiError, UnindexedOrderError},
exchange::mock::{
account::{AccountState, Debit},
orders::{OpenOrder, Reservation, RestingOrder},
},
fee::{FeeModel, FeeModelConfig, Liquidity},
fill::{FillContext, FillModel, SimFillConfig},
market::{MarketDepth, MarketSnapshot},
order::{
Order, OrderKind, TimeInForce, UnindexedOrder,
id::{ClientOrderId, OrderId, VenueOrderId},
request::{OrderRequestCancel, OrderRequestOpen, UnindexedOrderResponseCancel},
state::{Cancelled, Expired, Filled, Open, OrderState, UnindexedOrderState},
},
trade::{AssetFees, Trade, TradeId},
};
use chrono::{DateTime, Utc};
use fnv::FnvHashMap;
use itertools::Itertools;
use rust_decimal::Decimal;
use rustrade_instrument::{
Side, Underlying,
asset::name::AssetNameExchange,
exchange::ExchangeId,
instrument::{Instrument, kind::InstrumentKind, name::InstrumentNameExchange},
};
use rustrade_integration::collection::snapshot::Snapshot;
use smol_str::ToSmolStr;
#[derive(Debug)]
pub struct VenueOutcome<Response> {
pub events: Vec<UnindexedAccountEvent>,
pub response: Response,
}
pub type OpenOutcome = VenueOutcome<Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>>;
pub type CancelOutcome = VenueOutcome<UnindexedOrderResponseCancel>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct VenueInstrumentMarket {
pub snapshot: MarketSnapshot,
pub depth: MarketDepth,
pub time_exchange: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VenueRegime {
RequestPriced,
MarketDriven,
}
#[derive(Debug)]
pub struct SimulatedVenue {
pub exchange: ExchangeId,
pub fee_model: FeeModelConfig,
pub fill_model: SimFillConfig,
pub instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
pub account: AccountState,
market: FnvHashMap<InstrumentNameExchange, VenueInstrumentMarket>,
order_sequence: u64,
trade_sequence: u64,
time_exchange_latest: DateTime<Utc>,
regime: VenueRegime,
}
impl SimulatedVenue {
pub fn new(
config: &MockExecutionConfig,
instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
) -> Self {
Self::with_regime(config, instruments, VenueRegime::RequestPriced)
}
pub fn new_market_driven(
config: &MockExecutionConfig,
instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
) -> Self {
Self::with_regime(config, instruments, VenueRegime::MarketDriven)
}
fn with_regime(
config: &MockExecutionConfig,
instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
regime: VenueRegime,
) -> Self {
Self {
exchange: config.mocked_exchange,
fee_model: config.fee_model,
fill_model: config.fill_model,
instruments,
account: AccountState::from(config.initial_state.clone()),
market: FnvHashMap::default(),
order_sequence: 0,
trade_sequence: 0,
time_exchange_latest: Default::default(),
regime,
}
}
pub fn regime(&self) -> VenueRegime {
self.regime
}
#[must_use]
pub fn advance_time(&mut self, time_exchange: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
self.time_exchange_latest = time_exchange;
self.account.update_time_exchange(time_exchange);
self.sweep_expired(time_exchange)
}
fn sweep_expired(&mut self, time_exchange: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
let expired = self.account.orders().expired_as_of(time_exchange);
if expired.is_empty() {
return Vec::new();
}
let mut events = Vec::with_capacity(expired.len() * 2);
for cid in expired {
let Some(RestingOrder { order, reservation }) = self.account.orders_mut().remove(&cid)
else {
continue;
};
if let Some(Reservation { asset, amount }) = reservation {
let balance = self.account.release(&asset, amount, time_exchange);
events.push(self.build_account_event(Snapshot(balance)));
}
let expired_id = order.state.id.or_client_id(&order.key.cid);
let expired_order = Order {
key: order.key,
side: order.side,
price: order.price,
quantity: order.quantity,
kind: order.kind,
time_in_force: order.time_in_force,
state: Expired {
id: expired_id,
time_exchange,
filled_quantity: order.state.filled_quantity,
},
};
self.account.ack_expired(expired_order.clone());
events.push(self.build_account_event(Snapshot(UnindexedOrder::from(expired_order))));
}
events
}
pub fn time_exchange(&self) -> DateTime<Utc> {
self.time_exchange_latest
}
#[must_use]
pub fn apply_market(
&mut self,
instrument: &InstrumentNameExchange,
snapshot: MarketSnapshot,
depth: MarketDepth,
time_exchange: DateTime<Utc>,
) -> Vec<UnindexedAccountEvent> {
let entry = self.market.entry(instrument.clone()).or_default();
entry.snapshot = snapshot;
entry.depth = depth;
entry.time_exchange = time_exchange;
let mut events = self.sweep_expired(time_exchange);
events.append(&mut self.match_resting(instrument, snapshot, time_exchange));
events
}
fn match_resting(
&mut self,
instrument: &InstrumentNameExchange,
snapshot: MarketSnapshot,
time_exchange: DateTime<Utc>,
) -> Vec<UnindexedAccountEvent> {
let mut crossing: Vec<(ClientOrderId, Decimal)> = Vec::new();
for side in [Side::Buy, Side::Sell] {
crossing.extend(
self.account
.orders()
.resting(instrument, side)
.take_while(|order| {
order
.price
.is_some_and(|limit| crosses(side, limit, &snapshot))
})
.filter_map(|order| Some((order.key.cid.clone(), order.price?))),
);
}
if crossing.is_empty() {
return Vec::new();
}
let terms = match self.instrument_terms(instrument) {
Ok(terms) => terms,
Err(error) => panic!(
"SimulatedVenue holds resting orders on an instrument it cannot price: {error}"
),
};
let mut events = Vec::with_capacity(crossing.len() * 3);
for (cid, limit) in crossing {
let Some(RestingOrder { order, reservation }) = self.account.orders_mut().remove(&cid)
else {
continue;
};
let remaining = order.state.quantity_remaining(order.quantity);
debug_assert!(
remaining > Decimal::ZERO,
"resting order {cid} arrived with {} of {} already filled, so crossing it trades \
nothing: an order with no quantity left to trade does not belong on the book",
order.state.filled_quantity,
order.quantity
);
let settlement =
self.settlement(&terms, order.side, remaining, limit, Liquidity::Maker);
let balance = self.settle_resting(&order, reservation, &settlement, time_exchange);
let order_id = order.state.id.or_client_id(&order.key.cid);
let trade = Trade {
id: self.trade_id_sequence_fetch_add(),
order_id: order_id.clone(),
instrument: order.key.instrument.clone(),
strategy: order.key.strategy.clone(),
time_exchange,
side: order.side,
price: limit,
quantity: remaining,
order_filled_quantity: Some(order.state.filled_quantity + remaining),
fees: settlement.fees,
};
self.account.ack_trade(trade.clone());
self.account.ack_filled(cid);
let filled = Order {
key: order.key,
side: order.side,
price: order.price,
quantity: order.quantity,
kind: order.kind,
time_in_force: order.time_in_force,
state: OrderState::fully_filled(Filled::new(
order_id,
time_exchange,
order.quantity,
order.state.filled_quantity.is_zero().then_some(limit),
)),
};
events.push(self.build_account_event(Snapshot(balance)));
events.push(self.build_account_event(trade));
events.push(self.build_account_event(Snapshot(filled)));
}
events
}
fn settle_resting(
&mut self,
order: &OpenOrder,
reservation: Option<Reservation>,
settlement: &Settlement,
time_exchange: DateTime<Utc>,
) -> AssetBalance<AssetNameExchange> {
let Some(Reservation { asset, amount }) = reservation else {
#[allow(clippy::expect_used)] return self
.account
.commit(&settlement.settled(), time_exchange)
.unwrap_or_else(|insufficient| {
panic!(
"SimulatedVenue cannot afford the fill of resting order {}, which was \
seeded by `initial_state` and so reserved nothing: {} of {} free, {} \
required",
order.key.cid, insufficient.free, settlement.asset, insufficient.required
)
});
};
debug_assert!(
asset == settlement.asset && amount == settlement.amount,
"resting order {} reserved {amount} of {asset} but its fill settles {} of {}: the \
reservation and the fill disagree, so a balance the account never held would be \
reported",
order.key.cid,
settlement.amount,
settlement.asset
);
self.account.settle(&asset, amount, time_exchange)
}
pub fn market(&self, instrument: &InstrumentNameExchange) -> Option<&VenueInstrumentMarket> {
self.market.get(instrument)
}
pub fn order_sequence(&self) -> u64 {
self.order_sequence
}
pub fn balances(&self, assets: &[AssetNameExchange]) -> Vec<AssetBalance<AssetNameExchange>> {
self.account
.balances()
.filter(|balance| assets.is_empty() || assets.contains(&balance.asset))
.cloned()
.collect()
}
pub fn orders_open(
&self,
instruments: &[InstrumentNameExchange],
) -> Vec<Order<ExchangeId, InstrumentNameExchange, Open>> {
self.account
.orders_open()
.filter(|order| instruments.is_empty() || instruments.contains(&order.key.instrument))
.cloned()
.collect()
}
pub fn trades(
&self,
time_since: DateTime<Utc>,
) -> Vec<Trade<AssetNameExchange, InstrumentNameExchange>> {
self.account.trades(time_since).cloned().collect()
}
pub fn open_order(
&mut self,
request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
) -> OpenOutcome {
let market = self.pricing_market(&request);
let (response, notifications) = self.open_order_inner(request, market);
let events = match notifications {
Some(OpenOrderNotifications::Filled { balance, trade }) => vec![
self.build_account_event(balance),
self.build_account_event(trade),
],
Some(OpenOrderNotifications::Rested { balance }) => {
vec![self.build_account_event(balance)]
}
None => Vec::new(),
};
VenueOutcome { events, response }
}
pub fn cancel_order(
&mut self,
request: OrderRequestCancel<ExchangeId, InstrumentNameExchange>,
) -> CancelOutcome {
let time_exchange = self.time_exchange();
let Some(RestingOrder { order, reservation }) =
self.account.orders_mut().remove(&request.key.cid)
else {
return VenueOutcome {
events: Vec::new(),
response: UnindexedOrderResponseCancel {
state: Err(self.cancel_missing_reason(&request.key.cid)),
key: request.key,
},
};
};
let events = match reservation {
Some(Reservation { asset, amount }) => {
let balance = self.account.release(&asset, amount, time_exchange);
vec![self.build_account_event(Snapshot(balance))]
}
None => Vec::new(),
};
let cancelled = Cancelled {
id: order.state.id.or_client_id(&order.key.cid),
time_exchange,
filled_quantity: order.state.filled_quantity,
};
self.account.ack_cancelled(Order {
key: order.key,
side: order.side,
price: order.price,
quantity: order.quantity,
kind: order.kind,
time_in_force: order.time_in_force,
state: cancelled.clone(),
});
VenueOutcome {
events,
response: UnindexedOrderResponseCancel {
key: request.key,
state: Ok(cancelled),
},
}
}
fn cancel_missing_reason(&self, cid: &ClientOrderId) -> UnindexedOrderError {
if self.account.is_filled(cid) {
ApiError::OrderAlreadyFullyFilled.into()
} else if self.account.is_cancelled(cid) {
ApiError::OrderAlreadyCancelled.into()
} else if self.account.is_expired(cid) {
ApiError::OrderAlreadyExpired.into()
} else {
UnindexedOrderError::Rejected(ApiError::OrderRejected(format!(
"SimulatedVenue is not holding an open order with {cid}"
)))
}
}
pub fn account_snapshot(&self) -> UnindexedAccountSnapshot {
let balances = self.account.balances().cloned().collect();
let orders_open = self
.account
.orders_open()
.cloned()
.map(UnindexedOrder::from);
let orders_cancelled = self
.account
.orders_cancelled()
.cloned()
.map(UnindexedOrder::from);
let orders_expired = self
.account
.orders_expired()
.cloned()
.map(UnindexedOrder::from);
let orders_all = orders_open.chain(orders_cancelled).chain(orders_expired);
let orders_all = orders_all
.sorted_unstable_by_key(|order| (order.key.instrument.clone(), order.key.cid.clone()));
let orders_by_instrument = orders_all.chunk_by(|order| order.key.instrument.clone());
let instruments = orders_by_instrument
.into_iter()
.map(|(instrument, orders)| InstrumentAccountSnapshot {
instrument,
orders: orders.into_iter().collect(),
orders_complete: true,
position: None,
isolated: None,
})
.collect();
UnindexedAccountSnapshot {
exchange: self.exchange,
balances,
instruments,
}
}
fn pricing_market(
&self,
request: &OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
) -> Option<MarketSnapshot> {
match self.regime {
VenueRegime::RequestPriced => request.state.market,
VenueRegime::MarketDriven => self
.market(&request.key.instrument)
.map(|market| market.snapshot),
}
}
fn open_order_inner(
&mut self,
request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
market: Option<MarketSnapshot>,
) -> (
Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
Option<OpenOrderNotifications>,
) {
if let Err(error) = self.validate_order_kind_supported(request.state.kind) {
return (build_open_order_err_response(request, error), None);
}
if let Err(error) =
validate_time_in_force_supported(request.state.kind, request.state.time_in_force)
{
return (build_open_order_err_response(request, error), None);
}
let terms = match self.instrument_terms(&request.key.instrument) {
Ok(terms) => terms,
Err(error) => return (build_open_order_err_response(request, error), None),
};
let now = self.time_exchange();
if request.state.kind != OrderKind::Limit {
return match disposition(request.state.kind, request.state.time_in_force, true, now) {
Disposition::FillAndCancel => {
self.fill_on_arrival(request, market, &terms, Remainder::Cancel)
}
Disposition::FillOrNothing => {
self.fill_on_arrival(request, market, &terms, Remainder::Kill)
}
Disposition::Expire => self.expire_on_arrival(request, now),
Disposition::FillAndRest | Disposition::CancelUnfilled | Disposition::Rest => {
self.cancel_on_arrival(request, now)
}
};
}
let Some(limit) = request.state.price else {
let reason = format!(
"cannot open {} for {}: OrderKind::Limit carries no limit price",
request.key.instrument, request.key.exchange
);
return (
build_open_order_err_response(
request,
UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
),
None,
);
};
let marketable = market.is_some_and(|market| crosses(request.state.side, limit, &market));
match disposition(
request.state.kind,
request.state.time_in_force,
marketable,
now,
) {
Disposition::FillAndRest => {
self.fill_on_arrival(request, market, &terms, Remainder::Rest { limit })
}
Disposition::FillAndCancel => {
self.fill_on_arrival(request, market, &terms, Remainder::Cancel)
}
Disposition::FillOrNothing => {
self.fill_on_arrival(request, market, &terms, Remainder::Kill)
}
Disposition::Rest => self.rest_order(request, limit, &terms, Decimal::ZERO),
Disposition::CancelUnfilled => self.cancel_on_arrival(request, now),
Disposition::Expire => self.expire_on_arrival(request, now),
}
}
fn cancel_on_arrival(
&mut self,
request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
now: DateTime<Utc>,
) -> (
Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
Option<OpenOrderNotifications>,
) {
let cancelled = Cancelled {
id: self.order_id_sequence_fetch_add(),
time_exchange: now,
filled_quantity: Decimal::ZERO,
};
self.account.ack_cancelled(Order {
key: request.key.clone(),
side: request.state.side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: cancelled.clone(),
});
(
Order {
key: request.key,
side: request.state.side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: OrderState::inactive(cancelled),
},
None,
)
}
fn expire_on_arrival(
&mut self,
request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
now: DateTime<Utc>,
) -> (
Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
Option<OpenOrderNotifications>,
) {
let expired = Expired {
id: self.order_id_sequence_fetch_add(),
time_exchange: now,
filled_quantity: Decimal::ZERO,
};
self.account.ack_expired(Order {
key: request.key.clone(),
side: request.state.side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: expired.clone(),
});
(
Order {
key: request.key,
side: request.state.side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: OrderState::expired(expired),
},
None,
)
}
fn fill_on_arrival(
&mut self,
request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
market: Option<MarketSnapshot>,
terms: &InstrumentTerms,
remainder: Remainder,
) -> (
Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
Option<OpenOrderNotifications>,
) {
let market_supplied = market.is_some();
let market = market.unwrap_or_default();
let maybe_fill_price = self
.fill_model
.fill_price(&FillContext::new(request.state.side, &market))
.or(request.state.price);
let Some(fill_price) = maybe_fill_price else {
let cause = if market_supplied {
"no market price available yet"
} else {
"the request carried no market snapshot (RequestOpen::market was None)"
};
let reason = format!(
"cannot price {} for {}: {cause} and OrderKind::{:?} carries no limit price",
request.key.instrument, request.key.exchange, request.state.kind
);
return (
build_open_order_err_response(
request,
UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
),
None,
);
};
let fill_price = match request.state.price {
Some(limit) if request.state.kind == OrderKind::Limit => match request.state.side {
Side::Buy => fill_price.min(limit),
Side::Sell => fill_price.max(limit),
},
_ => fill_price,
};
let time_exchange = self.time_exchange();
let side = request.state.side;
let filled_quantity = match self.depth_available(&request.key.instrument, side) {
Some(available) => request.state.quantity.min(available),
None => request.state.quantity,
};
let unfilled = request.state.quantity - filled_quantity;
if filled_quantity <= Decimal::ZERO || (unfilled > Decimal::ZERO && remainder.is_kill()) {
return match remainder {
Remainder::Rest { limit } => self.rest_order(request, limit, terms, Decimal::ZERO),
Remainder::Cancel | Remainder::Kill => {
self.cancel_on_arrival(request, time_exchange)
}
};
}
let fill = self.settlement(terms, side, filled_quantity, fill_price, Liquidity::Taker);
let held = match remainder {
Remainder::Rest { limit } if unfilled > Decimal::ZERO => {
Some(self.settlement(terms, side, unfilled, limit, Liquidity::Maker))
}
_ => None,
};
let debit = match &held {
Some(held) => fill.settled_holding(held),
None => fill.settled(),
};
let balance = match self.account.commit(&debit, time_exchange) {
Ok(balance) => balance,
Err(insufficient) => {
return (
build_open_order_err_response(
request,
ApiError::BalanceInsufficient(
debit.asset,
format!(
"Available Balance: {}, Required Balance inc. fees: {}",
insufficient.free, insufficient.required
),
),
),
None,
);
}
};
self.consume_depth(&request.key.instrument, side, filled_quantity);
let order_id = self.order_id_sequence_fetch_add();
let trade = Trade {
id: self.trade_id_sequence_fetch_add(),
order_id: order_id.clone(),
instrument: request.key.instrument.clone(),
strategy: request.key.strategy.clone(),
time_exchange,
side,
price: fill_price,
quantity: filled_quantity,
order_filled_quantity: Some(filled_quantity),
fees: fill.fees,
};
self.account.ack_trade(trade.clone());
let (state, released) = self.retire_filled(
&request,
order_id,
Fill {
quantity: filled_quantity,
price: fill_price,
},
held,
time_exchange,
);
(
Order {
key: request.key,
side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state,
},
Some(OpenOrderNotifications::Filled {
balance: Snapshot(released.unwrap_or(balance)),
trade,
}),
)
}
fn retire_filled(
&mut self,
request: &OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
order_id: OrderId,
fill: Fill,
held: Option<Settlement>,
time_exchange: DateTime<Utc>,
) -> (UnindexedOrderState, Option<AssetBalance<AssetNameExchange>>) {
match held {
Some(held) => {
let open = Open::new(
VenueOrderId::Assigned(order_id),
time_exchange,
fill.quantity,
);
let released = self.book_rested(
Order {
key: request.key.clone(),
side: request.state.side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: open.clone(),
},
Reservation {
asset: held.asset,
amount: held.amount,
},
time_exchange,
);
(OrderState::active(open), released)
}
None if fill.quantity == request.state.quantity => {
let filled = Filled::new(
order_id,
time_exchange,
request.state.quantity,
Some(fill.price),
);
self.account.ack_filled(request.key.cid.clone());
(OrderState::fully_filled(filled), None)
}
None => {
let cancelled = Cancelled {
id: order_id,
time_exchange,
filled_quantity: fill.quantity,
};
self.account.ack_cancelled(Order {
key: request.key.clone(),
side: request.state.side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: cancelled.clone(),
});
(OrderState::inactive(cancelled), None)
}
}
}
fn depth_available(&self, instrument: &InstrumentNameExchange, side: Side) -> Option<Decimal> {
self.market
.get(instrument)
.and_then(|market| market.depth.available_to(side))
}
fn consume_depth(
&mut self,
instrument: &InstrumentNameExchange,
side: Side,
quantity: Decimal,
) {
if let Some(market) = self.market.get_mut(instrument) {
market.depth.consume(side, quantity);
}
}
fn book_rested(
&mut self,
order: OpenOrder,
reservation: Reservation,
time_exchange: DateTime<Utc>,
) -> Option<AssetBalance<AssetNameExchange>> {
let Reservation { asset, amount } =
self.account.orders_mut().insert(order, Some(reservation))?;
Some(self.account.release(&asset, amount, time_exchange))
}
fn rest_order(
&mut self,
request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
limit: Decimal,
terms: &InstrumentTerms,
filled_quantity: Decimal,
) -> (
Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
Option<OpenOrderNotifications>,
) {
let time_exchange = self.time_exchange();
let settlement = self.settlement(
terms,
request.state.side,
request.state.quantity - filled_quantity,
limit,
Liquidity::Maker,
);
let balance_snapshot = match self.account.commit(&settlement.reserved(), time_exchange) {
Ok(balance_snapshot) => balance_snapshot,
Err(insufficient) => {
return (
build_open_order_err_response(
request,
ApiError::BalanceInsufficient(
settlement.asset,
format!(
"Available Balance: {}, Required Balance inc. fees: {}",
insufficient.free, insufficient.required
),
),
),
None,
);
}
};
let open = Open::new(
VenueOrderId::Assigned(self.order_id_sequence_fetch_add()),
time_exchange,
filled_quantity,
);
let released = self.book_rested(
Order {
key: request.key.clone(),
side: request.state.side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: open.clone(),
},
Reservation {
asset: settlement.asset,
amount: settlement.amount,
},
time_exchange,
);
let order_response = Order {
key: request.key,
side: request.state.side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: OrderState::active(open),
};
(
order_response,
Some(OpenOrderNotifications::Rested {
balance: Snapshot(released.unwrap_or(balance_snapshot)),
}),
)
}
fn instrument_terms(
&self,
instrument: &InstrumentNameExchange,
) -> Result<InstrumentTerms, UnindexedApiError> {
let instrument = self.find_instrument_data(instrument)?;
let cash_settled = Self::settlement_of_supported_kind(instrument)?;
Ok(InstrumentTerms {
underlying: instrument.underlying.clone(),
contract_size: instrument.kind.contract_size(),
cash_settled,
})
}
fn settlement(
&self,
terms: &InstrumentTerms,
side: Side,
quantity: Decimal,
price: Decimal,
liquidity: Liquidity,
) -> Settlement {
let notional_quote = price * quantity.abs() * terms.contract_size;
let fees_quote =
self.fee_model
.compute_fee(price, quantity, terms.contract_size, liquidity);
let (asset, amount) = match (terms.cash_settled, side) {
(true, _) | (false, Side::Buy) => {
(terms.underlying.quote.clone(), notional_quote + fees_quote)
}
(false, Side::Sell) => {
debug_assert!(
!matches!(self.fee_model, FeeModelConfig::PerContract(_)),
"PerContractFeeModel produces nonsensical base-denominated fees on the spot \
sell path"
);
let fees_base = if price.is_zero() {
Decimal::ZERO
} else {
fees_quote / price
};
(terms.underlying.base.clone(), quantity.abs() + fees_base)
}
};
Settlement {
asset,
amount,
fees: AssetFees::new(terms.underlying.quote.clone(), fees_quote, Some(fees_quote)),
}
}
pub fn validate_order_kind_supported(
&self,
order_kind: OrderKind,
) -> Result<(), UnindexedOrderError> {
let reason = match (order_kind, self.regime) {
(OrderKind::Market, _) | (OrderKind::Limit, VenueRegime::MarketDriven) => return Ok(()),
(OrderKind::Limit, VenueRegime::RequestPriced) => format!(
"SimulatedVenue does not support OrderKind::{order_kind:?} in \
VenueRegime::RequestPriced: it has no market feed, so an order that rested could \
never be matched. Construct it with SimulatedVenue::new_market_driven, and feed \
it with apply_market"
),
_ => format!("SimulatedVenue does not support OrderKind::{order_kind:?}"),
};
Err(UnindexedOrderError::Rejected(ApiError::OrderRejected(
reason,
)))
}
fn settlement_of_supported_kind(
instrument: &Instrument<ExchangeId, AssetNameExchange>,
) -> Result<bool, UnindexedApiError> {
match &instrument.kind {
InstrumentKind::Spot => Ok(false),
InstrumentKind::Cfd(_) => Ok(true),
unsupported => Err(ApiError::InstrumentInvalid(
instrument.name_exchange.clone(),
format!(
"SimulatedVenue does not support {}; only Spot and Cfd are modelled",
match unsupported {
InstrumentKind::Perpetual(_) => "InstrumentKind::Perpetual",
InstrumentKind::Future(_) => "InstrumentKind::Future",
InstrumentKind::Option(_) => "InstrumentKind::Option",
InstrumentKind::Spot | InstrumentKind::Cfd(_) => "InstrumentKind",
}
),
)),
}
}
pub fn find_instrument_data(
&self,
instrument: &InstrumentNameExchange,
) -> Result<&Instrument<ExchangeId, AssetNameExchange>, UnindexedApiError> {
self.instruments.get(instrument).ok_or_else(|| {
ApiError::InstrumentInvalid(
instrument.clone(),
format!("SimulatedVenue is not set-up for managing: {instrument}"),
)
})
}
fn order_id_sequence_fetch_add(&mut self) -> OrderId {
let sequence = self.order_sequence;
self.order_sequence += 1;
OrderId(sequence.to_smolstr())
}
fn trade_id_sequence_fetch_add(&mut self) -> TradeId {
let sequence = self.trade_sequence;
self.trade_sequence += 1;
TradeId(sequence.to_smolstr())
}
fn build_account_event<Kind>(&self, kind: Kind) -> UnindexedAccountEvent
where
Kind: Into<AccountEventKind<ExchangeId, AssetNameExchange, InstrumentNameExchange>>,
{
UnindexedAccountEvent {
exchange: self.exchange,
kind: kind.into(),
}
}
}
fn crosses(side: Side, limit: Decimal, market: &MarketSnapshot) -> bool {
match side {
Side::Buy => match market.best_ask {
Some(best_ask) => best_ask <= limit,
None => market.last_price.is_some_and(|last| last <= limit),
},
Side::Sell => match market.best_bid {
Some(best_bid) => best_bid >= limit,
None => market.last_price.is_some_and(|last| last >= limit),
},
}
}
fn validate_time_in_force_supported(
order_kind: OrderKind,
time_in_force: TimeInForce,
) -> Result<(), UnindexedOrderError> {
const NO_CALENDAR: &str = "a session calendar, which this venue does not have";
let missing = match (order_kind, time_in_force) {
(_, TimeInForce::AtOpen | TimeInForce::AtClose) => NO_CALENDAR,
(OrderKind::Market, TimeInForce::GoodUntilCancelled { post_only: true }) => {
"an order that both takes liquidity and refuses to"
}
(OrderKind::Market, _) => return Ok(()),
(OrderKind::Limit, TimeInForce::GoodUntilEndOfDay) => NO_CALENDAR,
(
OrderKind::Limit,
TimeInForce::GoodUntilCancelled { .. }
| TimeInForce::ImmediateOrCancel
| TimeInForce::FillOrKill
| TimeInForce::GoodTillDate { .. },
) => return Ok(()),
(_, _) => return Ok(()),
};
Err(UnindexedOrderError::Rejected(ApiError::OrderRejected(
format!(
"SimulatedVenue does not support TimeInForce::{time_in_force} on \
OrderKind::{order_kind:?}: it does not model {missing}"
),
)))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Disposition {
FillAndRest,
FillAndCancel,
FillOrNothing,
Rest,
CancelUnfilled,
Expire,
}
fn disposition(
order_kind: OrderKind,
time_in_force: TimeInForce,
marketable: bool,
now: DateTime<Utc>,
) -> Disposition {
match time_in_force {
TimeInForce::GoodUntilCancelled { post_only: true } if marketable => {
Disposition::CancelUnfilled
}
TimeInForce::GoodTillDate { expiry } if now >= expiry => Disposition::Expire,
TimeInForce::FillOrKill if marketable => Disposition::FillOrNothing,
_ if marketable => match (order_kind, time_in_force) {
(
OrderKind::Limit,
TimeInForce::GoodUntilCancelled { .. } | TimeInForce::GoodTillDate { .. },
) => Disposition::FillAndRest,
_ => Disposition::FillAndCancel,
},
TimeInForce::ImmediateOrCancel | TimeInForce::FillOrKill => Disposition::CancelUnfilled,
_ => Disposition::Rest,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Remainder {
Rest { limit: Decimal },
Cancel,
Kill,
}
impl Remainder {
fn is_kill(self) -> bool {
matches!(self, Self::Kill)
}
}
fn build_open_order_err_response<E>(
request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
error: E,
) -> Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>
where
E: Into<UnindexedOrderError>,
{
Order {
key: request.key,
side: request.state.side,
price: request.state.price,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: OrderState::inactive(error.into()),
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
enum OpenOrderNotifications {
Filled {
balance: Snapshot<AssetBalance<AssetNameExchange>>,
trade: Trade<AssetNameExchange, InstrumentNameExchange>,
},
Rested {
balance: Snapshot<AssetBalance<AssetNameExchange>>,
},
}
#[derive(Debug, Clone)]
struct InstrumentTerms {
underlying: Underlying<AssetNameExchange>,
contract_size: Decimal,
cash_settled: bool,
}
#[derive(Debug, Clone, Copy)]
struct Fill {
quantity: Decimal,
price: Decimal,
}
#[derive(Debug, Clone)]
struct Settlement {
asset: AssetNameExchange,
amount: Decimal,
fees: AssetFees<AssetNameExchange>,
}
impl Settlement {
fn settled(&self) -> Debit {
Debit {
asset: self.asset.clone(),
settled: self.amount,
reserved: Decimal::ZERO,
}
}
fn reserved(&self) -> Debit {
Debit {
asset: self.asset.clone(),
settled: Decimal::ZERO,
reserved: self.amount,
}
}
fn settled_holding(&self, remainder: &Settlement) -> Debit {
debug_assert!(
self.asset == remainder.asset,
"one order settled {} of {} and reserved {} of {}: an order pays with one asset, so \
two settlements of it cannot name two",
self.amount,
self.asset,
remainder.amount,
remainder.asset
);
Debit {
asset: self.asset.clone(),
settled: self.amount,
reserved: remainder.amount,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
use crate::{
error::OrderError,
exchange::mock::{fixtures::*, orders::as_open},
fee::PercentageFeeModel,
fill::BidAskFillModel,
order::{
OrderEvent, OrderKey, TimeInForce,
id::{ClientOrderId, StrategyId},
request::RequestCancel,
state::{ActiveOrderState, InactiveOrderState},
},
};
use rustrade_instrument::{
Underlying,
instrument::{
kind::cfd::CfdContract, name::InstrumentNameInternal, quote::InstrumentQuoteAsset,
},
};
fn make_venue(btc: &str, usdt: &str) -> SimulatedVenue {
make_venue_with_fee(btc, usdt, FeeModelConfig::default())
}
fn make_venue_with_fee(btc: &str, usdt: &str, fee_model: FeeModelConfig) -> SimulatedVenue {
SimulatedVenue::new(&spot_config(btc, usdt, fee_model), spot_instruments())
}
fn maker_taker_fee() -> FeeModelConfig {
FeeModelConfig::Percentage(PercentageFeeModel::maker_taker(d("0.0002"), d("0.001")))
}
fn make_market_venue(btc: &str, usdt: &str) -> SimulatedVenue {
make_market_venue_with(btc, usdt, maker_taker_fee(), SimFillConfig::default())
}
fn make_market_venue_with(
btc: &str,
usdt: &str,
fee_model: FeeModelConfig,
fill_model: SimFillConfig,
) -> SimulatedVenue {
SimulatedVenue::new_market_driven(
&spot_config_with_fill(btc, usdt, fee_model, fill_model),
spot_instruments(),
)
}
fn balance_of(event: &UnindexedAccountEvent) -> &AssetBalance<AssetNameExchange> {
match &event.kind {
AccountEventKind::BalanceSnapshot(balance) => &balance.0,
other => panic!("expected a balance restatement, got: {other:?}"),
}
}
fn trade_of(
event: &UnindexedAccountEvent,
) -> &Trade<AssetNameExchange, InstrumentNameExchange> {
match &event.kind {
AccountEventKind::Trade(trade) => trade,
other => panic!("expected a trade, got: {other:?}"),
}
}
fn order_of(
event: &UnindexedAccountEvent,
) -> &Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState> {
match &event.kind {
AccountEventKind::OrderSnapshot(order) => &order.0,
other => panic!("expected an order snapshot, got: {other:?}"),
}
}
fn rested_open(outcome: &OpenOutcome) -> &Open {
match &outcome.response.state {
OrderState::Active(ActiveOrderState::Open(open)) => open,
other => panic!("expected a resting open order, got: {other:?}"),
}
}
fn filled_price(
order: &Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
) -> Decimal {
match &order.state {
OrderState::Inactive(InactiveOrderState::FullyFilled(filled)) => filled
.avg_price
.expect("a filled order reports the price it filled at"),
other => panic!("expected a fully filled order, got: {other:?}"),
}
}
fn venue_resting_one_buy(limit: &str) -> (SimulatedVenue, OpenOutcome) {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
book("49000", "49100"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty(),
"an empty book fills nothing"
);
let outcome = venue.open_order(limit_request("resting", Side::Buy, "1", limit, gtc()));
assert_eq!(
outcome.events.len(),
1,
"a rested order restates one balance and books no trade"
);
(venue, outcome)
}
fn advance(venue: &mut SimulatedVenue, time_exchange: DateTime<Utc>) {
assert!(
venue.advance_time(time_exchange).is_empty(),
"advancing to {time_exchange} retired an order this test never gave a deadline"
);
}
fn time(seconds: i64) -> DateTime<Utc> {
DateTime::<Utc>::from_timestamp(1_700_000_000 + seconds, 0).expect("representable instant")
}
fn filled_balance(outcome: &OpenOutcome) -> &AssetBalance<AssetNameExchange> {
match outcome.events.first().map(|event| &event.kind) {
Some(AccountEventKind::BalanceSnapshot(balance)) => &balance.0,
other => panic!("a filled open must emit its balance first, got: {other:?}"),
}
}
fn filled_trade(outcome: &OpenOutcome) -> &Trade<AssetNameExchange, InstrumentNameExchange> {
match outcome.events.get(1).map(|event| &event.kind) {
Some(AccountEventKind::Trade(trade)) => trade,
other => panic!("a filled open must emit its trade after its balance, got: {other:?}"),
}
}
#[test]
fn a_filled_open_reports_its_balance_before_its_trade_and_debits_monotonically() {
const FILLS: usize = 4;
let mut venue = make_venue("100", "10000000");
let mut balances = Vec::with_capacity(FILLS);
for _ in 0..FILLS {
let outcome = venue.open_order(buy_request("0.01", market_prices("50000")));
assert_eq!(
outcome.events.len(),
2,
"a fill owes exactly one balance and one trade"
);
let balance = filled_balance(&outcome);
let trade = filled_trade(&outcome);
assert_eq!(balance.asset, quote(), "a spot buy debits quote");
assert_eq!(trade.price, d("50000"));
balances.push(balance.balance.total);
}
let expected = (1..=FILLS)
.map(|nth| d("10000000") - d("500") * Decimal::from(nth))
.collect::<Vec<_>>();
assert_eq!(
balances, expected,
"successive fills must restate a strictly decreasing balance"
);
}
#[test]
fn a_filled_open_is_in_the_ledger_before_its_events_are_delivered() {
let mut venue = make_venue("100", "10000000");
let before = venue.time_exchange();
let outcome = venue.open_order(buy_request("0.01", market_prices("50000")));
assert_eq!(outcome.events.len(), 2, "the fill must owe events");
let trades = venue.trades(before);
assert_eq!(trades.len(), 1, "the venue must acknowledge its own trade");
assert_eq!(trades[0].price, d("50000"));
}
#[test]
fn a_fill_is_stamped_with_the_instant_the_venue_was_last_advanced() {
let mut venue = make_venue("100", "10000000");
let time_exchange = "2025-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
advance(&mut venue, time_exchange);
assert_eq!(venue.time_exchange(), time_exchange);
let outcome = venue.open_order(buy_request("0.01", market_prices("50000")));
assert_eq!(filled_trade(&outcome).time_exchange, time_exchange);
assert_eq!(filled_balance(&outcome).time_exchange, time_exchange);
}
#[test]
fn advancing_the_clock_does_not_restamp_a_resting_order() {
let arrived = "2025-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
let much_later = "2025-06-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
let mut venue = make_venue("100", "10000000");
venue.account = AccountState::new(
venue
.account
.balances()
.cloned()
.map(|balance| (balance.asset.clone(), balance))
.collect(),
[Order {
key: OrderKey {
exchange: EXCHANGE,
instrument: instrument_name(),
strategy: StrategyId::new("test"),
cid: ClientOrderId::new("resting"),
},
side: Side::Buy,
price: Some(d("50000")),
quantity: d("1"),
kind: OrderKind::Limit,
time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
state: Open {
id: VenueOrderId::Assigned(OrderId::new("resting")),
time_exchange: arrived,
filled_quantity: Decimal::ZERO,
},
}]
.into_iter()
.collect(),
Default::default(),
Vec::new(),
);
advance(&mut venue, much_later);
let resting = venue.orders_open(&[]);
assert_eq!(resting.len(), 1, "the seeded order must still be open");
assert_eq!(
resting[0].state.time_exchange, arrived,
"an order does not become a different order because time passed"
);
let usdt = venue.balances(&[quote()]);
assert_eq!(usdt[0].time_exchange, much_later);
}
#[test]
fn a_cancel_for_an_unknown_order_is_rejected_as_unknown() {
let mut venue = make_venue("100", "10000000");
let key = buy_request("1", market_prices("50000")).key;
let outcome = venue.cancel_order(OrderEvent {
key: key.clone(),
state: RequestCancel { id: None },
});
assert!(
outcome.events.is_empty(),
"a rejected cancel moves nothing, so it owes no events"
);
assert_eq!(
outcome.response.key, key,
"the response must name its request"
);
match outcome.response.state {
Err(OrderError::Rejected(ApiError::OrderRejected(ref reason))) => {
assert!(
reason.contains("test-cid"),
"the rejection must name the order it could not find, got: {reason}"
);
}
ref other => panic!("expected a rejected cancel, got: {other:?}"),
}
assert!(
venue.account_snapshot().instruments.is_empty(),
"a rejected cancel must not enter the ledger"
);
}
const CFD_CONTRACT_SIZE: &str = "25";
fn cfd_instrument_name() -> InstrumentNameExchange {
InstrumentNameExchange::new("spx500_usd")
}
fn make_cfd_venue(usd: &str, fee_model: FeeModelConfig) -> SimulatedVenue {
make_cfd_venue_with_balances(
vec![funded(AssetNameExchange::new("usd"), d(usd))],
fee_model,
)
}
fn make_cfd_venue_with_balances(
balances: Vec<AssetBalance<AssetNameExchange>>,
fee_model: FeeModelConfig,
) -> SimulatedVenue {
SimulatedVenue::new(
&config_from_balances(balances, fee_model),
instruments_of(Instrument {
exchange: EXCHANGE,
name_internal: InstrumentNameInternal::new("spx500_usd"),
name_exchange: cfd_instrument_name(),
underlying: Underlying {
base: AssetNameExchange::new("spx500"),
quote: AssetNameExchange::new("usd"),
},
quote: InstrumentQuoteAsset::UnderlyingQuote,
kind: InstrumentKind::Cfd(CfdContract {
contract_size: d(CFD_CONTRACT_SIZE),
settlement_asset: AssetNameExchange::new("gbp"),
}),
spec: None,
data_venue: None,
}),
)
}
fn cfd_request(
side: Side,
quantity: &str,
market: Option<MarketSnapshot>,
) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
request(cfd_instrument_name(), side, quantity, market)
}
#[test]
fn cfd_buy_debits_the_contract_size_scaled_notional() {
let mut venue = make_cfd_venue("200000", FeeModelConfig::default());
let outcome = venue.open_order(cfd_request(Side::Buy, "1", market_prices("5000")));
assert!(
outcome.response.state.is_accepted(),
"cfd buy should fill: {:?}",
outcome.response.state
);
assert_eq!(outcome.events.len(), 2);
let usd = venue
.account
.balance_mut(&AssetNameExchange::new("usd"))
.unwrap();
assert_eq!(usd.balance.free, d("200000") - d("125000"));
assert_eq!(usd.balance.total, usd.balance.free);
}
#[test]
fn cfd_sell_needs_no_base_inventory_and_debits_quote() {
let mut venue = make_cfd_venue("200000", FeeModelConfig::default());
let outcome = venue.open_order(cfd_request(Side::Sell, "1", market_prices("5000")));
assert!(
outcome.response.state.is_accepted(),
"cfd short should fill without base inventory: {:?}",
outcome.response.state
);
let balance = filled_balance(&outcome);
assert_eq!(
balance.asset,
AssetNameExchange::new("usd"),
"a cash-settled short debits quote, not base"
);
assert_eq!(balance.balance.free, d("75000"));
}
#[test]
fn cfd_buy_is_rejected_when_only_the_unscaled_notional_is_funded() {
let mut venue = make_cfd_venue("10000", FeeModelConfig::default());
let outcome = venue.open_order(cfd_request(Side::Buy, "1", market_prices("5000")));
assert!(outcome.events.is_empty());
match outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::BalanceInsufficient(ref asset, _),
))) => {
assert_eq!(*asset, AssetNameExchange::new("usd"));
}
other => panic!("expected BalanceInsufficient, got: {other:?}"),
}
}
#[test]
fn cfd_fee_is_quote_denominated_on_both_sides() {
for side in [Side::Buy, Side::Sell] {
let mut venue = make_cfd_venue(
"200000",
FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001"))),
);
let outcome = venue.open_order(cfd_request(side, "1", market_prices("5000")));
assert!(
outcome.response.state.is_accepted(),
"{side:?} should fill: {:?}",
outcome.response.state
);
assert_eq!(
filled_trade(&outcome).fees.asset,
AssetNameExchange::new("usd"),
"{side:?} fee asset"
);
assert_eq!(
filled_trade(&outcome).fees.fees,
d("125"),
"{side:?} fee must be 0.1% of the contract_size-scaled notional"
);
assert_eq!(
filled_balance(&outcome).balance.free,
d("200000") - d("125000") - d("125"),
"{side:?} debit must be notional + fee"
);
}
}
#[test]
#[should_panic(expected = "SimulatedVenue has Balance for all configured Instrument assets")]
fn cfd_panics_when_the_quote_asset_is_unfunded() {
let mut venue = make_cfd_venue_with_balances(
vec![funded(AssetNameExchange::new("gbp"), d("200000"))],
FeeModelConfig::default(),
);
let _ = venue.open_order(cfd_request(Side::Buy, "1", market_prices("5000")));
}
#[test]
fn sell_order_decrements_base_balance_not_quote() {
let mut venue = make_venue("1.0", "10000");
let initial_usdt = d("10000");
let outcome = venue.open_order(sell_request("0.5", market_prices("50000")));
assert!(
outcome.response.state.is_accepted(),
"sell should succeed: {:?}",
outcome.response.state
);
assert_eq!(outcome.events.len(), 2, "a successful sell must notify");
let btc = venue.account.balance_mut(&base()).unwrap();
assert_eq!(
btc.balance.free,
d("0.5"),
"base balance should decrease by quantity sold"
);
let usdt = venue.account.balance_mut("e()).unwrap();
assert_eq!(
usdt.balance.free, initial_usdt,
"quote balance should be unchanged on sell"
);
}
#[test]
fn unsupported_instrument_kinds_are_rejected_rather_than_filled_as_spot() {
use rustrade_instrument::instrument::kind::{
future::FutureContract,
option::{OptionContract, OptionExercise, OptionKind},
perpetual::PerpetualContract,
};
let settlement = AssetNameExchange::new("USDT");
let expiry = Utc::now();
let unsupported = [
InstrumentKind::Perpetual(PerpetualContract {
contract_size: d("10"),
settlement_asset: settlement.clone(),
}),
InstrumentKind::Future(FutureContract {
contract_size: d("10"),
settlement_asset: settlement.clone(),
expiry,
}),
InstrumentKind::Option(OptionContract {
contract_size: d("10"),
settlement_asset: settlement.clone(),
kind: OptionKind::Call,
exercise: OptionExercise::European,
expiry,
strike: d("50000"),
}),
];
for kind in unsupported {
let mut venue = make_venue("10", "10000000");
venue.instruments.get_mut(&instrument_name()).unwrap().kind = kind.clone();
let outcome = venue.open_order(buy_request("1.0", market_prices("50000")));
assert!(
outcome.events.is_empty(),
"{kind:?} must produce no account events"
);
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(
UnindexedOrderError::Connectivity(_) | UnindexedOrderError::Rejected(_)
))
),
"{kind:?} must be rejected, got {:?}",
outcome.response.state
);
let usdt = venue.account.balance_mut("e()).unwrap();
assert_eq!(
usdt.balance.free,
d("10000000"),
"{kind:?} must not move the quote balance"
);
}
}
#[test]
fn sell_order_insufficient_balance_names_base_asset() {
let mut venue = make_venue("0.1", "10000");
let outcome = venue.open_order(sell_request("1.0", market_prices("50000")));
assert!(
outcome.events.is_empty(),
"failed order must produce no account events"
);
match outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::BalanceInsufficient(ref asset, _),
))) => {
assert_eq!(
*asset,
base(),
"BalanceInsufficient must name the base asset (BTC), not the quote (USDT)"
);
}
other => panic!("expected BalanceInsufficient, got: {other:?}"),
}
}
#[test]
fn bid_ask_fill_model_fills_at_ask_price_and_deducts_correct_balance() {
let mut venue = make_venue("0", "10000"); venue.fill_model = SimFillConfig::BidAsk(BidAskFillModel);
let market = Some(MarketSnapshot {
best_bid: Some(d("99.5")),
best_ask: Some(d("100.5")),
last_price: Some(d("100.0")),
});
let outcome = venue.open_order(buy_request("1", market));
assert!(
outcome.response.state.is_accepted(),
"buy should succeed: {:?}",
outcome.response.state
);
assert_eq!(
filled_trade(&outcome).price,
d("100.5"),
"fill price must be best_ask"
);
let usdt = venue.account.balance_mut("e()).unwrap();
assert_eq!(
usdt.balance.free,
d("9899.5"),
"quote balance must decrease by fill_price * qty"
);
}
#[test]
fn percentage_fee_model_deducts_correct_fee_on_buy() {
let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
let mut venue = make_venue_with_fee("0", "10000", fee_model);
let outcome = venue.open_order(buy_request("10", market_prices("100")));
assert!(
outcome.response.state.is_accepted(),
"buy should succeed: {:?}",
outcome.response.state
);
assert_eq!(
filled_trade(&outcome).fees.fees,
d("1"),
"trade fee must be 1 USDT"
);
let usdt = venue.account.balance_mut("e()).unwrap();
assert_eq!(
usdt.balance.free,
d("8999"),
"quote balance must decrease by notional + fee"
);
}
#[test]
fn percentage_fee_model_deducts_correct_fee_on_sell() {
let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
let mut venue = make_venue_with_fee("10", "0", fee_model);
let outcome = venue.open_order(sell_request("1", market_prices("100")));
assert!(
outcome.response.state.is_accepted(),
"sell should succeed: {:?}",
outcome.response.state
);
assert_eq!(
filled_trade(&outcome).fees.fees,
d("0.1"),
"trade fee must be 0.1 USDT"
);
let btc = venue.account.balance_mut(&base()).unwrap();
assert_eq!(
btc.balance.free,
d("8.999"),
"base balance must decrease by quantity + fee_in_base"
);
}
#[test]
fn percentage_fee_with_zero_price_returns_zero_fee() {
let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
let mut venue = make_venue_with_fee("10", "0", fee_model);
let outcome = venue.open_order(sell_request("1", market_prices("0")));
assert!(
outcome.response.state.is_accepted(),
"sell at zero price should succeed: {:?}",
outcome.response.state
);
assert_eq!(
filled_trade(&outcome).fees.fees,
Decimal::ZERO,
"fee must be zero when price is zero"
);
let btc = venue.account.balance_mut(&base()).unwrap();
assert_eq!(
btc.balance.free,
d("9"),
"base balance must decrease by quantity only"
);
}
#[test]
fn a_market_order_that_cannot_be_priced_is_rejected_rather_than_panicking() {
for market in [None, Some(MarketSnapshot::default())] {
let mut venue = make_venue("10", "10000000");
let outcome = venue.open_order(buy_request("1.0", market));
assert!(
outcome.events.is_empty(),
"a rejected order must not notify a fill (market={market:?})"
);
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(
UnindexedOrderError::Rejected(_)
))
),
"expected a rejection, got {:?} (market={market:?})",
outcome.response.state
);
let usd = venue.account.balance_mut("e()).unwrap();
assert_eq!(
usd.balance.free,
d("10000000"),
"a rejected order must not move the balance (market={market:?})"
);
}
}
#[test]
fn an_unpriceable_rejection_distinguishes_an_absent_snapshot_from_an_empty_one() {
let reason_of = |market| {
let mut venue = make_venue("10", "10000000");
match venue.open_order(buy_request("1.0", market)).response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(
UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
)) => reason,
other => panic!("expected an OrderRejected, got {other:?}"),
}
};
assert!(
reason_of(None).contains("no market snapshot"),
"an absent snapshot must be named as such, got: {}",
reason_of(None)
);
assert!(
reason_of(Some(MarketSnapshot::default())).contains("no market price available yet"),
"an empty snapshot must read as a cold start, got: {}",
reason_of(Some(MarketSnapshot::default()))
);
}
#[test]
fn a_market_order_fills_at_the_price_its_snapshot_carried() {
let mut venue = make_venue("0", "10000");
let outcome = venue.open_order(buy_request(
"1.0",
Some(MarketSnapshot::from_last_price(Some(d("100")))),
));
let OrderState::Inactive(InactiveOrderState::FullyFilled(ref filled)) =
outcome.response.state
else {
panic!("expected a filled order, got {:?}", outcome.response.state)
};
assert_eq!(
filled.avg_price,
Some(d("100")),
"a market order must fill at its snapshot's last price"
);
assert_eq!(filled.filled_quantity, d("1.0"));
assert_eq!(filled_trade(&outcome).price, d("100"));
let usd = venue.account.balance_mut("e()).unwrap();
assert_eq!(
usd.balance.free,
d("9900"),
"a 1 BTC buy at 100 must debit 100 quote"
);
}
#[test]
fn a_configured_quote_reservation_survives_a_fill() {
let mut venue = make_venue("10", "10000000");
venue.account.balance_mut("e()).unwrap().balance.total = d("20000000");
let outcome = venue.open_order(buy_request("1.0", market_prices("50000")));
assert!(
matches!(outcome.response.state, OrderState::Inactive(_)),
"a market order fills on arrival, got {:?}",
outcome.response.state
);
let usdt = venue.account.balance_mut("e()).unwrap().balance;
assert_eq!(
usdt.free,
d("10000000") - d("50000"),
"the fill draws the notional from the spendable side"
);
assert_eq!(
usdt.total - usdt.free,
d("10000000"),
"the configured reservation must be untouched by an unrelated fill"
);
}
#[test]
fn a_configured_base_reservation_survives_a_fill() {
let mut venue = make_venue("10", "10000000");
venue.account.balance_mut(&base()).unwrap().balance.total = d("20");
let outcome = venue.open_order(sell_request("1.0", market_prices("50000")));
assert!(
matches!(outcome.response.state, OrderState::Inactive(_)),
"a market order fills on arrival, got {:?}",
outcome.response.state
);
let btc = venue.account.balance_mut(&base()).unwrap().balance;
assert_eq!(
btc.free,
d("10") - d("1"),
"the fill delivers one base unit from the spendable side"
);
assert_eq!(
btc.total - btc.free,
d("10"),
"the configured reservation must be untouched by an unrelated fill"
);
}
#[test]
fn a_reservation_is_not_spendable() {
let mut venue = make_venue("10", "100");
venue.account.balance_mut("e()).unwrap().balance.total = d("10000000");
let outcome = venue.open_order(buy_request("1.0", market_prices("50000")));
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(
UnindexedOrderError::Rejected(ApiError::BalanceInsufficient(_, _))
))
),
"expected an insufficient-balance rejection, got {:?}",
outcome.response.state
);
assert!(
outcome.events.is_empty(),
"a refused order moves nothing, so it restates nothing"
);
let usdt = venue.account.balance_mut("e()).unwrap().balance;
assert_eq!(usdt.free, d("100"), "a refused reserve must not move free");
assert_eq!(
usdt.total,
d("10000000"),
"a refused reserve must not move total"
);
}
#[test]
fn an_account_snapshot_orders_one_instruments_orders_deterministically() {
const CIDS: [&str; 8] = ["h", "d", "a", "g", "c", "f", "b", "e"];
let resting = |cid: &str| UnindexedOrder {
key: OrderKey {
exchange: EXCHANGE,
instrument: instrument_name(),
strategy: StrategyId::new("test"),
cid: ClientOrderId::new(cid),
},
side: Side::Buy,
price: Some(d("50000")),
quantity: d("1"),
kind: OrderKind::Limit,
time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
state: OrderState::active(Open {
id: VenueOrderId::Assigned(OrderId::new(cid)),
time_exchange: Default::default(),
filled_quantity: Decimal::ZERO,
}),
};
let mut config = spot_config("10", "10000000", FeeModelConfig::default());
config.initial_state.instruments = vec![InstrumentAccountSnapshot {
instrument: instrument_name(),
orders: CIDS.into_iter().map(resting).collect(),
orders_complete: true,
position: None,
isolated: None,
}];
let venue = SimulatedVenue::new(&config, spot_instruments());
let cids = venue
.account_snapshot()
.instruments
.into_iter()
.flat_map(|instrument| instrument.orders)
.map(|order| order.key.cid)
.collect::<Vec<_>>();
let mut expected = CIDS.map(ClientOrderId::new).to_vec();
expected.sort();
assert_eq!(
cids, expected,
"orders on one instrument must be ordered by a key no two of them share"
);
}
#[test]
fn a_limit_order_is_rejected_before_a_fill_model_is_consulted() {
let mut venue = make_venue("100", "10000000");
venue.fill_model = SimFillConfig::Midpoint(crate::fill::MidpointFillModel);
let mut request = buy_request("1", market_prices("50000"));
request.state.kind = OrderKind::Limit;
request.state.price = Some(d("40000"));
let outcome = venue.open_order(request);
assert!(
outcome.events.is_empty(),
"a rejected order moves no balance and books no trade"
);
match outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::OrderRejected(ref reason),
))) => assert!(
reason.contains("does not support OrderKind::Limit"),
"the rejection must name the unsupported kind, got: {reason}"
),
other => panic!("a limit order must be rejected by kind, got: {other:?}"),
}
}
#[test]
fn a_request_priced_venue_rejects_a_limit_order_and_says_what_would_accept_one() {
let mut venue = make_venue("10", "1000000");
assert_eq!(venue.regime(), VenueRegime::RequestPriced);
let outcome = venue.open_order(limit_request("a", Side::Buy, "1", "48000", gtc()));
assert!(
outcome.events.is_empty(),
"a rejected order moves no balance"
);
match outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::OrderRejected(ref reason),
))) => {
assert!(
reason.contains("new_market_driven"),
"the rejection must name the constructor that would accept it, got: {reason}"
);
}
ref other => panic!("a limit order must be rejected by the regime, got: {other:?}"),
}
}
#[test]
fn a_limit_order_that_does_not_cross_rests_and_reserves_what_its_fill_will_cost() {
let (venue, rested) = venue_resting_one_buy("48000");
let reserved = d("48009.6");
let balance = venue.balances(&[quote()]).remove(0);
assert_eq!(
balance.balance.total,
d("1000000"),
"resting an order spends nothing: `total` moves only on a fill"
);
assert_eq!(
balance.balance.free,
d("1000000") - reserved,
"`free` falls by the notional plus the MAKER fee the fill will charge"
);
assert_eq!(
balance.balance.used(),
reserved,
"the held portion is what a client sees as committed to the order"
);
assert_eq!(
rested_open(&rested).filled_quantity,
Decimal::ZERO,
"the client is told an order is working with none of its quantity done"
);
let open = venue
.orders_open(&[instrument_name()])
.pop()
.expect("the order is on the book");
assert_eq!(open.price, Some(d("48000")));
assert_eq!(
open.state,
*rested_open(&rested),
"the book and the response describe the same order"
);
}
#[test]
fn a_resting_order_fills_at_its_own_limit_and_is_never_improved_by_the_book() {
let (mut venue, _) = venue_resting_one_buy("48000");
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
book("47800", "47900"),
MarketDepth::UNKNOWN,
time(2),
);
assert_eq!(
events.len(),
3,
"one filled order owes a balance, a trade and a terminal order snapshot"
);
let trade = trade_of(&events[1]);
assert_eq!(
trade.price,
d("48000"),
"a resting order fills at its own limit; 47900 would credit it improvement no maker \
can obtain"
);
assert_eq!(
trade.fees.fees,
d("9.6"),
"2bp MAKER on 48000, not 10bp taker: the resting side supplied liquidity"
);
assert_eq!(
filled_price(order_of(&events[2])),
d("48000"),
"the terminal snapshot reports the same price as the trade"
);
}
#[test]
fn a_resting_fill_reports_its_trade_before_the_order_that_produced_it() {
let (mut venue, _) = venue_resting_one_buy("48000");
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
book("47800", "47900"),
MarketDepth::UNKNOWN,
time(2),
);
assert!(
matches!(events[0].kind, AccountEventKind::BalanceSnapshot(_)),
"the balance the fill paid with comes first, got: {:?}",
events[0].kind
);
assert!(
matches!(events[1].kind, AccountEventKind::Trade(_)),
"then the trade, got: {:?}",
events[1].kind
);
assert!(
matches!(events[2].kind, AccountEventKind::OrderSnapshot(_)),
"and only then the order it terminated, got: {:?}",
events[2].kind
);
}
#[test]
fn a_resting_fill_reports_its_order_as_terminal_rather_than_as_a_complete_open() {
let (mut venue, _) = venue_resting_one_buy("48000");
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
book("47800", "47900"),
MarketDepth::UNKNOWN,
time(2),
);
assert!(
matches!(
order_of(&events[2]).state,
OrderState::Inactive(InactiveOrderState::FullyFilled(_))
),
"got: {:?}",
order_of(&events[2]).state
);
assert!(
venue.orders_open(&[]).is_empty(),
"a filled order leaves the book"
);
}
#[test]
fn a_resting_order_settles_exactly_what_it_reserved() {
let (mut venue, _) = venue_resting_one_buy("48000");
advance(&mut venue, time(2));
let reserved = d("48009.6");
let free_while_resting = venue.balances(&[quote()]).remove(0).balance.free;
let events = venue.apply_market(
&instrument_name(),
book("47800", "47900"),
MarketDepth::UNKNOWN,
time(2),
);
let balance = balance_of(&events[0]);
assert_eq!(
balance.balance.total,
d("1000000") - reserved,
"`total` falls by exactly what was held"
);
assert_eq!(
balance.balance.free, free_while_resting,
"`free` was already reduced when the order rested, so the fill does not move it again"
);
assert_eq!(
balance.balance.used(),
Decimal::ZERO,
"nothing is held any more: the order that held it is gone"
);
}
fn seeded_part_filled(price: &str, quantity: &str, filled: &str) -> OpenOrder {
as_open(Order {
key: OrderKey {
exchange: EXCHANGE,
instrument: instrument_name(),
strategy: StrategyId::new("test"),
cid: ClientOrderId::new("part_filled"),
},
side: Side::Buy,
price: Some(d(price)),
quantity: d(quantity),
kind: OrderKind::Limit,
time_in_force: gtc(),
state: OrderState::active(Open::new(
VenueOrderId::Assigned(OrderId::new("part_filled")),
time(1),
d(filled),
)),
})
.expect("the seeded order is Open")
}
#[test]
fn a_part_filled_order_on_the_book_settles_and_prints_only_its_remainder() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.account
.orders_mut()
.insert(
seeded_part_filled("48000", "1", "0.4"),
None,
)
.is_none(),
"an empty book displaces nothing"
);
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
book("47800", "47900"),
MarketDepth::UNKNOWN,
time(2),
);
assert_eq!(
events.len(),
3,
"one filled order owes a balance, a trade and a terminal order snapshot"
);
let trade = trade_of(&events[1]);
assert_eq!(
trade.quantity,
d("0.6"),
"0.6 is what was left to trade; printing 1 would report a fill that did not happen"
);
assert_eq!(
trade.fees.fees,
d("5.76"),
"2bp maker on 0.6 at 48000, not on the whole quantity"
);
assert_eq!(
balance_of(&events[0]).balance.total,
d("1000000") - d("28805.76"),
"the fill costs what the remainder costs; 48009.6 would pay a second time for the 0.4 \
that traded elsewhere"
);
}
#[test]
fn a_fill_completing_a_part_filled_order_reports_no_average_price() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.account
.orders_mut()
.insert(seeded_part_filled("48000", "1", "0.4"), None)
.is_none(),
"an empty book displaces nothing"
);
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
book("47800", "47900"),
MarketDepth::UNKNOWN,
time(2),
);
let filled = match &order_of(&events[2]).state {
OrderState::Inactive(InactiveOrderState::FullyFilled(filled)) => filled,
other => panic!("expected a fully filled order, got: {other:?}"),
};
assert_eq!(
filled.filled_quantity,
d("1"),
"the order has now done its whole quantity, which is what this field reports"
);
assert_eq!(
filled.avg_price, None,
"48000 is one of the two prices behind that quantity, not their mean"
);
}
#[test]
fn resting_a_part_filled_order_reserves_and_settles_exactly_its_remainder() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
let terms = venue
.instrument_terms(&instrument_name())
.expect("the spot fixture is priceable");
let (response, notifications) = venue.rest_order(
limit_request("part_filled", Side::Buy, "1", "48000", gtc()),
d("48000"),
&terms,
d("0.4"),
);
match &response.state {
OrderState::Active(ActiveOrderState::Open(open)) => assert_eq!(
open.filled_quantity,
d("0.4"),
"the order rests carrying what it has already done"
),
other => panic!("expected a resting open order, got: {other:?}"),
}
match notifications {
Some(OpenOrderNotifications::Rested { balance }) => assert_eq!(
balance.0.balance.free,
d("1000000") - d("28805.76"),
"0.6 at 48000 plus 2bp maker is held; 48009.6 would hold against the 0.4 that has \
already settled"
),
other => panic!("a rested order restates one balance, got: {other:?}"),
}
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
book("47800", "47900"),
MarketDepth::UNKNOWN,
time(2),
);
assert_eq!(events.len(), 3);
assert_eq!(
trade_of(&events[1]).quantity,
d("0.6"),
"the cross prints the remainder that was held for"
);
assert_eq!(
balance_of(&events[0]).balance.total,
d("1000000") - d("28805.76"),
"and settles exactly that: one restatement, with nothing left over to release"
);
assert_eq!(
balance_of(&events[0]).balance.used(),
Decimal::ZERO,
"nothing is held any more: the order that held it is gone"
);
}
#[test]
fn a_marketable_order_aggresses_the_way_its_own_terms_asked_for() {
let now = time(1);
let gtd = TimeInForce::GoodTillDate { expiry: time(9) };
assert_eq!(
disposition(OrderKind::Limit, gtc(), true, now),
Disposition::FillAndRest,
"a limit order with somewhere to wait keeps whatever it could not fill"
);
assert_eq!(
disposition(OrderKind::Limit, gtd, true, now),
Disposition::FillAndRest,
"a deadline it has not reached is still somewhere to wait"
);
assert_eq!(
disposition(OrderKind::Limit, TimeInForce::ImmediateOrCancel, true, now),
Disposition::FillAndCancel,
"immediate-or-cancel asked not to wait"
);
assert_eq!(
disposition(OrderKind::Limit, TimeInForce::FillOrKill, true, now),
Disposition::FillOrNothing,
"fill-or-kill would rather trade nothing than trade in part"
);
assert_eq!(
disposition(OrderKind::Market, gtc(), true, now),
Disposition::FillAndCancel,
"an order carrying no limit price has nothing to wait at, whatever its time in force"
);
assert_eq!(
disposition(OrderKind::Market, gtd, true, now),
Disposition::FillAndCancel,
"including one whose deadline is still ahead of it"
);
assert_eq!(
disposition(OrderKind::Market, TimeInForce::FillOrKill, true, now),
Disposition::FillOrNothing,
"and all-or-nothing stays all-or-nothing whatever the kind"
);
}
#[test]
fn a_marketable_limit_order_fills_on_arrival_at_the_book_bounded_by_its_limit() {
let mut venue = make_market_venue_with(
"10",
"1000000",
maker_taker_fee(),
SimFillConfig::BidAsk(BidAskFillModel),
);
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
book("47800", "47900"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let outcome = venue.open_order(limit_request("taker", Side::Buy, "1", "48000", gtc()));
assert_eq!(outcome.events.len(), 2, "a filled open owes balance, trade");
let trade = trade_of(&outcome.events[1]);
assert_eq!(
trade.price,
d("47900"),
"the aggressor takes the offer, which is better than its limit"
);
assert_eq!(
trade.fees.fees,
d("47.9"),
"10bp TAKER on 47900: it crossed the spread rather than resting"
);
assert!(
venue.orders_open(&[]).is_empty(),
"it filled on arrival, so it never rested"
);
}
#[test]
fn a_marketable_limit_order_never_fills_worse_than_its_limit() {
let mut venue = make_market_venue_with(
"10",
"1000000",
FeeModelConfig::default(),
SimFillConfig::default(),
);
advance(&mut venue, time(1));
let stale = MarketSnapshot {
best_bid: Some(d("46900")),
best_ask: Some(d("47000")),
last_price: Some(d("48000")),
};
assert!(
venue
.apply_market(&instrument_name(), stale, MarketDepth::UNKNOWN, time(1))
.is_empty()
);
let outcome = venue.open_order(limit_request("clamped", Side::Buy, "1", "47100", gtc()));
assert_eq!(
trade_of(&outcome.events[1]).price,
d("47100"),
"the model priced it at 48000, which the order never agreed to pay"
);
}
#[test]
fn the_two_pricing_rules_agree_where_the_book_meets_the_limit() {
let limit = "48000";
let touching = || book("47900", "48000");
let mut aggressor = make_market_venue_with(
"10",
"1000000",
maker_taker_fee(),
SimFillConfig::BidAsk(BidAskFillModel),
);
advance(&mut aggressor, time(1));
assert!(
aggressor
.apply_market(
&instrument_name(),
touching(),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let on_arrival = aggressor.open_order(limit_request("a", Side::Buy, "1", limit, gtc()));
let arrival_price = trade_of(&on_arrival.events[1]).price;
let mut maker = make_market_venue_with(
"10",
"1000000",
maker_taker_fee(),
SimFillConfig::BidAsk(BidAskFillModel),
);
advance(&mut maker, time(1));
assert!(
maker
.apply_market(
&instrument_name(),
book("49000", "49100"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let rested = maker.open_order(limit_request("b", Side::Buy, "1", limit, gtc()));
assert_eq!(rested.events.len(), 1, "it rested rather than filling");
advance(&mut maker, time(2));
let resting_events = maker.apply_market(
&instrument_name(),
touching(),
MarketDepth::UNKNOWN,
time(2),
);
let resting_price = trade_of(&resting_events[1]).price;
assert_eq!(
arrival_price,
d(limit),
"an order arriving when the book is at its limit fills at the limit"
);
assert_eq!(
resting_price, arrival_price,
"the two pricing rules must agree exactly at the crossing point; they may differ only \
where the order genuinely is, or is not, marketable"
);
}
#[test]
fn matching_fills_in_price_time_order_and_stops_at_the_first_order_that_does_not_cross() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
book("49000", "49100"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
for (cid, price) in [("worst", "47000"), ("mid", "48000"), ("best", "48500")] {
let outcome = venue.open_order(limit_request(cid, Side::Buy, "1", price, gtc()));
assert_eq!(outcome.events.len(), 1, "{cid} must rest");
}
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
book("47900", "48000"),
MarketDepth::UNKNOWN,
time(2),
);
let filled: Vec<_> = events
.iter()
.filter_map(|event| match &event.kind {
AccountEventKind::Trade(trade) => Some(trade.price),
_ => None,
})
.collect();
assert_eq!(
filled,
vec![d("48500"), d("48000")],
"best bid first, and each at its own limit"
);
assert_eq!(
venue
.orders_open(&[])
.into_iter()
.map(|order| order.key.cid)
.collect::<Vec<_>>(),
vec![ClientOrderId::new("worst")],
"the order the market did not reach is untouched"
);
}
#[test]
fn a_feed_with_no_book_matches_on_the_trade_price() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
traded("49000"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let outcome = venue.open_order(limit_request("t", Side::Buy, "1", "48000", gtc()));
assert_eq!(outcome.events.len(), 1, "49000 traded does not reach 48000");
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
traded("47500"),
MarketDepth::UNKNOWN,
time(2),
);
assert_eq!(events.len(), 3, "a print through the limit fills it");
assert_eq!(trade_of(&events[1]).price, d("48000"));
}
#[test]
fn a_price_inside_the_spread_does_not_cross_a_resting_order() {
let (mut venue, _) = venue_resting_one_buy("48000");
advance(&mut venue, time(2));
let inside = MarketSnapshot {
best_bid: Some(d("47900")),
best_ask: Some(d("48100")),
last_price: Some(d("47950")),
};
assert!(
venue
.apply_market(&instrument_name(), inside, MarketDepth::UNKNOWN, time(2))
.is_empty(),
"the offer is 48100, so a 48000 bid is not crossed however the trade price reads"
);
assert_eq!(
venue.orders_open(&[]).len(),
1,
"the order is still resting"
);
}
#[test]
fn a_resting_sell_reserves_base_and_settles_it() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
book("47000", "47100"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let outcome = venue.open_order(limit_request("s", Side::Sell, "1", "48000", gtc()));
assert_eq!(outcome.events.len(), 1, "47000 bid does not reach 48000");
assert_eq!(
balance_of(&outcome.events[0]).asset,
base(),
"a spot sell delivers the base asset, so that is what is held"
);
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
book("48100", "48200"),
MarketDepth::UNKNOWN,
time(2),
);
assert_eq!(trade_of(&events[1]).price, d("48000"), "at its own limit");
let balance = balance_of(&events[0]);
assert_eq!(balance.asset, base());
assert_eq!(balance.balance.total, d("10") - d("1") - d("0.0002"));
}
#[test]
fn a_maker_rebate_is_held_and_settled_as_a_smaller_amount_than_the_notional() {
let rebate =
FeeModelConfig::Percentage(PercentageFeeModel::maker_taker(d("-0.0001"), d("0.001")));
let mut venue = make_market_venue_with("10", "1000000", rebate, SimFillConfig::default());
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
book("49000", "49100"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let outcome = venue.open_order(limit_request("r", Side::Buy, "1", "48000", gtc()));
let held = d("48000") - d("4.8");
assert_eq!(
balance_of(&outcome.events[0]).balance.used(),
held,
"a rebate means less is held than the notional"
);
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
book("47800", "47900"),
MarketDepth::UNKNOWN,
time(2),
);
assert_eq!(
balance_of(&events[0]).balance.total,
d("1000000") - held,
"and exactly that less is settled, so the rebate is kept"
);
assert_eq!(trade_of(&events[1]).fees.fees, d("-4.8"));
}
#[test]
fn a_limit_order_that_cannot_be_afforded_is_rejected_without_resting() {
let mut venue = make_market_venue("10", "1000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
book("49000", "49100"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let outcome = venue.open_order(limit_request("poor", Side::Buy, "1", "48000", gtc()));
assert!(
outcome.events.is_empty(),
"a refused reservation moves nothing"
);
assert!(matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::BalanceInsufficient(..)
)))
));
assert!(venue.orders_open(&[]).is_empty(), "it never rested");
assert_eq!(
venue.balances(&[quote()]).remove(0).balance.free,
d("1000"),
"the ledger is left exactly as it was"
);
}
#[test]
fn a_cancel_releases_the_reservation_and_restates_the_balance() {
let (mut venue, _) = venue_resting_one_buy("48000");
advance(&mut venue, time(2));
let outcome = venue.cancel_order(OrderEvent {
key: OrderKey {
exchange: EXCHANGE,
instrument: instrument_name(),
strategy: StrategyId::new("test"),
cid: ClientOrderId::new("resting"),
},
state: RequestCancel { id: None },
});
assert_eq!(outcome.events.len(), 1, "a cancel restates one balance");
let balance = balance_of(&outcome.events[0]);
assert_eq!(
balance.balance.free,
d("1000000"),
"everything held against the order is spendable again"
);
assert_eq!(
balance.balance.total,
d("1000000"),
"nothing ever left the account, so `total` never moved"
);
let cancelled = outcome.response.state.expect("the order was on the book");
assert_eq!(cancelled.filled_quantity, Decimal::ZERO);
assert_eq!(cancelled.time_exchange, time(2));
assert!(venue.orders_open(&[]).is_empty());
}
#[test]
fn a_cancel_that_loses_the_race_to_its_own_fill_reports_the_fill() {
let (mut venue, _) = venue_resting_one_buy("48000");
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
book("47800", "47900"),
MarketDepth::UNKNOWN,
time(2),
);
assert_eq!(events.len(), 3, "the order filled first");
let outcome = venue.cancel_order(OrderEvent {
key: OrderKey {
exchange: EXCHANGE,
instrument: instrument_name(),
strategy: StrategyId::new("test"),
cid: ClientOrderId::new("resting"),
},
state: RequestCancel { id: None },
});
assert!(outcome.events.is_empty(), "a failed cancel moves nothing");
assert!(
matches!(
outcome.response.state,
Err(OrderError::Rejected(ApiError::OrderAlreadyFullyFilled))
),
"got: {:?}",
outcome.response.state
);
}
#[test]
fn a_second_cancel_reports_the_order_as_already_cancelled() {
let (mut venue, _) = venue_resting_one_buy("48000");
let cancel = || OrderEvent {
key: OrderKey {
exchange: EXCHANGE,
instrument: instrument_name(),
strategy: StrategyId::new("test"),
cid: ClientOrderId::new("resting"),
},
state: RequestCancel { id: None },
};
assert!(venue.cancel_order(cancel()).response.state.is_ok());
let second = venue.cancel_order(cancel());
assert!(
matches!(
second.response.state,
Err(OrderError::Rejected(ApiError::OrderAlreadyCancelled))
),
"got: {:?}",
second.response.state
);
}
#[test]
fn a_cancelled_order_enters_the_account_snapshot() {
let (mut venue, _) = venue_resting_one_buy("48000");
venue.cancel_order(OrderEvent {
key: OrderKey {
exchange: EXCHANGE,
instrument: instrument_name(),
strategy: StrategyId::new("test"),
cid: ClientOrderId::new("resting"),
},
state: RequestCancel { id: None },
});
let snapshot = venue.account_snapshot();
let orders = &snapshot.instruments[0].orders;
assert_eq!(orders.len(), 1);
assert!(matches!(
orders[0].state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
));
}
#[test]
fn a_limit_order_is_rejected_for_every_time_in_force_this_venue_cannot_honour() {
let unsupported = [
TimeInForce::GoodUntilEndOfDay,
TimeInForce::AtOpen,
TimeInForce::AtClose,
];
for time_in_force in unsupported {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
book("49000", "49100"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let outcome =
venue.open_order(limit_request("tif", Side::Buy, "1", "48000", time_in_force));
assert!(
outcome.events.is_empty(),
"{time_in_force} must move no balance"
);
assert!(
venue.orders_open(&[]).is_empty(),
"{time_in_force} must not rest"
);
match outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::OrderRejected(ref reason),
))) => assert!(
reason.contains("does not model"),
"the rejection must say what is missing, got: {reason}"
),
ref other => panic!("{time_in_force} must be rejected, got: {other:?}"),
}
}
}
#[test]
fn a_market_order_is_accepted_for_every_time_in_force_that_only_bounds_how_long_it_works() {
for time_in_force in [
TimeInForce::GoodUntilCancelled { post_only: false },
TimeInForce::ImmediateOrCancel,
TimeInForce::FillOrKill,
TimeInForce::GoodTillDate { expiry: time(9) },
TimeInForce::GoodUntilEndOfDay,
] {
let mut venue = make_venue("10", "1000000");
let mut request = buy_request("1", market_prices("48000"));
request.state.time_in_force = time_in_force;
let outcome = venue.open_order(request);
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::FullyFilled(_))
),
"{time_in_force} on a market order must still fill, got: {:?}",
outcome.response.state
);
}
}
#[test]
fn a_market_order_is_rejected_for_a_time_in_force_it_cannot_keep() {
for time_in_force in [
TimeInForce::GoodUntilCancelled { post_only: true },
TimeInForce::AtOpen,
TimeInForce::AtClose,
] {
let mut venue = make_venue("10", "1000000");
let mut request = buy_request("1", market_prices("48000"));
request.state.time_in_force = time_in_force;
let outcome = venue.open_order(request);
assert!(
outcome.events.is_empty(),
"{time_in_force} must move no balance"
);
match outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::OrderRejected(ref reason),
))) => assert!(
reason.contains("does not model"),
"the rejection must say what is missing, got: {reason}"
),
ref other => panic!("{time_in_force} must be rejected, got: {other:?}"),
}
}
}
#[test]
fn a_post_only_order_that_would_take_liquidity_is_cancelled_having_traded_nothing() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
book("49000", "49100"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let outcome = venue.open_order(limit_request(
"post",
Side::Buy,
"1",
"50000",
TimeInForce::GoodUntilCancelled { post_only: true },
));
assert!(outcome.events.is_empty(), "nothing was reserved or traded");
assert!(venue.orders_open(&[]).is_empty(), "it must not rest");
assert!(venue.trades(time(0)).is_empty(), "it must not trade");
match outcome.response.state {
OrderState::Inactive(InactiveOrderState::Cancelled(ref cancelled)) => {
assert_eq!(cancelled.filled_quantity, Decimal::ZERO)
}
ref other => panic!("a marketable post-only order must be cancelled, got: {other:?}"),
}
}
#[test]
fn a_post_only_order_that_would_rest_is_accepted_and_reserves() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
book("49000", "49100"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let outcome = venue.open_order(limit_request(
"post",
Side::Buy,
"1",
"48000",
TimeInForce::GoodUntilCancelled { post_only: true },
));
assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
assert_eq!(venue.orders_open(&[]).len(), 1);
assert_eq!(
balance_of(&outcome.events[0]).balance.free,
d("1000000") - d("48000") - d("9.6"),
"resting post-only reserves the notional plus the 2bp maker fee its fill will charge"
);
}
#[test]
fn a_non_marketable_immediate_order_is_cancelled_having_traded_nothing() {
for time_in_force in [TimeInForce::ImmediateOrCancel, TimeInForce::FillOrKill] {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
book("49000", "49100"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let outcome =
venue.open_order(limit_request("ioc", Side::Buy, "1", "48000", time_in_force));
assert!(
outcome.events.is_empty(),
"{time_in_force} must move no balance"
);
assert!(
venue.orders_open(&[]).is_empty(),
"{time_in_force} must not rest"
);
match outcome.response.state {
OrderState::Inactive(InactiveOrderState::Cancelled(ref cancelled)) => assert_eq!(
cancelled.filled_quantity,
Decimal::ZERO,
"{time_in_force} traded nothing"
),
ref other => panic!("{time_in_force} must be cancelled, got: {other:?}"),
}
}
}
#[test]
fn a_marketable_immediate_order_fills_in_full() {
for time_in_force in [TimeInForce::ImmediateOrCancel, TimeInForce::FillOrKill] {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
traded("49000"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let outcome =
venue.open_order(limit_request("ioc", Side::Buy, "1", "50000", time_in_force));
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::FullyFilled(_))
),
"{time_in_force} must fill, got: {:?}",
outcome.response.state
);
}
}
#[test]
fn a_good_till_date_order_rests_until_its_deadline() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
let outcome = venue.open_order(limit_request(
"gtd",
Side::Buy,
"1",
"48000",
TimeInForce::GoodTillDate { expiry: time(9) },
));
assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
advance(&mut venue, time(8));
assert_eq!(venue.orders_open(&[]).len(), 1, "still working at time(8)");
}
#[test]
fn advancing_to_a_deadline_releases_the_reservation_then_reports_the_order() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
let outcome = venue.open_order(limit_request(
"gtd",
Side::Buy,
"1",
"48000",
TimeInForce::GoodTillDate { expiry: time(9) },
));
let reserved = balance_of(&outcome.events[0]).balance.free;
let events = venue.advance_time(time(9));
assert_eq!(events.len(), 2, "one release and one terminal snapshot");
assert_eq!(
balance_of(&events[0]).balance.free,
reserved + d("48000") + d("9.6"),
"the release restates exactly what resting took"
);
let order = order_of(&events[1]);
match order.state {
OrderState::Inactive(InactiveOrderState::Expired(ref expired)) => {
assert_eq!(expired.time_exchange, time(9));
assert_eq!(expired.filled_quantity, Decimal::ZERO);
}
ref other => panic!("the order must be reported expired, got: {other:?}"),
}
assert!(venue.orders_open(&[]).is_empty(), "it must leave the book");
}
#[test]
fn a_deadline_is_reached_at_its_instant_not_after_it() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
let _ = venue.open_order(limit_request(
"gtd",
Side::Buy,
"1",
"48000",
TimeInForce::GoodTillDate { expiry: time(5) },
));
advance(&mut venue, time(4));
assert_eq!(venue.orders_open(&[]).len(), 1);
assert_eq!(venue.advance_time(time(5)).len(), 2);
assert!(venue.orders_open(&[]).is_empty());
}
#[test]
fn a_deadline_is_swept_before_the_tick_that_would_have_filled_it() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
let _ = venue.open_order(limit_request(
"gtd",
Side::Buy,
"1",
"48000",
TimeInForce::GoodTillDate { expiry: time(5) },
));
let events = venue.apply_market(
&instrument_name(),
book("46900", "47000"),
MarketDepth::UNKNOWN,
time(5),
);
assert_eq!(events.len(), 2, "a release and an expiry, not a fill");
assert!(
venue.trades(time(0)).is_empty(),
"an expired order must not trade on the tick that retired it"
);
assert!(matches!(
order_of(&events[1]).state,
OrderState::Inactive(InactiveOrderState::Expired(_))
));
}
#[test]
fn a_good_till_date_order_that_has_already_expired_is_never_rested() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(9));
let outcome = venue.open_order(limit_request(
"stale",
Side::Buy,
"1",
"48000",
TimeInForce::GoodTillDate { expiry: time(5) },
));
assert!(outcome.events.is_empty(), "nothing was reserved");
assert!(venue.orders_open(&[]).is_empty(), "it must not rest");
match outcome.response.state {
OrderState::Inactive(InactiveOrderState::Expired(ref expired)) => {
assert_eq!(expired.time_exchange, time(9), "stamped when it was found");
assert_eq!(expired.filled_quantity, Decimal::ZERO);
}
ref other => panic!("a stale deadline must expire, not {other:?}"),
}
}
#[test]
fn a_stale_deadline_retires_an_order_the_book_would_have_filled() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(9));
assert!(
venue
.apply_market(
&instrument_name(),
traded("47000"),
MarketDepth::UNKNOWN,
time(9)
)
.is_empty()
);
let outcome = venue.open_order(limit_request(
"stale",
Side::Buy,
"1",
"48000",
TimeInForce::GoodTillDate { expiry: time(5) },
));
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::Expired(_))
),
"a marketable order past its deadline must expire, not fill: {:?}",
outcome.response.state
);
assert!(
venue.trades(time(0)).is_empty(),
"it must not trade on its way out"
);
}
#[test]
fn a_cancel_after_a_deadline_is_answered_already_expired() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
let _ = venue.open_order(limit_request(
"gtd",
Side::Buy,
"1",
"48000",
TimeInForce::GoodTillDate { expiry: time(5) },
));
assert_eq!(venue.advance_time(time(6)).len(), 2);
let outcome = venue.cancel_order(OrderEvent {
key: OrderKey {
exchange: EXCHANGE,
instrument: instrument_name(),
strategy: StrategyId::new("test"),
cid: ClientOrderId::new("gtd"),
},
state: RequestCancel { id: None },
});
assert_eq!(
outcome.response.state,
Err(OrderError::Rejected(ApiError::OrderAlreadyExpired)),
"a cancel that lost to a deadline must not read as an unknown order"
);
}
#[test]
fn an_expired_order_enters_the_account_snapshot() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
let _ = venue.open_order(limit_request(
"gtd",
Side::Buy,
"1",
"48000",
TimeInForce::GoodTillDate { expiry: time(5) },
));
assert_eq!(venue.advance_time(time(5)).len(), 2);
let snapshot = venue.account_snapshot();
let orders = &snapshot.instruments[0].orders;
assert_eq!(orders.len(), 1);
assert!(matches!(
orders[0].state,
OrderState::Inactive(InactiveOrderState::Expired(_))
));
}
#[test]
fn deadlines_are_swept_earliest_first() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
for (cid, expiry) in [("late", time(7)), ("early", time(5)), ("middle", time(6))] {
let _ = venue.open_order(limit_request(
cid,
Side::Buy,
"1",
"48000",
TimeInForce::GoodTillDate { expiry },
));
}
let events = venue.advance_time(time(9));
let swept: Vec<_> = events
.iter()
.skip(1)
.step_by(2)
.map(|event| order_of(event).key.cid.0.to_string())
.collect();
assert_eq!(swept, ["early", "middle", "late"]);
}
#[test]
fn a_deadline_on_a_quiet_instrument_is_swept_by_a_tick_on_another() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
let _ = venue.open_order(limit_request(
"gtd",
Side::Buy,
"1",
"48000",
TimeInForce::GoodTillDate { expiry: time(5) },
));
let elsewhere = InstrumentNameExchange::new("eth_usdt");
let events = venue.apply_market(
&elsewhere,
book("2000", "2001"),
MarketDepth::UNKNOWN,
time(6),
);
assert_eq!(
events.len(),
2,
"the quiet instrument's order still retires"
);
assert!(venue.orders_open(&[]).is_empty());
}
#[test]
fn expiring_an_order_seeded_with_no_reservation_restates_no_balance() {
let expiry = time(5);
let seeded = Order {
key: OrderKey {
exchange: EXCHANGE,
instrument: instrument_name(),
strategy: StrategyId::new("test"),
cid: ClientOrderId::new("seeded"),
},
side: Side::Buy,
price: Some(d("48000")),
quantity: d("1"),
kind: OrderKind::Limit,
time_in_force: TimeInForce::GoodTillDate { expiry },
state: OrderState::active(Open::new(
VenueOrderId::Assigned(OrderId::new("seeded")),
time(1),
Decimal::ZERO,
)),
};
let mut venue = make_market_venue("10", "1000000");
assert!(
venue
.account
.orders_mut()
.insert(
as_open(seeded).expect("the seeded order is Open"),
None,
)
.is_none(),
"an empty book displaces nothing"
);
let events = venue.advance_time(expiry);
assert_eq!(events.len(), 1, "the terminal snapshot alone, no release");
assert!(matches!(
order_of(&events[0]).state,
OrderState::Inactive(InactiveOrderState::Expired(_))
));
}
#[test]
fn a_market_driven_venue_prices_a_market_order_from_its_own_book() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
traded("49000"),
MarketDepth::UNKNOWN,
time(1)
)
.is_empty()
);
let outcome = venue.open_order(buy_request("1.0", Some(traded("40000"))));
assert_eq!(
filled_price(&outcome.response),
d("49000"),
"the fill is struck against the venue's book, not the snapshot the request carried"
);
}
#[test]
fn a_request_priced_venue_still_prices_a_market_order_from_the_request() {
let mut venue = make_venue("10", "1000000");
let outcome = venue.open_order(buy_request("1.0", Some(traded("40000"))));
assert_eq!(
filled_price(&outcome.response),
d("40000"),
"with no book of its own, the request's snapshot is still the only price source"
);
}
#[test]
fn a_market_driven_venue_with_no_book_rejects_a_market_order_rather_than_using_the_request() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
let outcome = venue.open_order(buy_request("1.0", Some(traded("49000"))));
assert!(outcome.events.is_empty(), "nothing was debited or traded");
match outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(
UnindexedOrderError::Rejected(ApiError::OrderRejected(ref reason)),
)) => assert!(
reason.contains("no market snapshot"),
"the rejection must name the venue's absent market, got: {reason}"
),
ref other => panic!("an unpriceable market order must be rejected, got: {other:?}"),
}
}
fn sized(amount: &str) -> MarketDepth {
MarketDepth::new(Some(d(amount)), Some(d(amount)))
}
fn venue_offering(depth: MarketDepth) -> SimulatedVenue {
venue_offering_funded("1000000", depth)
}
fn venue_offering_funded(usdt: &str, depth: MarketDepth) -> SimulatedVenue {
let mut venue = make_market_venue_with(
"10",
usdt,
maker_taker_fee(),
SimFillConfig::BidAsk(BidAskFillModel),
);
advance(&mut venue, time(1));
assert!(
venue
.apply_market(&instrument_name(), book("47800", "47900"), depth, time(1))
.is_empty(),
"an empty book fills nothing"
);
venue
}
fn free_quote(venue: &SimulatedVenue) -> Decimal {
venue.balances(&[quote()])[0].balance.free
}
fn total_quote(venue: &SimulatedVenue) -> Decimal {
venue.balances(&[quote()])[0].balance.total
}
#[test]
fn a_market_order_fills_what_the_book_has_and_cancels_what_it_has_not() {
let mut venue = venue_offering(sized("0.4"));
let outcome = venue.open_order(buy_request("1", None));
assert_eq!(outcome.events.len(), 2, "a fill owes balance, then trade");
let trade = trade_of(&outcome.events[1]);
assert_eq!(
trade.quantity,
d("0.4"),
"it took the whole offer and no more"
);
assert_eq!(trade.price, d("47900"));
match &outcome.response.state {
OrderState::Inactive(InactiveOrderState::Cancelled(cancelled)) => assert_eq!(
cancelled.filled_quantity,
d("0.4"),
"the remainder retires carrying what did trade"
),
other => panic!("a market order's unfillable remainder is cancelled, got: {other:?}"),
}
assert!(
venue.orders_open(&[]).is_empty(),
"a market order has no price to rest the remainder at"
);
}
#[test]
fn fill_or_kill_refuses_the_partial_fill_immediate_or_cancel_accepts() {
let mut immediate = venue_offering(sized("0.4"));
let mut all_or_nothing = venue_offering(sized("0.4"));
let ioc = immediate.open_order(limit_request(
"ioc",
Side::Buy,
"1",
"48000",
TimeInForce::ImmediateOrCancel,
));
let fok = all_or_nothing.open_order(limit_request(
"fok",
Side::Buy,
"1",
"48000",
TimeInForce::FillOrKill,
));
assert_eq!(
trade_of(&ioc.events[1]).quantity,
d("0.4"),
"immediate-or-cancel keeps what the book could give it"
);
assert!(
fok.events.is_empty(),
"fill-or-kill would rather trade nothing than trade in part"
);
assert!(
all_or_nothing.trades(time(0)).is_empty(),
"and nothing reached the ledger either"
);
assert_eq!(
free_quote(&all_or_nothing),
d("1000000"),
"a killed order moves no balance"
);
}
#[test]
fn a_capped_limit_order_fills_what_it_can_and_rests_the_remainder() {
let mut venue = venue_offering(sized("0.4"));
let outcome = venue.open_order(limit_request("split", Side::Buy, "1", "48000", gtc()));
assert_eq!(
outcome.events.len(),
2,
"one arrival restates one balance and prints one trade, even settling and holding at \
once"
);
let trade = trade_of(&outcome.events[1]);
assert_eq!(trade.quantity, d("0.4"));
assert_eq!(
trade.price,
d("47900"),
"the part that traded took the offer"
);
let open = rested_open(&outcome);
assert_eq!(
open.filled_quantity,
d("0.4"),
"the remainder rests carrying what has already traded"
);
assert_eq!(open.quantity_remaining(d("1")), d("0.6"));
assert_eq!(venue.orders_open(&[]).len(), 1);
assert_eq!(total_quote(&venue), d("1000000") - d("19179.16"));
assert_eq!(
free_quote(&venue),
d("1000000") - d("19179.16") - d("28805.76")
);
}
#[test]
fn an_order_filled_in_part_then_crossed_settles_the_remainder_and_no_more() {
let mut venue = venue_offering(sized("0.4"));
let arrival = venue.open_order(limit_request("split", Side::Buy, "1", "48000", gtc()));
let taker = trade_of(&arrival.events[1]).clone();
let fills = venue.apply_market(
&instrument_name(),
book("48000", "48000"),
MarketDepth::UNKNOWN,
time(2),
);
assert_eq!(fills.len(), 3, "a resting fill owes balance, trade, order");
let maker = trade_of(&fills[1]);
assert_eq!(maker.quantity, d("0.6"), "only the remainder crosses");
assert_eq!(
maker.price,
d("48000"),
"a maker is paid the price it quoted, not the one the book moved to"
);
assert_eq!(
taker.quantity + maker.quantity,
d("1"),
"the two fills are the whole order, and no more than it"
);
assert_ne!(
taker.id, maker.id,
"one order printing twice must print two distinguishable trades"
);
assert_eq!(
taker.order_id, maker.order_id,
"and both must still say which order they belong to"
);
let spent = d("19179.16") + d("28805.76");
assert_eq!(total_quote(&venue), d("1000000") - spent);
assert_eq!(
free_quote(&venue),
d("1000000") - spent,
"the order is done, so nothing is still held against it"
);
}
#[test]
fn a_second_order_on_one_observation_sees_only_what_the_first_left() {
let mut venue = venue_offering(sized("1"));
let first = venue.open_order(limit_request(
"first",
Side::Buy,
"0.6",
"48000",
TimeInForce::ImmediateOrCancel,
));
assert_eq!(trade_of(&first.events[1]).quantity, d("0.6"));
let second = venue.open_order(limit_request(
"second",
Side::Buy,
"0.6",
"48000",
TimeInForce::ImmediateOrCancel,
));
assert_eq!(
trade_of(&second.events[1]).quantity,
d("0.4"),
"the offer was displayed once, so the two of them share it"
);
let third = venue.open_order(limit_request(
"third",
Side::Buy,
"0.6",
"48000",
TimeInForce::ImmediateOrCancel,
));
assert!(
third.events.is_empty(),
"the offer is exhausted until the next observation replaces it"
);
}
#[test]
fn a_new_observation_replaces_the_remaining_size_rather_than_adding_to_it() {
let mut venue = venue_offering(sized("1"));
let taken = venue.open_order(limit_request(
"first",
Side::Buy,
"0.6",
"48000",
TimeInForce::ImmediateOrCancel,
));
assert_eq!(trade_of(&taken.events[1]).quantity, d("0.6"));
assert!(
venue
.apply_market(
&instrument_name(),
book("47800", "47900"),
sized("1"),
time(2)
)
.is_empty()
);
let after = venue.open_order(limit_request(
"second",
Side::Buy,
"1",
"48000",
TimeInForce::ImmediateOrCancel,
));
assert_eq!(
trade_of(&after.events[1]).quantity,
d("1"),
"the fresh observation offers a whole unit again"
);
}
#[test]
fn a_feed_supplying_no_size_caps_nothing() {
let mut venue = venue_offering(MarketDepth::UNKNOWN);
let outcome = venue.open_order(limit_request(
"uncapped",
Side::Buy,
"5",
"48000",
TimeInForce::ImmediateOrCancel,
));
assert_eq!(
trade_of(&outcome.events[1]).quantity,
d("5"),
"an absent size caps nothing, however large the order"
);
}
#[test]
fn a_marketable_order_with_nothing_on_offer_rests_rather_than_filling() {
let mut venue = venue_offering(MarketDepth::new(Some(d("1")), Some(Decimal::ZERO)));
let outcome = venue.open_order(limit_request("nothing", Side::Buy, "1", "48000", gtc()));
assert_eq!(
outcome.events.len(),
1,
"nothing traded, so it owes only the hold it took"
);
assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
}
#[test]
fn an_order_that_cannot_fund_both_legs_is_refused_with_nothing_moved() {
const FUNDED: &str = "47950";
let mut uncapped = venue_offering_funded(FUNDED, MarketDepth::UNKNOWN);
let affordable =
uncapped.open_order(limit_request("whole", Side::Buy, "1", "48000", gtc()));
assert_eq!(
trade_of(&affordable.events[1]).quantity,
d("1"),
"uncapped, this very order is affordable and fills outright"
);
let mut venue = venue_offering_funded(FUNDED, sized("0.4"));
let outcome = venue.open_order(limit_request("split", Side::Buy, "1", "48000", gtc()));
assert!(outcome.events.is_empty(), "a refused order moves nothing");
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(_))
),
"and says so, rather than filling the part it could afford: {:?}",
outcome.response.state
);
assert_eq!(free_quote(&venue), d(FUNDED), "the ledger is as it was");
assert_eq!(total_quote(&venue), d(FUNDED));
assert!(venue.trades(time(0)).is_empty(), "and nothing traded");
assert!(venue.orders_open(&[]).is_empty());
}
#[test]
fn replacing_a_resting_order_releases_what_was_held_against_it() {
let (mut venue, first) = venue_resting_one_buy("48000");
assert_eq!(
free_quote(&venue),
d("1000000") - d("48009.6"),
"the first order holds its whole notional plus the maker fee"
);
let second = venue.open_order(limit_request("resting", Side::Buy, "0.5", "48000", gtc()));
assert_eq!(
venue.orders_open(&[]).len(),
1,
"one cid, one resting order"
);
assert_eq!(
free_quote(&venue),
d("1000000") - d("24004.8"),
"only the replacement's own reservation is still held"
);
assert_eq!(
balance_of(&second.events[0]).balance.free,
free_quote(&venue),
"and the balance it restated is the one that is true afterwards"
);
assert_eq!(
total_quote(&venue),
d("1000000"),
"nothing settled: a replaced order traded nothing"
);
drop(first);
}
}