use crate::{
AccountEventKind, InstrumentAccountSnapshot, UnindexedAccountEvent, UnindexedAccountSnapshot,
balance::{AssetBalance, MarginDetails},
borrow_capacity::BorrowCapacity,
cash_flow::{CashFlow, CashFlowId, CashFlowKind, InterestTiming},
client::mock::MockExecutionConfig,
error::{ApiError, BorrowReject, BorrowRejectReason, UnindexedApiError, UnindexedOrderError},
exchange::mock::{
account::{AccountState, BalanceInsufficient, Committed, Credit, Debit, Delivery, Lending},
orders::{AlreadyResting, OpenOrder, Reservation, RestingOrder},
},
fee::{FeeAssetConfig, FeeModel, FeeModelConfig, Liquidity, SpotFeeAsset},
fill::{FillContext, FillModel, MarketOrderTiming, NextBarPrice, StaleMarketPolicy},
holding_cost::{HeldPosition, HoldingCostModel},
margin::{InterestDue, MarginEffect, MarginInterestModel, SpotMarginConfig},
margin_debt::{MarginDebtKind, MarginDebtUpdate},
market::{MarketBar, MarketDepth, MarketObservation, MarketSnapshot},
order::{
Order, OrderKind, TimeInForce, UnindexedInactiveOrder, UnindexedOrder, UnindexedOrderKey,
id::{ClientOrderId, OrderId, VenueOrderId},
request::{OrderRequestCancel, OrderRequestOpen, UnindexedOrderResponseCancel},
state::{Cancelled, Expired, Filled, Open, OrderState, UnindexedOrderState},
},
position::{Position, PositionReport},
shortability::ShortabilityProvider,
slippage::Slippage,
trade::{AssetFees, TimeSource, Trade, TradeId, TradeOrigin},
};
use chrono::{DateTime, Utc};
use fnv::{FnvHashMap, FnvHashSet};
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;
use std::{sync::Arc, time::Duration};
use tracing::warn;
mod liquidation;
mod spot_pairs;
use spot_pairs::{SpotPair, SpotPairs};
#[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>,
pub bar: Option<MarketBar>,
pub fresh_at: Option<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 spot_fee_asset: SpotFeeAsset,
fill_model: Arc<dyn FillModel>,
slippage: Slippage,
instruments: FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
spot_pairs: SpotPairs,
pub account: AccountState,
market: FnvHashMap<InstrumentNameExchange, VenueInstrumentMarket>,
positions: FnvHashMap<InstrumentNameExchange, NetPosition>,
order_sequence: u64,
order_id_start: u128,
trade_sequence: u64,
time_exchange_latest: DateTime<Utc>,
regime: VenueRegime,
holding_costs: Vec<Arc<dyn HoldingCostModel>>,
accrued_through: Option<DateTime<Utc>>,
next_charge: Option<DateTime<Utc>>,
cash_flow_sequence: u64,
valued_at_entry: bool,
shortability: Option<Arc<dyn ShortabilityProvider>>,
market_order_timing: MarketOrderTiming,
stale_market: StaleMarketPolicy,
spot_margin: Option<SpotMarginConfig>,
fee_asset: Option<FeeAssetConfig>,
margin_interest: Option<Arc<dyn MarginInterestModel>>,
next_interest: Option<DateTime<Utc>>,
next_interest_loans: u64,
reported_debt: FnvHashMap<AssetNameExchange, MarginDetails>,
liquidation: liquidation::LiquidationState,
}
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 {
let positions = seeded_positions(&config.initial_state, &instruments);
let mut account = match AccountState::try_from(config.initial_state.clone()) {
Ok(account) => account,
Err(error) => panic!("invalid MockExecutionConfig::initial_state: {error}"),
};
if config.spot_margin.is_some() {
account.enable_margin();
}
let order_id_start = first_unseeded_order_id(&account);
let reported_debt = account
.balances()
.filter_map(|balance| Some((balance.asset.clone(), balance.balance.margin?)))
.collect();
Self {
exchange: config.mocked_exchange,
fee_model: config.fee_model,
spot_fee_asset: config.spot_fee_asset,
fill_model: Arc::new(config.fill_model),
slippage: Slippage::new(config.slippage),
spot_pairs: SpotPairs::new(&instruments),
instruments,
account,
market: FnvHashMap::default(),
positions,
order_sequence: 0,
order_id_start,
trade_sequence: 0,
time_exchange_latest: Default::default(),
regime,
holding_costs: Vec::new(),
accrued_through: None,
next_charge: None,
cash_flow_sequence: 0,
valued_at_entry: false,
shortability: None,
market_order_timing: config.market_order_timing,
stale_market: config.stale_market,
spot_margin: config.spot_margin.clone(),
fee_asset: config.fee_asset.clone(),
margin_interest: None,
next_interest: None,
next_interest_loans: 0,
reported_debt,
liquidation: Default::default(),
}
}
#[must_use]
pub fn with_holding_costs(mut self, models: Vec<Arc<dyn HoldingCostModel>>) -> Self {
self.holding_costs = models;
self.refresh_next_charge();
self
}
#[must_use]
pub fn with_margin_interest(mut self, model: Arc<dyn MarginInterestModel>) -> Self {
assert!(
self.spot_margin.is_some(),
"SimulatedVenue given a margin interest model without MockExecutionConfig::spot_margin: \
a cash account takes no loans to charge"
);
self.margin_interest = Some(model);
self.refresh_next_interest();
self
}
#[must_use]
pub fn with_fill_model(mut self, model: Arc<dyn FillModel>) -> Self {
self.fill_model = model;
self
}
#[must_use]
pub fn with_shortability(mut self, provider: Arc<dyn ShortabilityProvider>) -> Self {
self.shortability = Some(provider);
self
}
pub fn next_deadline(&self) -> Option<DateTime<Utc>> {
let expiry = self.account.orders().next_expiry();
expiry
.into_iter()
.chain(self.next_charge)
.chain(self.next_interest)
.min()
}
fn refresh_next_charge(&mut self) {
self.next_charge = self.accrued_through.and_then(|after| {
self.positions
.keys()
.flat_map(|instrument| {
self.holding_costs
.iter()
.filter_map(move |model| next_boundary(model.as_ref(), instrument, after))
})
.min()
});
}
fn refresh_next_interest(&mut self) {
self.next_interest_loans = self.account.loans_changed();
let Some(model) = self.margin_interest.as_deref() else {
return;
};
self.next_interest = self.accrued_through.and_then(|after| {
self.account
.loans()
.filter_map(|(asset, _)| next_interest_boundary(model, asset, after))
.min()
});
}
fn refresh_next_interest_if_loans_changed(&mut self) {
if self.account.loans_changed() != self.next_interest_loans {
self.refresh_next_interest();
}
}
pub fn regime(&self) -> VenueRegime {
self.regime
}
#[must_use]
pub fn advance_time(&mut self, time_exchange: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
let time_exchange = self.not_before_reached(time_exchange);
let mut events = self.accrue(time_exchange);
self.time_exchange_latest = time_exchange;
self.account.update_time_exchange(time_exchange);
events.append(&mut self.sweep_expired(time_exchange));
self.check_margin(time_exchange, &mut events);
self.refresh_next_interest_if_loans_changed();
self.report_debt_changes(&mut events);
events
}
fn not_before_reached(&self, time_exchange: DateTime<Utc>) -> DateTime<Utc> {
self.accrued_through
.map_or(time_exchange, |reached| reached.max(time_exchange))
}
fn accrue(&mut self, now: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
let after = match self.accrued_through {
None => {
self.accrued_through = Some(now);
self.refresh_next_charge();
self.refresh_next_interest();
return Vec::new();
}
Some(after) if now <= after => return Vec::new(),
Some(after) => after,
};
self.refresh_next_interest_if_loans_changed();
self.accrued_through = Some(now);
let falls = |next: Option<DateTime<Utc>>| next.is_some_and(|due| due <= now);
if !falls(self.next_charge) && !falls(self.next_interest) {
return Vec::new();
}
let mut due = Vec::new();
for instrument in self.positions.keys() {
for (index, model) in self.holding_costs.iter().enumerate() {
let boundaries = boundaries_through(after, now, |from| {
next_boundary(model.as_ref(), instrument, from)
});
due.extend(
boundaries.map(|boundary| (boundary, Due::Holding(instrument.clone(), index))),
);
}
}
if let Some(model) = self.margin_interest.as_deref() {
for (asset, principal) in self.account.loans() {
let boundaries = boundaries_through(after, now, |from| {
next_interest_boundary(model, asset, from)
});
due.extend(
boundaries.map(|boundary| (boundary, Due::Interest(asset.clone(), principal))),
);
}
}
due.sort_unstable();
let mut events = Vec::with_capacity(due.len() * 2);
for (boundary, due) in due {
match due {
Due::Holding(instrument, index) => {
self.charge_holding_cost(boundary, &instrument, index, &mut events);
}
Due::Interest(asset, principal) => {
self.charge_interest_boundary(boundary, &asset, principal, &mut events);
}
}
}
self.refresh_next_charge();
self.refresh_next_interest();
events
}
fn charge_interest_boundary(
&mut self,
boundary: DateTime<Utc>,
asset: &AssetNameExchange,
principal: Decimal,
events: &mut Vec<UnindexedAccountEvent>,
) {
if let Some((balance, flow)) =
self.charge_interest(asset, principal, InterestDue::Boundary(boundary))
{
events.push(self.build_account_event(Snapshot(balance)));
events.push(self.build_account_event(AccountEventKind::CashFlow(flow)));
}
}
fn charge_interest(
&mut self,
asset: &AssetNameExchange,
principal: Decimal,
due: InterestDue,
) -> Option<(
AssetBalance<AssetNameExchange>,
CashFlow<AssetNameExchange, InstrumentNameExchange>,
)> {
let model = self.margin_interest.as_deref()?;
let charge = model.charge(asset, principal, due)?;
assert!(
charge.interest >= Decimal::ZERO,
"MarginInterestModel {model:?} charged negative interest {} on {principal} {asset} \
due {due:?}",
charge.interest
);
if charge.interest.is_zero() {
return None;
}
let time = due.time();
let balance = self.account.charge_interest(asset, charge.interest, time);
let id = CashFlowId::new(self.cash_flow_sequence.to_smolstr());
self.cash_flow_sequence += 1;
let flow = CashFlow::new(
CashFlowKind::MarginInterest {
rate: charge.rate,
principal: Some(principal),
timing: Some(match due {
InterestDue::Borrow(_) => InterestTiming::OnBorrow,
InterestDue::Boundary(_) => InterestTiming::Periodic,
}),
},
asset.clone(),
-charge.interest,
None,
time,
Some(id),
);
Some((balance, flow))
}
fn charge_holding_cost(
&mut self,
boundary: DateTime<Utc>,
instrument: &InstrumentNameExchange,
model: usize,
events: &mut Vec<UnindexedAccountEvent>,
) {
let (Some(position), Some(terms)) = (
self.positions.get(instrument).copied(),
self.instruments.get(instrument),
) else {
return;
};
let quote = terms.underlying.quote.clone();
let contract_size = terms.kind.contract_size();
let price = self.valuation(instrument, position);
let held = HeldPosition::new(
instrument,
position.quantity,
contract_size,
position.entry,
price,
);
let Some(charge) = self.holding_costs[model].charge(&held, boundary) else {
return;
};
if charge.amount.is_zero() {
return;
}
let balance = self.account.charge("e, charge.amount, boundary);
let free = balance.balance.free;
if free.is_sign_negative() && !(free - charge.amount).is_sign_negative() {
warn!(
exchange = %self.exchange,
%instrument,
asset = %quote,
amount = %charge.amount,
free = %balance.balance.free,
%boundary,
"SimulatedVenue charged a holding cost the account could not cover: free balance \
is negative, and this venue liquidates no CFD position"
);
}
let id = CashFlowId::new(self.cash_flow_sequence.to_smolstr());
self.cash_flow_sequence += 1;
events.push(self.build_account_event(Snapshot(balance)));
events.push(
self.build_account_event(AccountEventKind::CashFlow(CashFlow::new(
charge.kind,
quote,
charge.amount,
Some(instrument.clone()),
boundary,
Some(id),
))),
);
}
fn valuation(&mut self, instrument: &InstrumentNameExchange, position: NetPosition) -> Decimal {
let price = self
.market(instrument)
.and_then(|market| mid_or_last(&market.snapshot));
price.unwrap_or_else(|| {
if !self.valued_at_entry {
self.valued_at_entry = true;
warn!(
exchange = %self.exchange,
%instrument,
entry = %position.entry,
"SimulatedVenue has no market price to value a holding cost at, so values it \
at the position's entry price; warned once per venue"
);
}
position.entry
})
}
fn margin_effect(&self, requested: Option<MarginEffect>) -> MarginEffect {
match &self.spot_margin {
Some(config) => requested.unwrap_or(config.margin_effect()),
None => MarginEffect::NoSideEffect,
}
}
fn refused_margin_effect(
&self,
requested: Option<MarginEffect>,
terms: &InstrumentTerms,
) -> Option<MarginEffect> {
let borrows_here = self.spot_margin.is_some() && !terms.cash_settled;
requested.filter(|effect| !borrows_here && *effect != MarginEffect::NoSideEffect)
}
fn release_reservation(
&mut self,
reservation: Reservation,
time_exchange: DateTime<Utc>,
) -> Released {
let repays = self.repays_at_cancel();
let balance = self
.account
.release_reservation(&reservation, repays, time_exchange);
Released {
balance,
interest: reservation.unreported_interest,
}
}
fn repays_at_cancel(&self) -> bool {
self.spot_margin
.as_ref()
.is_some_and(SpotMarginConfig::auto_repay_at_cancel)
}
fn borrowable(
&self,
debit: &Debit,
effect: MarginEffect,
terms: &InstrumentTerms,
market: Option<&MarketSnapshot>,
) -> Decimal {
let Some(config) = self.spot_margin.as_ref() else {
return Decimal::ZERO;
};
if effect != MarginEffect::Borrow
|| terms.cash_settled
|| self.liquidation.under_way()
|| self.account.free(&debit.asset) >= debit.settled + debit.reserved
{
return Decimal::ZERO;
}
let priced = market.map(|market| (&terms.underlying, market));
self.leverage_room(config, &debit.asset, priced)
.unwrap_or(Decimal::ZERO)
}
fn leverage_room(
&self,
config: &SpotMarginConfig,
asset: &AssetNameExchange,
priced: Option<(&Underlying<AssetNameExchange>, &MarketSnapshot)>,
) -> Option<Decimal> {
let asset_price = self.price_in(asset, config.valuation_asset(), priced)?;
let Valued { equity, debt } = self.valued(config, MarginCheck::Borrow, priced)?;
let headroom = (config.max_leverage() - Decimal::ONE) * equity - debt;
Some(headroom.max(Decimal::ZERO) / asset_price)
}
fn insufficient_error(
&self,
insufficient: BalanceInsufficient,
side: Side,
) -> UnindexedApiError {
let BalanceInsufficient {
asset,
free,
required,
borrowable,
min_borrow,
max_borrow,
} = insufficient;
let mut message =
format!("Available Balance: {free}, Required Balance inc. fees: {required}");
if let Some(borrowable) = borrowable {
message.push_str(&format!(", Borrowable: {borrowable}"));
}
if let Some(min_borrow) = min_borrow {
message.push_str(&format!(", Minimum Borrow: {min_borrow}"));
}
let Some(max_borrow) = max_borrow else {
return ApiError::BalanceInsufficient(Some(asset), message);
};
let message = format!("{asset} borrow exceeds Max Borrow: {max_borrow}; {message}");
let reject = BorrowReject::new(BorrowRejectReason::BorrowLimitExceeded, None, message);
let reject = match (side, borrowable) {
(Side::Sell, Some(left)) => {
let least = self
.spot_margin
.as_ref()
.map_or(Decimal::ZERO, |config| config.min_borrow(&asset));
reject.with_available(if left < least { Decimal::ZERO } else { left })
}
_ => reject,
};
ApiError::BorrowRejected(reject)
}
fn valued(
&self,
config: &SpotMarginConfig,
check: MarginCheck,
priced: Option<(&Underlying<AssetNameExchange>, &MarketSnapshot)>,
) -> Option<Valued> {
let mut valued = Valued::default();
for balance in self.account.balances() {
let owed = owed(balance);
let Some(price) = self.price_in(&balance.asset, config.valuation_asset(), priced)
else {
if owed > Decimal::ZERO {
return None;
}
continue;
};
let net = balance.balance.net_asset();
let ratio = match net > Decimal::ZERO {
true => check.collateral_ratio(config, &balance.asset),
false => Decimal::ONE,
};
valued.equity += net * ratio * price;
valued.debt += owed * price;
}
Some(valued)
}
fn price_in(
&self,
asset: &AssetNameExchange,
valuation: &AssetNameExchange,
priced: Option<(&Underlying<AssetNameExchange>, &MarketSnapshot)>,
) -> Option<Decimal> {
if asset == valuation {
return Some(Decimal::ONE);
}
let snapshot_of = |pair: &SpotPair| {
self.market(&pair.instrument)
.map(|market| market.snapshot)
.or_else(|| {
let (underlying, snapshot) = priced?;
let (base, quote) = match pair.inverse {
true => (valuation, asset),
false => (asset, valuation),
};
(underlying.base == *base && underlying.quote == *quote).then_some(*snapshot)
})
};
self.spot_pairs
.between(asset, valuation)
.iter()
.find_map(|pair| {
let price = mid_or_last(&snapshot_of(pair)?)?;
if price <= Decimal::ZERO {
return None;
}
Some(match pair.inverse {
true => Decimal::ONE / price,
false => price,
})
})
}
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 {
if let Some(resting) = self.account.remove_order(&cid) {
events.extend(self.retire_expired(resting, time_exchange));
}
}
events
}
fn retire_expired(
&mut self,
RestingOrder { order, reservation }: RestingOrder,
time_exchange: DateTime<Utc>,
) -> Vec<UnindexedAccountEvent> {
let flows = reservation
.as_ref()
.map_or(0, |reservation| reservation.unreported_interest.len());
let mut events = Vec::with_capacity(2 + flows);
if let Some(reservation) = reservation {
let Released { balance, interest } =
self.release_reservation(reservation, time_exchange);
events.push(self.build_account_event(Snapshot(balance)));
self.push_interest(&mut events, interest, time_exchange);
}
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: Some(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,
observation: MarketObservation,
time_exchange: DateTime<Utc>,
) -> Vec<UnindexedAccountEvent> {
let time_exchange = self.not_before_reached(time_exchange);
let mut events = self.accrue(time_exchange);
let MarketObservation {
snapshot,
depth,
bar,
fresh,
} = observation;
let entry = self.market.entry(instrument.clone()).or_default();
entry.snapshot = snapshot;
entry.depth = depth;
entry.time_exchange = time_exchange;
if bar.is_some() {
entry.bar = bar;
}
if fresh {
entry.fresh_at = Some(time_exchange);
}
events.append(&mut self.sweep_expired(time_exchange));
let crosses_resting = fresh || !matches!(self.stale_market, StaleMarketPolicy::Wait { .. });
match self.market_order_timing {
MarketOrderTiming::NextBar(price) => {
if let Some(bar) = bar {
if self.bar_releases(&observation) {
events.append(&mut self.release(
instrument,
Release::Bar(&bar, price),
time_exchange,
));
} else {
events.append(&mut self.cancel_held_immediate(instrument, time_exchange));
}
}
if crosses_resting {
events.append(&mut self.match_resting(instrument, snapshot, time_exchange));
}
}
MarketOrderTiming::OnArrival => {
if crosses_resting {
events.append(&mut self.match_resting(instrument, snapshot, time_exchange));
}
if fresh && matches!(self.stale_market, StaleMarketPolicy::Wait { .. }) {
events.append(&mut self.release(
instrument,
Release::Market(&snapshot),
time_exchange,
));
}
}
}
self.check_margin(time_exchange, &mut events);
self.refresh_next_interest_if_loans_changed();
self.report_debt_changes(&mut events);
events
}
pub fn fills_ahead_of(
&self,
instrument: &InstrumentNameExchange,
observation: &MarketObservation,
) -> bool {
matches!(self.market_order_timing, MarketOrderTiming::NextBar(_))
&& self.bar_releases(observation)
&& self.account.orders().holds(instrument)
}
fn bar_releases(&self, observation: &MarketObservation) -> bool {
observation.bar.is_some()
&& (observation.fresh || !matches!(self.stale_market, StaleMarketPolicy::Wait { .. }))
}
#[must_use]
pub fn expire_held(&mut self, time_exchange: DateTime<Utc>) -> Vec<UnindexedAccountEvent> {
let held = self.account.orders().held();
let mut events = Vec::with_capacity(held.len() * 2);
for cid in held {
if let Some(resting) = self.account.remove_order(&cid) {
events.extend(self.retire_expired(resting, time_exchange));
}
}
self.refresh_next_interest_if_loans_changed();
self.report_debt_changes(&mut events);
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() * 4);
for (cid, limit) in crossing {
let Some(RestingOrder { order, reservation }) = self.account.remove_order(&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.fill_settlement(
&terms,
self.net_position(&terms, instrument),
order.side,
remaining,
limit,
Liquidity::Maker,
);
let mut committed = match self.settle_resting(
&order,
&terms,
reservation,
&settlement,
time_exchange,
) {
RestingSettlement::Settled(committed) => committed,
RestingSettlement::Unaffordable { released } => {
events.extend(self.cancel_unfilled(order, released, time_exchange));
continue;
}
};
self.take_fee_apart(&settlement, &mut committed, time_exchange);
let trade = self.print_fill(
&terms,
&order,
Fill {
quantity: remaining,
price: limit,
},
settlement.fees,
time_exchange,
);
let order_id = trade.order_id.clone();
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: Filled::new(
order_id,
time_exchange,
order.quantity,
order.state.filled_quantity.is_zero().then_some(limit),
),
};
self.account.ack_filled(filled.clone());
let filled = filled.map_state(OrderState::fully_filled);
self.push_committed(&mut events, committed, Some(trade));
events.push(self.build_account_event(Snapshot(filled)));
}
events
}
fn release(
&mut self,
instrument: &InstrumentNameExchange,
release: Release<'_>,
time_exchange: DateTime<Utc>,
) -> Vec<UnindexedAccountEvent> {
let held = self.account.take_held(instrument);
if held.is_empty() {
return Vec::new();
}
let terms = match self.instrument_terms(instrument) {
Ok(terms) => terms,
Err(error) => {
panic!("SimulatedVenue holds orders on an instrument it cannot price: {error}")
}
};
let mut events = Vec::with_capacity(held.len() * 4);
for cid in held {
let Some(resting) = self.account.remove_order(&cid) else {
continue;
};
debug_assert_eq!(
resting.order.kind == OrderKind::Limit,
resting.order.price.is_some(),
"SimulatedVenue held {} as {:?} with price {:?}: only a limit order is held with \
a price",
resting.order.key.cid,
resting.order.kind,
resting.order.price
);
events.extend(match resting.order.price {
Some(limit) => {
self.release_held_limit(resting, limit, release, &terms, time_exchange)
}
None => self.fill_held(resting, release, &terms, time_exchange),
});
}
events
}
fn cancel_held_immediate(
&mut self,
instrument: &InstrumentNameExchange,
time_exchange: DateTime<Utc>,
) -> Vec<UnindexedAccountEvent> {
let orders = self.account.orders();
let immediate = orders
.held_on(instrument)
.iter()
.filter(|cid| {
orders.get(cid).is_some_and(|order| {
matches!(
order.time_in_force,
TimeInForce::ImmediateOrCancel | TimeInForce::FillOrKill
)
})
})
.cloned()
.collect::<Vec<_>>();
let mut events = Vec::with_capacity(immediate.len() * 2);
for cid in immediate {
if let Some(RestingOrder { order, reservation }) = self.account.remove_order(&cid) {
let released = reservation
.map(|reservation| self.release_reservation(reservation, time_exchange));
events.extend(self.cancel_unfilled(order, released, time_exchange));
}
}
events
}
fn fill_held(
&mut self,
RestingOrder { order, reservation }: RestingOrder,
release: Release<'_>,
terms: &InstrumentTerms,
time_exchange: DateTime<Utc>,
) -> Vec<UnindexedAccountEvent> {
let side = order.side;
let quantity = order.state.quantity_remaining(order.quantity);
let reference = match release {
Release::Bar(bar, price) => {
let reference = price.reference(side, bar);
let at_reference = MarketSnapshot::from_last_price(Some(reference));
Some(
self.fill_model
.fill_price(&FillContext::new(side, quantity, &at_reference))
.unwrap_or(reference),
)
}
Release::Market(market) => self
.fill_model
.fill_price(&FillContext::new(side, quantity, market)),
};
let Some(reference) = reference else {
let released =
reservation.map(|reservation| self.release_reservation(reservation, time_exchange));
return self.cancel_unfilled(order, released, time_exchange);
};
let filled_quantity = match self.depth_available(&order.key.instrument, side) {
Some(available) => quantity.min(available),
None => quantity,
};
let kill = order.time_in_force == TimeInForce::FillOrKill;
if filled_quantity <= Decimal::ZERO || (filled_quantity < quantity && kill) {
let released =
reservation.map(|reservation| self.release_reservation(reservation, time_exchange));
return self.cancel_unfilled(order, released, time_exchange);
}
let slippage_before = self.slippage.clone();
let fill_price = self.slippage.apply(side, reference);
let settlement = self.fill_settlement(
terms,
self.net_position(terms, &order.key.instrument),
side,
filled_quantity,
fill_price,
Liquidity::Taker,
);
let mut committed =
match self.settle_resting_afresh(terms, reservation, &settlement, time_exchange) {
RestingSettlement::Settled(committed) => committed,
RestingSettlement::Unaffordable { released } => {
self.slippage = slippage_before;
return self.cancel_unfilled(order, released, time_exchange);
}
};
self.take_fee_apart(&settlement, &mut committed, time_exchange);
self.consume_depth(&order.key.instrument, side, filled_quantity);
let fill = Fill {
quantity: filled_quantity,
price: fill_price,
};
let trade = self.print_fill(terms, &order, fill, settlement.fees, time_exchange);
let terminal = if filled_quantity == quantity {
let filled = Order {
key: order.key,
side,
price: order.price,
quantity: order.quantity,
kind: order.kind,
time_in_force: order.time_in_force,
state: Filled::new(
trade.order_id.clone(),
time_exchange,
order.quantity,
Some(fill_price),
),
};
self.account.ack_filled(filled.clone());
filled.map_state(OrderState::fully_filled)
} else {
let cancelled = Order {
key: order.key,
side,
price: order.price,
quantity: order.quantity,
kind: order.kind,
time_in_force: order.time_in_force,
state: Cancelled {
id: trade.order_id.clone(),
time_exchange,
filled_quantity: Some(filled_quantity),
},
};
self.account.ack_cancelled(cancelled.clone());
UnindexedOrder::from(cancelled)
};
let mut events = Vec::with_capacity(4);
self.push_committed(&mut events, committed, Some(trade));
events.push(self.build_account_event(Snapshot(terminal)));
events
}
fn release_held_limit(
&mut self,
RestingOrder { order, reservation }: RestingOrder,
limit: Decimal,
release: Release<'_>,
terms: &InstrumentTerms,
time_exchange: DateTime<Utc>,
) -> Vec<UnindexedAccountEvent> {
let side = order.side;
let quantity = order.state.quantity_remaining(order.quantity);
let (market, reference) = match release {
Release::Bar(bar, price) => {
let reference = price.reference(side, bar);
(
MarketSnapshot::from_last_price(Some(reference)),
Some(reference),
)
}
Release::Market(market) => (*market, None),
};
let marketable = crosses(side, limit, &market);
let post_only = matches!(
order.time_in_force,
TimeInForce::GoodUntilCancelled { post_only: true }
);
if marketable && post_only {
let released =
reservation.map(|reservation| self.release_reservation(reservation, time_exchange));
return self.cancel_unfilled(order, released, time_exchange);
}
let filled_quantity = match self.depth_available(&order.key.instrument, side) {
_ if !marketable => Decimal::ZERO,
Some(available) => quantity.min(available),
None => quantity,
};
let unfilled = quantity - filled_quantity;
let slippage_before = self.slippage.clone();
let position = self.net_position(terms, &order.key.instrument);
let fill = (filled_quantity > Decimal::ZERO).then(|| {
let price = self
.fill_model
.fill_price(&FillContext::new(side, quantity, &market))
.or(reference)
.unwrap_or(limit);
let price = clamp_to_limit(side, self.slippage.apply(side, price), limit);
let settlement = self.fill_settlement(
terms,
position,
side,
filled_quantity,
price,
Liquidity::Taker,
);
(price, settlement)
});
let held = (unfilled > Decimal::ZERO).then(|| {
let position = match &fill {
Some((price, _)) => position.after(side, filled_quantity, *price),
None => position,
};
self.settlement(terms, position, side, unfilled, limit, Liquidity::Maker)
});
let (debit, credit) = match (&fill, &held) {
(Some((_, fill)), Some(held)) => (fill.settled_holding(held), Some(&fill.credit)),
(Some((_, fill)), None) => (fill.settled(), Some(&fill.credit)),
(None, Some(held)) => (held.reserved(), None),
(None, None) => unreachable!(
"SimulatedVenue released {} with nothing to trade or rest: it holds only an order \
with a positive quantity, which `open_order_inner` checks, and such an order \
either trades or rests some of it",
order.key.cid
),
};
let effect = reservation.as_ref().map_or_else(
|| self.margin_effect(None),
|reservation| reservation.effect,
);
let (mut committed, loan, excess_borrowed) =
match self.commit_afresh(terms, reservation, &debit, credit, time_exchange) {
Recommitted::Committed {
commitment,
loan,
excess_borrowed,
} => (commitment, loan, excess_borrowed),
Recommitted::Unaffordable { released } => {
self.slippage = slippage_before;
return self.cancel_unfilled(order, released, time_exchange);
}
};
let mut events = Vec::with_capacity(4);
let mut filled_total = order.state.filled_quantity;
let mut trade = None;
if let Some((price, settlement)) = fill {
self.take_fee_apart(&settlement, &mut committed, time_exchange);
self.consume_depth(&order.key.instrument, side, filled_quantity);
let fill = Fill {
quantity: filled_quantity,
price,
};
trade = Some(self.print_fill(terms, &order, fill, settlement.fees, time_exchange));
filled_total += filled_quantity;
}
let terminal = match held {
Some(held) => {
let open = Open::new(order.state.id.clone(), time_exchange, filled_total);
let rested = Order {
key: order.key,
side,
price: order.price,
quantity: order.quantity,
kind: order.kind,
time_in_force: order.time_in_force,
state: open,
};
let unreported_interest = match trade {
Some(_) => Vec::new(),
None => {
let mut held = std::mem::take(&mut committed.held_interest);
held.extend(committed.interest.take().map(|flow| *flow));
held
}
};
self.book_rested(
rested.clone(),
Reservation {
borrowed: loan.min(held.requirement()),
excess_borrowed,
amount: held.requirement(),
asset: held.asset,
effect,
unreported_interest,
},
);
rested.map_state(OrderState::active)
}
None => {
let Some(trade) = &trade else {
unreachable!("an order with nothing left to rest has traded");
};
let filled = Order {
key: order.key,
side,
price: order.price,
quantity: order.quantity,
kind: order.kind,
time_in_force: order.time_in_force,
state: Filled::new(
trade.order_id.clone(),
time_exchange,
order.quantity,
Some(trade.price),
),
};
self.account.ack_filled(filled.clone());
filled.map_state(OrderState::fully_filled)
}
};
self.push_committed(&mut events, committed, trade);
events.push(self.build_account_event(Snapshot(terminal)));
events
}
fn print_fill(
&mut self,
terms: &InstrumentTerms,
order: &OpenOrder,
fill: Fill,
fees: AssetFees<AssetNameExchange>,
time_exchange: DateTime<Utc>,
) -> Trade<AssetNameExchange, InstrumentNameExchange> {
let Fill { quantity, price } = fill;
self.record_fill(terms, &order.key.instrument, order.side, fill);
let trade = Trade {
id: self.trade_id_sequence_fetch_add(),
order_id: order.state.id.or_client_id(&order.key.cid),
instrument: order.key.instrument.clone(),
strategy: order.key.strategy.clone(),
time_exchange,
side: order.side,
price,
quantity,
order_filled_quantity: Some(order.state.filled_quantity + quantity),
fees,
origin: TradeOrigin::Order,
position_side: None,
client_order_id: Some(order.key.cid.clone()),
time_source: TimeSource::Venue,
};
self.account.ack_trade(trade.clone());
trade
}
fn settle_resting(
&mut self,
order: &OpenOrder,
terms: &InstrumentTerms,
reservation: Option<Reservation>,
settlement: &Settlement,
time_exchange: DateTime<Utc>,
) -> RestingSettlement {
let delivery = match &settlement.credit {
_ if settlement.fee_apart => {
return self.settle_resting_afresh(terms, reservation, settlement, time_exchange);
}
Credit::Delivery(delivery) if !delivery.amount.is_sign_negative() => delivery,
Credit::PaidBack(_) | Credit::Delivery(_) => {
return self.settle_resting_afresh(terms, reservation, settlement, time_exchange);
}
};
let effect = reservation.as_ref().map_or_else(
|| self.margin_effect(None),
|reservation| reservation.effect,
);
let Some(Reservation {
asset,
amount,
unreported_interest,
..
}) = reservation
else {
let debit = settlement.settled();
return RestingSettlement::Settled(
self.commit(
&debit,
Some(&settlement.credit),
effect,
terms,
None,
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, insufficient.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
);
let paid = self.account.settle(&asset, amount, time_exchange);
let received = self.account.deliver(delivery, effect, time_exchange);
RestingSettlement::Settled(Commitment {
held_interest: unreported_interest,
..Commitment::from(Committed {
paid,
received: Some(received),
borrowed: Decimal::ZERO,
excess_borrowed: Decimal::ZERO,
})
})
}
fn settle_resting_afresh(
&mut self,
terms: &InstrumentTerms,
reservation: Option<Reservation>,
settlement: &Settlement,
time_exchange: DateTime<Utc>,
) -> RestingSettlement {
match self.commit_afresh(
terms,
reservation,
&settlement.settled(),
Some(&settlement.credit),
time_exchange,
) {
Recommitted::Committed { commitment, .. } => RestingSettlement::Settled(commitment),
Recommitted::Unaffordable { released } => RestingSettlement::Unaffordable { released },
}
}
fn commit_afresh(
&mut self,
terms: &InstrumentTerms,
mut reservation: Option<Reservation>,
debit: &Debit,
credit: Option<&Credit>,
time_exchange: DateTime<Utc>,
) -> Recommitted {
let held_interest = reservation
.as_mut()
.map(|reservation| std::mem::take(&mut reservation.unreported_interest))
.unwrap_or_default();
let effect = reservation.as_ref().map_or_else(
|| self.margin_effect(None),
|reservation| reservation.effect,
);
let released = reservation.map(|reservation| {
let balance =
self.account
.release(&reservation.asset, reservation.amount, time_exchange);
(reservation, balance)
});
let repays_at_cancel = self.repays_at_cancel();
match self.commit(debit, credit, effect, terms, None, time_exchange) {
Ok(mut commitment) => {
let mut loan = commitment.committed.shortfall_borrowed();
let mut excess_borrowed = commitment.committed.excess_borrowed;
if let Some((reservation, _)) = &released {
loan += reservation.borrowed;
excess_borrowed += reservation.excess_borrowed;
}
if let Some((reservation, _)) = released.filter(|_| repays_at_cancel) {
let surplus = reservation.amount - (debit.settled + debit.reserved);
let unused = reservation.borrowed.min(surplus);
if unused > Decimal::ZERO {
debug_assert_eq!(commitment.committed.paid.asset, reservation.asset);
commitment.committed.paid =
self.account
.repay_loan(&reservation.asset, unused, time_exchange);
loan -= unused;
}
}
commitment.held_interest = held_interest;
Recommitted::Committed {
commitment,
loan,
excess_borrowed,
}
}
Err(_) => Recommitted::Unaffordable {
released: released.map(|(reservation, balance)| Released {
balance: match !repays_at_cancel || reservation.loan().is_zero() {
true => balance,
false => self.account.repay_loan(
&reservation.asset,
reservation.loan(),
time_exchange,
),
},
interest: held_interest,
}),
},
}
}
fn push_committed(
&self,
events: &mut Vec<UnindexedAccountEvent>,
commitment: Commitment,
trade: Option<Trade<AssetNameExchange, InstrumentNameExchange>>,
) {
let Commitment {
committed: Committed { paid, received, .. },
interest,
held_interest,
fee,
} = commitment;
let reported_at = trade
.as_ref()
.map_or(paid.time_exchange, |trade| trade.time_exchange);
events.push(self.build_account_event(Snapshot(paid)));
if let Some(received) = received {
events.push(self.build_account_event(Snapshot(received)));
}
if let Some(fee) = fee {
events.push(self.build_account_event(Snapshot(*fee)));
}
if let Some(trade) = trade {
events.push(self.build_account_event(trade));
}
self.push_interest(
events,
held_interest.into_iter().chain(interest.map(|flow| *flow)),
reported_at,
);
}
fn push_interest(
&self,
events: &mut Vec<UnindexedAccountEvent>,
interest: impl IntoIterator<Item = CashFlow<AssetNameExchange, InstrumentNameExchange>>,
time_exchange: DateTime<Utc>,
) {
events.extend(interest.into_iter().map(|mut flow| {
flow.time_exchange = time_exchange;
self.build_account_event(AccountEventKind::CashFlow(flow))
}));
}
fn take_fee_apart(
&mut self,
settlement: &Settlement,
commitment: &mut Commitment,
time_exchange: DateTime<Utc>,
) {
if !settlement.fee_apart || settlement.fees.fees.is_zero() {
return;
}
commitment.fee = Some(Box::new(self.account.charge(
&settlement.fees.asset,
-settlement.fees.fees,
time_exchange,
)));
}
fn commit(
&mut self,
debit: &Debit,
credit: Option<&Credit>,
effect: MarginEffect,
terms: &InstrumentTerms,
market: Option<&MarketSnapshot>,
time_exchange: DateTime<Utc>,
) -> Result<Commitment, BalanceInsufficient> {
let lending = Lending {
borrowable: self.borrowable(debit, effect, terms, market),
min_borrow: self
.spot_margin
.as_ref()
.map_or(Decimal::ZERO, |config| config.min_borrow(&debit.asset)),
max_borrow: self
.spot_margin
.as_ref()
.and_then(|config| config.max_borrow(&debit.asset)),
};
let mut committed = self
.account
.commit(debit, credit, effect, lending, time_exchange)?;
let interest = match committed.borrowed > Decimal::ZERO {
true => self
.charge_interest(
&debit.asset,
committed.borrowed,
InterestDue::Borrow(time_exchange),
)
.map(|(balance, flow)| {
committed.paid = balance;
Box::new(flow)
}),
false => None,
};
Ok(Commitment {
interest,
..Commitment::from(committed)
})
}
fn cancel_unfilled(
&mut self,
order: OpenOrder,
released: Option<Released>,
time_exchange: DateTime<Utc>,
) -> Vec<UnindexedAccountEvent> {
let flows = released
.as_ref()
.map_or(0, |released| released.interest.len());
let mut events = Vec::with_capacity(2 + flows);
if let Some(Released { balance, interest }) = released {
events.push(self.build_account_event(Snapshot(balance)));
self.push_interest(&mut events, interest, time_exchange);
}
let id = order.state.id.or_client_id(&order.key.cid);
let 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 {
id,
time_exchange,
filled_quantity: Some(order.state.filled_quantity),
},
};
self.account.ack_cancelled(cancelled.clone());
events.push(self.build_account_event(Snapshot(UnindexedOrder::from(cancelled))));
events
}
pub fn instruments(
&self,
) -> &FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
&self.instruments
}
pub fn instrument(
&self,
name: &InstrumentNameExchange,
) -> Option<&Instrument<ExchangeId, AssetNameExchange>> {
self.instruments.get(name)
}
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 borrow_capacity(
&self,
instrument: &InstrumentNameExchange,
side: Side,
) -> Result<BorrowCapacity, UnindexedApiError> {
let terms = self.instrument_terms(instrument)?;
let config = match &self.spot_margin {
Some(config) if !terms.cash_settled => config,
Some(_) => {
return Err(ApiError::InstrumentInvalid(
instrument.clone(),
format!(
"SimulatedVenue lends nothing against {instrument}: it is cash-settled"
),
));
}
None => {
return Err(ApiError::InstrumentInvalid(
instrument.clone(),
format!(
"SimulatedVenue lends nothing against {instrument}: it has no spot margin \
configuration"
),
));
}
};
let asset = match side {
Side::Buy => &terms.underlying.quote,
Side::Sell => &terms.underlying.base,
};
let mut capacity = BorrowCapacity::new(asset.clone());
let cap = config.max_borrow(asset);
if let Some(cap) = cap {
capacity = capacity.with_account_limit(cap);
}
let cap_room = cap.map(|cap| self.account.cap_room(asset, cap));
let min_borrow = config.min_borrow(asset);
let ruled_out = self.liquidation.under_way()
|| cap_room.is_some_and(|room| room.is_zero() || room < min_borrow);
let room = match ruled_out {
true => Some(Decimal::ZERO),
false => self
.values_every_holding(config.valuation_asset())
.then(|| self.leverage_room(config, asset, None))
.flatten()
.map(|room| cap_room.map_or(room, |cap_room| cap_room.min(room))),
};
if let Some(room) = room {
capacity = capacity.with_borrowable_now(match room < min_borrow {
true => Decimal::ZERO,
false => room,
});
}
Ok(capacity)
}
fn values_every_holding(&self, valuation: &AssetNameExchange) -> bool {
self.account.balances().all(|balance| {
balance.balance.total.is_zero()
|| self.price_in(&balance.asset, valuation, None).is_some()
})
}
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 orders_ended(&self, orders: &[UnindexedOrderKey]) -> Vec<UnindexedInactiveOrder> {
let mut reported = FnvHashSet::default();
orders
.iter()
.filter_map(|key| {
let order = self
.account
.order_ended(&key.cid)
.filter(|order| order.key.instrument == key.instrument)?;
reported.insert(&key.cid).then(|| Order {
key: key.clone(),
..order
})
})
.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 mut events = match notifications {
Some(OpenOrderNotifications::Filled { commitment, trade }) => {
let mut events = Vec::with_capacity(3);
self.push_committed(&mut events, commitment, Some(trade));
events
}
Some(OpenOrderNotifications::Rested { balance }) => {
vec![self.build_account_event(balance)]
}
None => Vec::new(),
};
self.refresh_next_interest_if_loans_changed();
self.report_debt_changes(&mut events);
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.remove_order(&request.key.cid)
else {
return VenueOutcome {
events: Vec::new(),
response: UnindexedOrderResponseCancel {
state: Err(self.cancel_missing_reason(&request.key.cid)),
key: request.key,
},
};
};
let mut events = match reservation {
Some(reservation) => {
let Released { balance, interest } =
self.release_reservation(reservation, time_exchange);
let mut events = vec![self.build_account_event(Snapshot(balance))];
self.push_interest(&mut events, interest, time_exchange);
events
}
None => Vec::new(),
};
self.refresh_next_interest_if_loans_changed();
self.report_debt_changes(&mut events);
let cancelled = Cancelled {
id: order.state.id.or_client_id(&order.key.cid),
time_exchange,
filled_quantity: Some(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 report_debt_changes(&mut self, events: &mut Vec<UnindexedAccountEvent>) {
if self.spot_margin.is_none() {
return;
}
let mut index = 0;
while index < events.len() {
index += 1;
let AccountEventKind::BalanceSnapshot(Snapshot(balance)) = &events[index - 1].kind
else {
continue;
};
let Some(debt) = balance.balance.margin else {
continue;
};
let before = self
.reported_debt
.insert(balance.asset.clone(), debt)
.unwrap_or_default();
if before == debt {
continue;
}
let kind = if debt.borrowed > before.borrowed {
MarginDebtKind::Borrow
} else if debt.borrowed < before.borrowed || debt.interest < before.interest {
MarginDebtKind::Repay
} else {
MarginDebtKind::Interest
};
let update = MarginDebtUpdate::new(
balance.asset.clone(),
None,
debt,
kind,
kind.to_smolstr(),
balance.time_exchange,
);
events.insert(
index,
self.build_account_event(AccountEventKind::MarginDebt(update)),
);
}
}
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 mut orders_by_instrument: FnvHashMap<_, Vec<_>> = orders_by_instrument
.into_iter()
.map(|(instrument, orders)| (instrument, orders.collect()))
.collect();
let mut instruments: Vec<_> = self
.instruments
.iter()
.filter_map(|(name, instrument)| {
let orders = orders_by_instrument.remove(name);
let position = self.position_report(name, instrument);
if orders.is_none() && position == PositionReport::Unreported {
return None;
}
Some(InstrumentAccountSnapshot {
instrument: name.clone(),
orders: orders.unwrap_or_default(),
orders_complete: true,
position,
isolated: None,
})
})
.collect();
instruments.extend(
orders_by_instrument
.into_iter()
.map(|(instrument, orders)| InstrumentAccountSnapshot {
instrument,
orders,
orders_complete: true,
position: PositionReport::Unreported,
isolated: None,
}),
);
instruments.sort_unstable_by(|a, b| a.instrument.cmp(&b.instrument));
UnindexedAccountSnapshot {
exchange: self.exchange,
balances,
instruments,
}
}
fn position_report(
&self,
name: &InstrumentNameExchange,
instrument: &Instrument<ExchangeId, AssetNameExchange>,
) -> PositionReport {
if !matches!(instrument.kind, InstrumentKind::Cfd(_)) {
return PositionReport::Unreported;
}
let position = self.positions.get(name).copied().unwrap_or_default();
PositionReport::from_position(Position::new(
position.quantity,
(!position.quantity.is_zero()).then_some(position.entry),
None,
None,
None,
None,
None,
self.time_exchange(),
))
}
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);
}
if request.state.quantity <= Decimal::ZERO {
let reason = format!(
"cannot open {} for {}: quantity {} is not positive",
request.key.instrument, request.key.exchange, request.state.quantity
);
return (
build_open_order_err_response(
request,
UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
),
None,
);
}
let terms = match self.instrument_terms(&request.key.instrument) {
Ok(terms) => terms,
Err(error) => return (build_open_order_err_response(request, error), None),
};
if let Some(effect) = self.refused_margin_effect(request.state.margin_effect, &terms) {
return (
build_open_order_err_response(
request,
UnindexedOrderError::UnsupportedMarginEffect(effect),
),
None,
);
}
if self.account.orders().contains(&request.key.cid) {
let reason = format!(
"{} already names an order resting on {}",
request.key.cid, request.key.exchange
);
return (
build_open_order_err_response(request, ApiError::DuplicateClientOrderId(reason)),
None,
);
}
if let Err(error) = self.check_shortability(&request, &terms, market.as_ref()) {
return (build_open_order_err_response(request, error), None);
}
let now = self.time_exchange();
let limit = match request.state.kind {
OrderKind::Limit => match request.state.price {
Some(limit) => Some(limit),
None => {
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,
);
}
},
_ => None,
};
if let StaleMarketPolicy::Wait { max_age } = self.stale_market
&& !matches!(
request.state.time_in_force,
TimeInForce::GoodTillDate { expiry } if now >= expiry
)
{
match self.market_age(&request.key.instrument, max_age, now) {
MarketAge::NoFeed => {
let reason = format!(
"cannot accept {} on {}: StaleMarketPolicy::Wait needs a market feed to \
measure {}'s age from, and this venue has none",
request.key.cid, request.key.exchange, request.key.instrument
);
return (
build_open_order_err_response(
request,
UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
),
None,
);
}
MarketAge::Stale { fresh_at } => {
if matches!(
request.state.time_in_force,
TimeInForce::ImmediateOrCancel | TimeInForce::FillOrKill
) {
let last = fresh_at.map_or_else(
|| "never observed fresh".to_owned(),
|fresh_at| format!("last observed fresh at {fresh_at}"),
);
let reason = format!(
"cannot fill {} on {}: {}'s market is stale under \
StaleMarketPolicy::Wait (max_age {max_age:?}; {last}), and \
TimeInForce::{} does not wait for a fresh observation",
request.key.cid,
request.key.exchange,
request.key.instrument,
request.state.time_in_force
);
return (
build_open_order_err_response(
request,
UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
),
None,
);
}
return self.hold_on_arrival(request, market, &terms);
}
MarketAge::Fresh | MarketAge::Unpriced => {}
}
}
let Some(limit) = limit else {
return match disposition(request.state.kind, request.state.time_in_force, true, now) {
Disposition::FillAndCancel | Disposition::FillOrNothing
if matches!(self.market_order_timing, MarketOrderTiming::NextBar(_)) =>
{
self.hold_on_arrival(request, market, &terms)
}
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 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 market_age(
&self,
instrument: &InstrumentNameExchange,
max_age: Duration,
now: DateTime<Utc>,
) -> MarketAge {
if self.regime == VenueRegime::RequestPriced {
return MarketAge::NoFeed;
}
let Some(market) = self
.market(instrument)
.filter(|market| !market.snapshot.is_empty())
else {
return MarketAge::Unpriced;
};
let Some(fresh_at) = market.fresh_at else {
return MarketAge::Stale { fresh_at: None };
};
let stale = chrono::TimeDelta::from_std(max_age)
.is_ok_and(|max_age| now.signed_duration_since(fresh_at) > max_age);
if stale {
MarketAge::Stale {
fresh_at: Some(fresh_at),
}
} else {
MarketAge::Fresh
}
}
fn hold_on_arrival(
&mut self,
request: OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
market: Option<MarketSnapshot>,
terms: &InstrumentTerms,
) -> (
Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>,
Option<OpenOrderNotifications>,
) {
if matches!(self.market_order_timing, MarketOrderTiming::NextBar(_))
&& self
.market(&request.key.instrument)
.and_then(|market| market.bar)
.is_none()
{
let reason = format!(
"cannot hold {} on {} for {}'s next bar: MarketOrderTiming::NextBar holds it for \
one and needs a bar feed, and this venue has been fed no bar for it",
request.key.cid, request.key.exchange, request.key.instrument
);
return (
build_open_order_err_response(
request,
UnindexedOrderError::Rejected(ApiError::OrderRejected(reason)),
),
None,
);
}
let side = request.state.side;
let limit = match request.state.kind {
OrderKind::Limit => request.state.price,
_ => None,
};
let post_only = matches!(
request.state.time_in_force,
TimeInForce::GoodUntilCancelled { post_only: true }
);
let taker_estimate = || {
market.and_then(|market| {
self.fill_model
.fill_price(&FillContext::new(side, request.state.quantity, &market))
})
};
let (estimate, liquidity) = match limit {
Some(limit)
if !post_only && market.is_some_and(|market| crosses(side, limit, &market)) =>
{
let estimate = taker_estimate().unwrap_or(limit);
(
Some(clamp_to_limit(side, estimate, limit)),
Liquidity::Taker,
)
}
Some(limit) => (Some(limit), Liquidity::Maker),
None => (taker_estimate(), Liquidity::Taker),
};
let Some(estimate) = estimate else {
let reason = format!(
"cannot price {} for {}: no market price available yet 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 time_exchange = self.time_exchange();
let settlement = self.settlement(
terms,
self.net_position(terms, &request.key.instrument),
side,
request.state.quantity,
estimate,
liquidity,
);
let debit = settlement.reserved();
let effect = self.margin_effect(request.state.margin_effect);
let (committed, interest) =
match self.commit(&debit, None, effect, terms, market.as_ref(), time_exchange) {
Ok(Commitment {
committed,
interest,
..
}) => (committed, interest),
Err(insufficient) => {
return (
build_open_order_err_response(
request,
self.insufficient_error(insufficient, side),
),
None,
);
}
};
let open = Open::new(
VenueOrderId::Assigned(self.order_id_sequence_fetch_add()),
time_exchange,
Decimal::ZERO,
);
let order = Order {
key: request.key,
side,
price: limit,
quantity: request.state.quantity,
kind: request.state.kind,
time_in_force: request.state.time_in_force,
state: open,
};
let reservation = Reservation {
amount: settlement.requirement(),
asset: settlement.asset,
borrowed: committed.shortfall_borrowed(),
excess_borrowed: committed.excess_borrowed,
effect,
unreported_interest: interest.map(|flow| vec![*flow]).unwrap_or_default(),
};
if let Err(AlreadyResting(refused)) = self.account.hold(order.clone(), Some(reservation)) {
unreachable!(
"SimulatedVenue held {} over a resting order: open_order_inner checks first",
refused.order.key.cid
);
}
(
order.map_state(OrderState::active),
Some(OpenOrderNotifications::Rested {
balance: Snapshot(committed.paid),
}),
)
}
fn check_shortability(
&self,
request: &OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
terms: &InstrumentTerms,
market: Option<&MarketSnapshot>,
) -> Result<(), UnindexedApiError> {
let Some(provider) = &self.shortability else {
return Ok(());
};
if !terms.cash_settled || request.state.side != Side::Sell {
return Ok(());
}
let instrument = &request.key.instrument;
let resting_sells: Decimal = self
.account
.orders_open()
.filter(|order| order.key.instrument == *instrument && order.side == Side::Sell)
.map(|order| order.state.quantity_remaining(order.quantity))
.sum();
let committed = self.net_position(terms, instrument).quantity - resting_sells;
let short_before = (-committed).max(Decimal::ZERO);
let short_after = (request.state.quantity - committed).max(Decimal::ZERO);
if short_after <= short_before {
return Ok(());
}
let now = self.time_exchange();
let exchange = request.key.exchange;
let shortability = provider.shortability(instrument, now);
if let Some(delisting_at) = shortability.delisting_at
&& delisting_at <= now
{
return Err(ApiError::BorrowRejected(BorrowReject::new(
BorrowRejectReason::NotShortable,
None,
format!(
"{instrument} cannot be sold short on {exchange} at {now}: it is delisted at \
{delisting_at}"
),
)));
}
if shortability.shortable == Some(false) {
return Err(ApiError::BorrowRejected(BorrowReject::new(
BorrowRejectReason::NotShortable,
None,
format!("{instrument} cannot be sold short on {exchange} at {now}"),
)));
}
if let Some(available) = shortability.available
&& short_after > available
{
let message = format!(
"selling {} {instrument} on {exchange} would take its short, resting sells \
included, to {short_after}, past the {available} there is to borrow at {now}",
request.state.quantity
);
let left = (available - short_before).max(Decimal::ZERO);
return Err(ApiError::BorrowRejected(
BorrowReject::new(BorrowRejectReason::InventoryUnavailable, None, message)
.with_available(left),
));
}
let marketable = || {
market.is_some_and(|market| match request.state.kind {
OrderKind::Limit => request
.state
.price
.is_some_and(|limit| crosses(Side::Sell, limit, market)),
_ => self
.fill_model
.fill_price(&FillContext::new(
Side::Sell,
request.state.quantity,
market,
))
.is_some(),
})
};
if provider.short_sale_restricted(instrument, now) && marketable() {
return Err(ApiError::BorrowRejected(BorrowReject::new(
BorrowRejectReason::ShortSaleRestricted,
None,
format!(
"{instrument} is under a short-sale restriction on {exchange} at {now}: a \
short sale must be priced above the best bid"
),
)));
}
Ok(())
}
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: Some(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: Some(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 limit = match request.state.kind {
OrderKind::Limit => request.state.price,
_ => None,
};
let maybe_reference = self
.fill_model
.fill_price(&FillContext::new(
request.state.side,
request.state.quantity,
&market,
))
.or(limit);
let Some(reference) = maybe_reference 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 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 slippage_before = self.slippage.clone();
let fill_price = self.slippage.apply(side, reference);
let fill_price = match limit {
Some(limit) => clamp_to_limit(side, fill_price, limit),
None => fill_price,
};
let position = self.net_position(terms, &request.key.instrument);
let fill = self.fill_settlement(
terms,
position,
side,
filled_quantity,
fill_price,
Liquidity::Taker,
);
let held = match remainder {
Remainder::Rest { limit } if unfilled > Decimal::ZERO => Some(self.settlement(
terms,
position.after(side, filled_quantity, fill_price),
side,
unfilled,
limit,
Liquidity::Maker,
)),
_ => None,
};
let debit = match &held {
Some(held) => fill.settled_holding(held),
None => fill.settled(),
};
let effect = self.margin_effect(request.state.margin_effect);
let mut commitment = match self.commit(
&debit,
Some(&fill.credit),
effect,
terms,
Some(&market),
time_exchange,
) {
Ok(commitment) => commitment,
Err(insufficient) => {
self.slippage = slippage_before;
return (
build_open_order_err_response(
request,
self.insufficient_error(insufficient, side),
),
None,
);
}
};
self.take_fee_apart(&fill, &mut commitment, time_exchange);
self.consume_depth(&request.key.instrument, side, filled_quantity);
self.record_fill(
terms,
&request.key.instrument,
side,
Fill {
quantity: filled_quantity,
price: fill_price,
},
);
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,
origin: TradeOrigin::Order,
position_side: None,
client_order_id: Some(request.key.cid.clone()),
time_source: TimeSource::Venue,
};
self.account.ack_trade(trade.clone());
let state = self.retire_filled(
&request,
order_id,
Fill {
quantity: filled_quantity,
price: fill_price,
},
held,
&commitment.committed,
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 { commitment, trade }),
)
}
fn retire_filled(
&mut self,
request: &OrderRequestOpen<ExchangeId, InstrumentNameExchange>,
order_id: OrderId,
fill: Fill,
held: Option<Settlement>,
committed: &Committed,
time_exchange: DateTime<Utc>,
) -> UnindexedOrderState {
match held {
Some(held) => {
let open = Open::new(
VenueOrderId::Assigned(order_id),
time_exchange,
fill.quantity,
);
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 {
borrowed: committed.shortfall_borrowed().min(held.requirement()),
excess_borrowed: committed.excess_borrowed,
amount: held.requirement(),
asset: held.asset,
effect: self.margin_effect(request.state.margin_effect),
unreported_interest: Vec::new(),
},
);
OrderState::active(open)
}
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(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: filled.clone(),
});
OrderState::fully_filled(filled)
}
None => {
let cancelled = Cancelled {
id: order_id,
time_exchange,
filled_quantity: Some(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)
}
}
}
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) {
if let Err(AlreadyResting(refused)) = self.account.book(order, Some(reservation)) {
unreachable!(
"SimulatedVenue booked {} over a resting order: open_order_inner checks first",
refused.order.key.cid
);
}
}
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,
self.net_position(terms, &request.key.instrument),
request.state.side,
request.state.quantity - filled_quantity,
limit,
Liquidity::Maker,
);
let debit = settlement.reserved();
let market = self
.market(&request.key.instrument)
.map(|market| market.snapshot);
let effect = self.margin_effect(request.state.margin_effect);
let (committed, interest) =
match self.commit(&debit, None, effect, terms, market.as_ref(), time_exchange) {
Ok(Commitment {
committed,
interest,
..
}) => (committed, interest),
Err(insufficient) => {
let error = self.insufficient_error(insufficient, request.state.side);
return (build_open_order_err_response(request, error), None);
}
};
let open = Open::new(
VenueOrderId::Assigned(self.order_id_sequence_fetch_add()),
time_exchange,
filled_quantity,
);
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 {
amount: settlement.requirement(),
asset: settlement.asset,
borrowed: committed.shortfall_borrowed(),
excess_borrowed: committed.excess_borrowed,
effect,
unreported_interest: interest.map(|flow| vec![*flow]).unwrap_or_default(),
},
);
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(committed.paid),
}),
)
}
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 net_position(
&self,
terms: &InstrumentTerms,
instrument: &InstrumentNameExchange,
) -> NetPosition {
if !terms.cash_settled {
return NetPosition::default();
}
self.positions.get(instrument).copied().unwrap_or_default()
}
fn record_fill(
&mut self,
terms: &InstrumentTerms,
instrument: &InstrumentNameExchange,
side: Side,
fill: Fill,
) {
if !terms.cash_settled {
return;
}
let opened_or_closed = match self.positions.get_mut(instrument) {
Some(position) => {
*position = position.after(side, fill.quantity, fill.price);
let closed = position.quantity.is_zero();
if closed {
self.positions.remove(instrument);
}
closed
}
None => {
let position = NetPosition::default().after(side, fill.quantity, fill.price);
let opened = !position.quantity.is_zero();
if opened {
self.positions.insert(instrument.clone(), position);
}
opened
}
};
if opened_or_closed && !self.holding_costs.is_empty() {
self.refresh_next_charge();
}
}
fn settlement(
&self,
terms: &InstrumentTerms,
position: NetPosition,
side: Side,
quantity: Decimal,
price: Decimal,
liquidity: Liquidity,
) -> Settlement {
let fees_quote =
self.fee_model
.compute_fee(price, quantity.abs(), terms.contract_size, liquidity);
self.settlement_with_fee(terms, position, side, quantity, price, fees_quote)
}
fn fill_settlement(
&self,
terms: &InstrumentTerms,
position: NetPosition,
side: Side,
quantity: Decimal,
price: Decimal,
liquidity: Liquidity,
) -> Settlement {
let settlement = self.settlement(terms, position, side, quantity, price, liquidity);
match self.fee_in_fee_asset(terms, &settlement.fees) {
Some(fees) => Settlement {
fees,
fee_apart: true,
..self.settlement_with_fee(terms, position, side, quantity, price, Decimal::ZERO)
},
None => settlement,
}
}
fn fee_in_fee_asset(
&self,
terms: &InstrumentTerms,
fees: &AssetFees<AssetNameExchange>,
) -> Option<AssetFees<AssetNameExchange>> {
let config = self.fee_asset.as_ref()?;
let asset = config.asset();
let underlying = &terms.underlying;
let fees_quote = fees.fees_quote?;
if terms.cash_settled
|| *asset == underlying.base
|| *asset == underlying.quote
|| fees_quote <= Decimal::ZERO
{
return None;
}
let price = self.price_in(asset, &underlying.quote, None)?;
let fees_quote = fees_quote * (Decimal::ONE - config.discount());
let fee = fees_quote.checked_div(price)?;
(self.account.free(asset) >= fee)
.then(|| AssetFees::new(asset.clone(), fee, Some(fees_quote)))
}
fn settlement_with_fee(
&self,
terms: &InstrumentTerms,
position: NetPosition,
side: Side,
quantity: Decimal,
price: Decimal,
fees_quote: Decimal,
) -> Settlement {
let quantity = quantity.abs();
let fees = AssetFees::new(terms.underlying.quote.clone(), fees_quote, Some(fees_quote));
if terms.cash_settled {
let closing = position.closing(side, quantity);
let opening = quantity - closing;
let open_margin = price * opening * terms.contract_size;
let close_proceeds = closing
* terms.contract_size
* match position.is_long() {
true => price,
false => position.entry + position.entry - price,
};
let (credit, shortfall) = match close_proceeds.is_sign_negative() {
true => (Decimal::ZERO, -close_proceeds),
false => (close_proceeds, Decimal::ZERO),
};
return Settlement {
asset: terms.underlying.quote.clone(),
amount: open_margin + fees_quote + shortfall,
credit: Credit::PaidBack(credit),
fees,
fee_apart: false,
};
}
let notional = price * quantity * terms.contract_size;
match side {
Side::Buy
if self.spot_fee_asset == SpotFeeAsset::Received
&& price > Decimal::ZERO
&& !fees_quote.is_zero() =>
{
let fee_base = fees_quote / price;
Settlement {
asset: terms.underlying.quote.clone(),
amount: notional,
credit: Credit::Delivery(Delivery {
asset: terms.underlying.base.clone(),
amount: quantity - fee_base,
}),
fees: AssetFees::new(terms.underlying.base.clone(), fee_base, Some(fees_quote)),
fee_apart: false,
}
}
Side::Buy => Settlement {
asset: terms.underlying.quote.clone(),
amount: notional + fees_quote,
credit: Credit::Delivery(Delivery {
asset: terms.underlying.base.clone(),
amount: quantity,
}),
fees,
fee_apart: false,
},
Side::Sell => Settlement {
asset: terms.underlying.base.clone(),
amount: quantity,
credit: Credit::Delivery(Delivery {
asset: terms.underlying.quote.clone(),
amount: notional - fees_quote,
}),
fees,
fee_apart: false,
},
}
}
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((self.order_id_start + u128::from(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 next_boundary(
model: &dyn HoldingCostModel,
instrument: &InstrumentNameExchange,
after: DateTime<Utc>,
) -> Option<DateTime<Utc>> {
let boundary = model.next_boundary(instrument, after)?;
assert!(
boundary > after,
"HoldingCostModel {model:?} returned boundary {boundary} for {instrument}, which is not \
after {after}"
);
Some(boundary)
}
fn next_interest_boundary(
model: &dyn MarginInterestModel,
asset: &AssetNameExchange,
after: DateTime<Utc>,
) -> Option<DateTime<Utc>> {
let boundary = model.next_boundary(asset, after)?;
assert!(
boundary > after,
"MarginInterestModel {model:?} returned boundary {boundary} for {asset}, which is not \
after {after}"
);
Some(boundary)
}
fn boundaries_through(
after: DateTime<Utc>,
through: DateTime<Utc>,
mut next: impl FnMut(DateTime<Utc>) -> Option<DateTime<Utc>>,
) -> impl Iterator<Item = DateTime<Utc>> {
std::iter::successors(next(after), move |&boundary| next(boundary))
.take_while(move |&boundary| boundary <= through)
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Due {
Holding(InstrumentNameExchange, usize),
Interest(AssetNameExchange, Decimal),
}
fn seeded_positions(
initial_state: &UnindexedAccountSnapshot,
instruments: &FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>>,
) -> FnvHashMap<InstrumentNameExchange, NetPosition> {
initial_state
.instruments
.iter()
.filter(|snapshot| {
instruments
.get(&snapshot.instrument)
.is_some_and(|instrument| matches!(instrument.kind, InstrumentKind::Cfd(_)))
})
.filter_map(|snapshot| {
let position = snapshot.position.open()?;
let Some(entry) = position.entry_price else {
panic!(
"SimulatedVenue's initial_state reports an open position in {} with no entry \
price, so the margin and PnL its close would credit cannot be computed",
snapshot.instrument
);
};
Some((
snapshot.instrument.clone(),
NetPosition {
quantity: position.quantity,
entry,
},
))
})
.collect()
}
fn owed(balance: &AssetBalance<AssetNameExchange>) -> Decimal {
balance
.balance
.margin
.map_or(Decimal::ZERO, |margin| margin.owed())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MarginCheck {
Borrow,
Liquidation,
}
impl MarginCheck {
fn collateral_ratio(self, config: &SpotMarginConfig, asset: &AssetNameExchange) -> Decimal {
match self {
Self::Borrow => config.borrow_collateral_ratio(asset),
Self::Liquidation => config.liquidation_collateral_ratio(asset),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
struct Valued {
equity: Decimal,
debt: Decimal,
}
fn mid_or_last(snapshot: &MarketSnapshot) -> Option<Decimal> {
match (snapshot.best_bid, snapshot.best_ask) {
(Some(bid), Some(ask)) => Some((bid + ask) / Decimal::TWO),
_ => snapshot.last_price,
}
}
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 clamp_to_limit(side: Side, price: Decimal, limit: Decimal) -> Decimal {
match side {
Side::Buy => price.min(limit),
Side::Sell => price.max(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 MarketAge {
NoFeed,
Unpriced,
Fresh,
Stale { fresh_at: Option<DateTime<Utc>> },
}
#[derive(Debug, Clone, Copy)]
enum Release<'a> {
Bar(&'a MarketBar, NextBarPrice),
Market(&'a MarketSnapshot),
}
#[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 {
commitment: Commitment,
trade: Trade<AssetNameExchange, InstrumentNameExchange>,
},
Rested {
balance: Snapshot<AssetBalance<AssetNameExchange>>,
},
}
#[derive(Debug)]
struct Commitment {
committed: Committed,
interest: Option<Box<CashFlow<AssetNameExchange, InstrumentNameExchange>>>,
held_interest: Vec<CashFlow<AssetNameExchange, InstrumentNameExchange>>,
fee: Option<Box<AssetBalance<AssetNameExchange>>>,
}
impl From<Committed> for Commitment {
fn from(committed: Committed) -> Self {
Self {
committed,
interest: None,
held_interest: Vec::new(),
fee: None,
}
}
}
#[derive(Debug)]
#[must_use = "its interest must be reported, or carried into the order's next reservation"]
struct Released {
balance: AssetBalance<AssetNameExchange>,
interest: Vec<CashFlow<AssetNameExchange, InstrumentNameExchange>>,
}
#[derive(Debug, Clone)]
struct InstrumentTerms {
underlying: Underlying<AssetNameExchange>,
contract_size: Decimal,
cash_settled: bool,
}
#[derive(Debug)]
enum RestingSettlement {
Settled(Commitment),
Unaffordable { released: Option<Released> },
}
#[allow(clippy::large_enum_variant)]
enum Recommitted {
Committed {
commitment: Commitment,
loan: Decimal,
excess_borrowed: Decimal,
},
Unaffordable { released: Option<Released> },
}
#[derive(Debug, Clone, Copy)]
struct Fill {
quantity: Decimal,
price: Decimal,
}
#[derive(Debug, Clone)]
struct Settlement {
asset: AssetNameExchange,
amount: Decimal,
credit: Credit,
fees: AssetFees<AssetNameExchange>,
fee_apart: bool,
}
impl Settlement {
fn requirement(&self) -> Decimal {
match &self.credit {
Credit::PaidBack(credit) => (self.amount - credit).max(Decimal::ZERO),
Credit::Delivery(_) => self.amount,
}
}
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.requirement(),
}
}
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.requirement(),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct NetPosition {
quantity: Decimal,
entry: Decimal,
}
impl NetPosition {
fn is_long(&self) -> bool {
self.quantity.is_sign_positive() && !self.quantity.is_zero()
}
fn closing(&self, side: Side, quantity: Decimal) -> Decimal {
let opposes = match side {
Side::Buy => self.quantity.is_sign_negative() && !self.quantity.is_zero(),
Side::Sell => self.is_long(),
};
match opposes {
true => quantity.min(self.quantity.abs()),
false => Decimal::ZERO,
}
}
fn after(self, side: Side, quantity: Decimal, price: Decimal) -> Self {
let signed = match side {
Side::Buy => quantity,
Side::Sell => -quantity,
};
let next = self.quantity + signed;
let entry = if next.is_zero() {
Decimal::ZERO
} else if self.quantity.is_zero()
|| next.is_sign_negative() != self.quantity.is_sign_negative()
{
price
} else if signed.is_sign_negative() == self.quantity.is_sign_negative() {
(self.entry * self.quantity.abs() + price * quantity) / next.abs()
} else {
self.entry
};
Self {
quantity: next,
entry,
}
}
}
fn first_unseeded_order_id(account: &AccountState) -> u128 {
let open = account
.orders_open()
.filter_map(|order| match &order.state.id {
VenueOrderId::Assigned(id) => Some(id),
VenueOrderId::ClientAssigned => None,
});
let cancelled = account.orders_cancelled().map(|order| &order.state.id);
open.chain(cancelled)
.filter_map(|id| id.0.parse::<u64>().ok())
.max()
.map_or(0, |highest| u128::from(highest) + 1)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
use crate::{
balance::{Balance, MarginDetails},
cash_flow::CashFlowKind,
error::OrderError,
exchange::mock::{
fixtures::*,
orders::{OpenOrders, as_open},
},
fee::{PerContractFeeModel, PercentageFeeModel},
fill::{BidAskFillModel, NextBarPrice, SimFillConfig},
holding_cost::{
AccrualDays, BorrowFeeModel, DailyAccrual, DayCount, FinancingModel, FinancingRates,
FundingModel, HoldingCharge, RateSeries, WeekendsOnly,
},
margin::{InterestCharge, InterestSchedule, MarginEffect, PeriodicMarginInterest},
order::{
OrderEvent, OrderKey, TimeInForce,
id::{ClientOrderId, StrategyId},
request::RequestCancel,
state::{ActiveOrderState, InactiveOrderState},
},
shortability::{Shortability, ShortabilityTable},
slippage::SlippageConfig,
};
use chrono::{NaiveTime, TimeDelta};
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 sans_debt(events: &[UnindexedAccountEvent]) -> Vec<&UnindexedAccountEvent> {
events
.iter()
.filter(|event| !matches!(event.kind, AccountEventKind::MarginDebt(_)))
.collect()
}
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(),
MarketObservation::new(book("49000", "49100")),
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 sans_debt(&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_received(outcome: &OpenOutcome) -> &AssetBalance<AssetNameExchange> {
match sans_debt(&outcome.events).get(1).map(|event| &event.kind) {
Some(AccountEventKind::BalanceSnapshot(balance)) => &balance.0,
other => panic!("a filled spot open must emit what it received second, got: {other:?}"),
}
}
fn filled_trade(outcome: &OpenOutcome) -> &Trade<AssetNameExchange, InstrumentNameExchange> {
let Some(at) = outcome
.events
.iter()
.position(|event| matches!(event.kind, AccountEventKind::Trade(_)))
else {
panic!(
"a filled open must emit its trade, got: {:?}",
outcome.events
);
};
assert!(
at > 0
&& outcome.events[..at].iter().all(|event| matches!(
event.kind,
AccountEventKind::BalanceSnapshot(_)
| AccountEventKind::CashFlow(_)
| AccountEventKind::MarginDebt(_)
)),
"a filled open must emit its balances before its trade, got: {:?}",
outcome.events
);
trade_of(&outcome.events[at])
}
#[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(),
3,
"a spot fill owes the balance it paid with, the one it received, and one trade"
);
let balance = filled_balance(&outcome);
let received = filled_received(&outcome);
let trade = filled_trade(&outcome);
assert_eq!(balance.asset, quote(), "a spot buy debits quote");
assert_eq!(received.asset, base(), "and credits base");
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(), 3, "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 the_first_instant_may_precede_the_epoch() {
let mut venue = make_venue("100", "10000000");
let start = "1965-03-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
advance(&mut venue, start);
assert_eq!(venue.time_exchange(), start);
}
#[test]
fn a_request_behind_a_later_market_tick_is_stamped_at_the_tick() {
let mut venue = make_cfd_venue_charging("200000", Vec::new());
let tick = time(10);
let fills = venue.apply_market(
&cfd_instrument_name(),
MarketObservation::new(MarketSnapshot {
best_bid: Some(d("5000")),
best_ask: Some(d("5000")),
last_price: Some(d("5000")),
}),
tick,
);
assert!(fills.is_empty());
assert!(venue.advance_time(time(5)).is_empty());
assert_eq!(venue.time_exchange(), tick, "the clock is held at the tick");
let outcome = venue.open_order(cfd_request(Side::Buy, "1", None));
assert_eq!(filled_trade(&outcome).time_exchange, tick);
assert_eq!(filled_balance(&outcome).time_exchange, tick);
}
#[test]
fn a_market_tick_behind_the_clock_is_applied_at_the_clock() {
let (mut venue, _) = venue_resting_one_buy("48000");
let reached = time(10);
advance(&mut venue, reached);
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("47800", "47900")),
time(5),
);
assert_eq!(
events.len(),
4,
"the tick still crosses the resting order: two balances, a trade, an order"
);
assert_eq!(balance_of(&events[0]).time_exchange, reached);
assert_eq!(balance_of(&events[1]).time_exchange, reached);
assert_eq!(trade_of(&events[2]).time_exchange, reached);
assert!(
venue
.account
.balances()
.all(|balance| balance.time_exchange == reached),
"no balance is restamped earlier than the venue has reported"
);
let market = venue.market(&instrument_name()).unwrap();
assert_eq!(market.time_exchange, reached);
assert_eq!(market.fresh_at, Some(reached));
assert_eq!(venue.time_exchange(), reached);
}
#[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(),
{
let mut book = OpenOrders::default();
book.insert(
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,
},
},
None,
)
.unwrap();
book
},
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(cfd_instrument()),
)
}
fn cfd_instrument() -> Instrument<ExchangeId, AssetNameExchange> {
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(Some(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")));
}
fn usd() -> AssetNameExchange {
AssetNameExchange::new("usd")
}
fn free_usd(venue: &SimulatedVenue) -> Decimal {
venue.balances(&[usd()]).remove(0).balance.free
}
fn total_usd(venue: &SimulatedVenue) -> Decimal {
venue.balances(&[usd()]).remove(0).balance.total
}
fn cfd_fill(
venue: &mut SimulatedVenue,
side: Side,
quantity: &str,
price: &str,
) -> OpenOutcome {
let outcome = venue.open_order(cfd_request(side, quantity, market_prices(price)));
assert!(
outcome.response.state.is_accepted(),
"{side:?} {quantity} @ {price} should fill: {:?}",
outcome.response.state
);
outcome
}
fn cfd_position(venue: &SimulatedVenue) -> Option<(Decimal, Option<Decimal>)> {
let snapshot = venue.account_snapshot();
let Some(instrument) = snapshot
.instruments
.iter()
.find(|instrument| instrument.instrument == cfd_instrument_name())
else {
panic!("a CFD instrument is always in the snapshot, flat or not");
};
match &instrument.position {
PositionReport::Open(position) => Some((position.quantity, position.entry_price)),
PositionReport::Flat => None,
PositionReport::Unreported => panic!("a CFD position is reported, not Unreported"),
}
}
#[test]
fn a_cfd_round_trip_returns_the_margin_and_settles_the_pnl() {
for (side, open, close, end) in [
(Side::Buy, "5000", "5100", "152500"), (Side::Sell, "5000", "4900", "152500"), (Side::Sell, "5000", "5200", "145000"), ] {
let mut venue = make_cfd_venue("150000", FeeModelConfig::default());
cfd_fill(&mut venue, side, "1", open);
assert_eq!(
free_usd(&venue),
d("25000"),
"{side:?}: opening posts 125,000"
);
let closing = match side {
Side::Buy => Side::Sell,
Side::Sell => Side::Buy,
};
let outcome = cfd_fill(&mut venue, closing, "1", close);
assert_eq!(
filled_balance(&outcome).balance.free,
d(end),
"{side:?} @ {close}"
);
assert_eq!(total_usd(&venue), d(end), "{side:?}: nothing is left held");
assert_eq!(
cfd_position(&venue),
None,
"{side:?}: the round trip is flat"
);
}
}
#[test]
fn a_cfd_flip_closes_the_position_and_opens_the_rest_in_one_fill() {
let mut venue = make_cfd_venue("300000", FeeModelConfig::default());
cfd_fill(&mut venue, Side::Buy, "1", "5000");
assert_eq!(free_usd(&venue), d("175000"));
cfd_fill(&mut venue, Side::Sell, "3", "5100");
assert_eq!(free_usd(&venue), d("175000") + d("127500") - d("255000"));
assert_eq!(cfd_position(&venue), Some((d("-2"), Some(d("5100")))));
}
#[test]
fn a_partial_cfd_reduce_keeps_the_entry_and_pays_back_its_share() {
let mut venue = make_cfd_venue("300000", FeeModelConfig::default());
cfd_fill(&mut venue, Side::Buy, "2", "5000");
assert_eq!(free_usd(&venue), d("50000"));
cfd_fill(&mut venue, Side::Sell, "1", "5200");
assert_eq!(free_usd(&venue), d("180000"));
assert_eq!(
cfd_position(&venue),
Some((d("2") - d("1"), Some(d("5000"))))
);
}
#[test]
fn increasing_a_cfd_position_averages_its_entry() {
let mut venue = make_cfd_venue("300000", FeeModelConfig::default());
cfd_fill(&mut venue, Side::Buy, "1", "5000");
cfd_fill(&mut venue, Side::Buy, "1", "5200");
assert_eq!(free_usd(&venue), d("300000") - d("125000") - d("130000"));
assert_eq!(cfd_position(&venue), Some((d("2"), Some(d("5100")))));
}
#[test]
fn a_cfd_close_is_charged_its_fee_on_top_of_the_proceeds() {
let mut venue = make_cfd_venue(
"200000",
FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001"))),
);
cfd_fill(&mut venue, Side::Buy, "1", "5000"); let outcome = cfd_fill(&mut venue, Side::Sell, "1", "5100");
assert_eq!(filled_trade(&outcome).fees.fees, d("127.5"));
assert_eq!(
free_usd(&venue),
d("200000") + d("2500") - d("125") - d("127.5")
);
}
#[test]
fn a_cfd_short_that_loses_more_than_its_margin_pays_the_shortfall() {
let mut venue = make_cfd_venue("200000", FeeModelConfig::default());
cfd_fill(&mut venue, Side::Sell, "1", "1000"); cfd_fill(&mut venue, Side::Buy, "1", "2500");
assert_eq!(free_usd(&venue), d("200000") - d("37500"));
assert_eq!(cfd_position(&venue), None);
}
#[test]
fn a_spot_instrument_still_reports_no_position() {
let mut venue = make_venue("10", "1000000");
let _ = venue.open_order(request(
instrument_name(),
Side::Buy,
"1",
market_prices("50000"),
));
let snapshot = venue.account_snapshot();
assert!(
snapshot
.instruments
.iter()
.all(|instrument| instrument.position == PositionReport::Unreported),
"a spot holding is a balance, not a position"
);
}
#[test]
fn a_cfd_position_in_the_initial_state_is_where_the_venue_starts() {
let config = MockExecutionConfig::new(
EXCHANGE,
UnindexedAccountSnapshot {
exchange: EXCHANGE,
balances: vec![funded(usd(), d("10000"))],
instruments: vec![InstrumentAccountSnapshot {
instrument: cfd_instrument_name(),
orders: vec![],
orders_complete: true,
position: PositionReport::from_position(Position::new(
d("-1"),
Some(d("5000")),
None,
None,
None,
None,
None,
time(0),
)),
isolated: None,
}],
},
0,
FeeModelConfig::default(),
SimFillConfig::default(),
SpotFeeAsset::default(),
crate::slippage::SlippageConfig::default(),
crate::fill::MarketOrderTiming::default(),
crate::fill::StaleMarketPolicy::default(),
None,
None,
);
let mut venue = SimulatedVenue::new(&config, instruments_of(cfd_instrument()));
assert_eq!(cfd_position(&venue), Some((d("-1"), Some(d("5000")))));
cfd_fill(&mut venue, Side::Buy, "1", "4800");
assert_eq!(free_usd(&venue), d("10000") + d("130000"));
}
#[test]
#[should_panic(expected = "no entry price")]
fn a_cfd_position_in_the_initial_state_without_an_entry_price_panics() {
let config = MockExecutionConfig::new(
EXCHANGE,
UnindexedAccountSnapshot {
exchange: EXCHANGE,
balances: vec![funded(usd(), d("10000"))],
instruments: vec![InstrumentAccountSnapshot {
instrument: cfd_instrument_name(),
orders: vec![],
orders_complete: true,
position: PositionReport::from_position(Position::new(
d("-1"),
None,
None,
None,
None,
None,
None,
time(0),
)),
isolated: None,
}],
},
0,
FeeModelConfig::default(),
SimFillConfig::default(),
SpotFeeAsset::default(),
crate::slippage::SlippageConfig::default(),
crate::fill::MarketOrderTiming::default(),
crate::fill::StaleMarketPolicy::default(),
None,
None,
);
let _ = SimulatedVenue::new(&config, instruments_of(cfd_instrument()));
}
fn make_cfd_market_venue(usd: &str) -> SimulatedVenue {
let mut venue = SimulatedVenue::new_market_driven(
&config_from_balances(vec![funded(self::usd(), d(usd))], FeeModelConfig::default()),
instruments_of(cfd_instrument()),
);
advance(&mut venue, time(1));
venue
}
fn cfd_limit(
cid: &str,
side: Side,
quantity: &str,
price: &str,
) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
let mut request = limit_request(cid, side, quantity, price, gtc());
request.key.instrument = cfd_instrument_name();
request
}
fn cfd_market(
venue: &mut SimulatedVenue,
bid: &str,
ask: &str,
last: &str,
) -> Vec<UnindexedAccountEvent> {
let time_exchange = venue.time_exchange();
venue.apply_market(
&cfd_instrument_name(),
MarketObservation::new(MarketSnapshot {
best_bid: Some(d(bid)),
best_ask: Some(d(ask)),
last_price: Some(d(last)),
}),
time_exchange,
)
}
fn cfd_market_fill(venue: &mut SimulatedVenue, side: Side, quantity: &str) {
let outcome = venue.open_order(cfd_request(side, quantity, None));
assert!(
outcome.response.state.is_accepted(),
"{side:?} {quantity} should fill: {:?}",
outcome.response.state
);
}
#[test]
fn a_resting_order_that_only_reduces_a_cfd_position_holds_nothing() {
let mut venue = make_cfd_market_venue("125000");
assert!(cfd_market(&mut venue, "4990", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
assert_eq!(
free_usd(&venue),
Decimal::ZERO,
"the long took the whole balance"
);
let outcome = venue.open_order(cfd_limit("close", Side::Sell, "1", "5200"));
assert!(rested_open(&outcome).filled_quantity.is_zero());
assert_eq!(
total_usd(&venue) - free_usd(&venue),
Decimal::ZERO,
"nothing is held"
);
let events = cfd_market(&mut venue, "5200", "5210", "5200");
assert_eq!(balance_of(&events[0]).balance.free, d("130000"));
assert_eq!(filled_price(order_of(&events[2])), d("5200"));
assert_eq!(cfd_position(&venue), None);
}
#[test]
fn a_resting_order_that_opens_a_cfd_position_holds_its_margin_until_cancelled() {
let mut venue = make_cfd_market_venue("200000");
assert!(cfd_market(&mut venue, "4990", "5000", "5000").is_empty());
let _ = venue.open_order(cfd_limit("open", Side::Buy, "1", "4800"));
assert_eq!(free_usd(&venue), d("200000") - d("120000"));
assert_eq!(total_usd(&venue), d("200000"));
let cancelled = venue.cancel_order(OrderRequestCancel {
key: cfd_limit("open", Side::Buy, "1", "4800").key,
state: RequestCancel { id: None },
});
assert!(cancelled.response.state.is_ok());
assert_eq!(free_usd(&venue), d("200000"));
}
#[test]
fn a_resting_cfd_order_is_costed_against_the_position_when_it_fills() {
let mut venue = make_cfd_market_venue("300000");
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
let _ = venue.open_order(cfd_limit("close", Side::Sell, "1", "5200"));
assert_eq!(
free_usd(&venue),
d("175000"),
"a reducing order holds nothing"
);
cfd_market_fill(&mut venue, Side::Sell, "1"); assert_eq!(free_usd(&venue), d("300000"));
assert_eq!(cfd_position(&venue), None);
let events = cfd_market(&mut venue, "5200", "5210", "5200");
assert_eq!(
balance_of(&events[0]).balance.free,
d("300000") - d("130000"),
"the resting sell now opens a short of 1 at 5,200"
);
assert_eq!(cfd_position(&venue), Some((d("-1"), Some(d("5200")))));
}
#[test]
fn a_resting_cfd_order_the_account_can_no_longer_afford_is_cancelled() {
let mut venue = make_cfd_market_venue("126000");
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
let _ = venue.open_order(cfd_limit("close", Side::Sell, "1", "5200"));
cfd_market_fill(&mut venue, Side::Sell, "1");
assert_eq!(free_usd(&venue), d("126000"));
let events = cfd_market(&mut venue, "5200", "5210", "5200");
let Some((last, released)) = events.split_last() else {
panic!("an unaffordable fill reports its cancelled order");
};
assert!(
matches!(
order_of(last).state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
),
"{:?}",
order_of(last).state
);
assert!(
released
.iter()
.all(|event| balance_of(event).balance.free == d("126000")),
"no balance moved"
);
assert_eq!(free_usd(&venue), d("126000"), "nothing moved");
assert_eq!(cfd_position(&venue), None);
assert!(venue.account.is_cancelled(&ClientOrderId::new("close")));
}
#[test]
fn a_resting_cfd_order_cancelled_as_unaffordable_releases_its_hold() {
let mut venue = make_cfd_market_venue("260000");
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
let _ = venue.open_order(cfd_limit("flip", Side::Sell, "3", "5200"));
assert_eq!(
free_usd(&venue),
d("260000") - d("125000") - (d("260000") - d("130000")),
"holds the short's 260,000 net of the long's 130,000"
);
cfd_market_fill(&mut venue, Side::Sell, "1"); assert_eq!(free_usd(&venue), d("130000"));
assert_eq!(total_usd(&venue), d("260000"));
let events = cfd_market(&mut venue, "5200", "5210", "5200");
let Some((last, released)) = events.split_last() else {
panic!("an unaffordable fill reports its cancelled order");
};
assert!(
matches!(
order_of(last).state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
),
"{:?}",
order_of(last).state
);
let [released] = released else {
panic!("exactly the released hold restates: {released:?}");
};
assert_eq!(
balance_of(released).balance.free,
d("260000"),
"the hold is given back"
);
assert_eq!(free_usd(&venue), d("260000"));
assert_eq!(total_usd(&venue), d("260000"), "nothing settled");
assert_eq!(cfd_position(&venue), None);
assert!(venue.account.is_cancelled(&ClientOrderId::new("flip")));
}
#[test]
fn sell_order_decrements_base_and_credits_quote_proceeds() {
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(), 3, "a successful sell must notify");
assert_eq!(
filled_balance(&outcome).asset,
base(),
"a sell pays with base"
);
assert_eq!(
filled_received(&outcome).asset,
quote(),
"and receives quote"
);
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 + d("25000"),
"quote balance should rise by the proceeds"
);
}
#[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 instruments = spot_instruments();
instruments.get_mut(&instrument_name()).unwrap().kind = kind.clone();
let config = spot_config("10", "10000000", FeeModelConfig::default());
let mut venue = SimulatedVenue::new(&config, instruments);
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(Some(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 = Arc::new(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"
);
}
fn slipped_config(slippage: SlippageConfig) -> MockExecutionConfig {
MockExecutionConfig {
slippage,
..spot_config("10", "1000000", FeeModelConfig::default())
}
}
fn last(price: &str) -> Option<MarketSnapshot> {
Some(MarketSnapshot {
last_price: Some(d(price)),
..MarketSnapshot::default()
})
}
fn uniform(seed: u64) -> SlippageConfig {
SlippageConfig::uniform(d("0.01"), d("0.025"), seed).unwrap()
}
#[test]
fn a_fixed_slippage_moves_a_market_fill_against_the_order_and_settles_that_price() {
let config = slipped_config(SlippageConfig::fixed(d("0.01")).unwrap());
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let bought = venue.open_order(buy_request("2", last("100")));
assert_eq!(filled_trade(&bought).price, d("101"), "a buy pays 1% above");
assert_eq!(
venue.account.balance_mut("e()).unwrap().balance.free,
d("999798"),
"the quote debited is the slipped price times the quantity"
);
let sold = venue.open_order(sell_request("2", last("100")));
assert_eq!(
filled_trade(&sold).price,
d("99"),
"a sell receives 1% below"
);
assert_eq!(
venue.account.balance_mut("e()).unwrap().balance.free,
d("999996"),
"the round trip cost the 1% on each side"
);
}
#[test]
fn a_slipped_marketable_limit_never_fills_past_its_limit() {
let fill = |rate: &str, side: Side, limit: &str| {
let config = slipped_config(SlippageConfig::fixed(d(rate)).unwrap());
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
let fed = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("49000", "49100")),
time(1),
);
assert!(fed.is_empty(), "an empty book fills nothing");
let outcome = venue.open_order(limit_request("cid", side, "1", limit, gtc()));
filled_trade(&outcome).price
};
assert_eq!(
fill("0.01", Side::Buy, "49200"),
d("49200"),
"49591 slipped, clamped to the limit"
);
assert_eq!(
fill("0.01", Side::Sell, "48800"),
d("48800"),
"48510 slipped, clamped to the limit"
);
assert_eq!(
fill("0.001", Side::Buy, "49200"),
d("49149.1"),
"inside the limit, the slipped price stands"
);
}
#[test]
fn a_resting_fill_is_not_slipped_and_takes_no_draw() {
let config = MockExecutionConfig {
slippage: uniform(5),
..spot_config("10", "1000000", maker_taker_fee())
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
let fed = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("49000", "49100")),
time(1),
);
assert!(fed.is_empty(), "an empty book fills nothing");
let rested = venue.open_order(limit_request("resting", Side::Buy, "1", "48000", gtc()));
rested_open(&rested);
assert_eq!(
venue.slippage,
Slippage::new(config.slippage),
"resting drew nothing"
);
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("47800", "47900")),
time(2),
);
assert_eq!(trade_of(&events[2]).price, d("48000"));
assert_eq!(
venue.slippage,
Slippage::new(config.slippage),
"the maker's fill drew nothing"
);
}
#[test]
fn an_order_the_book_cannot_fill_takes_no_draw() {
let config = slipped_config(uniform(3));
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
let fed = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("47800", "47900"))
.with_depth(MarketDepth::new(Some(d("1")), Some(Decimal::ZERO))),
time(1),
);
assert!(fed.is_empty(), "an empty book fills nothing");
let rested = venue.open_order(limit_request("rests", Side::Buy, "1", "48000", gtc()));
assert_eq!(rested_open(&rested).filled_quantity, Decimal::ZERO);
let cancelled = venue.open_order(limit_request(
"cancels",
Side::Buy,
"1",
"48000",
TimeInForce::ImmediateOrCancel,
));
assert!(
matches!(
cancelled.response.state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
),
"{:?}",
cancelled.response.state
);
assert_eq!(venue.slippage, Slippage::new(config.slippage));
}
fn slipped_venue_offering(
usdt: &str,
slippage: SlippageConfig,
depth: MarketDepth,
) -> SimulatedVenue {
let config = MockExecutionConfig {
slippage,
..spot_config("10", usdt, FeeModelConfig::default())
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
let fed = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("47800", "47900")).with_depth(depth),
time(1),
);
assert!(fed.is_empty(), "an empty book fills nothing");
venue
}
#[test]
fn a_part_filled_order_is_slipped_once_and_rests_the_rest_at_its_limit() {
let mut venue = slipped_venue_offering("1000000", uniform(8), sized("1"));
let mut expected = Slippage::new(uniform(8));
let price = expected.apply(Side::Buy, d("47900"));
let outcome = venue.open_order(limit_request("part", Side::Buy, "3", "50000", gtc()));
let trade = filled_trade(&outcome);
assert_eq!((trade.quantity, trade.price), (d("1"), price));
assert_eq!(rested_open(&outcome).filled_quantity, d("1"));
assert_eq!(venue.slippage, expected, "one fill, one draw");
assert_eq!(
venue
.account
.orders()
.resting(&instrument_name(), Side::Buy)
.map(|order| order.price)
.collect::<Vec<_>>(),
vec![Some(d("50000"))],
"the remainder rests at its limit, unslipped"
);
}
#[test]
fn a_killed_order_takes_no_draw() {
let mut venue = slipped_venue_offering("1000000", uniform(8), sized("1"));
let outcome = venue.open_order(limit_request(
"kill",
Side::Buy,
"3",
"50000",
TimeInForce::FillOrKill,
));
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
),
"{:?}",
outcome.response.state
);
assert_eq!(venue.slippage, Slippage::new(uniform(8)));
}
#[test]
fn a_part_filled_order_refused_for_its_hold_hands_back_its_draw() {
let mut venue = slipped_venue_offering("60000", uniform(8), sized("1"));
let outcome = venue.open_order(limit_request("split", Side::Buy, "3", "50000", gtc()));
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::BalanceInsufficient(..)
)))
),
"{:?}",
outcome.response.state
);
assert_eq!(venue.slippage, Slippage::new(uniform(8)));
}
#[test]
fn a_refused_order_hands_back_its_draw() {
let config = slipped_config(uniform(11));
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let refused = venue.open_order(buy_request("1000000", last("100")));
assert!(
matches!(
refused.response.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::BalanceInsufficient(..)
)))
),
"{:?}",
refused.response.state
);
assert_eq!(venue.slippage, Slippage::new(config.slippage));
let mut fresh = SimulatedVenue::new(&config, spot_instruments());
assert_eq!(
filled_trade(&venue.open_order(buy_request("1", last("100")))).price,
filled_trade(&fresh.open_order(buy_request("1", last("100")))).price,
);
}
fn slipped_prices(config: &MockExecutionConfig) -> Vec<Decimal> {
let mut venue = SimulatedVenue::new(config, spot_instruments());
(0..20)
.map(|i| {
let request = if i % 2 == 0 {
buy_request("0.1", last("100"))
} else {
sell_request("0.1", last("100"))
};
filled_trade(&venue.open_order(request)).price
})
.collect()
}
#[test]
fn a_seeded_venue_reproduces_its_prices_within_the_range() {
let prices = slipped_prices(&slipped_config(uniform(42)));
assert_eq!(prices, slipped_prices(&slipped_config(uniform(42))));
assert_ne!(prices, slipped_prices(&slipped_config(uniform(43))));
for (i, price) in prices.iter().enumerate() {
let (low, high) = if i % 2 == 0 {
(d("101"), d("102.5"))
} else {
(d("97.5"), d("99"))
};
assert!(
low <= *price && *price <= high,
"fill {i} at {price} outside [{low}, {high}]"
);
}
}
#[test]
fn a_zero_rate_is_identical_to_no_slippage() {
let none = slipped_prices(&slipped_config(SlippageConfig::none()));
let zero = slipped_prices(&slipped_config(
SlippageConfig::uniform(Decimal::ZERO, Decimal::ZERO, 42).unwrap(),
));
assert_eq!(
none.iter().map(Decimal::to_string).collect::<Vec<_>>(),
zero.iter().map(Decimal::to_string).collect::<Vec<_>>()
);
}
#[derive(Debug)]
struct SizeImpact;
impl FillModel for SizeImpact {
fn fill_price(&self, fill: &FillContext<'_>) -> Option<Decimal> {
fill.market.last_price.map(|last| last + fill.quantity)
}
}
#[test]
fn a_custom_fill_model_sees_the_quantity_and_is_still_slipped() {
let config = slipped_config(SlippageConfig::fixed(d("0.01")).unwrap());
let mut venue =
SimulatedVenue::new(&config, spot_instruments()).with_fill_model(Arc::new(SizeImpact));
let outcome = venue.open_order(buy_request("3", last("100")));
assert_eq!(
filled_trade(&outcome).price,
d("104.03"),
"(100 + 3) x 1.01"
);
}
#[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("9"),
"base balance must decrease by the quantity alone"
);
let usdt = venue.account.balance_mut("e()).unwrap();
assert_eq!(
usdt.balance.free,
d("99.9"),
"quote balance must rise by the proceeds less the fee"
);
}
#[test]
fn a_spot_round_trip_returns_the_base_and_moves_quote_by_pnl_less_fees() {
let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
let mut venue = make_venue_with_fee("0", "1000", fee_model);
let buy = venue.open_order(buy_request("1", market_prices("100")));
assert_eq!(
filled_balance(&buy).balance,
Balance::new(d("899.9"), d("899.9"))
);
assert_eq!(filled_received(&buy).balance, Balance::new(d("1"), d("1")));
let sell = venue.open_order(sell_request("1", market_prices("110")));
assert_eq!(filled_balance(&sell).balance, Balance::new(d("0"), d("0")));
assert_eq!(
filled_received(&sell).balance,
Balance::new(d("1009.79"), d("1009.79"))
);
let fees = filled_trade(&buy).fees.fees + filled_trade(&sell).fees.fees;
assert_eq!(
d("1009.79") - d("1000"),
d("10") - fees,
"quote moves by the realised PnL less the fees the trades report"
);
}
#[test]
fn a_buy_creates_the_base_balance_the_account_never_held() {
let mut venue = SimulatedVenue::new(
&config_from_balances(vec![funded(quote(), d("1000"))], FeeModelConfig::default()),
spot_instruments(),
);
let buy = venue.open_order(buy_request("2", market_prices("100")));
let received = filled_received(&buy);
assert_eq!(received.asset, base());
assert_eq!(received.balance, Balance::new(d("2"), d("2")));
let sell = venue.open_order(sell_request("2", market_prices("100")));
assert!(
sell.response.state.is_accepted(),
"the base the buy received can be sold: {:?}",
sell.response.state
);
assert_eq!(filled_balance(&sell).balance, Balance::new(d("0"), d("0")));
assert_eq!(
filled_received(&sell).balance,
Balance::new(d("1000"), d("1000"))
);
}
#[test]
fn a_per_unit_commission_on_a_spot_sell_comes_out_of_the_proceeds() {
let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
commission_per_contract: d("0.5"),
});
let mut venue = make_venue_with_fee("10", "0", fee_model);
let sell = venue.open_order(sell_request("2", market_prices("100")));
assert_eq!(filled_trade(&sell).fees.fees, d("1"));
assert_eq!(filled_balance(&sell).balance, Balance::new(d("8"), d("8")));
assert_eq!(
filled_received(&sell).balance,
Balance::new(d("199"), d("199"))
);
}
#[test]
fn a_sell_whose_fee_exceeds_its_proceeds_needs_the_excess_in_quote() {
let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
commission_per_contract: d("2"),
});
let mut short_of_quote = make_venue_with_fee("10", "0", fee_model);
let refused = short_of_quote.open_order(sell_request("1", market_prices("1")));
match refused.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::BalanceInsufficient(Some(asset), _),
))) => assert_eq!(asset, quote(), "the asset short is quote, not base"),
other => panic!("expected BalanceInsufficient, got: {other:?}"),
}
assert!(refused.events.is_empty(), "a refusal moves nothing");
assert_eq!(
short_of_quote.account.balance_mut(&base()).unwrap().balance,
Balance::new(d("10"), d("10"))
);
let mut covered = make_venue_with_fee("10", "5", fee_model);
let filled = covered.open_order(sell_request("1", market_prices("1")));
assert_eq!(
filled_balance(&filled).balance,
Balance::new(d("9"), d("9"))
);
assert_eq!(
filled_received(&filled).balance,
Balance::new(d("4"), d("4"))
);
}
#[test]
fn a_resting_sell_whose_fee_exceeds_its_proceeds_is_cancelled_when_quote_cannot_cover() {
let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
commission_per_contract: d("2"),
});
let mut venue = make_market_venue_with("10", "0", fee_model, SimFillConfig::default());
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("0.5", "0.6")),
time(1)
)
.is_empty()
);
let rested = venue.open_order(limit_request("s", Side::Sell, "1", "1", gtc()));
assert_eq!(rested.events.len(), 1, "a 0.5 bid does not reach 1");
assert_eq!(
balance_of(&rested.events[0]).balance,
Balance::new(d("10"), d("9"))
);
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("1.1", "1.2")),
time(2),
);
let [released, cancelled] = events.as_slice() else {
panic!("expected the released hold, then the cancel: {events:?}");
};
assert_eq!(balance_of(released).balance, Balance::new(d("10"), d("10")));
assert!(
matches!(
order_of(cancelled).state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
),
"{:?}",
order_of(cancelled).state
);
assert!(venue.trades(time(0)).is_empty(), "nothing traded");
}
#[test]
fn a_resting_sell_whose_fee_exceeds_its_proceeds_fills_when_quote_covers_it() {
let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
commission_per_contract: d("2"),
});
let mut venue = make_market_venue_with("10", "5", fee_model, SimFillConfig::default());
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("0.5", "0.6")),
time(1)
)
.is_empty()
);
let rested = venue.open_order(limit_request("s", Side::Sell, "1", "1", gtc()));
assert_eq!(rested.events.len(), 1, "a 0.5 bid does not reach 1");
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("1.1", "1.2")),
time(2),
);
assert_eq!(
events.len(),
4,
"the base paid, the quote received, the trade, then the terminal order"
);
assert_eq!(balance_of(&events[0]).balance, Balance::new(d("9"), d("9")));
assert_eq!(balance_of(&events[1]).balance, Balance::new(d("4"), d("4")));
assert_eq!(trade_of(&events[2]).fees.fees, d("2"));
assert!(
matches!(
order_of(&events[3]).state,
OrderState::Inactive(InactiveOrderState::FullyFilled(_))
),
"{:?}",
order_of(&events[3]).state
);
}
fn received(mut venue: SimulatedVenue) -> SimulatedVenue {
venue.spot_fee_asset = SpotFeeAsset::Received;
venue
}
#[test]
fn received_takes_a_buys_fee_from_the_base_it_receives() {
let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
let mut venue = received(make_venue_with_fee("0", "10000", fee_model));
let outcome = venue.open_order(buy_request("10", market_prices("100")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new(d("9000"), d("9000"))
);
assert_eq!(filled_received(&outcome).asset, base());
assert_eq!(
filled_received(&outcome).balance,
Balance::new(d("9.99"), d("9.99"))
);
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(base(), d("0.01"), Some(d("1")))
);
}
#[test]
fn received_takes_a_sells_fee_from_its_proceeds() {
let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
let mut venue = received(make_venue_with_fee("10", "0", fee_model));
let outcome = venue.open_order(sell_request("1", market_prices("100")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new(d("9"), d("9"))
);
assert_eq!(
filled_received(&outcome).balance,
Balance::new(d("99.9"), d("99.9"))
);
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(quote(), d("0.1"), Some(d("0.1")))
);
}
#[test]
fn a_received_buy_whose_fee_exceeds_its_quantity_needs_the_excess_in_base() {
let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
commission_per_contract: d("2"),
});
let mut short_of_base = received(make_venue_with_fee("0", "10", fee_model));
let refused = short_of_base.open_order(buy_request("1", market_prices("1")));
match refused.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::BalanceInsufficient(Some(asset), _),
))) => assert_eq!(asset, base(), "the asset short is base, not quote"),
other => panic!("expected BalanceInsufficient, got: {other:?}"),
}
assert!(refused.events.is_empty(), "a refusal moves nothing");
assert_eq!(
short_of_base.account.balance_mut("e()).unwrap().balance,
Balance::new(d("10"), d("10"))
);
let mut covered = received(make_venue_with_fee("5", "10", fee_model));
let filled = covered.open_order(buy_request("1", market_prices("1")));
assert_eq!(
filled_balance(&filled).balance,
Balance::new(d("9"), d("9"))
);
assert_eq!(
filled_received(&filled).balance,
Balance::new(d("4"), d("4"))
);
}
#[test]
fn a_resting_received_buy_holds_only_its_notional() {
let fee_model = FeeModelConfig::Percentage(PercentageFeeModel::new(d("0.001")));
let mut venue = received(make_market_venue_with(
"0",
"10000",
fee_model,
SimFillConfig::default(),
));
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("101", "102")),
time(1)
)
.is_empty()
);
let rested = venue.open_order(limit_request("b", Side::Buy, "10", "100", gtc()));
assert_eq!(rested.events.len(), 1, "a 102 ask does not reach 100");
assert_eq!(
balance_of(&rested.events[0]).balance,
Balance::new(d("10000"), d("9000"))
);
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("98", "99")),
time(2),
);
assert_eq!(
events.len(),
4,
"the quote paid, the base received, the trade, then the terminal order"
);
assert_eq!(
balance_of(&events[0]).balance,
Balance::new(d("9000"), d("9000"))
);
assert_eq!(
balance_of(&events[1]).balance,
Balance::new(d("9.99"), d("9.99"))
);
assert_eq!(
trade_of(&events[2]).fees,
AssetFees::new(base(), d("0.01"), Some(d("1")))
);
}
#[test]
fn a_received_buy_at_a_zero_price_is_charged_in_quote() {
let fee_model = FeeModelConfig::PerContract(PerContractFeeModel {
commission_per_contract: d("1"),
});
let mut venue = received(make_venue_with_fee("0", "10", fee_model));
let outcome = venue.open_order(buy_request("1", market_prices("0")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new(d("9"), d("9"))
);
assert_eq!(
filled_received(&outcome).balance,
Balance::new(d("1"), d("1"))
);
assert_eq!(filled_trade(&outcome).fees.asset, quote());
}
#[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:?})"
);
}
}
fn priced_market_order(
side: Side,
price: &str,
market: Option<MarketSnapshot>,
) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
let mut request = request(instrument_name(), side, "1", market);
request.state.price = Some(d(price));
request
}
#[test]
fn a_market_order_is_never_filled_at_a_price_it_carries() {
let mut market_driven = make_market_venue("10", "10000000");
advance(&mut market_driven, time(1));
let cases = [
("request-priced", make_venue("10", "10000000"), None),
(
"request-priced",
make_venue("10", "10000000"),
Some(MarketSnapshot::default()),
),
("market-driven", market_driven, None),
];
for (regime, mut venue, market) in cases {
let outcome = venue.open_order(priced_market_order(Side::Buy, "1", market));
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(
UnindexedOrderError::Rejected(ApiError::OrderRejected(_))
))
),
"expected the unpriceable rejection, got {:?} ({regime}, market={market:?})",
outcome.response.state
);
assert!(
outcome.events.is_empty(),
"nothing filled ({regime}, market={market:?})"
);
}
}
#[test]
fn a_market_order_is_not_clamped_by_a_price_it_carries() {
for (side, stray) in [(Side::Buy, "90"), (Side::Sell, "110")] {
let mut venue = make_venue("10", "10000000");
let market = Some(MarketSnapshot {
last_price: Some(d("100")),
..MarketSnapshot::default()
});
let outcome = venue.open_order(priced_market_order(side, stray, market));
assert_eq!(
filled_trade(&outcome).price,
d("100"),
"a {side:?} fills at the market, not clamped to its stray {stray}"
);
}
}
#[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: PositionReport::Unreported,
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 minted_order_ids_start_past_the_highest_decimal_id_seeded() {
let resting = |cid: &str, id: &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(id)),
time_exchange: Default::default(),
filled_quantity: Decimal::ZERO,
}),
};
let seeded_venue = |orders| {
let mut config = spot_config("10", "10000000", FeeModelConfig::default());
config.initial_state.instruments = vec![InstrumentAccountSnapshot {
instrument: instrument_name(),
orders,
orders_complete: true,
position: PositionReport::Unreported,
isolated: None,
}];
SimulatedVenue::new(&config, spot_instruments())
};
let mut venue = seeded_venue(vec![
resting("a", "3"),
resting("b", "1"),
resting("c", "zzz"),
]);
assert_eq!(venue.order_id_sequence_fetch_add(), OrderId::new("4"));
assert_eq!(venue.order_id_sequence_fetch_add(), OrderId::new("5"));
assert_eq!(venue.order_sequence(), 2);
let mut cancelled = resting("d", "9");
cancelled.state = OrderState::inactive(Cancelled::new(
OrderId::new("9"),
Default::default(),
Some(Decimal::ZERO),
));
let mut venue = seeded_venue(vec![resting("a", "3"), cancelled]);
assert_eq!(venue.order_id_sequence_fetch_add(), OrderId::new("10"));
let mut venue = seeded_venue(vec![resting("c", "zzz")]);
assert_eq!(venue.order_id_sequence_fetch_add(), OrderId::new("0"));
}
#[test]
fn minted_order_ids_start_at_zero_when_nothing_is_seeded() {
let config = spot_config("10", "10000000", FeeModelConfig::default());
let mut venue = SimulatedVenue::new(&config, spot_instruments());
assert_eq!(venue.order_id_sequence_fetch_add(), OrderId::new("0"));
}
#[test]
fn a_seeded_u64_max_order_id_does_not_overflow_the_next() {
let mut account = AccountState::try_from(
spot_config("10", "10000000", FeeModelConfig::default()).initial_state,
)
.unwrap();
let mut seeded = seeded_part_filled("48000", "1", "0");
seeded.state.id = VenueOrderId::Assigned(OrderId::new(u64::MAX.to_string()));
assert!(
account.book(seeded, None).is_ok(),
"the book is vacant under this id"
);
assert_eq!(first_unseeded_order_id(&account), u128::from(u64::MAX) + 1);
}
#[test]
fn an_order_for_a_quantity_that_is_not_positive_is_rejected_before_the_ledger_moves() {
for request in [
buy_request("0", market_prices("50000")),
sell_request("-1", market_prices("50000")),
] {
let mut venue = make_venue("100", "10000000");
let quantity = request.state.quantity;
let outcome = venue.open_order(request);
assert_eq!(
rejection_reason(&outcome),
format!(
"cannot open {} for {EXCHANGE}: quantity {quantity} is not positive",
instrument_name()
)
);
assert!(outcome.events.is_empty(), "a refused order moves nothing");
assert!(venue.trades(time(0)).is_empty(), "and nothing traded");
assert!(venue.orders_open(&[]).is_empty(), "or rests");
assert_eq!(free_quote(&venue), d("10000000"));
assert_eq!(total_base(&venue), d("100"));
}
let mut venue = make_venue("100", "10000000");
let outcome = venue.open_order(limit_request("limit", Side::Buy, "0", "50000", gtc()));
let reason = rejection_reason(&outcome);
assert!(
reason.contains("does not support OrderKind::Limit"),
"{reason}"
);
}
#[test]
fn a_limit_order_is_rejected_before_a_fill_model_is_consulted() {
let mut venue = make_venue("100", "10000000");
venue.fill_model = Arc::new(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(),
MarketObservation::new(book("47800", "47900")),
time(2),
);
assert_eq!(
events.len(),
4,
"one filled spot order owes two balances, a trade and a terminal order snapshot"
);
let trade = trade_of(&events[2]);
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[3])),
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(),
MarketObservation::new(book("47800", "47900")),
time(2),
);
assert_eq!(
balance_of(&events[0]).asset,
quote(),
"the balance the fill paid with comes first"
);
assert_eq!(
balance_of(&events[1]).asset,
base(),
"then the balance it received"
);
assert!(
matches!(events[2].kind, AccountEventKind::Trade(_)),
"then the trade, got: {:?}",
events[2].kind
);
assert!(
matches!(events[3].kind, AccountEventKind::OrderSnapshot(_)),
"and only then the order it terminated, got: {:?}",
events[3].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(),
MarketObservation::new(book("47800", "47900")),
time(2),
);
assert!(
matches!(
order_of(&events[3]).state,
OrderState::Inactive(InactiveOrderState::FullyFilled(_))
),
"got: {:?}",
order_of(&events[3]).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(),
MarketObservation::new(book("47800", "47900")),
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]
#[should_panic(expected = "cannot afford the fill of resting order part_filled")]
fn a_seeded_order_the_account_cannot_afford_panics_when_it_fills() {
let mut venue = make_market_venue("10", "100");
advance(&mut venue, time(1));
assert!(
venue
.account
.book(seeded_part_filled("48000", "1", "0"), None)
.is_ok()
);
advance(&mut venue, time(2));
let _ = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("47800", "47900")),
time(2),
);
}
#[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
.book(
seeded_part_filled("48000", "1", "0.4"),
None,
)
.is_ok(),
"the book is vacant under this id"
);
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("47800", "47900")),
time(2),
);
assert_eq!(
events.len(),
4,
"one filled spot order owes two balances, a trade and a terminal order snapshot"
);
let trade = trade_of(&events[2]);
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!(
trade.order_filled_quantity,
Some(d("1")),
"the order's cumulative is the 0.4 it arrived with plus the 0.6 this cross traded"
);
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
.book(seeded_part_filled("48000", "1", "0.4"), None)
.is_ok(),
"the book is vacant under this id"
);
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("47800", "47900")),
time(2),
);
let filled = match &order_of(&events[3]).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(),
MarketObservation::new(book("47800", "47900")),
time(2),
);
assert_eq!(events.len(), 4);
assert_eq!(
trade_of(&events[2]).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(),
MarketObservation::new(book("47800", "47900")),
time(1)
)
.is_empty()
);
let outcome = venue.open_order(limit_request("taker", Side::Buy, "1", "48000", gtc()));
assert_eq!(
outcome.events.len(),
3,
"a filled open owes both balances, then the trade"
);
let trade = filled_trade(&outcome);
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(), MarketObservation::new(stale), time(1))
.is_empty()
);
let outcome = venue.open_order(limit_request("clamped", Side::Buy, "1", "47100", gtc()));
assert_eq!(
filled_trade(&outcome).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(),
MarketObservation::new(touching()),
time(1)
)
.is_empty()
);
let on_arrival = aggressor.open_order(limit_request("a", Side::Buy, "1", limit, gtc()));
let arrival_price = filled_trade(&on_arrival).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(),
MarketObservation::new(book("49000", "49100")),
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(),
MarketObservation::new(touching()),
time(2),
);
let resting_price = trade_of(&resting_events[2]).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(),
MarketObservation::new(book("49000", "49100")),
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(),
MarketObservation::new(book("47900", "48000")),
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(),
MarketObservation::new(traded("49000")),
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(),
MarketObservation::new(traded("47500")),
time(2),
);
assert_eq!(events.len(), 4, "a print through the limit fills it");
assert_eq!(trade_of(&events[2]).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(), MarketObservation::new(inside), 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(),
MarketObservation::new(book("47000", "47100")),
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(),
MarketObservation::new(book("48100", "48200")),
time(2),
);
assert_eq!(trade_of(&events[2]).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"));
let received = balance_of(&events[1]);
assert_eq!(received.asset, quote());
assert_eq!(received.balance.total, d("1000000") + d("48000") - d("9.6"));
}
#[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(),
MarketObservation::new(book("49000", "49100")),
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(),
MarketObservation::new(book("47800", "47900")),
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[2]).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(),
MarketObservation::new(book("49000", "49100")),
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, Some(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(),
MarketObservation::new(book("47800", "47900")),
time(2),
);
assert_eq!(events.len(), 4, "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(),
MarketObservation::new(book("49000", "49100")),
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(),
MarketObservation::new(book("49000", "49100")),
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, Some(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(),
MarketObservation::new(book("49000", "49100")),
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(),
MarketObservation::new(book("49000", "49100")),
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,
Some(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(),
MarketObservation::new(traded("49000")),
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, Some(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(),
MarketObservation::new(book("46900", "47000")),
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, Some(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(),
MarketObservation::new(traded("47000")),
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,
MarketObservation::new(book("2000", "2001")),
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
.book(
as_open(seeded).expect("the seeded order is Open"),
None,
)
.is_ok(),
"the book is vacant under this id"
);
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(),
MarketObservation::new(traded("49000")),
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(),
MarketObservation::new(book("47800", "47900")).with_depth(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(),
3,
"a fill owes both balances, then the trade"
);
let trade = filled_trade(&outcome);
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,
Some(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!(
filled_trade(&ioc).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(),
3,
"one arrival restates each asset once and prints one trade, even settling and holding \
at once"
);
let trade = filled_trade(&outcome);
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 a_capped_spot_sell_settles_and_holds_base_and_receives_quote_for_what_traded() {
let mut venue = venue_offering(sized("0.4"));
let outcome = venue.open_order(limit_request("split", Side::Sell, "1", "47700", gtc()));
assert_eq!(
outcome.events.len(),
3,
"the base paid, the quote received, then the trade"
);
let trade = filled_trade(&outcome);
assert_eq!(trade.quantity, d("0.4"));
assert_eq!(trade.price, d("47800"), "the part that traded took the bid");
let paid = filled_balance(&outcome);
assert_eq!(paid.asset, base());
assert_eq!(paid.balance, Balance::new(d("9.6"), d("9")));
let received = filled_received(&outcome);
assert_eq!(received.asset, quote());
assert_eq!(
received.balance,
Balance::new(d("1019100.88"), d("1019100.88"))
);
assert_eq!(rested_open(&outcome).quantity_remaining(d("1")), d("0.6"));
}
#[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 = filled_trade(&arrival).clone();
let fills = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("48000", "48000")),
time(2),
);
assert_eq!(
fills.len(),
4,
"a resting fill owes both balances, the trade, the order"
);
let maker = trade_of(&fills[2]);
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!(filled_trade(&first).quantity, d("0.6"));
let second = venue.open_order(limit_request(
"second",
Side::Buy,
"0.6",
"48000",
TimeInForce::ImmediateOrCancel,
));
assert_eq!(
filled_trade(&second).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!(filled_trade(&taken).quantity, d("0.6"));
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("47800", "47900")).with_depth(sized("1")),
time(2)
)
.is_empty()
);
let after = venue.open_order(limit_request(
"second",
Side::Buy,
"1",
"48000",
TimeInForce::ImmediateOrCancel,
));
assert_eq!(
filled_trade(&after).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!(
filled_trade(&outcome).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!(
filled_trade(&affordable).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());
}
fn is_duplicate_cid(state: &UnindexedOrderState) -> bool {
matches!(
state,
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::DuplicateClientOrderId(_)
)))
)
}
#[test]
fn an_open_under_a_resting_id_is_rejected_and_the_resting_order_is_untouched() {
let (mut venue, _) = venue_resting_one_buy("48000");
let free = free_quote(&venue);
let second = venue.open_order(limit_request("resting", Side::Buy, "0.5", "47000", gtc()));
assert!(
is_duplicate_cid(&second.response.state),
"rejected as a duplicate: {:?}",
second.response.state
);
assert!(second.events.is_empty(), "a rejection moves nothing");
let [resting] = &venue.orders_open(&[])[..] else {
panic!("one cid, one resting order");
};
assert_eq!(
(resting.quantity, resting.price),
(d("1"), Some(d("48000"))),
"the resting order is the first one"
);
assert_eq!(
free_quote(&venue),
free,
"its hold is still held, and only its"
);
assert_eq!(total_quote(&venue), d("1000000"));
}
#[test]
fn a_market_order_under_a_resting_id_is_rejected_before_it_trades() {
let (mut venue, _) = venue_resting_one_buy("48000");
let mut market = buy_request("0.1", None);
market.key.cid = ClientOrderId::new("resting");
let outcome = venue.open_order(market);
assert!(
is_duplicate_cid(&outcome.response.state),
"rejected as a duplicate: {:?}",
outcome.response.state
);
assert!(venue.trades(time(0)).is_empty(), "nothing traded");
assert_eq!(venue.orders_open(&[]).len(), 1);
}
#[test]
fn a_malformed_open_under_a_resting_id_is_rejected_as_malformed() {
let (mut venue, _) = venue_resting_one_buy("48000");
let outcome = venue.open_order(limit_request(
"resting",
Side::Buy,
"1",
"47000",
TimeInForce::AtOpen,
));
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(_))
) && !is_duplicate_cid(&outcome.response.state),
"rejected on its time in force: {:?}",
outcome.response.state
);
}
#[test]
fn a_cancelled_orders_id_is_free_again_and_the_latest_order_is_reported() {
let (mut venue, _) = venue_resting_one_buy("48000");
let cancel = |venue: &mut SimulatedVenue| {
venue.cancel_order(OrderRequestCancel {
key: limit_request("resting", Side::Buy, "1", "48000", gtc()).key,
state: RequestCancel { id: None },
})
};
assert!(cancel(&mut venue).response.state.is_ok());
let second = venue.open_order(limit_request("resting", Side::Buy, "0.5", "47000", gtc()));
assert!(
matches!(
second.response.state,
OrderState::Active(ActiveOrderState::Open(_))
),
"the id is free again: {:?}",
second.response.state
);
assert!(
venue.orders_ended(&[key_of("resting")]).is_empty(),
"the order under the id is working, whatever became of the one before it"
);
let snapshot = venue.account_snapshot();
let listed = snapshot
.instruments
.iter()
.flat_map(|instrument| &instrument.orders)
.filter(|order| order.key.cid == ClientOrderId::new("resting"))
.count();
assert_eq!(
listed, 1,
"a snapshot lists the id once, as the working order"
);
assert!(
cancel(&mut venue).response.state.is_ok(),
"and the cancel reaches it"
);
let [ended] = &venue.orders_ended(&[key_of("resting")])[..] else {
panic!("the second order has ended");
};
assert_eq!(ended.quantity, d("0.5"), "the latest order under the id");
}
#[test]
fn a_filled_orders_id_is_free_again() {
let (mut venue, _) = venue_resting_one_buy("48000");
let filled = venue.open_order(limit_request("reused", Side::Buy, "1", "49100", gtc()));
assert!(
matches!(
filled.response.state,
OrderState::Inactive(InactiveOrderState::FullyFilled(_))
),
"crosses the ask, so fills on arrival"
);
let second = venue.open_order(limit_request("reused", Side::Buy, "1", "47000", gtc()));
assert!(
matches!(
second.response.state,
OrderState::Active(ActiveOrderState::Open(_))
),
"the id is free again: {:?}",
second.response.state
);
let cancelled = venue.cancel_order(OrderRequestCancel {
key: limit_request("reused", Side::Buy, "1", "47000", gtc()).key,
state: RequestCancel { id: None },
});
assert!(
cancelled.response.state.is_ok(),
"the cancel reaches the working order: {:?}",
cancelled.response.state
);
}
fn key_of(cid: &str) -> UnindexedOrderKey {
OrderKey {
exchange: EXCHANGE,
instrument: instrument_name(),
strategy: StrategyId::new("asker"),
cid: ClientOrderId::new(cid),
}
}
#[test]
fn orders_ended_reports_each_ended_order_and_omits_the_rest() {
let mut venue = make_market_venue("10", "1000000");
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("49000", "49100")),
time(1)
)
.is_empty()
);
let filled = venue.open_order(limit_request("filled", Side::Buy, "1", "49100", gtc()));
assert!(
matches!(
filled.response.state,
OrderState::Inactive(InactiveOrderState::FullyFilled(_))
),
"crosses the ask, so fills on arrival"
);
let _ = venue.open_order(limit_request("working", Side::Buy, "1", "48000", gtc()));
let _ = venue.open_order(limit_request("cancelled", Side::Buy, "1", "47500", gtc()));
let _ = venue.cancel_order(OrderEvent {
key: key_of("cancelled"),
state: RequestCancel { id: None },
});
let _ = venue.open_order(limit_request(
"expired",
Side::Buy,
"1",
"47000",
TimeInForce::GoodTillDate { expiry: time(9) },
));
assert_eq!(
venue.advance_time(time(9)).len(),
2,
"the deadline retires it"
);
let asked = ["filled", "working", "cancelled", "expired", "unknown"].map(key_of);
let ended = venue.orders_ended(&asked);
assert_eq!(ended.len(), 3, "exactly the three ended orders: {ended:?}");
assert!(
ended
.iter()
.all(|order| order.key.strategy == StrategyId::new("asker")),
"each comes back under the key it was asked for"
);
let ended_as = |cid: &str| {
let Some(order) = ended
.iter()
.find(|order| order.key.cid == ClientOrderId::new(cid))
else {
panic!("{cid} is reported: {ended:?}");
};
order
};
let (filled, cancelled, expired) = (
ended_as("filled"),
ended_as("cancelled"),
ended_as("expired"),
);
match &filled.state {
InactiveOrderState::FullyFilled(state) => {
assert_eq!(state.filled_quantity, d("1"));
assert_eq!(state.avg_price, Some(d("49100")));
}
other => panic!("filled, got {other:?}"),
}
match &cancelled.state {
InactiveOrderState::Cancelled(state) => {
assert_eq!(state.filled_quantity, Some(Decimal::ZERO))
}
other => panic!("cancelled, got {other:?}"),
}
match &expired.state {
InactiveOrderState::Expired(state) => assert_eq!(state.time_exchange, time(9)),
other => panic!("expired, got {other:?}"),
}
assert!(
venue.orders_ended(&[]).is_empty(),
"an empty request asks about nothing, not about everything"
);
}
#[test]
fn orders_ended_reports_a_market_remainder_as_cancelled_with_its_fill() {
let mut venue = venue_offering(sized("0.4"));
let outcome = venue.open_order(buy_request("1", None));
let ended = venue.orders_ended(std::slice::from_ref(&outcome.response.key));
let [order] = ended.as_slice() else {
panic!("the remainder's order is reported: {ended:?}");
};
match &order.state {
InactiveOrderState::Cancelled(state) => {
assert_eq!(state.filled_quantity, Some(d("0.4")))
}
other => panic!("cancelled carrying its fill, got {other:?}"),
}
}
#[test]
fn orders_ended_reports_a_resting_fill_and_its_average_price_as_the_fill_did() {
let (mut venue, _) = venue_resting_one_buy("48000");
assert!(
venue
.account
.book(seeded_part_filled("48000", "1", "0.4"), None)
.is_ok(),
"the book is vacant under this id"
);
advance(&mut venue, time(2));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("47800", "47900")),
time(2),
);
assert_eq!(
events.len(),
8,
"two fills, each two balances, a trade and an order"
);
let ended = venue.orders_ended(&[key_of("resting"), key_of("part_filled")]);
let avg_price_of = |cid: &str| {
let Some(order) = ended
.iter()
.find(|order| order.key.cid == ClientOrderId::new(cid))
else {
panic!("{cid} is reported: {ended:?}");
};
match &order.state {
InactiveOrderState::FullyFilled(filled) => {
assert_eq!(filled.filled_quantity, d("1"));
filled.avg_price
}
other => panic!("{cid} filled, got {other:?}"),
}
};
assert_eq!(avg_price_of("resting"), Some(d("48000")));
assert_eq!(
avg_price_of("part_filled"),
None,
"this venue saw only one of the two fills behind it"
);
}
#[test]
fn orders_ended_reports_an_order_the_initial_state_seeded_as_cancelled() {
let seeded = Order {
key: key_of("seeded"),
side: Side::Buy,
price: Some(d("48000")),
quantity: d("1"),
kind: OrderKind::Limit,
time_in_force: gtc(),
state: OrderState::inactive(Cancelled {
id: OrderId::new("seeded"),
time_exchange: time(0),
filled_quantity: Some(d("0.25")),
}),
};
let venue = SimulatedVenue::new(
&spot_config_holding("10", "1000000", seeded),
spot_instruments(),
);
let ended = venue.orders_ended(&[key_of("seeded")]);
let [order] = ended.as_slice() else {
panic!("the seeded cancel is reported: {ended:?}");
};
match &order.state {
InactiveOrderState::Cancelled(cancelled) => {
assert_eq!(cancelled.filled_quantity, Some(d("0.25")))
}
other => panic!("cancelled, got {other:?}"),
}
}
#[test]
fn orders_ended_reports_an_id_once_and_only_for_its_own_instrument() {
let mut venue = venue_offering(sized("0.4"));
let outcome = venue.open_order(buy_request("1", None));
let key = outcome.response.key.clone();
let other_instrument = OrderKey {
instrument: InstrumentNameExchange::new("ETHUSDT"),
..key.clone()
};
let second_ask = OrderKey {
strategy: StrategyId::new("second"),
..key.clone()
};
let ended = venue.orders_ended(&[key.clone(), second_ask]);
let [order] = ended.as_slice() else {
panic!("one entry for one id: {ended:?}");
};
assert_eq!(order.key, key, "the first key that finds it");
let ended = venue.orders_ended(&[other_instrument.clone(), key.clone()]);
let [order] = ended.as_slice() else {
panic!("a key that finds nothing does not stop a later one: {ended:?}");
};
assert_eq!(order.key, key);
assert!(
venue.orders_ended(&[other_instrument]).is_empty(),
"a key for another instrument does not find it"
);
}
fn hourly_funding(rate: &str) -> Arc<dyn HoldingCostModel> {
Arc::new(
FundingModel::new(TimeDelta::hours(1))
.with_rates(cfd_instrument_name(), RateSeries::constant(d(rate))),
)
}
fn boundary(n: i64) -> DateTime<Utc> {
time(2800 + 3600 * n)
}
fn make_cfd_venue_charging(
usd: &str,
models: Vec<Arc<dyn HoldingCostModel>>,
) -> SimulatedVenue {
let mut venue = SimulatedVenue::new_market_driven(
&config_from_balances(vec![funded(self::usd(), d(usd))], FeeModelConfig::default()),
instruments_of(cfd_instrument()),
)
.with_holding_costs(models);
advance(&mut venue, time(1));
venue
}
fn charges(
events: &[UnindexedAccountEvent],
) -> Vec<(
&AssetBalance<AssetNameExchange>,
&CashFlow<AssetNameExchange, InstrumentNameExchange>,
)> {
sans_debt(events)
.chunks(2)
.map(
|pair| match (&pair[0].kind, pair.get(1).map(|event| &event.kind)) {
(
AccountEventKind::BalanceSnapshot(balance),
Some(AccountEventKind::CashFlow(flow)),
) => (&balance.0, flow),
other => panic!("a charge is [balance, cash flow], got: {other:?}"),
},
)
.collect()
}
#[test]
fn a_position_held_across_a_boundary_is_charged_there_as_balance_then_cash_flow() {
let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
assert!(cfd_market(&mut venue, "4990", "5010", "4000").is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
assert_eq!(free_usd(&venue), d("100000"));
let events = venue.advance_time(boundary(0));
let charged = charges(&events);
assert_eq!(charged.len(), 1);
let (balance, flow) = charged[0];
assert_eq!(balance.asset, usd());
assert_eq!(balance.balance, Balance::new(d("99987.5"), d("99987.5")));
assert_eq!(balance.time_exchange, boundary(0));
assert_eq!(
*flow,
CashFlow::new(
CashFlowKind::Funding {
rate: Some(d("0.0001")),
position_quantity: Some(d("1")),
},
usd(),
d("-12.5"),
Some(cfd_instrument_name()),
boundary(0),
Some(CashFlowId::new("0")),
)
);
assert_eq!(total_usd(&venue), d("99987.5"));
}
#[test]
fn a_gap_of_several_boundaries_is_caught_up_once_each() {
let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Sell, "1");
let events = venue.advance_time(boundary(2) + TimeDelta::seconds(1));
let charged = charges(&events);
let stamps: Vec<_> = charged.iter().map(|(_, flow)| flow.time_exchange).collect();
assert_eq!(stamps, vec![boundary(0), boundary(1), boundary(2)]);
let ids: Vec<_> = charged.iter().map(|(_, flow)| flow.id.clone()).collect();
assert_eq!(
ids,
["0", "1", "2"].map(|id| Some(CashFlowId::new(id))).to_vec()
);
assert!(
charged.iter().all(|(_, flow)| flow.amount == d("12.5")),
"a short receives a positive funding rate"
);
assert_eq!(free_usd(&venue), d("75000") + d("37.5"));
assert!(
venue
.advance_time(boundary(2) + TimeDelta::seconds(1))
.is_empty()
);
assert!(
venue.advance_time(boundary(1)).is_empty(),
"the clock moving back"
);
assert_eq!(free_usd(&venue), d("75000") + d("37.5"));
}
#[test]
fn an_earlier_instant_moves_neither_the_clock_nor_the_accrual_back() {
let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Sell, "1");
let reached = boundary(1) + TimeDelta::seconds(1);
assert_eq!(charges(&venue.advance_time(reached)).len(), 2);
let free = free_usd(&venue);
assert!(
venue.advance_time(boundary(0)).is_empty(),
"nothing is charged again"
);
assert_eq!(venue.time_exchange(), reached);
assert!(
venue
.account
.balances()
.all(|balance| balance.time_exchange == reached),
"no balance is restamped earlier than the venue has reported"
);
assert_eq!(free_usd(&venue), free);
let events = venue.advance_time(boundary(2));
let stamps: Vec<_> = charges(&events)
.iter()
.map(|(_, flow)| flow.time_exchange)
.collect();
assert_eq!(stamps, vec![boundary(2)]);
}
#[test]
fn charging_starts_at_the_first_clock_move_and_values_at_entry_without_a_market() {
let mut venue = make_cfd_venue("200000", FeeModelConfig::default())
.with_holding_costs(vec![hourly_funding("0.0001")]);
cfd_fill(&mut venue, Side::Buy, "1", "4000");
assert!(
venue
.advance_time(boundary(0) + TimeDelta::seconds(1))
.is_empty(),
"the first move charges nothing before it"
);
let events = venue.advance_time(boundary(1));
let charged = charges(&events);
assert_eq!(charged.len(), 1);
assert_eq!(charged[0].1.amount, d("-10"));
}
#[test]
fn a_position_opened_at_a_boundary_is_not_charged_for_it_and_one_closed_there_is() {
let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
assert!(venue.advance_time(boundary(0)).is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
let events = venue.advance_time(boundary(1));
let stamps: Vec<_> = charges(&events)
.iter()
.map(|(_, flow)| flow.time_exchange)
.collect();
assert_eq!(stamps, vec![boundary(1)]);
cfd_market_fill(&mut venue, Side::Sell, "1");
assert!(venue.advance_time(boundary(3)).is_empty(), "flat");
}
#[test]
fn a_market_tick_past_a_boundary_charges_it_at_the_market_before_the_tick() {
let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
let six_thousand = MarketSnapshot {
best_bid: Some(d("6000")),
best_ask: Some(d("6000")),
last_price: Some(d("6000")),
};
let events = venue.apply_market(
&cfd_instrument_name(),
MarketObservation::new(six_thousand),
boundary(0) + TimeDelta::seconds(1),
);
let charged = charges(&events);
assert_eq!(charged.len(), 1);
assert_eq!(charged[0].1.amount, d("-12.5"), "valued at 5000, not 6000");
}
#[test]
fn a_charge_the_account_cannot_cover_takes_free_below_zero() {
let mut venue = make_cfd_venue_charging("125000", vec![hourly_funding("0.0001")]);
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
assert_eq!(free_usd(&venue), Decimal::ZERO);
let events = venue.advance_time(boundary(0));
assert_eq!(
charges(&events)[0].0.balance,
Balance::new(d("-12.5"), d("-12.5"))
);
}
fn warnings<T>(f: impl FnOnce() -> T) -> (T, Vec<String>) {
let (value, events) = rustrade_instrument::test_utils::logs::capture(f);
let warnings = events
.iter()
.filter(|event| event.level == tracing::Level::WARN)
.map(|event| event.message().to_owned())
.collect();
(value, warnings)
}
#[test]
fn a_negative_balance_is_warned_of_on_the_charge_that_crosses_zero() {
let rates = RateSeries::new(vec![
(DateTime::<Utc>::MIN_UTC, d("0.0001")),
(boundary(3), d("-0.00001")),
])
.unwrap();
let funding =
FundingModel::new(TimeDelta::hours(1)).with_rates(cfd_instrument_name(), rates);
let mut venue = make_cfd_venue_charging("125000", vec![Arc::new(funding)]);
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
let (events, logged) = warnings(|| venue.advance_time(boundary(2)));
assert_eq!(charges(&events).len(), 3, "three charges, all below zero");
assert_eq!(logged.len(), 1, "{logged:?}");
assert!(logged[0].contains("could not cover"), "{logged:?}");
let (events, logged) = warnings(|| venue.advance_time(boundary(3)));
assert_eq!(charges(&events)[0].1.amount, d("1.25"), "the long receives");
assert_eq!(free_usd(&venue), d("-36.25"));
assert!(logged.is_empty(), "a credit is not warned of: {logged:?}");
}
#[test]
fn a_zero_charge_is_not_reported() {
let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0")]);
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
assert!(venue.advance_time(boundary(1)).is_empty());
}
#[test]
fn the_next_deadline_is_the_earliest_expiry_or_boundary() {
let mut venue = make_cfd_venue_charging("200000", vec![hourly_funding("0.0001")]);
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
assert_eq!(venue.next_deadline(), None, "flat, no orders");
cfd_market_fill(&mut venue, Side::Buy, "1");
assert_eq!(venue.next_deadline(), Some(boundary(0)));
let expiry = boundary(0) - TimeDelta::seconds(60);
let mut gtd = limit_request(
"gtd",
Side::Sell,
"1",
"6000",
TimeInForce::GoodTillDate { expiry },
);
gtd.key.instrument = cfd_instrument_name();
assert!(venue.open_order(gtd).response.state.is_accepted());
assert_eq!(venue.next_deadline(), Some(expiry));
let events = venue.advance_time(expiry);
assert!(!events.is_empty(), "the order expired");
assert_eq!(venue.next_deadline(), Some(boundary(0)));
}
#[test]
fn a_short_pays_its_borrow_by_settlement_day_and_receives_its_financing() {
let cutoff = NaiveTime::from_hms_opt(21, 0, 0).unwrap();
let borrow = BorrowFeeModel::new(DailyAccrual::new(
cutoff,
Utc,
AccrualDays::Settlement {
calendar: Arc::new(WeekendsOnly),
lag: 1,
},
DayCount::Act360,
))
.with_rates(cfd_instrument_name(), RateSeries::constant(d("0.36")));
let financing = FinancingModel::new(DailyAccrual::new(
cutoff,
Utc,
AccrualDays::Calendar,
DayCount::Act365,
))
.with_rates(
cfd_instrument_name(),
FinancingRates::new(
RateSeries::constant(d("0.073")),
RateSeries::constant(d("-0.0365")),
),
);
let mut venue =
make_cfd_venue_charging("200000", vec![Arc::new(borrow), Arc::new(financing)]);
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Sell, "1");
let friday = DateTime::parse_from_rfc3339("2023-11-17T00:00:00Z")
.unwrap()
.to_utc();
let events = venue.advance_time(friday);
let charged: Vec<_> = charges(&events)
.iter()
.map(|(_, flow)| (flow.time_exchange, flow.kind.clone(), flow.amount))
.collect();
let wednesday = DateTime::parse_from_rfc3339("2023-11-15T21:00:00Z")
.unwrap()
.to_utc();
let thursday = wednesday + TimeDelta::days(1);
let borrow_fee = CashFlowKind::BorrowFee {
rate: Some(d("0.36")),
quantity: Some(d("25")),
};
let financed = CashFlowKind::Financing {
rate: Some(d("-0.0365")),
notional: Some(d("125000")),
};
assert_eq!(
charged,
vec![
(wednesday, borrow_fee.clone(), d("-125")),
(wednesday, financed.clone(), d("12.5")),
(thursday, borrow_fee, d("-375")),
(thursday, financed, d("12.5")),
]
);
}
#[test]
fn positions_charged_at_one_boundary_are_charged_in_instrument_order() {
let nas = InstrumentNameExchange::new("nas100_usd");
let dax = InstrumentNameExchange::new("dax40_usd");
let mut instruments = instruments_of(cfd_instrument());
let mut funding = FundingModel::new(TimeDelta::hours(1))
.with_rates(cfd_instrument_name(), RateSeries::constant(d("0.0001")));
for name in [&nas, &dax] {
let mut other = cfd_instrument();
other.name_exchange = name.clone();
instruments.insert(name.clone(), other);
funding = funding.with_rates(name.clone(), RateSeries::constant(d("0.0001")));
}
let mut venue = SimulatedVenue::new_market_driven(
&config_from_balances(vec![funded(usd(), d("1000000"))], FeeModelConfig::default()),
instruments,
)
.with_holding_costs(vec![Arc::new(funding)]);
advance(&mut venue, time(1));
for name in [cfd_instrument_name(), nas.clone(), dax.clone()] {
let fills = venue.apply_market(
&name,
MarketObservation::new(market_prices("5000").unwrap()),
time(1),
);
assert!(fills.is_empty());
let outcome = venue.open_order(request(name, Side::Buy, "1", None));
assert!(outcome.response.state.is_accepted());
}
let events = venue.advance_time(boundary(0));
let order: Vec<_> = charges(&events)
.iter()
.map(|(_, flow)| flow.instrument.clone().unwrap())
.collect();
assert_eq!(order, vec![dax, nas, cfd_instrument_name()]);
}
#[test]
fn a_position_flipped_before_a_boundary_is_charged_as_what_it_became() {
let mut venue = make_cfd_venue_charging("400000", vec![hourly_funding("0.0001")]);
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
cfd_market_fill(&mut venue, Side::Sell, "2");
let events = venue.advance_time(boundary(0));
let charged = charges(&events);
assert_eq!(charged.len(), 1);
assert_eq!(
charged[0].1.amount,
d("12.5"),
"a short receives a positive rate"
);
}
#[test]
#[should_panic(expected = "which is not after")]
fn a_model_whose_boundary_does_not_advance_panics_rather_than_charging_forever() {
#[derive(Debug)]
struct Stuck;
impl HoldingCostModel for Stuck {
fn next_boundary(
&self,
_: &InstrumentNameExchange,
after: DateTime<Utc>,
) -> Option<DateTime<Utc>> {
Some(after)
}
fn charge(&self, _: &HeldPosition<'_>, _: DateTime<Utc>) -> Option<HoldingCharge> {
None
}
}
let mut venue = make_cfd_venue_charging("200000", vec![Arc::new(Stuck)]);
assert!(cfd_market(&mut venue, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut venue, Side::Buy, "1");
let _ = venue.advance_time(time(2));
}
fn make_cfd_venue_lending(table: ShortabilityTable) -> SimulatedVenue {
let mut venue = SimulatedVenue::new_market_driven(
&config_from_balances(
vec![funded(self::usd(), d("10000000"))],
FeeModelConfig::default(),
),
instruments_of(cfd_instrument()),
)
.with_shortability(Arc::new(table));
advance(&mut venue, time(1));
assert!(cfd_market(&mut venue, "4990", "5000", "5000").is_empty());
venue
}
fn lending(shortability: Shortability) -> ShortabilityTable {
ShortabilityTable::new().with_shortability(cfd_instrument_name(), time(0), shortability)
}
fn borrow_rejection(outcome: &OpenOutcome) -> &BorrowReject {
assert!(
outcome.events.is_empty(),
"a refused short moves nothing, so it owes no events"
);
match &outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::BorrowRejected(reject),
))) => reject,
other => panic!("expected BorrowRejected, got: {other:?}"),
}
}
fn open_cfd(venue: &mut SimulatedVenue, side: Side, quantity: &str) -> OpenOutcome {
venue.open_order(cfd_request(side, quantity, None))
}
#[test]
fn a_short_of_an_instrument_that_is_not_shortable_is_refused_before_the_ledger_moves() {
let mut venue = make_cfd_venue_lending(lending(Shortability::new().with_shortable(false)));
let outcome = open_cfd(&mut venue, Side::Sell, "1");
let reject = borrow_rejection(&outcome);
assert_eq!(reject.reason, BorrowRejectReason::NotShortable);
assert_eq!(reject.venue_code, None);
assert_eq!(reject.available, None);
assert!(reject.message.contains("spx500_usd"), "{}", reject.message);
assert_eq!(free_usd(&venue), d("10000000"));
assert_eq!(cfd_position(&venue), None);
assert!(venue.orders_open(&[]).is_empty());
}
#[test]
fn a_short_at_or_after_the_delisting_time_is_refused_and_one_before_it_is_not() {
let delisted = Shortability::new()
.with_shortable(true)
.with_delisting_at(time(1));
let mut venue = make_cfd_venue_lending(lending(delisted));
let outcome = open_cfd(&mut venue, Side::Sell, "1");
let reject = borrow_rejection(&outcome);
assert_eq!(reject.reason, BorrowRejectReason::NotShortable);
assert!(reject.message.contains("delisted"), "{}", reject.message);
assert_eq!(cfd_position(&venue), None);
let ahead = Shortability::new()
.with_shortable(true)
.with_delisting_at(time(2));
let mut venue = make_cfd_venue_lending(lending(ahead));
cfd_market_fill(&mut venue, Side::Sell, "1");
assert!(cfd_position(&venue).is_some(), "not delisted yet");
}
#[test]
fn a_buy_and_a_sell_that_only_reduces_a_long_are_never_refused() {
let mut venue = make_cfd_venue_lending(lending(Shortability::new().with_shortable(false)));
cfd_market_fill(&mut venue, Side::Buy, "2");
cfd_market_fill(&mut venue, Side::Sell, "1");
cfd_market_fill(&mut venue, Side::Sell, "1");
assert_eq!(cfd_position(&venue), None);
let outcome = open_cfd(&mut venue, Side::Sell, "1");
assert_eq!(
borrow_rejection(&outcome).reason,
BorrowRejectReason::NotShortable,
"once flat, the next sell opens a short"
);
}
#[test]
fn a_sell_reducing_a_long_that_resting_sells_already_close_opens_a_short() {
let mut venue = make_cfd_venue_lending(lending(Shortability::new().with_shortable(false)));
cfd_market_fill(&mut venue, Side::Buy, "2");
let rested = venue.open_order(cfd_limit("close", Side::Sell, "2", "5200"));
assert!(rested_open(&rested).filled_quantity.is_zero());
let outcome = open_cfd(&mut venue, Side::Sell, "1");
assert_eq!(
borrow_rejection(&outcome).reason,
BorrowRejectReason::NotShortable
);
}
#[test]
fn a_short_past_what_is_available_net_of_the_own_short_and_resting_sells_is_refused() {
let mut venue = make_cfd_venue_lending(lending(
Shortability::new()
.with_shortable(true)
.with_available(d("5")),
));
cfd_market_fill(&mut venue, Side::Sell, "2");
let rested = venue.open_order(cfd_limit("rest", Side::Sell, "2", "5200"));
assert!(rested_open(&rested).filled_quantity.is_zero());
let free = free_usd(&venue);
let outcome = open_cfd(&mut venue, Side::Sell, "2");
let reject = borrow_rejection(&outcome);
assert_eq!(reject.reason, BorrowRejectReason::InventoryUnavailable);
assert_eq!(reject.available, Some(d("1")));
assert_eq!(free_usd(&venue), free);
assert_eq!(
cfd_position(&venue).map(|(quantity, _)| quantity),
Some(d("-2"))
);
cfd_market_fill(&mut venue, Side::Sell, "1");
assert_eq!(
cfd_position(&venue).map(|(quantity, _)| quantity),
Some(d("-3"))
);
}
#[test]
fn what_is_available_is_never_reported_below_zero() {
let mut venue = make_cfd_venue_lending(
ShortabilityTable::new()
.with_shortability(cfd_instrument_name(), time(0), Shortability::new())
.with_shortability(
cfd_instrument_name(),
time(2),
Shortability::new().with_available(d("1")),
),
);
cfd_market_fill(&mut venue, Side::Sell, "3");
advance(&mut venue, time(2));
let outcome = open_cfd(&mut venue, Side::Sell, "1");
let reject = borrow_rejection(&outcome);
assert_eq!(reject.reason, BorrowRejectReason::InventoryUnavailable);
assert_eq!(reject.available, Some(Decimal::ZERO));
}
#[test]
fn a_flip_is_gated_on_the_short_it_opens_only() {
let mut venue = make_cfd_venue_lending(lending(Shortability::new().with_available(d("3"))));
cfd_market_fill(&mut venue, Side::Buy, "5");
let outcome = open_cfd(&mut venue, Side::Sell, "9");
let reject = borrow_rejection(&outcome);
assert_eq!(reject.reason, BorrowRejectReason::InventoryUnavailable);
assert_eq!(reject.available, Some(d("3")));
assert_eq!(
cfd_position(&venue).map(|(quantity, _)| quantity),
Some(d("5"))
);
cfd_market_fill(&mut venue, Side::Sell, "8");
assert_eq!(
cfd_position(&venue).map(|(quantity, _)| quantity),
Some(d("-3"))
);
}
#[test]
fn a_short_sale_restriction_refuses_a_marketable_short_and_lets_one_above_the_bid_rest() {
let mut venue =
make_cfd_venue_lending(ShortabilityTable::new().with_short_sale_restriction(
cfd_instrument_name(),
time(0),
time(10),
));
cfd_market_fill(&mut venue, Side::Buy, "1");
cfd_market_fill(&mut venue, Side::Sell, "1");
let market = open_cfd(&mut venue, Side::Sell, "1");
assert_eq!(
borrow_rejection(&market).reason,
BorrowRejectReason::ShortSaleRestricted
);
for (cid, limit) in [("at-bid", "4990"), ("below-bid", "4980")] {
let outcome = venue.open_order(cfd_limit(cid, Side::Sell, "1", limit));
assert_eq!(
borrow_rejection(&outcome).reason,
BorrowRejectReason::ShortSaleRestricted,
"a sell limit at {limit} crosses the 4990 bid"
);
}
let above = venue.open_order(cfd_limit("above-bid", Side::Sell, "1", "4995"));
assert!(rested_open(&above).filled_quantity.is_zero());
assert_eq!(cfd_position(&venue), None);
advance(&mut venue, time(10));
cfd_market_fill(&mut venue, Side::Sell, "1");
assert_eq!(
cfd_position(&venue).map(|(quantity, _)| quantity),
Some(d("-1"))
);
}
#[test]
fn a_restricted_market_short_the_fill_model_can_price_is_refused_for_the_restriction() {
let mut venue =
make_cfd_venue_lending(ShortabilityTable::new().with_short_sale_restriction(
cfd_instrument_name(),
time(0),
time(10),
));
venue.fill_model = Arc::new(SimFillConfig::Midpoint(crate::fill::MidpointFillModel));
let outcome = open_cfd(&mut venue, Side::Sell, "1");
assert_eq!(
borrow_rejection(&outcome).reason,
BorrowRejectReason::ShortSaleRestricted
);
}
#[test]
fn missing_shortability_data_allows_the_short() {
let mut venue = make_cfd_venue_lending(
ShortabilityTable::new()
.with_shortability(
cfd_instrument_name(),
time(100),
Shortability::new().with_shortable(false),
)
.with_shortability(
InstrumentNameExchange::new("other"),
time(0),
Shortability::new().with_shortable(false),
),
);
cfd_market_fill(&mut venue, Side::Sell, "1");
assert_eq!(
cfd_position(&venue).map(|(quantity, _)| quantity),
Some(d("-1"))
);
}
#[test]
fn a_spot_short_is_refused_for_its_balance_not_by_the_provider() {
let table = ShortabilityTable::new().with_shortability(
instrument_name(),
time(0),
Shortability::new().with_shortable(false),
);
let mut venue = make_venue("0", "10000").with_shortability(Arc::new(table));
let outcome = venue.open_order(sell_request("1", market_prices("50000")));
match outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::BalanceInsufficient(..),
))) => {}
other => panic!("expected BalanceInsufficient, got: {other:?}"),
}
}
#[test]
fn a_restricted_short_with_no_price_is_refused_as_unpriceable() {
let reason_of = |fill_model: SimFillConfig, market| {
let mut venue = make_cfd_venue("10000000", FeeModelConfig::default())
.with_shortability(Arc::new(
ShortabilityTable::new().with_short_sale_restriction(
cfd_instrument_name(),
time(0),
time(10),
),
));
venue.fill_model = Arc::new(fill_model);
advance(&mut venue, time(1));
match venue
.open_order(cfd_request(Side::Sell, "1", market))
.response
.state
{
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::OrderRejected(reason),
))) => reason,
other => panic!("expected the unpriceable OrderRejected, got: {other:?}"),
}
};
let absent = reason_of(SimFillConfig::default(), None);
assert!(absent.contains("no market snapshot"), "{absent}");
let empty = reason_of(SimFillConfig::default(), Some(MarketSnapshot::default()));
assert!(empty.contains("no market price available yet"), "{empty}");
let bid_only = MarketSnapshot {
best_bid: Some(d("4990")),
..MarketSnapshot::default()
};
let midpoint = reason_of(
SimFillConfig::Midpoint(crate::fill::MidpointFillModel),
Some(bid_only),
);
assert!(
midpoint.contains("no market price available yet"),
"{midpoint}"
);
}
#[test]
fn the_first_refusal_that_applies_is_the_one_reported() {
let restricted = |shortability| {
lending(shortability).with_short_sale_restriction(
cfd_instrument_name(),
time(0),
time(10),
)
};
let mut venue = make_cfd_venue_lending(restricted(
Shortability::new()
.with_shortable(false)
.with_available(Decimal::ZERO),
));
let outcome = open_cfd(&mut venue, Side::Sell, "1");
assert_eq!(
borrow_rejection(&outcome).reason,
BorrowRejectReason::NotShortable
);
let mut venue = make_cfd_venue_lending(restricted(
Shortability::new().with_available(Decimal::ZERO),
));
let outcome = open_cfd(&mut venue, Side::Sell, "1");
assert_eq!(
borrow_rejection(&outcome).reason,
BorrowRejectReason::InventoryUnavailable
);
}
#[test]
fn a_short_sale_restriction_refuses_a_marketable_flip_but_not_a_close() {
let mut venue =
make_cfd_venue_lending(ShortabilityTable::new().with_short_sale_restriction(
cfd_instrument_name(),
time(0),
time(10),
));
cfd_market_fill(&mut venue, Side::Buy, "2");
let outcome = open_cfd(&mut venue, Side::Sell, "3");
assert_eq!(
borrow_rejection(&outcome).reason,
BorrowRejectReason::ShortSaleRestricted
);
assert_eq!(
cfd_position(&venue).map(|(quantity, _)| quantity),
Some(d("2"))
);
cfd_market_fill(&mut venue, Side::Sell, "2");
assert_eq!(cfd_position(&venue), None);
}
fn next_bar_config(usdt: &str, price: NextBarPrice) -> MockExecutionConfig {
MockExecutionConfig {
market_order_timing: MarketOrderTiming::NextBar(price),
..spot_config("10", usdt, FeeModelConfig::default())
}
}
fn ohlc(open: &str, high: &str, low: &str, close: &str) -> MarketBar {
MarketBar::new(d(open), d(high), d(low), d(close))
}
fn feed_bar(
venue: &mut SimulatedVenue,
seconds: i64,
bar: MarketBar,
) -> Vec<UnindexedAccountEvent> {
feed_bar_sized(venue, seconds, bar, MarketDepth::UNKNOWN)
}
fn feed_bar_sized(
venue: &mut SimulatedVenue,
seconds: i64,
bar: MarketBar,
depth: MarketDepth,
) -> Vec<UnindexedAccountEvent> {
let snapshot = MarketSnapshot::from_last_price(Some(bar.close));
venue.apply_market(
&instrument_name(),
MarketObservation::new(snapshot)
.with_depth(depth)
.with_bar(Some(bar)),
time(seconds),
)
}
fn venue_holding(price: NextBarPrice, usdt: &str) -> SimulatedVenue {
let mut venue =
SimulatedVenue::new_market_driven(&next_bar_config(usdt, price), spot_instruments());
advance(&mut venue, time(1));
assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
venue
}
fn market_order(
cid: &str,
side: Side,
quantity: &str,
time_in_force: TimeInForce,
) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
let mut request = request(instrument_name(), side, quantity, None);
request.key.cid = ClientOrderId::new(cid);
request.state.time_in_force = time_in_force;
request
}
fn ioc_buy(cid: &str, quantity: &str) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
market_order(cid, Side::Buy, quantity, TimeInForce::ImmediateOrCancel)
}
fn total_base(venue: &SimulatedVenue) -> Decimal {
venue.balances(&[base()])[0].balance.total
}
fn rejection_reason(outcome: &OpenOutcome) -> &str {
match &outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::OrderRejected(reason),
))) => reason,
other => panic!("expected an OrderRejected, got: {other:?}"),
}
}
#[test]
fn a_market_order_held_for_the_next_bar_is_open_and_holds_its_cost_at_the_current_price() {
let mut venue = venue_holding(NextBarPrice::Open, "10000");
let outcome = venue.open_order(ioc_buy("held", "1"));
assert_eq!(
rested_open(&outcome).filled_quantity,
Decimal::ZERO,
"accepted as open, with nothing traded yet"
);
assert_eq!(
outcome.events.len(),
1,
"it owes the one balance its hold restates"
);
assert_eq!(
balance_of(&outcome.events[0]).balance.free,
d("9900"),
"held at the close the order arrived on"
);
assert_eq!(total_quote(&venue), d("10000"), "and nothing paid yet");
assert!(venue.trades(time(0)).is_empty());
assert_eq!(venue.orders_open(&[]).len(), 1);
assert!(venue.account.orders().holds(&instrument_name()));
}
#[test]
fn the_next_bar_fills_a_held_order_at_its_open_and_settles_that_price() {
let mut venue = venue_holding(NextBarPrice::Open, "10000");
let _ = venue.open_order(ioc_buy("held", "1"));
let events = feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108"));
assert_eq!(
events.len(),
4,
"the balance paid, the one received, the trade, then the order: {events:?}"
);
let paid = balance_of(&events[0]);
assert_eq!(paid.asset, quote());
assert_eq!(
(paid.balance.total, paid.balance.free),
(d("9898"), d("9898")),
"the hold is released and the fill settled at the open, not the close it was held at"
);
assert_eq!(balance_of(&events[1]).asset, base());
let trade = trade_of(&events[2]);
assert_eq!(
(trade.price, trade.quantity, trade.time_exchange),
(d("102"), d("1"), time(2)),
"filled at the bar's open, stamped at the bar"
);
assert!(
matches!(
order_of(&events[3]).state,
OrderState::Inactive(InactiveOrderState::FullyFilled(_))
),
"{:?}",
order_of(&events[3]).state
);
assert!(venue.orders_open(&[]).is_empty());
assert!(!venue.account.orders().holds(&instrument_name()));
assert_eq!(total_base(&venue), d("11"));
assert_eq!(free_quote(&venue), d("9898"));
}
#[test]
fn adverse_extreme_fills_a_held_buy_at_the_bar_high_and_a_sell_at_its_low() {
for (side, expected) in [(Side::Buy, "110"), (Side::Sell, "98")] {
let mut venue = venue_holding(NextBarPrice::AdverseExtreme, "10000");
let _ = venue.open_order(market_order(
"held",
side,
"1",
TimeInForce::ImmediateOrCancel,
));
let events = feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108"));
assert_eq!(trade_of(&events[2]).price, d(expected), "a {side:?}");
}
}
#[test]
fn a_tick_between_bars_does_not_release_a_held_order() {
let mut venue = venue_holding(NextBarPrice::Open, "10000");
let _ = venue.open_order(ioc_buy("held", "1"));
let ticked = venue.apply_market(
&instrument_name(),
MarketObservation::new(traded("101")),
time(2),
);
assert!(ticked.is_empty(), "a tick is not a bar: {ticked:?}");
assert!(venue.account.orders().holds(&instrument_name()));
let events = feed_bar(&mut venue, 3, ohlc("103", "104", "102", "103"));
assert_eq!(
trade_of(&events[2]).price,
d("103"),
"the next bar fills it"
);
}
#[test]
fn a_held_order_fills_at_the_bar_price_moved_by_the_configured_slippage() {
let config = MockExecutionConfig {
slippage: SlippageConfig::fixed(d("0.01")).unwrap(),
..next_bar_config("10000", NextBarPrice::Open)
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
let _ = venue.open_order(ioc_buy("held", "1"));
let events = feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108"));
assert_eq!(
trade_of(&events[2]).price,
d("103.02"),
"the open, 1% against the buy"
);
}
#[test]
fn a_market_order_with_no_bar_to_wait_for_is_rejected_naming_the_setting() {
let mut ticked = SimulatedVenue::new_market_driven(
&next_bar_config("10000", NextBarPrice::Open),
spot_instruments(),
);
advance(&mut ticked, time(1));
assert!(
ticked
.apply_market(
&instrument_name(),
MarketObservation::new(traded("100")),
time(1)
)
.is_empty()
);
let mut request_priced = SimulatedVenue::new(
&next_bar_config("10000", NextBarPrice::Open),
spot_instruments(),
);
for (venue, request) in [
(&mut ticked, ioc_buy("held", "1")),
(&mut request_priced, buy_request("1", market_prices("100"))),
] {
let outcome = venue.open_order(request);
assert!(outcome.events.is_empty());
assert!(
rejection_reason(&outcome).contains("MarketOrderTiming::NextBar"),
"{}",
rejection_reason(&outcome)
);
assert_eq!(
free_quote(venue),
d("10000"),
"a refused order holds nothing"
);
}
}
#[test]
fn a_held_order_the_account_cannot_cover_is_refused_on_arrival() {
let mut venue = venue_holding(NextBarPrice::Open, "99");
let outcome = venue.open_order(ioc_buy("held", "1"));
assert!(
matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::BalanceInsufficient(..)
)))
),
"{:?}",
outcome.response.state
);
assert!(!venue.account.orders().holds(&instrument_name()));
}
#[test]
fn a_held_order_is_cancelled_like_a_resting_one_and_releases_its_hold() {
let mut venue = venue_holding(NextBarPrice::Open, "10000");
let request = ioc_buy("held", "1");
let key = request.key.clone();
let _ = venue.open_order(request);
let cancelled = venue.cancel_order(OrderEvent {
key,
state: RequestCancel { id: None },
});
assert!(cancelled.response.state.is_ok());
assert_eq!(free_quote(&venue), d("10000"));
assert!(!venue.account.orders().holds(&instrument_name()));
assert!(
feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108")).is_empty(),
"the next bar has nothing left to fill"
);
}
#[test]
fn a_held_market_order_carrying_a_price_is_held_without_it() {
let mut venue = venue_holding(NextBarPrice::Open, "10000");
let request = priced_market_order(Side::Buy, "200", None);
let key = request.key.clone();
let outcome = venue.open_order(request);
assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
assert_eq!(outcome.response.price, None, "reported as it is held");
assert_eq!(venue.orders_open(&[])[0].price, None);
let ticked = venue.apply_market(
&instrument_name(),
MarketObservation::new(traded("150")),
time(2),
);
assert!(
ticked.is_empty(),
"a tick below the stray price matches nothing: {ticked:?}"
);
let cancelled = venue.cancel_order(OrderEvent {
key,
state: RequestCancel { id: None },
});
assert!(cancelled.response.state.is_ok());
assert!(!venue.account.orders().holds(&instrument_name()));
}
#[test]
fn a_held_order_whose_deadline_the_bar_reaches_expires_unfilled() {
let mut venue = venue_holding(NextBarPrice::Open, "10000");
let _ = venue.open_order(market_order(
"gtd",
Side::Buy,
"1",
TimeInForce::GoodTillDate { expiry: time(2) },
));
let events = feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108"));
assert_eq!(
events.len(),
2,
"the released hold, then the order: {events:?}"
);
assert!(
matches!(
order_of(&events[1]).state,
OrderState::Inactive(InactiveOrderState::Expired(_))
),
"{:?}",
order_of(&events[1]).state
);
assert!(venue.trades(time(0)).is_empty());
assert_eq!(free_quote(&venue), d("10000"));
assert!(!venue.account.orders().holds(&instrument_name()));
}
#[test]
fn a_held_spot_sell_holds_the_base_it_would_deliver() {
let mut venue = venue_holding(NextBarPrice::Open, "10000");
let free_base = |venue: &SimulatedVenue| venue.balances(&[base()])[0].balance.free;
let _ = venue.open_order(market_order(
"sell",
Side::Sell,
"1",
TimeInForce::ImmediateOrCancel,
));
assert_eq!(free_base(&venue), d("9"));
assert_eq!(total_base(&venue), d("10"), "and nothing delivered yet");
let events = feed_bar(&mut venue, 2, ohlc("102", "110", "98", "108"));
assert_eq!(trade_of(&events[2]).price, d("102"));
assert_eq!(total_base(&venue), d("9"));
assert_eq!(free_base(&venue), d("9"));
}
#[test]
fn a_held_order_the_account_can_no_longer_afford_at_the_bar_is_cancelled() {
let mut venue = venue_holding(NextBarPrice::Open, "105");
let _ = venue.open_order(ioc_buy("held", "1"));
assert_eq!(free_quote(&venue), d("5"));
let events = feed_bar(&mut venue, 2, ohlc("110", "112", "108", "111"));
assert_eq!(
events.len(),
2,
"the released hold, then the order: {events:?}"
);
assert_eq!(balance_of(&events[0]).balance.free, d("105"));
assert!(
matches!(
&order_of(&events[1]).state,
OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
if cancelled.filled_quantity == Some(Decimal::ZERO)
),
"{:?}",
order_of(&events[1]).state
);
assert!(venue.trades(time(0)).is_empty());
assert_eq!(free_quote(&venue), d("105"));
}
#[test]
fn a_bar_with_less_size_than_a_held_order_fills_immediate_or_cancel_in_part_and_kills_fill_or_kill()
{
let bar = ohlc("102", "110", "98", "108");
let mut immediate = venue_holding(NextBarPrice::Open, "10000");
let _ = immediate.open_order(ioc_buy("ioc", "1"));
let events = feed_bar_sized(&mut immediate, 2, bar, sized("0.4"));
assert_eq!(trade_of(&events[2]).quantity, d("0.4"));
assert!(
matches!(
&order_of(&events[3]).state,
OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
if cancelled.filled_quantity == Some(d("0.4"))
),
"immediate-or-cancel keeps what the bar gave it: {:?}",
order_of(&events[3]).state
);
assert_eq!(free_quote(&immediate), d("10000") - d("40.8"));
let mut all_or_nothing = venue_holding(NextBarPrice::Open, "10000");
let _ =
all_or_nothing.open_order(market_order("fok", Side::Buy, "1", TimeInForce::FillOrKill));
let events = feed_bar_sized(&mut all_or_nothing, 2, bar, sized("0.4"));
assert_eq!(
events.len(),
2,
"the released hold, then the order: {events:?}"
);
assert!(all_or_nothing.trades(time(0)).is_empty());
assert_eq!(free_quote(&all_or_nothing), d("10000"));
}
#[test]
fn a_bar_with_size_for_the_whole_held_order_fills_fill_or_kill() {
let mut venue = venue_holding(NextBarPrice::Open, "10000");
let _ = venue.open_order(market_order("fok", Side::Buy, "1", TimeInForce::FillOrKill));
let events = feed_bar_sized(&mut venue, 2, ohlc("102", "110", "98", "108"), sized("1"));
assert_eq!(trade_of(&events[2]).quantity, d("1"));
assert!(
matches!(
order_of(&events[3]).state,
OrderState::Inactive(InactiveOrderState::FullyFilled(_))
),
"{:?}",
order_of(&events[3]).state
);
}
#[test]
fn orders_held_on_one_instrument_fill_in_arrival_order_against_one_size() {
let mut venue = venue_holding(NextBarPrice::Open, "10000");
let _ = venue.open_order(ioc_buy("first", "1"));
let _ = venue.open_order(ioc_buy("second", "1"));
let events = feed_bar_sized(&mut venue, 2, ohlc("102", "110", "98", "108"), sized("1.5"));
let fills = events
.iter()
.filter_map(|event| match &event.kind {
AccountEventKind::Trade(trade) => Some((
trade.client_order_id.clone().unwrap().0.to_string(),
trade.quantity,
)),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(
fills,
[
("first".to_string(), d("1")),
("second".to_string(), d("0.5"))
],
"earliest first, and the second gets what the first left"
);
}
#[test]
fn expiring_the_held_orders_retires_each_as_expired_and_releases_its_hold() {
let mut venue = venue_holding(NextBarPrice::Open, "10000");
let _ = venue.open_order(ioc_buy("held", "1"));
let events = venue.expire_held(time(5));
assert_eq!(
events.len(),
2,
"the released hold, then the order: {events:?}"
);
assert_eq!(balance_of(&events[0]).balance.free, d("10000"));
let order = order_of(&events[1]);
assert!(
matches!(
&order.state,
OrderState::Inactive(InactiveOrderState::Expired(expired))
if expired.filled_quantity == Some(Decimal::ZERO)
&& expired.time_exchange == time(5)
),
"{:?}",
order.state
);
assert!(venue.orders_open(&[]).is_empty());
assert!(
venue.expire_held(time(6)).is_empty(),
"nothing is held twice"
);
assert!(feed_bar(&mut venue, 7, ohlc("102", "110", "98", "108")).is_empty());
}
#[test]
fn a_limit_order_is_not_held_for_the_next_bar() {
let mut venue = venue_holding(NextBarPrice::Open, "10000");
let outcome = venue.open_order(limit_request("limit", Side::Buy, "1", "101", gtc()));
assert_eq!(
filled_trade(&outcome).price,
d("100"),
"a marketable limit still fills on arrival"
);
assert!(!venue.account.orders().holds(&instrument_name()));
}
fn stale_config(max_age: Duration) -> MockExecutionConfig {
MockExecutionConfig {
stale_market: StaleMarketPolicy::Wait { max_age },
..spot_config("10", "10000", FeeModelConfig::default())
}
}
const MAX_AGE: Duration = Duration::from_secs(10);
fn venue_waiting() -> SimulatedVenue {
let mut venue =
SimulatedVenue::new_market_driven(&stale_config(MAX_AGE), spot_instruments());
advance(&mut venue, time(1));
assert!(observe(&mut venue, 1, traded("100")).is_empty());
venue
}
fn observe(
venue: &mut SimulatedVenue,
seconds: i64,
snapshot: MarketSnapshot,
) -> Vec<UnindexedAccountEvent> {
venue.apply_market(
&instrument_name(),
MarketObservation::new(snapshot),
time(seconds),
)
}
fn observe_idle(
venue: &mut SimulatedVenue,
seconds: i64,
snapshot: MarketSnapshot,
) -> Vec<UnindexedAccountEvent> {
venue.apply_market(
&instrument_name(),
MarketObservation::new(snapshot).with_fresh(false),
time(seconds),
)
}
fn gtc_buy(cid: &str, quantity: &str) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
market_order(cid, Side::Buy, quantity, gtc())
}
fn trades_in(
events: &[UnindexedAccountEvent],
) -> Vec<&Trade<AssetNameExchange, InstrumentNameExchange>> {
events
.iter()
.filter_map(|event| match &event.kind {
AccountEventKind::Trade(trade) => Some(trade),
_ => None,
})
.collect()
}
#[test]
fn a_market_observed_fresh_within_the_max_age_fills_on_arrival() {
let mut venue = venue_waiting();
advance(&mut venue, time(11));
let outcome = venue.open_order(gtc_buy("fresh", "1"));
assert_eq!(filled_trade(&outcome).price, d("100"));
assert!(!venue.account.orders().holds(&instrument_name()));
}
#[test]
fn a_market_order_on_a_stale_market_waits_open_holding_its_cost() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let outcome = venue.open_order(gtc_buy("waiting", "1"));
assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
assert_eq!(
outcome.events.len(),
1,
"it owes the one balance its hold restates"
);
assert_eq!(
balance_of(&outcome.events[0]).balance.free,
d("9900"),
"held at the stale price it arrived on"
);
assert!(venue.trades(time(0)).is_empty());
assert!(venue.account.orders().holds(&instrument_name()));
}
#[test]
fn the_next_fresh_observation_fills_a_waiting_order_at_its_price() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_buy("waiting", "1"));
let events = observe(&mut venue, 20, traded("105"));
assert_eq!(
events.len(),
4,
"the balance paid, the one received, the trade, then the order: {events:?}"
);
assert_eq!(
balance_of(&events[0]).balance.free,
d("9895"),
"the hold is released and the fill settled at the fresh price"
);
let trade = trade_of(&events[2]);
assert_eq!(
(trade.price, trade.quantity, trade.time_exchange),
(d("105"), d("1"), time(20)),
"filled at the observation's price, stamped at it"
);
assert!(matches!(
order_of(&events[3]).state,
OrderState::Inactive(InactiveOrderState::FullyFilled(_))
));
assert!(!venue.account.orders().holds(&instrument_name()));
assert_eq!(total_base(&venue), d("11"));
}
#[test]
fn a_waiting_order_fills_after_the_resting_orders_its_observation_crosses() {
let mut venue = venue_waiting();
let _ = venue.open_order(limit_request("resting", Side::Buy, "1", "99", gtc()));
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_buy("waiting", "1"));
let events = observe(&mut venue, 20, traded("98"));
let prices = trades_in(&events)
.iter()
.map(|trade| (trade.client_order_id.clone(), trade.price))
.collect::<Vec<_>>();
assert_eq!(
prices,
vec![
(Some(ClientOrderId::new("resting")), d("99")),
(Some(ClientOrderId::new("waiting")), d("98")),
],
"the resting order was crossed by the observation, the waiting one arrived after it"
);
}
#[test]
fn an_observation_that_is_not_fresh_neither_refreshes_the_market_nor_releases_an_order() {
let mut venue = venue_waiting();
assert!(observe_idle(&mut venue, 5, traded("101")).is_empty());
advance(&mut venue, time(12));
let outcome = venue.open_order(gtc_buy("waiting", "1"));
assert_eq!(
balance_of(&outcome.events[0]).balance.free,
d("9899"),
"it waits, holding the price the idle observation carried"
);
assert!(observe_idle(&mut venue, 13, traded("102")).is_empty());
assert!(venue.account.orders().holds(&instrument_name()));
let events = observe(&mut venue, 14, traded("103"));
assert_eq!(trade_of(&events[2]).price, d("103"));
}
#[test]
fn a_market_never_observed_fresh_is_stale() {
let mut venue =
SimulatedVenue::new_market_driven(&stale_config(MAX_AGE), spot_instruments());
advance(&mut venue, time(1));
assert!(observe_idle(&mut venue, 1, traded("100")).is_empty());
let outcome = venue.open_order(ioc_buy("immediate", "1"));
let reason = rejection_reason(&outcome);
assert!(reason.contains("never observed fresh"), "{reason}");
let outcome = venue.open_order(gtc_buy("waiting", "1"));
assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
}
#[test]
fn an_immediate_or_cancel_or_fill_or_kill_order_on_a_stale_market_is_rejected_naming_the_policy()
{
for time_in_force in [TimeInForce::ImmediateOrCancel, TimeInForce::FillOrKill] {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let outcome = venue.open_order(market_order("now", Side::Buy, "1", time_in_force));
assert!(outcome.events.is_empty());
let reason = rejection_reason(&outcome);
assert!(
reason.contains("StaleMarketPolicy::Wait")
&& reason.contains(&time(1).to_string())
&& reason.contains(&time_in_force.to_string()),
"{reason}"
);
assert_eq!(
free_quote(&venue),
d("10000"),
"a refused order holds nothing"
);
assert!(!venue.account.orders().holds(&instrument_name()));
}
}
#[test]
fn a_market_order_on_an_unpriced_instrument_is_rejected_as_unpriceable_not_as_stale() {
let mut venue =
SimulatedVenue::new_market_driven(&stale_config(MAX_AGE), spot_instruments());
advance(&mut venue, time(1));
assert!(observe(&mut venue, 1, MarketSnapshot::default()).is_empty());
for request in [gtc_buy("waiting", "1"), ioc_buy("immediate", "1")] {
let outcome = venue.open_order(request);
let reason = rejection_reason(&outcome);
assert!(
reason.contains("cannot price") && !reason.contains("StaleMarketPolicy"),
"{reason}"
);
}
}
#[test]
fn a_venue_with_no_market_feed_rejects_market_orders_naming_the_policy() {
let mut venue = SimulatedVenue::new(&stale_config(MAX_AGE), spot_instruments());
let outcome = venue.open_order(buy_request("1", market_prices("100")));
assert!(
rejection_reason(&outcome).contains("StaleMarketPolicy::Wait needs a market feed"),
"{}",
rejection_reason(&outcome)
);
assert_eq!(free_quote(&venue), d("10000"));
}
#[test]
fn a_max_age_longer_than_any_run_never_makes_a_market_stale() {
let mut venue =
SimulatedVenue::new_market_driven(&stale_config(Duration::MAX), spot_instruments());
advance(&mut venue, time(1));
assert!(observe(&mut venue, 1, traded("100")).is_empty());
advance(&mut venue, time(1_000_000));
let outcome = venue.open_order(ioc_buy("immediate", "1"));
assert_eq!(filled_trade(&outcome).price, d("100"));
}
#[test]
fn a_waiting_order_the_fresh_market_cannot_price_is_cancelled_and_releases_its_hold() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_buy("waiting", "1"));
let events = observe(
&mut venue,
20,
MarketSnapshot::new(Some(d("99")), None, None),
);
assert_eq!(
events.len(),
2,
"the released hold, then the order: {events:?}"
);
assert_eq!(balance_of(&events[0]).balance.free, d("10000"));
assert!(matches!(
&order_of(&events[1]).state,
OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
if cancelled.filled_quantity == Some(Decimal::ZERO)
));
assert!(!venue.account.orders().holds(&instrument_name()));
}
#[test]
fn only_a_fresh_observation_releases_a_waiting_order_never_the_clock() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_buy("waiting", "1"));
advance(&mut venue, time(1_000));
assert!(venue.account.orders().holds(&instrument_name()));
assert!(venue.trades(time(0)).is_empty());
}
#[test]
fn a_waiting_order_still_waiting_at_the_end_of_the_data_expires() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_buy("waiting", "1"));
let events = venue.expire_held(time(30));
assert_eq!(events.len(), 2, "{events:?}");
assert_eq!(balance_of(&events[0]).balance.free, d("10000"));
assert!(matches!(
order_of(&events[1]).state,
OrderState::Inactive(InactiveOrderState::Expired(_))
));
}
#[test]
fn only_a_bar_released_under_next_bar_fills_ahead_of_its_observation() {
let bar = ohlc("102", "110", "98", "108");
let fresh_bar = MarketObservation::new(traded("108")).with_bar(Some(bar));
let idle_bar = fresh_bar.with_fresh(false);
let tick = MarketObservation::new(traded("108"));
let mut waiting = venue_waiting();
advance(&mut waiting, time(12));
let _ = waiting.open_order(gtc_buy("waiting", "1"));
assert!(waiting.account.orders().holds(&instrument_name()));
assert!(!waiting.fills_ahead_of(&instrument_name(), &fresh_bar));
let mut held = venue_holding(NextBarPrice::Open, "10000");
assert!(
!held.fills_ahead_of(&instrument_name(), &fresh_bar),
"nothing held yet"
);
let _ = held.open_order(ioc_buy("held", "1"));
assert!(held.fills_ahead_of(&instrument_name(), &fresh_bar));
assert!(held.fills_ahead_of(&instrument_name(), &idle_bar));
assert!(!held.fills_ahead_of(&instrument_name(), &tick));
let mut both = venue_holding_fresh_bars();
let _ = both.open_order(gtc_buy("held", "1"));
assert!(both.fills_ahead_of(&instrument_name(), &fresh_bar));
assert!(!both.fills_ahead_of(&instrument_name(), &idle_bar));
}
fn venue_holding_fresh_bars() -> SimulatedVenue {
let config = MockExecutionConfig {
stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
..next_bar_config("10000", NextBarPrice::Open)
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
venue
}
#[test]
fn under_next_bar_an_idle_bar_cancels_held_immediate_orders_and_leaves_the_rest_held() {
let mut venue = venue_holding_fresh_bars();
let _ = venue.open_order(ioc_buy("immediate", "1"));
let _ = venue.open_order(market_order("all", Side::Buy, "1", TimeInForce::FillOrKill));
let _ = venue.open_order(gtc_buy("patient", "1"));
let bar = ohlc("100", "100", "100", "100");
let snapshot = MarketSnapshot::from_last_price(Some(bar.close));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(snapshot)
.with_bar(Some(bar))
.with_fresh(false),
time(2),
);
assert_eq!(
events.len(),
4,
"a released hold and a cancel for each: {events:?}"
);
for (order, cid) in [(&events[1], "immediate"), (&events[3], "all")] {
let order = order_of(order);
assert_eq!(order.key.cid, ClientOrderId::new(cid));
assert!(matches!(
&order.state,
OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
if cancelled.filled_quantity == Some(Decimal::ZERO)
));
}
assert_eq!(
free_quote(&venue),
d("9900"),
"only the patient order's hold remains"
);
assert_eq!(
venue.account.orders().held_on(&instrument_name()),
[ClientOrderId::new("patient")]
);
let events = feed_bar(&mut venue, 3, ohlc("103", "104", "102", "103"));
assert_eq!(
trade_of(&events[2]).price,
d("103"),
"the next bar something traded in fills it at its open"
);
}
#[test]
fn under_next_bar_an_immediate_order_on_a_stale_market_is_rejected_and_a_patient_one_held() {
let mut venue = venue_holding_fresh_bars();
advance(&mut venue, time(12));
let outcome = venue.open_order(ioc_buy("immediate", "1"));
assert!(
rejection_reason(&outcome).contains("StaleMarketPolicy::Wait"),
"{}",
rejection_reason(&outcome)
);
let outcome = venue.open_order(gtc_buy("patient", "1"));
assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
assert!(venue.account.orders().holds(&instrument_name()));
}
fn gtc_limit(
cid: &str,
side: Side,
quantity: &str,
limit: &str,
) -> OrderRequestOpen<ExchangeId, InstrumentNameExchange> {
limit_request(cid, side, quantity, limit, gtc())
}
fn post_only() -> TimeInForce {
TimeInForce::GoodUntilCancelled { post_only: true }
}
fn resting_on(venue: &SimulatedVenue, side: Side) -> Vec<ClientOrderId> {
venue
.account
.orders()
.resting(&instrument_name(), side)
.map(|order| order.key.cid.clone())
.collect()
}
#[test]
fn a_limit_order_on_a_stale_market_waits_open_holding_what_it_would_cost_arriving_now() {
for (limit, free) in [("105", "9900"), ("95", "9905")] {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let outcome = venue.open_order(gtc_limit("waiting", Side::Buy, "1", limit));
assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
assert_eq!(outcome.events.len(), 1, "{:?}", outcome.events);
assert_eq!(balance_of(&outcome.events[0]).balance.free, d(free));
assert!(venue.trades(time(0)).is_empty());
assert!(venue.account.orders().holds(&instrument_name()));
assert!(
resting_on(&venue, Side::Buy).is_empty(),
"held, so nothing matches it"
);
assert_eq!(
venue
.account
.orders()
.get(&ClientOrderId::new("waiting"))
.unwrap()
.price,
Some(d(limit)),
"held with its limit"
);
}
}
#[test]
fn a_held_limit_order_the_fresh_market_crosses_fills_as_a_taker_at_the_fresh_price() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
let events = observe(&mut venue, 20, traded("102"));
assert_eq!(events.len(), 4, "{events:?}");
assert_eq!(balance_of(&events[0]).balance.free, d("9898"));
let trade = trade_of(&events[2]);
assert_eq!(
(trade.price, trade.quantity, trade.time_exchange),
(d("102"), d("1"), time(20)),
"at the fresh market, better than its limit, stamped at the observation"
);
assert!(matches!(
&order_of(&events[3]).state,
OrderState::Inactive(InactiveOrderState::FullyFilled(filled))
if filled.avg_price == Some(d("102"))
));
assert!(!venue.account.orders().holds(&instrument_name()));
assert!(venue.account.orders().is_empty());
assert_eq!(total_base(&venue), d("11"));
}
#[test]
fn a_held_limit_order_is_capped_at_its_limit_however_far_the_slippage_moves_it() {
let config = MockExecutionConfig {
slippage: SlippageConfig::fixed(d("0.1")).unwrap(),
..stale_config(MAX_AGE)
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(observe(&mut venue, 1, traded("100")).is_empty());
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
let events = observe(&mut venue, 20, traded("102"));
assert_eq!(trade_of(&events[2]).price, d("105"));
}
#[test]
fn a_held_limit_order_the_fresh_market_does_not_cross_joins_the_book_at_its_limit() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
let events = observe(&mut venue, 20, traded("110"));
assert_eq!(
events.len(),
2,
"the hold restated, then the order: {events:?}"
);
assert_eq!(
balance_of(&events[0]).balance.free,
d("9895"),
"now held at its limit, as the maker it rests as"
);
let order = order_of(&events[1]);
assert!(matches!(
&order.state,
OrderState::Active(ActiveOrderState::Open(open))
if open.time_exchange == time(20) && open.filled_quantity.is_zero()
));
assert!(!venue.account.orders().holds(&instrument_name()));
assert_eq!(
resting_on(&venue, Side::Buy),
[ClientOrderId::new("waiting")]
);
let events = observe(&mut venue, 21, traded("104"));
assert_eq!(
trade_of(&events[2]).price,
d("105"),
"resting, it fills at its limit"
);
}
#[test]
fn a_limit_order_on_a_stale_market_is_decided_by_the_fresh_market_not_the_stale_one() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("crossing", Side::Buy, "1", "101"));
let _ = venue.open_order(gtc_limit("passive", Side::Buy, "1", "99"));
let events = observe(&mut venue, 20, traded("95"));
let prices = trades_in(&events)
.iter()
.map(|trade| (trade.client_order_id.clone(), trade.price))
.collect::<Vec<_>>();
assert_eq!(
prices,
vec![
(Some(ClientOrderId::new("crossing")), d("95")),
(Some(ClientOrderId::new("passive")), d("95")),
],
"earliest arrival first"
);
}
#[test]
fn a_held_limit_order_the_fresh_size_cannot_fill_rests_the_rest_at_its_limit() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("101", "102"))
.with_depth(MarketDepth::new(Some(d("1")), Some(d("0.4")))),
time(20),
);
assert_eq!(events.len(), 4, "{events:?}");
assert_eq!(
balance_of(&events[0]).balance.free,
d("9896.20"),
"0.4 paid at 102 and 0.6 held at 105: 10000 - 40.8 - 63"
);
let trade = trade_of(&events[2]);
assert_eq!(
(trade.price, trade.quantity, trade.order_filled_quantity),
(d("102"), d("0.4"), Some(d("0.4")))
);
assert!(matches!(
&order_of(&events[3]).state,
OrderState::Active(ActiveOrderState::Open(open)) if open.filled_quantity == d("0.4")
));
assert_eq!(
resting_on(&venue, Side::Buy),
[ClientOrderId::new("waiting")]
);
let events = observe(&mut venue, 21, traded("104"));
let trade = trade_of(&events[2]);
assert_eq!(
(trade.price, trade.quantity, trade.order_filled_quantity),
(d("105"), d("0.6"), Some(d("1"))),
"the remainder fills at its limit, as the maker"
);
assert_eq!(free_quote(&venue), d("9896.20"));
}
#[test]
fn a_held_post_only_order_the_fresh_market_crosses_is_cancelled_and_one_it_does_not_rests() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let outcome = venue.open_order(limit_request("maker", Side::Buy, "1", "105", post_only()));
assert_eq!(
balance_of(&outcome.events[0]).balance.free,
d("9895"),
"held at its limit, as the maker it promises to be"
);
let events = observe(&mut venue, 20, traded("104"));
assert_eq!(events.len(), 2, "{events:?}");
assert_eq!(balance_of(&events[0]).balance.free, d("10000"));
assert!(matches!(
&order_of(&events[1]).state,
OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
if cancelled.filled_quantity == Some(Decimal::ZERO)
));
advance(&mut venue, time(40));
let _ = venue.open_order(limit_request("passive", Side::Buy, "1", "100", post_only()));
let events = observe(&mut venue, 41, traded("104"));
assert!(matches!(
&order_of(&events[1]).state,
OrderState::Active(ActiveOrderState::Open(_))
));
assert_eq!(
resting_on(&venue, Side::Buy),
[ClientOrderId::new("passive")]
);
}
#[test]
fn an_immediate_or_cancel_or_fill_or_kill_limit_on_a_stale_market_is_rejected_whether_or_not_it_crosses()
{
for (time_in_force, limit) in [
(TimeInForce::ImmediateOrCancel, "105"),
(TimeInForce::ImmediateOrCancel, "95"),
(TimeInForce::FillOrKill, "105"),
(TimeInForce::FillOrKill, "95"),
] {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let outcome =
venue.open_order(limit_request("now", Side::Buy, "1", limit, time_in_force));
assert!(outcome.events.is_empty());
let reason = rejection_reason(&outcome);
assert!(
reason.contains("StaleMarketPolicy::Wait")
&& reason.contains(&time_in_force.to_string()),
"{reason}"
);
assert!(venue.account.orders().is_empty());
}
}
#[test]
fn a_limit_order_past_its_deadline_on_a_stale_market_expires_rather_than_waits() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let outcome = venue.open_order(limit_request(
"late",
Side::Buy,
"1",
"105",
TimeInForce::GoodTillDate { expiry: time(12) },
));
assert!(matches!(
outcome.response.state,
OrderState::Inactive(InactiveOrderState::Expired(_))
));
assert!(venue.account.orders().is_empty());
}
#[test]
fn a_held_limit_order_reaching_its_deadline_expires_and_releases_its_hold() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(limit_request(
"dated",
Side::Buy,
"1",
"105",
TimeInForce::GoodTillDate { expiry: time(15) },
));
let events = observe(&mut venue, 15, traded("102"));
assert!(trades_in(&events).is_empty(), "{events:?}");
assert!(events.iter().any(|event| matches!(
&event.kind,
AccountEventKind::OrderSnapshot(order)
if matches!(order.0.state, OrderState::Inactive(InactiveOrderState::Expired(_)))
)));
assert_eq!(free_quote(&venue), d("10000"));
assert!(venue.account.orders().is_empty());
}
#[test]
fn a_held_limit_order_can_be_cancelled_and_one_still_held_at_the_end_of_the_data_expires() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("cancelled", Side::Buy, "1", "105"));
let _ = venue.open_order(gtc_limit("expired", Side::Buy, "1", "95"));
let cancelled = venue.cancel_order(OrderRequestCancel {
key: gtc_limit("cancelled", Side::Buy, "1", "105").key,
state: RequestCancel { id: None },
});
assert!(cancelled.response.state.is_ok());
assert_eq!(free_quote(&venue), d("9905"));
assert_eq!(
venue.account.orders().held_on(&instrument_name()),
[ClientOrderId::new("expired")]
);
let events = venue.expire_held(time(30));
assert_eq!(events.len(), 2, "{events:?}");
assert_eq!(balance_of(&events[0]).balance.free, d("10000"));
assert!(matches!(
order_of(&events[1]).state,
OrderState::Inactive(InactiveOrderState::Expired(_))
));
assert!(venue.account.orders().is_empty());
}
#[test]
fn a_limit_order_on_an_unpriced_instrument_rests_as_it_does_without_the_policy() {
let mut venue =
SimulatedVenue::new_market_driven(&stale_config(MAX_AGE), spot_instruments());
advance(&mut venue, time(12));
let outcome = venue.open_order(gtc_limit("resting", Side::Buy, "1", "100"));
assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
assert!(!venue.account.orders().holds(&instrument_name()));
assert_eq!(
resting_on(&venue, Side::Buy),
[ClientOrderId::new("resting")]
);
}
#[test]
fn an_order_for_a_quantity_that_is_not_positive_is_refused_rather_than_held() {
for (now, quantity) in [(1, "0"), (1, "-1"), (12, "0"), (12, "-1")] {
for request in [
gtc_limit("limit", Side::Buy, quantity, "105"),
gtc_buy("market", quantity),
] {
let mut venue = venue_waiting();
advance(&mut venue, time(now));
let outcome = venue.open_order(request);
assert_eq!(
rejection_reason(&outcome),
format!(
"cannot open {} for {EXCHANGE}: quantity {quantity} is not positive",
instrument_name()
)
);
assert!(outcome.events.is_empty(), "a refused order moves nothing");
assert!(
venue.account.orders().is_empty(),
"and is neither held nor rested"
);
assert_eq!(free_quote(&venue), d("10000"));
assert!(
observe(&mut venue, 20, traded("102")).is_empty(),
"so a fresh observation has nothing to release"
);
}
}
}
#[test]
fn a_held_limit_order_the_account_can_no_longer_afford_is_cancelled_and_hands_back_its_draw() {
let config = MockExecutionConfig {
slippage: uniform(5),
..MockExecutionConfig {
stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
..spot_config("10", "100", FeeModelConfig::default())
}
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(observe(&mut venue, 1, traded("100")).is_empty());
advance(&mut venue, time(12));
let outcome = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
assert_eq!(balance_of(&outcome.events[0]).balance.free, Decimal::ZERO);
let events = observe(&mut venue, 20, traded("104"));
assert_eq!(
events.len(),
2,
"the released hold, then the order: {events:?}"
);
assert_eq!(balance_of(&events[0]).balance.free, d("100"));
assert!(matches!(
&order_of(&events[1]).state,
OrderState::Inactive(InactiveOrderState::Cancelled(cancelled))
if cancelled.filled_quantity == Some(Decimal::ZERO)
));
assert!(venue.account.orders().is_empty());
assert_eq!(venue.slippage, Slippage::new(uniform(5)));
}
#[test]
fn a_held_sell_limit_order_fills_at_the_fresh_price_or_rests_at_its_limit() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("crossed", Side::Sell, "1", "95"));
let _ = venue.open_order(gtc_limit("passive", Side::Sell, "1", "99"));
let events = observe(&mut venue, 20, traded("97"));
let prices = trades_in(&events)
.iter()
.map(|trade| (trade.client_order_id.clone(), trade.price))
.collect::<Vec<_>>();
assert_eq!(
prices,
vec![(Some(ClientOrderId::new("crossed")), d("97"))],
"above its limit, at the fresh price"
);
assert_eq!(
resting_on(&venue, Side::Sell),
[ClientOrderId::new("passive")]
);
assert_eq!(total_base(&venue), d("9"));
}
#[test]
fn a_held_limit_order_paying_its_fee_in_a_third_asset_takes_the_fee_apart_at_its_release() {
let config = MockExecutionConfig {
stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
..fee_asset_config("10")
};
let mut venue = fee_asset_venue_from(&config, true);
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "51000"));
let events = observe(&mut venue, 20, traded("50000"));
assert_eq!(
restated(&events)
.iter()
.map(|(asset, _)| asset.clone())
.collect::<Vec<_>>(),
[quote(), base(), bnb()],
"paid, received, then the fee: {events:?}"
);
let trade = trades_in(&events)[0];
assert_eq!(trade.price, d("50000"));
assert_eq!(
restated(&events)[2].1.free,
d("10") - d("50") * d("0.75") / d("600")
);
assert_eq!(restated(&events)[0].1.total, d("50000"), "no fee in USDT");
}
fn cfd_venue_with(usd: &str, stale_market: StaleMarketPolicy) -> SimulatedVenue {
let config = MockExecutionConfig {
stale_market,
..config_from_balances(vec![funded(self::usd(), d(usd))], FeeModelConfig::default())
};
let mut venue =
SimulatedVenue::new_market_driven(&config, instruments_of(cfd_instrument()));
advance(&mut venue, time(1));
venue
}
#[test]
fn a_held_cfd_limit_order_settles_against_the_position_at_its_release_as_on_arrival() {
let fresh = MarketObservation::new(MarketSnapshot {
best_bid: Some(d("4950")),
best_ask: Some(d("4960")),
last_price: Some(d("4950")),
});
for depth in [MarketDepth::UNKNOWN, MarketDepth::new(Some(d("1.5")), None)] {
let observation = fresh.with_depth(depth);
let mut held = cfd_venue_with(
"400000",
StaleMarketPolicy::Wait {
max_age: Duration::from_secs(10),
},
);
assert!(cfd_market(&mut held, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut held, Side::Buy, "1");
advance(&mut held, time(12));
let outcome = held.open_order(cfd_limit("flip", Side::Sell, "2", "4900"));
assert!(outcome.response.state.is_accepted());
assert!(held.account.orders().holds(&cfd_instrument_name()));
let released = held.apply_market(&cfd_instrument_name(), observation, time(12));
assert!(!trades_in(&released).is_empty(), "{released:?}");
let mut arriving = cfd_venue_with("400000", StaleMarketPolicy::Off);
assert!(cfd_market(&mut arriving, "5000", "5000", "5000").is_empty());
cfd_market_fill(&mut arriving, Side::Buy, "1");
advance(&mut arriving, time(12));
assert!(
arriving
.apply_market(&cfd_instrument_name(), observation, time(12))
.is_empty()
);
let _ = arriving.open_order(cfd_limit("flip", Side::Sell, "2", "4900"));
assert_eq!(
(free_usd(&held), total_usd(&held), cfd_position(&held)),
(
free_usd(&arriving),
total_usd(&arriving),
cfd_position(&arriving)
),
"depth {depth:?}"
);
assert_eq!(
resting_count(&held),
resting_count(&arriving),
"depth {depth:?}"
);
}
}
fn resting_count(venue: &SimulatedVenue) -> usize {
venue
.account
.orders()
.resting(&cfd_instrument_name(), Side::Sell)
.count()
}
#[test]
fn under_wait_an_observation_that_is_not_fresh_crosses_no_resting_order() {
let mut venue = venue_waiting();
let _ = venue.open_order(gtc_limit("resting", Side::Buy, "1", "99"));
let _ = venue.open_order(gtc_limit("selling", Side::Sell, "1", "101"));
assert!(observe_idle(&mut venue, 5, traded("95")).is_empty());
assert!(observe_idle(&mut venue, 6, traded("105")).is_empty());
assert_eq!(
resting_on(&venue, Side::Buy),
[ClientOrderId::new("resting")]
);
assert_eq!(
resting_on(&venue, Side::Sell),
[ClientOrderId::new("selling")]
);
let events = observe(&mut venue, 7, traded("98"));
assert_eq!(
trade_of(&events[2]).price,
d("99"),
"a fresh observation crosses it"
);
}
#[test]
fn without_the_policy_an_observation_that_is_not_fresh_still_crosses_a_resting_order() {
let mut venue = SimulatedVenue::new_market_driven(
&spot_config("10", "10000", FeeModelConfig::default()),
spot_instruments(),
);
advance(&mut venue, time(1));
assert!(observe(&mut venue, 1, traded("100")).is_empty());
let _ = venue.open_order(gtc_limit("resting", Side::Buy, "1", "99"));
let events = observe_idle(&mut venue, 5, traded("95"));
assert_eq!(trade_of(&events[2]).price, d("99"));
}
#[test]
fn a_held_limit_order_is_not_released_by_an_observation_that_is_not_fresh() {
let mut venue = venue_waiting();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "105"));
assert!(observe_idle(&mut venue, 13, traded("90")).is_empty());
assert!(venue.account.orders().holds(&instrument_name()));
let events = observe(&mut venue, 14, traded("103"));
assert_eq!(trade_of(&events[2]).price, d("103"));
}
#[test]
fn under_next_bar_a_limit_order_on_a_stale_market_is_decided_against_the_next_fresh_bar() {
let mut venue = venue_holding_fresh_bars();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("crossed", Side::Buy, "1", "104"));
let _ = venue.open_order(gtc_limit("passive", Side::Buy, "1", "101"));
let bar = ohlc("102", "106", "101", "105");
assert!(venue.fills_ahead_of(
&instrument_name(),
&MarketObservation::new(traded("105")).with_bar(Some(bar))
));
let events = feed_bar(&mut venue, 13, bar);
let prices = trades_in(&events)
.iter()
.map(|trade| (trade.client_order_id.clone(), trade.price))
.collect::<Vec<_>>();
assert_eq!(
prices,
vec![(Some(ClientOrderId::new("crossed")), d("102"))],
"the open crosses 104 and fills it there; it does not cross 101, which rests"
);
assert_eq!(
resting_on(&venue, Side::Buy),
[ClientOrderId::new("passive")]
);
}
#[test]
fn under_next_bar_a_held_limit_order_its_open_does_not_cross_can_be_crossed_by_its_close() {
let mut venue = venue_holding_fresh_bars();
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1", "101"));
let events = feed_bar(&mut venue, 13, ohlc("102", "103", "99", "100"));
let trades = trades_in(&events);
assert_eq!(trades.len(), 1, "{events:?}");
assert_eq!(
trades[0].price,
d("101"),
"rested at its limit when the open did not reach it, then crossed by the close"
);
assert!(venue.account.orders().is_empty());
}
#[test]
fn under_next_bar_a_limit_order_on_a_fresh_market_is_decided_on_arrival() {
let mut venue = venue_holding_fresh_bars();
let outcome = venue.open_order(gtc_limit("crossing", Side::Buy, "1", "104"));
assert_eq!(filled_trade(&outcome).price, d("100"));
}
#[test]
fn a_held_limit_buy_that_rests_a_remainder_holds_the_loan_left_against_it() {
let config = MockExecutionConfig {
stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
..margin_config("0", "100", "2")
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(observe(&mut venue, 1, traded("100")).is_empty());
advance(&mut venue, time(12));
let outcome = venue.open_order(gtc_limit("waiting", Side::Buy, "1.5", "100"));
assert_eq!(
balance_of(sans_debt(&outcome.events)[0]).balance,
Balance::new_margin(d("150"), Decimal::ZERO, d("50"), Decimal::ZERO)
);
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("79", "80"))
.with_depth(MarketDepth::new(Some(d("1")), Some(d("0.5")))),
time(20),
);
assert_eq!(
balance_of(sans_debt(&events)[0]).balance,
Balance::new_margin(d("100"), Decimal::ZERO, d("40"), Decimal::ZERO)
);
assert_eq!(trade_of(sans_debt(&events)[2]).quantity, d("0.5"));
let cancelled = venue.cancel_order(OrderRequestCancel {
key: gtc_limit("waiting", Side::Buy, "1.5", "100").key,
state: RequestCancel { id: None },
});
assert!(cancelled.response.state.is_ok());
assert_eq!(
venue.balances(&[quote()])[0].balance,
Balance::new_margin(d("60"), d("60"), Decimal::ZERO, Decimal::ZERO),
"cancelling the remainder repays the 40 it held"
);
}
#[test]
fn cancelling_a_released_remainder_repays_only_its_own_loan_and_not_one_already_owed() {
let config = MockExecutionConfig {
stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
spot_margin: Some(
SpotMarginConfig::new(quote(), d("2"), MarginEffect::Borrow).unwrap(),
),
..config_from_balances(
vec![
funded(base(), Decimal::ZERO),
AssetBalance::new(
quote(),
Balance::new_margin(d("120"), d("120"), d("20"), Decimal::ZERO),
Utc::now(),
),
],
FeeModelConfig::default(),
)
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(observe(&mut venue, 1, traded("100")).is_empty());
advance(&mut venue, time(12));
let _ = venue.open_order(gtc_limit("waiting", Side::Buy, "1.5", "100"));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("79", "80"))
.with_depth(MarketDepth::new(Some(d("1")), Some(d("0.5")))),
time(20),
);
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(d("100"), Decimal::ZERO, d("40"), Decimal::ZERO)
);
let cancelled = venue.cancel_order(OrderRequestCancel {
key: gtc_limit("waiting", Side::Buy, "1.5", "100").key,
state: RequestCancel { id: None },
});
assert!(cancelled.response.state.is_ok());
assert_eq!(
venue.balances(&[quote()])[0].balance,
Balance::new_margin(d("80"), d("80"), d("20"), Decimal::ZERO),
"the remainder repays the 20 it borrowed, and the 20 owed before stays owed"
);
}
fn margin_config(btc: &str, usdt: &str, max_leverage: &str) -> MockExecutionConfig {
MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d(max_leverage), MarginEffect::Borrow).unwrap(),
),
..spot_config(btc, usdt, FeeModelConfig::default())
}
}
fn margin_venue(btc: &str, usdt: &str, max_leverage: &str) -> SimulatedVenue {
SimulatedVenue::new(&margin_config(btc, usdt, max_leverage), spot_instruments())
}
fn margin_market_venue(btc: &str, usdt: &str) -> SimulatedVenue {
let mut venue =
SimulatedVenue::new_market_driven(&margin_config(btc, usdt, "3"), spot_instruments());
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("49000", "49100")),
time(1)
)
.is_empty()
);
venue
}
fn balance_now(venue: &SimulatedVenue, asset: AssetNameExchange) -> Balance {
venue.balances(&[asset])[0].balance
}
fn insufficient_reason(outcome: &OpenOutcome) -> &str {
match &outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(OrderError::Rejected(
ApiError::BalanceInsufficient(_, reason),
))) => reason,
other => panic!("expected BalanceInsufficient, got: {other:?}"),
}
}
#[test]
fn a_cash_venue_reports_no_loans_and_refuses_a_sell_of_base_it_lacks() {
let mut venue = make_venue("0.25", "100000");
let outcome = venue.open_order(sell_request("1", market_prices("50000")));
assert!(
!insufficient_reason(&outcome).contains("Borrowable"),
"{}",
insufficient_reason(&outcome)
);
assert_eq!(
balance_now(&venue, base()),
Balance::new(d("0.25"), d("0.25"))
);
}
#[test]
fn a_margin_sell_borrows_only_the_shortfall_of_base() {
let mut venue = margin_venue("0.25", "100000", "3");
let outcome = venue.open_order(sell_request("1", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), Decimal::ZERO),
"the sell delivers all the base it borrowed and what the account held"
);
assert_eq!(
filled_received(&outcome).balance,
Balance::new_margin(d("150000"), d("150000"), Decimal::ZERO, Decimal::ZERO)
);
assert_eq!(filled_trade(&outcome).quantity, d("1"));
}
#[test]
fn a_margin_sell_of_base_the_account_never_held_borrows_it() {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
),
..config_from_balances(vec![funded(quote(), d("10000"))], FeeModelConfig::default())
};
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.1"), Decimal::ZERO),
"the sell opens a base balance with the loan, and delivers what it borrowed"
);
}
#[test]
fn a_margin_buy_borrows_quote_valued_through_the_inverse_of_its_pair() {
let config = MockExecutionConfig {
spot_margin: Some(SpotMarginConfig::new(base(), d("2"), MarginEffect::Borrow).unwrap()),
..spot_config("1", "0", FeeModelConfig::default())
};
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(buy_request("1.5", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 50000"),
"{}",
insufficient_reason(&outcome)
);
let outcome = venue.open_order(buy_request("0.5", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("25000"), Decimal::ZERO)
);
assert_eq!(balance_now(&venue, base()).total, d("1.5"));
}
#[test]
fn a_request_snapshot_values_only_the_pair_it_names() {
let venue = SimulatedVenue::new(
&spot_config("1", "0", FeeModelConfig::default()),
spot_instruments(),
);
let snapshot = market_prices("50000").unwrap();
let eth = || AssetNameExchange::new("ETH");
let price_in = |asset: AssetNameExchange,
valuation: AssetNameExchange,
priced: Underlying<AssetNameExchange>| {
venue.price_in(&asset, &valuation, Some((&priced, &snapshot)))
};
assert_eq!(
price_in(base(), quote(), Underlying::new(base(), quote())),
Some(d("50000"))
);
assert_eq!(
price_in(quote(), base(), Underlying::new(base(), quote())),
Some(d("0.00002")),
"valued through the inverse of the pair the snapshot names"
);
assert_eq!(
price_in(base(), quote(), Underlying::new(base(), eth())),
None,
"a snapshot of another pair sharing the base prices nothing"
);
assert_eq!(
price_in(base(), quote(), Underlying::new(eth(), quote())),
None,
"a snapshot of another pair sharing the quote prices nothing"
);
}
#[test]
fn a_venue_reads_back_the_instruments_it_was_constructed_with() {
let instruments = fee_asset_instruments();
let venue = SimulatedVenue::new(
&spot_config("1", "0", FeeModelConfig::default()),
instruments.clone(),
);
assert_eq!(venue.instruments(), &instruments);
assert_eq!(
venue.instrument(&instrument_name()),
instruments.get(&instrument_name())
);
assert!(venue.instrument(&instrument_name()).is_some());
assert_eq!(
venue.instrument(&InstrumentNameExchange::new("ETHUSDT")),
None
);
}
#[test]
fn a_margin_buy_repays_interest_then_principal_and_frees_the_rest() {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
),
..config_from_balances(
vec![
AssetBalance::new(
base(),
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.01")),
Utc::now(),
),
funded(quote(), d("100000")),
],
FeeModelConfig::default(),
)
};
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(with_effect(
buy_request("1", market_prices("50000")),
MarginEffect::Repay,
));
assert_eq!(
filled_received(&outcome).balance,
Balance::new_margin(d("0.24"), d("0.24"), Decimal::ZERO, Decimal::ZERO),
"1 BTC repays 0.01 of interest and 0.75 of principal, leaving 0.24 free"
);
}
#[test]
fn a_buy_smaller_than_the_debt_repays_interest_before_principal() {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
),
..config_from_balances(
vec![
AssetBalance::new(
base(),
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.01")),
Utc::now(),
),
funded(quote(), d("100000")),
],
FeeModelConfig::default(),
)
};
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(with_effect(
buy_request("0.11", market_prices("50000")),
MarginEffect::Repay,
));
assert_eq!(
filled_received(&outcome).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.65"), Decimal::ZERO)
);
}
#[test]
fn a_borrow_beyond_the_leverage_limit_is_refused_naming_what_would_be_lent() {
let mut venue = margin_venue("0", "10000", "2");
let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0.2"),
"{}",
insufficient_reason(&outcome)
);
assert_eq!(
balance_now(&venue, base()),
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
"a refused order borrows nothing"
);
let outcome = venue.open_order(sell_request("0.2", market_prices("50000")));
assert_eq!(filled_trade(&outcome).quantity, d("0.2"));
}
#[test]
fn debt_already_owed_counts_against_what_the_venue_lends() {
let mut venue = margin_venue("0", "10000", "2");
let outcome = venue.open_order(sell_request("0.15", market_prices("50000")));
assert_eq!(filled_trade(&outcome).quantity, d("0.15"));
let outcome = venue.open_order(sell_request("0.06", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0.05"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn a_leverage_of_one_lends_nothing_but_every_balance_reports_its_loan() {
let mut venue = margin_venue("0", "10000", "1");
for balance in venue.balances(&[base(), quote()]) {
assert_eq!(
balance.balance.margin,
Some(MarginDetails::default()),
"{balance:?}"
);
}
let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn no_side_effect_lends_nothing_and_repays_nothing() {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("3"), MarginEffect::NoSideEffect).unwrap(),
),
..config_from_balances(
vec![
AssetBalance::new(
base(),
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.5"), Decimal::ZERO),
Utc::now(),
),
funded(quote(), d("100000")),
],
FeeModelConfig::default(),
)
};
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
assert!(
!insufficient_reason(&outcome).contains("Borrowable"),
"{}",
insufficient_reason(&outcome)
);
let outcome = venue.open_order(buy_request("1", market_prices("50000")));
assert_eq!(
filled_received(&outcome).balance,
Balance::new_margin(d("1"), d("1"), d("0.5"), Decimal::ZERO),
"the bought base is free; the loan is left as it was"
);
}
#[test]
fn an_asset_the_venue_cannot_value_is_not_lent() {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(AssetNameExchange::new("EUR"), d("3"), MarginEffect::Borrow)
.unwrap(),
),
..spot_config("0", "100000", FeeModelConfig::default())
};
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn a_cfd_order_is_not_lent_its_margin() {
let config = MockExecutionConfig {
spot_margin: Some(SpotMarginConfig::new(usd(), d("3"), MarginEffect::Borrow).unwrap()),
..config_from_balances(vec![funded(usd(), d("1000"))], FeeModelConfig::default())
};
let mut venue = SimulatedVenue::new(&config, instruments_of(cfd_instrument()));
let outcome = venue.open_order(cfd_request(Side::Sell, "1", market_prices("5000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0"),
"{}",
insufficient_reason(&outcome)
);
assert_eq!(
balance_now(&venue, usd()),
Balance::new_margin(d("1000"), d("1000"), Decimal::ZERO, Decimal::ZERO)
);
}
fn open_error(outcome: &OpenOutcome) -> &UnindexedOrderError {
match &outcome.response.state {
OrderState::Inactive(InactiveOrderState::OpenFailed(error)) => error,
other => panic!("expected the order to be refused, got: {other:?}"),
}
}
fn config_owing_base(effect: MarginEffect) -> MockExecutionConfig {
MockExecutionConfig {
spot_margin: Some(SpotMarginConfig::new(quote(), d("3"), effect).unwrap()),
..config_from_balances(
vec![
AssetBalance::new(
base(),
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.01")),
Utc::now(),
),
funded(quote(), d("100000")),
],
FeeModelConfig::default(),
)
}
}
fn market_venue_for(config: &MockExecutionConfig) -> SimulatedVenue {
let mut venue = SimulatedVenue::new_market_driven(config, spot_instruments());
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("49000", "49100")),
time(1),
)
.is_empty()
);
venue
}
#[test]
fn a_borrow_order_repays_nothing_from_what_it_receives() {
let mut venue =
SimulatedVenue::new(&config_owing_base(MarginEffect::Borrow), spot_instruments());
let outcome = venue.open_order(buy_request("1", market_prices("50000")));
assert_eq!(
filled_received(&outcome).balance,
Balance::new_margin(d("1"), d("1"), d("0.75"), d("0.01")),
"the bought base is free and the loan, interest included, is left owed"
);
}
#[test]
fn a_request_effect_overrides_the_configured_default() {
let mut venue = margin_venue("0", "100000", "3");
let outcome = venue.open_order(with_effect(
sell_request("1", market_prices("50000")),
MarginEffect::NoSideEffect,
));
assert!(
!insufficient_reason(&outcome).contains("Borrowable"),
"{}",
insufficient_reason(&outcome)
);
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("3"), MarginEffect::NoSideEffect).unwrap(),
),
..spot_config("0", "100000", FeeModelConfig::default())
};
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(with_effect(
sell_request("1", market_prices("50000")),
MarginEffect::Borrow,
));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("1"), Decimal::ZERO)
);
}
#[test]
fn a_repay_order_borrows_nothing() {
let mut venue = margin_venue("0.25", "100000", "3");
let outcome = venue.open_order(with_effect(
sell_request("1", market_prices("50000")),
MarginEffect::Repay,
));
assert!(
!insufficient_reason(&outcome).contains("Borrowable"),
"{}",
insufficient_reason(&outcome)
);
assert_eq!(
balance_now(&venue, base()),
Balance::new_margin(d("0.25"), d("0.25"), Decimal::ZERO, Decimal::ZERO),
"a refused order borrows nothing"
);
}
#[test]
fn a_cfd_order_naming_a_borrow_or_repay_is_refused_unsent() {
let config = MockExecutionConfig {
spot_margin: Some(SpotMarginConfig::new(usd(), d("3"), MarginEffect::Borrow).unwrap()),
..config_from_balances(vec![funded(usd(), d("1000"))], FeeModelConfig::default())
};
let mut venue = SimulatedVenue::new(&config, instruments_of(cfd_instrument()));
for effect in [MarginEffect::Borrow, MarginEffect::Repay] {
let outcome = venue.open_order(with_effect(
cfd_request(Side::Buy, "1", market_prices("5000")),
effect,
));
assert_eq!(
open_error(&outcome),
&UnindexedOrderError::UnsupportedMarginEffect(effect)
);
}
assert_eq!(
balance_now(&venue, usd()),
Balance::new_margin(d("1000"), d("1000"), Decimal::ZERO, Decimal::ZERO),
"a refused order touches nothing"
);
let outcome = venue.open_order(with_effect(
cfd_request(Side::Buy, "0.01", market_prices("5000")),
MarginEffect::NoSideEffect,
));
assert!(
!matches!(
&outcome.response.state,
OrderState::Inactive(InactiveOrderState::OpenFailed(
UnindexedOrderError::UnsupportedMarginEffect(_)
))
),
"{:?}",
outcome.response.state
);
}
#[test]
fn a_cash_venue_takes_no_effect_but_no_side_effect() {
let mut venue = make_venue("1", "100000");
for effect in [MarginEffect::Borrow, MarginEffect::Repay] {
let outcome = venue.open_order(with_effect(
sell_request("0.1", market_prices("50000")),
effect,
));
assert_eq!(
open_error(&outcome),
&UnindexedOrderError::UnsupportedMarginEffect(effect)
);
}
let outcome = venue.open_order(with_effect(
sell_request("0.1", market_prices("50000")),
MarginEffect::NoSideEffect,
));
assert_eq!(filled_trade(&outcome).quantity, d("0.1"));
}
#[test]
fn a_resting_order_repays_from_its_fill_as_it_was_placed_to() {
let mut venue = market_venue_for(&config_owing_base(MarginEffect::Borrow));
let rested = venue.open_order(with_effect(
limit_request("rest", Side::Buy, "1", "49050", gtc()),
MarginEffect::Repay,
));
assert_eq!(rested_open(&rested).filled_quantity, Decimal::ZERO);
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("48900", "49000")),
time(2),
);
assert_eq!(
balance_of(&events[1]).balance,
Balance::new_margin(d("0.24"), d("0.24"), Decimal::ZERO, Decimal::ZERO),
"1 BTC repays 0.01 of interest and 0.75 of principal, leaving 0.24 free"
);
}
#[test]
fn a_resting_order_placed_to_borrow_repays_nothing_from_its_fill() {
let mut venue = market_venue_for(&config_owing_base(MarginEffect::Repay));
venue.open_order(with_effect(
limit_request("rest", Side::Buy, "1", "49050", gtc()),
MarginEffect::Borrow,
));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("48900", "49000")),
time(2),
);
assert_eq!(
balance_of(&events[1]).balance,
Balance::new_margin(d("1"), d("1"), d("0.75"), d("0.01")),
"the loan is left owed although the venue's default would have repaid it"
);
}
#[test]
fn without_auto_repay_at_cancel_a_cancelled_order_leaves_its_loan_owed() {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow)
.unwrap()
.with_auto_repay_at_cancel(false),
),
..spot_config("0", "100000", FeeModelConfig::default())
};
let mut venue = market_venue_for(&config);
venue.open_order(limit_request("rest", Side::Sell, "1", "50000", gtc()));
let cancelled = venue.cancel_order(OrderRequestCancel {
key: limit_request("rest", Side::Sell, "1", "50000", gtc()).key,
state: RequestCancel { id: None },
});
assert!(cancelled.response.state.is_ok());
assert_eq!(
balance_of(&cancelled.events[0]).balance,
Balance::new_margin(d("1"), d("1"), d("1"), Decimal::ZERO),
"the released base is free again and the loan that paid for it stays owed"
);
}
#[test]
fn a_resting_sell_borrows_when_placed_and_a_cancel_repays_it() {
let mut venue = margin_market_venue("0", "100000");
let outcome = venue.open_order(limit_request("rest", Side::Sell, "1", "50000", gtc()));
assert_eq!(rested_open(&outcome).filled_quantity, Decimal::ZERO);
assert_eq!(
balance_of(&outcome.events[0]).balance,
Balance::new_margin(d("1"), Decimal::ZERO, d("1"), Decimal::ZERO),
"the borrowed base is held against the order"
);
let cancelled = venue.cancel_order(OrderRequestCancel {
key: limit_request("rest", Side::Sell, "1", "50000", gtc()).key,
state: RequestCancel { id: None },
});
assert!(cancelled.response.state.is_ok());
assert_eq!(
balance_of(&cancelled.events[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
"the cancel repays what the order borrowed"
);
}
#[test]
fn a_resting_sell_that_fills_keeps_its_loan() {
let mut venue = margin_market_venue("0", "100000");
venue.open_order(limit_request("rest", Side::Sell, "1", "50000", gtc()));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("50100", "50200")),
time(2),
);
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("1"), Decimal::ZERO)
);
assert_eq!(trade_of(&events[2]).quantity, d("1"));
}
#[test]
fn a_partly_filled_sell_holds_its_loan_against_the_remainder() {
let mut venue = margin_market_venue("0.4", "100000");
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("49000", "49100"))
.with_depth(MarketDepth::new(Some(d("0.5")), None)),
time(2),
)
.is_empty()
);
let outcome = venue.open_order(limit_request("part", Side::Sell, "1", "49000", gtc()));
assert_eq!(rested_open(&outcome).filled_quantity, d("0.5"));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(d("0.5"), Decimal::ZERO, d("0.6"), Decimal::ZERO),
"0.4 held plus 0.6 borrowed: 0.5 sold and 0.5 held against the rest"
);
let cancelled = venue.cancel_order(OrderRequestCancel {
key: limit_request("part", Side::Sell, "1", "49000", gtc()).key,
state: RequestCancel { id: None },
});
assert_eq!(
balance_of(&cancelled.events[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.1"), Decimal::ZERO),
"the fill spent the account's own 0.4 first, so the cancel repays 0.5 of the loan"
);
}
#[test]
fn a_held_market_sell_borrows_when_accepted_and_keeps_the_loan_when_its_bar_fills_it() {
let config = MockExecutionConfig {
market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
..margin_config("0", "1000", "3")
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
let outcome = venue.open_order(market_order("held", Side::Sell, "2", gtc()));
assert_eq!(
balance_of(&outcome.events[0]).balance,
Balance::new_margin(d("2"), Decimal::ZERO, d("2"), Decimal::ZERO)
);
let events = feed_bar(&mut venue, 2, ohlc("98", "99", "97", "98"));
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("2"), Decimal::ZERO)
);
assert_eq!(trade_of(&events[2]).price, d("98"));
}
#[test]
fn a_held_market_sell_that_expires_repays_its_loan() {
let config = MockExecutionConfig {
market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
..margin_config("0", "1000", "3")
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
venue.open_order(market_order("held", Side::Sell, "2", gtc()));
let events = venue.expire_held(time(2));
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO)
);
}
fn margin_venue_holding() -> SimulatedVenue {
let config = MockExecutionConfig {
market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
..margin_config("0", "100", "2")
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
venue
}
#[test]
fn a_held_buy_costed_higher_at_its_bar_borrows_the_difference() {
let mut venue = margin_venue_holding();
let outcome = venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
assert_eq!(
balance_of(sans_debt(&outcome.events)[0]).balance,
Balance::new_margin(d("150"), Decimal::ZERO, d("50"), Decimal::ZERO)
);
let events = feed_bar(&mut venue, 2, ohlc("120", "121", "119", "120"));
assert_eq!(
balance_of(sans_debt(&events)[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("80"), Decimal::ZERO)
);
assert_eq!(trade_of(sans_debt(&events)[2]).price, d("120"));
}
#[test]
fn a_held_buy_costed_lower_at_its_bar_repays_the_loan_it_no_longer_needs() {
let mut venue = margin_venue_holding();
venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
let events = feed_bar(&mut venue, 2, ohlc("80", "81", "79", "80"));
assert_eq!(
balance_of(sans_debt(&events)[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("20"), Decimal::ZERO)
);
assert_eq!(
balance_of(sans_debt(&events)[1]).balance,
Balance::new_margin(d("1.5"), d("1.5"), Decimal::ZERO, Decimal::ZERO)
);
assert_eq!(trade_of(sans_debt(&events)[2]).price, d("80"));
}
#[test]
fn a_held_buy_unaffordable_at_its_bar_is_cancelled_and_repays_its_loan() {
let mut venue = margin_venue_holding();
venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
let events = feed_bar(&mut venue, 2, ohlc("250", "251", "249", "250"));
assert_eq!(
balance_of(sans_debt(&events)[0]).balance,
Balance::new_margin(d("100"), d("100"), Decimal::ZERO, Decimal::ZERO),
"the cancel repays the 50 the order borrowed when it was accepted"
);
assert!(matches!(
order_of(sans_debt(&events)[1]).state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
));
}
fn margin_venue_holding_with(
adjust: impl FnOnce(SpotMarginConfig) -> SpotMarginConfig,
effect: MarginEffect,
) -> SimulatedVenue {
let mut config = MockExecutionConfig {
market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
..margin_config("0", "100", "2")
};
config.spot_margin = Some(adjust(
SpotMarginConfig::new(quote(), d("2"), effect).unwrap(),
));
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
venue
}
#[test]
fn without_auto_repay_at_cancel_a_held_buy_costed_lower_keeps_the_loan_it_no_longer_needs() {
let mut venue = margin_venue_holding_with(
|config| config.with_auto_repay_at_cancel(false),
MarginEffect::Borrow,
);
venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
let events = feed_bar(&mut venue, 2, ohlc("80", "81", "79", "80"));
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(d("30"), d("30"), d("50"), Decimal::ZERO)
);
}
#[test]
fn without_auto_repay_at_cancel_a_held_buy_unaffordable_at_its_bar_keeps_its_loan() {
let mut venue = margin_venue_holding_with(
|config| config.with_auto_repay_at_cancel(false),
MarginEffect::Borrow,
);
venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
let events = feed_bar(&mut venue, 2, ohlc("250", "251", "249", "250"));
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(d("150"), d("150"), d("50"), Decimal::ZERO),
"the cancel gives the hold back and leaves the 50 the order borrowed owed"
);
assert!(matches!(
order_of(&events[1]).state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
));
}
#[test]
fn without_auto_repay_at_cancel_a_held_sell_that_expires_keeps_its_loan() {
let mut venue = margin_venue_holding_with(
|config| config.with_auto_repay_at_cancel(false),
MarginEffect::Borrow,
);
venue.open_order(market_order("held", Side::Sell, "1", gtc()));
let events = venue.expire_held(time(2));
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(d("1"), d("1"), d("1"), Decimal::ZERO)
);
}
#[test]
fn a_held_buy_costed_afresh_at_its_bar_borrows_as_it_was_placed_to() {
let mut venue = margin_venue_holding_with(|config| config, MarginEffect::NoSideEffect);
let mut request = market_order("held", Side::Buy, "1.5", gtc());
request.state = request.state.with_margin_effect(MarginEffect::Borrow);
venue.open_order(request);
let events = feed_bar(&mut venue, 2, ohlc("120", "121", "119", "120"));
assert_eq!(
balance_of(sans_debt(&events)[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("80"), Decimal::ZERO)
);
assert_eq!(trade_of(sans_debt(&events)[2]).price, d("120"));
}
fn with_min_borrow(
mut config: MockExecutionConfig,
asset: AssetNameExchange,
min_borrow: &str,
) -> MockExecutionConfig {
config.spot_margin = config
.spot_margin
.map(|margin| margin.with_min_borrow(asset, d(min_borrow)).unwrap());
config
}
#[test]
fn a_shortfall_below_the_minimum_borrows_the_minimum_and_the_rest_stays_free() {
let config = with_min_borrow(margin_config("0.25", "100000", "3"), base(), "0.5");
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(d("0.45"), d("0.45"), d("0.5"), Decimal::ZERO),
"0.25 + 0.5 lent − 0.3 sold, and what the sell did not spend is free"
);
assert_eq!(filled_trade(&outcome).quantity, d("0.3"));
}
#[test]
fn interest_is_charged_on_the_whole_minimum_lent() {
let config = with_min_borrow(margin_config("0.25", "100000", "3"), base(), "0.5");
let mut venue = SimulatedVenue::new(&config, spot_instruments())
.with_margin_interest(Arc::new(hourly_interest(base())));
advance(&mut venue, time(1));
let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(d("0.45"), d("0.45"), d("0.5"), d("0.00005"))
);
assert_eq!(
*cash_flow_of(sans_debt(&outcome.events)[3]),
interest_flow(
base(),
"0.5",
"0.00005",
time(1),
InterestTiming::OnBorrow,
"0"
),
);
}
#[test]
fn a_shortfall_above_the_minimum_borrows_only_the_shortfall() {
let config = with_min_borrow(margin_config("0.25", "100000", "3"), base(), "0.5");
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("1", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), Decimal::ZERO)
);
}
#[test]
fn a_minimum_on_another_asset_lends_only_the_shortfall() {
let config = with_min_borrow(margin_config("0.25", "100000", "3"), quote(), "1000");
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.05"), Decimal::ZERO)
);
}
#[test]
fn a_minimum_borrow_beyond_what_the_venue_lends_refuses_the_order() {
let config = with_min_borrow(margin_config("0", "10000", "2"), base(), "0.21");
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0.2")
&& insufficient_reason(&outcome).ends_with("Minimum Borrow: 0.21"),
"the reason names what the venue would lend and the minimum it would not: {}",
insufficient_reason(&outcome)
);
assert_eq!(
balance_now(&venue, base()),
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
"a refused order borrows nothing"
);
}
#[test]
fn a_shortfall_above_the_minimum_refused_by_the_leverage_limit_names_no_minimum() {
let config = with_min_borrow(margin_config("0", "10000", "2"), base(), "0.05");
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0.2")
&& !insufficient_reason(&outcome).contains("Minimum Borrow"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn a_shortfall_beyond_what_the_venue_lends_names_no_minimum() {
let config = with_min_borrow(margin_config("0", "10000", "2"), base(), "0.5");
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0.2")
&& !insufficient_reason(&outcome).contains("Minimum Borrow"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn a_shortfall_of_exactly_what_the_venue_lends_names_the_minimum_that_refused_it() {
let config = with_min_borrow(margin_config("0", "10000", "2"), base(), "0.5");
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.2", market_prices("50000")));
assert!(
insufficient_reason(&outcome).ends_with("Minimum Borrow: 0.5"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn a_minimum_borrow_of_exactly_what_the_venue_lends_is_lent() {
let config = with_min_borrow(margin_config("0", "10000", "2"), base(), "0.2");
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(d("0.1"), d("0.1"), d("0.2"), Decimal::ZERO)
);
}
#[test]
fn a_resting_sell_lent_the_minimum_holds_its_shortfall_and_a_cancel_repays_the_whole_loan() {
let config = with_min_borrow(margin_config("0", "100000", "3"), base(), "0.5");
let mut venue = market_venue_for(&config);
let outcome = venue.open_order(limit_request("rest", Side::Sell, "0.1", "50000", gtc()));
assert_eq!(
balance_of(&outcome.events[0]).balance,
Balance::new_margin(d("0.5"), d("0.4"), d("0.5"), Decimal::ZERO),
"0.1 is held against the order, and the 0.4 lent beyond it is free"
);
let cancelled = venue.cancel_order(OrderRequestCancel {
key: limit_request("rest", Side::Sell, "0.1", "50000", gtc()).key,
state: RequestCancel { id: None },
});
assert!(cancelled.response.state.is_ok());
assert_eq!(
balance_of(&cancelled.events[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
"the cancel repays all 0.5 the order borrowed"
);
}
#[test]
fn without_auto_repay_at_cancel_a_cancel_leaves_the_whole_minimum_owed() {
let mut config = with_min_borrow(margin_config("0", "100000", "3"), base(), "0.5");
config.spot_margin = config
.spot_margin
.map(|margin| margin.with_auto_repay_at_cancel(false));
let mut venue = market_venue_for(&config);
venue.open_order(limit_request("rest", Side::Sell, "0.1", "50000", gtc()));
let cancelled = venue.cancel_order(OrderRequestCancel {
key: limit_request("rest", Side::Sell, "0.1", "50000", gtc()).key,
state: RequestCancel { id: None },
});
assert_eq!(
balance_of(&cancelled.events[0]).balance,
Balance::new_margin(d("0.5"), d("0.5"), d("0.5"), Decimal::ZERO)
);
}
#[test]
fn a_resting_sell_lent_the_minimum_that_fills_keeps_the_whole_loan() {
let config = with_min_borrow(margin_config("0", "100000", "3"), base(), "0.5");
let mut venue = market_venue_for(&config);
venue.open_order(limit_request("rest", Side::Sell, "0.1", "50000", gtc()));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("50100", "50200")),
time(2),
);
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(d("0.4"), d("0.4"), d("0.5"), Decimal::ZERO),
"the filled order's loan stays owed, the part it did not spend included"
);
assert_eq!(trade_of(&events[2]).quantity, d("0.1"));
}
#[test]
fn a_partly_filled_sell_lent_the_minimum_repays_the_unspent_loan_at_cancel() {
let config = with_min_borrow(margin_config("0.4", "100000", "3"), base(), "0.8");
let mut venue = market_venue_for(&config);
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("49000", "49100"))
.with_depth(MarketDepth::new(Some(d("0.5")), None)),
time(2),
)
.is_empty()
);
let outcome = venue.open_order(limit_request("part", Side::Sell, "1", "49000", gtc()));
assert_eq!(rested_open(&outcome).filled_quantity, d("0.5"));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(d("0.7"), d("0.2"), d("0.8"), Decimal::ZERO),
"0.4 + 0.8 lent − 0.5 sold; 0.5 held against the rest and 0.2 free"
);
let cancelled = venue.cancel_order(OrderRequestCancel {
key: limit_request("part", Side::Sell, "1", "49000", gtc()).key,
state: RequestCancel { id: None },
});
assert_eq!(
balance_of(&cancelled.events[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.1"), Decimal::ZERO),
"the 0.5 held against the rest and the 0.2 lent beyond the shortfall are repaid"
);
}
#[test]
fn a_held_market_sell_lent_the_minimum_that_expires_repays_the_whole_loan() {
let config = with_min_borrow(
MockExecutionConfig {
market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
..margin_config("0", "1000", "3")
},
base(),
"3",
);
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
let outcome = venue.open_order(market_order("held", Side::Sell, "2", gtc()));
assert_eq!(
balance_of(&outcome.events[0]).balance,
Balance::new_margin(d("3"), d("1"), d("3"), Decimal::ZERO)
);
let events = venue.expire_held(time(2));
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO)
);
}
#[test]
fn a_held_buy_lent_the_minimum_and_costed_lower_repays_only_the_held_loan_it_does_not_need() {
let mut venue = margin_venue_holding_with(
|config| config.with_min_borrow(quote(), d("80")).unwrap(),
MarginEffect::Borrow,
);
let outcome = venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
assert_eq!(
balance_of(&outcome.events[0]).balance,
Balance::new_margin(d("180"), d("30"), d("80"), Decimal::ZERO)
);
let events = feed_bar(&mut venue, 2, ohlc("80", "81", "79", "80"));
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(d("30"), d("30"), d("50"), Decimal::ZERO)
);
}
#[test]
fn a_held_buy_lent_the_minimum_that_needs_none_of_its_loan_keeps_only_the_excess_owed() {
let mut venue = margin_venue_holding_with(
|config| config.with_min_borrow(quote(), d("80")).unwrap(),
MarginEffect::Borrow,
);
venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
let events = feed_bar(&mut venue, 2, ohlc("20", "21", "19", "20"));
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(d("100"), d("100"), d("30"), Decimal::ZERO)
);
}
#[test]
fn a_held_buy_costed_higher_at_its_bar_borrows_at_least_the_minimum_more() {
let mut venue = margin_venue_holding_with(
|config| config.with_min_borrow(quote(), d("40")).unwrap(),
MarginEffect::Borrow,
);
let outcome = venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
assert_eq!(
balance_of(&outcome.events[0]).balance,
Balance::new_margin(d("150"), Decimal::ZERO, d("50"), Decimal::ZERO)
);
let events = feed_bar(&mut venue, 2, ohlc("120", "121", "119", "120"));
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(d("10"), d("10"), d("90"), Decimal::ZERO)
);
}
#[test]
fn a_held_buy_lent_the_minimum_and_unaffordable_at_its_bar_repays_the_whole_loan() {
let mut venue = margin_venue_holding_with(
|config| config.with_min_borrow(quote(), d("80")).unwrap(),
MarginEffect::Borrow,
);
venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
let events = feed_bar(&mut venue, 2, ohlc("250", "251", "249", "250"));
assert_eq!(
balance_of(sans_debt(&events)[0]).balance,
Balance::new_margin(d("100"), d("100"), Decimal::ZERO, Decimal::ZERO),
"the cancel repays all 80 the order borrowed"
);
assert!(matches!(
order_of(sans_debt(&events)[1]).state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
));
}
#[test]
fn a_held_limit_buy_lent_the_minimum_carries_it_to_the_remainder_it_rests() {
let config = with_min_borrow(
MockExecutionConfig {
stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
..margin_config("0", "100", "2")
},
quote(),
"80",
);
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments());
advance(&mut venue, time(1));
assert!(observe(&mut venue, 1, traded("100")).is_empty());
advance(&mut venue, time(12));
let outcome = venue.open_order(gtc_limit("waiting", Side::Buy, "1.5", "100"));
assert_eq!(
balance_of(&outcome.events[0]).balance,
Balance::new_margin(d("180"), d("30"), d("80"), Decimal::ZERO)
);
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("79", "80"))
.with_depth(MarketDepth::new(Some(d("1")), Some(d("0.5")))),
time(20),
);
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(d("130"), d("30"), d("70"), Decimal::ZERO)
);
let cancelled = venue.cancel_order(OrderRequestCancel {
key: gtc_limit("waiting", Side::Buy, "1.5", "100").key,
state: RequestCancel { id: None },
});
assert!(cancelled.response.state.is_ok());
assert_eq!(
venue.balances(&[quote()])[0].balance,
Balance::new_margin(d("60"), d("60"), Decimal::ZERO, Decimal::ZERO),
"cancelling the remainder repays the 40 it held and the 30 lent beyond the shortfall"
);
}
#[test]
fn a_held_limit_that_rests_untraded_at_its_release_keeps_its_interest_until_its_end() {
let config = MockExecutionConfig {
stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
..margin_config("0", "100", "2")
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments())
.with_margin_interest(Arc::new(hourly_interest(quote())));
advance(&mut venue, time(1));
assert!(observe(&mut venue, 1, traded("100")).is_empty());
advance(&mut venue, time(12));
let outcome = venue.open_order(gtc_limit("waiting", Side::Buy, "1.5", "100"));
assert_eq!(sans_debt(&outcome.events).len(), 1, "{:?}", outcome.events);
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("101", "102")),
time(20),
);
assert!(
!events
.iter()
.any(|event| matches!(event.kind, AccountEventKind::CashFlow(_))),
"still no position for the charge: {events:?}"
);
advance(&mut venue, time(30));
let cancelled = venue.cancel_order(OrderRequestCancel {
key: gtc_limit("waiting", Side::Buy, "1.5", "100").key,
state: RequestCancel { id: None },
});
assert_eq!(
*cash_flow_of(sans_debt(&cancelled.events)[1]),
interest_flow(
quote(),
"50",
"0.005",
time(30),
InterestTiming::OnBorrow,
"0"
),
"carried to the book, and reported at the order's end"
);
}
#[test]
fn a_held_limit_that_trades_in_part_at_its_release_reports_its_interest_once_after_the_trade() {
let config = MockExecutionConfig {
stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
..margin_config("0", "100", "2")
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments())
.with_margin_interest(Arc::new(hourly_interest(quote())));
advance(&mut venue, time(1));
assert!(observe(&mut venue, 1, traded("100")).is_empty());
advance(&mut venue, time(12));
let outcome = venue.open_order(gtc_limit("partial", Side::Buy, "1.5", "100"));
assert_eq!(sans_debt(&outcome.events).len(), 1, "{:?}", outcome.events);
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("98", "99")).with_depth(sized("0.5")),
time(20),
);
let events = sans_debt(&events);
let traded_at = events
.iter()
.position(|event| matches!(event.kind, AccountEventKind::Trade(_)))
.unwrap_or_else(|| panic!("a trade: {events:?}"));
assert_eq!(trade_of(events[traded_at]).quantity, d("0.5"));
assert_eq!(
*cash_flow_of(events[traded_at + 1]),
interest_flow(
quote(),
"50",
"0.005",
time(20),
InterestTiming::OnBorrow,
"0"
),
"the hold's charge after the trade, stamped at it: {events:?}"
);
assert_eq!(
events
.iter()
.filter(|event| matches!(event.kind, AccountEventKind::CashFlow(_)))
.count(),
1,
"reported once: {events:?}"
);
advance(&mut venue, time(30));
let cancelled = venue.cancel_order(OrderRequestCancel {
key: gtc_limit("partial", Side::Buy, "1.5", "100").key,
state: RequestCancel { id: None },
});
assert!(
!cancelled
.events
.iter()
.any(|event| matches!(event.kind, AccountEventKind::CashFlow(_))),
"the remainder carries no charge already reported: {:?}",
cancelled.events
);
}
#[test]
fn a_held_immediate_order_an_idle_bar_cancels_reports_its_interest_with_its_cancel() {
let config = MockExecutionConfig {
market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
stale_market: StaleMarketPolicy::Wait { max_age: MAX_AGE },
..margin_config("0", "100", "2")
};
let mut venue = SimulatedVenue::new_market_driven(&config, spot_instruments())
.with_margin_interest(Arc::new(hourly_interest(quote())));
advance(&mut venue, time(1));
assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
let outcome = venue.open_order(market_order(
"immediate",
Side::Buy,
"1.5",
TimeInForce::ImmediateOrCancel,
));
assert_eq!(sans_debt(&outcome.events).len(), 1, "{:?}", outcome.events);
let bar = ohlc("100", "100", "100", "100");
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(MarketSnapshot::from_last_price(Some(bar.close)))
.with_bar(Some(bar))
.with_fresh(false),
time(2),
);
let events = sans_debt(&events);
assert_eq!(events.len(), 3, "{events:?}");
assert_eq!(
*cash_flow_of(events[1]),
interest_flow(
quote(),
"50",
"0.005",
time(2),
InterestTiming::OnBorrow,
"0"
),
"the order never traded, so its cancel reports the charge, stamped then"
);
assert!(matches!(
order_of(events[2]).state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
));
}
fn margin_venue_with_eth(eth: &str, eth_owed: &str) -> SimulatedVenue {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("2"), MarginEffect::Borrow).unwrap(),
),
..config_from_balances(
vec![
funded(base(), Decimal::ZERO),
funded(quote(), d("10000")),
AssetBalance::new(
AssetNameExchange::new("ETH"),
Balance::new_margin(d(eth), d(eth), d(eth_owed), Decimal::ZERO),
Utc::now(),
),
],
FeeModelConfig::default(),
)
};
SimulatedVenue::new(&config, spot_instruments())
}
#[test]
fn an_asset_the_venue_cannot_value_counts_as_no_equity() {
let mut venue = margin_venue_with_eth("5", "0");
let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0.2"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn a_debt_the_venue_cannot_value_stops_it_lending() {
let mut venue = margin_venue_with_eth("5", "1");
let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn interest_owed_counts_as_debt_against_what_the_venue_lends() {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("2"), MarginEffect::Borrow).unwrap(),
),
..config_from_balances(
vec![
AssetBalance::new(
base(),
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.05"), d("0.01")),
Utc::now(),
),
funded(quote(), d("20000")),
],
FeeModelConfig::default(),
)
};
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("1", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0.28"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn an_asset_a_margin_account_first_receives_reports_its_loan() {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
),
..config_from_balances(
vec![funded(quote(), d("100000"))],
FeeModelConfig::default(),
)
};
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(buy_request("1", market_prices("50000")));
assert_eq!(
filled_received(&outcome).balance,
Balance::new_margin(d("1"), d("1"), Decimal::ZERO, Decimal::ZERO)
);
}
#[test]
fn an_asset_priced_at_zero_is_not_lent() {
let mut venue = margin_venue("0", "10000", "2");
let outcome = venue.open_order(sell_request("0.1", market_prices("0")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn a_pair_quoting_the_asset_in_another_does_not_value_it() {
let mut instruments = spot_instruments();
let aave_btc = Instrument {
exchange: EXCHANGE,
name_internal: InstrumentNameInternal::new("aavebtc"),
name_exchange: InstrumentNameExchange::new("AAVEBTC"),
underlying: Underlying {
base: AssetNameExchange::new("AAVE"),
quote: base(),
},
quote: InstrumentQuoteAsset::UnderlyingQuote,
kind: InstrumentKind::Spot,
spec: None,
data_venue: None,
};
instruments.insert(aave_btc.name_exchange.clone(), aave_btc);
let mut venue =
SimulatedVenue::new_market_driven(&margin_config("0", "10000", "2"), instruments);
advance(&mut venue, time(1));
for (name, snapshot) in [
(instrument_name(), book("50000", "50000")),
(InstrumentNameExchange::new("AAVEBTC"), book("0.05", "0.05")),
] {
assert!(
venue
.apply_market(&name, MarketObservation::new(snapshot), time(1))
.is_empty()
);
}
let outcome = venue.open_order(market_order(
"sell",
Side::Sell,
"0.3",
TimeInForce::ImmediateOrCancel,
));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0.2"),
"{}",
insufficient_reason(&outcome)
);
}
fn with_max_borrow(
mut config: MockExecutionConfig,
asset: AssetNameExchange,
max_borrow: &str,
) -> MockExecutionConfig {
config.spot_margin = config
.spot_margin
.map(|margin| margin.with_max_borrow(asset, d(max_borrow)).unwrap());
config
}
fn capped_venue(cap: &str) -> SimulatedVenue {
let config = with_max_borrow(margin_config("0", "10000", "3"), base(), cap);
SimulatedVenue::new(&config, spot_instruments())
}
#[test]
fn a_borrow_of_exactly_what_the_cap_leaves_is_lent() {
let mut venue = capped_venue("0.3");
let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.3"), Decimal::ZERO)
);
}
#[test]
fn a_borrow_past_a_cap_tighter_than_the_leverage_limit_is_rejected_as_over_the_limit() {
let mut venue = capped_venue("0.3");
let outcome = venue.open_order(sell_request("0.31", market_prices("50000")));
let reject = borrow_rejection(&outcome);
assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
assert_eq!(reject.venue_code, None);
assert_eq!(
reject.available,
Some(d("0.3")),
"a smaller sell could still borrow what the cap leaves"
);
assert!(
reject
.message
.starts_with("BTC borrow exceeds Max Borrow: 0.3;")
&& reject.message.ends_with("Borrowable: 0.3"),
"the reason names the cap and what it leaves: {}",
reject.message
);
assert_eq!(
balance_now(&venue, base()),
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
"a refused order borrows nothing"
);
}
#[test]
fn a_leverage_limit_tighter_than_the_cap_rejects_as_unaffordable_naming_no_cap() {
let mut venue = capped_venue("1");
let outcome = venue.open_order(sell_request("0.5", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0.4")
&& !insufficient_reason(&outcome).contains("Max Borrow"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn a_cap_leaving_exactly_what_the_leverage_limit_does_leaves_the_leverage_limit_deciding() {
let mut venue = capped_venue("0.4");
let outcome = venue.open_order(sell_request("0.5", market_prices("50000")));
assert!(
insufficient_reason(&outcome).contains("Borrowable: 0.4")
&& !insufficient_reason(&outcome).contains("Max Borrow"),
"{}",
insufficient_reason(&outcome)
);
}
#[test]
fn a_cap_on_another_asset_does_not_limit_this_one() {
let config = with_max_borrow(margin_config("0", "10000", "3"), quote(), "0");
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.3"), Decimal::ZERO)
);
}
#[test]
fn a_cap_of_zero_lends_none_of_the_asset() {
let mut venue = capped_venue("0");
let outcome = venue.open_order(sell_request("0.01", market_prices("50000")));
let reject = borrow_rejection(&outcome);
assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
assert_eq!(reject.available, Some(Decimal::ZERO));
}
#[test]
fn the_cap_counts_what_is_already_owed_interest_included() {
let config = with_max_borrow(margin_config("0", "10000", "3"), base(), "0.3");
let mut venue = SimulatedVenue::new(&config, spot_instruments())
.with_margin_interest(Arc::new(hourly_interest(base())));
advance(&mut venue, time(1));
venue.open_order(sell_request("0.2", market_prices("50000")));
let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
assert_eq!(borrow_rejection(&outcome).available, Some(d("0.09998")));
let outcome = venue.open_order(sell_request("0.09998", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance.margin,
Some(MarginDetails::new(d("0.29998"), d("0.00003"))),
"0.00002 on the first loan, and 0.000009998 on the second, rounded up to 0.00001"
);
}
#[test]
fn the_interest_a_new_loan_is_charged_when_made_is_not_counted_against_the_cap() {
let config = with_max_borrow(margin_config("0", "10000", "3"), base(), "0.3");
let mut venue = SimulatedVenue::new(&config, spot_instruments())
.with_margin_interest(Arc::new(hourly_interest(base())));
advance(&mut venue, time(1));
let outcome = venue.open_order(sell_request("0.3", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance.margin,
Some(MarginDetails::new(d("0.3"), d("0.00003"))),
"lent the whole 0.3 the cap leaves, its first charge taking the debt past the cap"
);
let outcome = venue.open_order(sell_request("0.01", market_prices("50000")));
let reject = borrow_rejection(&outcome);
assert_eq!(
reject.available,
Some(Decimal::ZERO),
"a debt past the cap leaves nothing to lend"
);
assert!(
reject.message.ends_with("Borrowable: 0"),
"{}",
reject.message
);
}
#[test]
fn a_minimum_borrow_past_what_the_cap_leaves_is_refused_with_nothing_available() {
let config = with_min_borrow(
with_max_borrow(margin_config("0", "10000", "3"), base(), "0.3"),
base(),
"0.5",
);
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
let reject = borrow_rejection(&outcome);
assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
assert_eq!(
reject.available,
Some(Decimal::ZERO),
"no loan can both reach the minimum and stay under the cap"
);
assert!(
reject
.message
.ends_with("Borrowable: 0.3, Minimum Borrow: 0.5"),
"{}",
reject.message
);
let outcome = venue.open_order(sell_request("0.35", market_prices("50000")));
let reject = borrow_rejection(&outcome);
assert_eq!(reject.available, Some(Decimal::ZERO));
assert!(
!reject.message.contains("Minimum Borrow"),
"{}",
reject.message
);
}
#[test]
fn a_minimum_borrow_within_what_the_cap_leaves_keeps_what_it_leaves_available() {
let config = with_min_borrow(
with_max_borrow(margin_config("0", "10000", "3"), base(), "0.3"),
base(),
"0.3",
);
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(sell_request("0.35", market_prices("50000")));
assert_eq!(borrow_rejection(&outcome).available, Some(d("0.3")));
}
#[test]
fn a_buy_past_the_cap_on_its_quote_is_rejected_with_no_available_in_base() {
let config = with_max_borrow(margin_config("1", "0", "3"), quote(), "5000");
let mut venue = SimulatedVenue::new(&config, spot_instruments());
let outcome = venue.open_order(buy_request("0.2", market_prices("50000")));
let reject = borrow_rejection(&outcome);
assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
assert_eq!(
reject.available, None,
"the loan is in USDT, and the order is sized in BTC"
);
assert!(
reject
.message
.starts_with("USDT borrow exceeds Max Borrow: 5000;"),
"{}",
reject.message
);
}
#[test]
fn a_resting_sell_past_the_cap_is_rejected_as_over_the_limit() {
let config = with_max_borrow(margin_config("0", "100000", "3"), base(), "0.3");
let mut venue = market_venue_for(&config);
let outcome = venue.open_order(limit_request("rest", Side::Sell, "0.4", "50000", gtc()));
let reject = borrow_rejection(&outcome);
assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
assert_eq!(reject.available, Some(d("0.3")));
}
#[test]
fn a_held_sell_past_the_cap_is_rejected_on_arrival_as_over_the_limit() {
let mut venue = margin_venue_holding_with(
|config| config.with_max_borrow(base(), d("0.5")).unwrap(),
MarginEffect::Borrow,
);
let outcome = venue.open_order(market_order("held", Side::Sell, "0.6", gtc()));
let reject = borrow_rejection(&outcome);
assert_eq!(reject.reason, BorrowRejectReason::BorrowLimitExceeded);
assert_eq!(reject.available, Some(d("0.5")));
}
#[test]
fn a_held_sell_lent_the_whole_cap_fills_at_its_bar_though_interest_took_the_debt_past_it() {
let mut venue = margin_venue_holding_with(
|config| config.with_max_borrow(base(), d("0.5")).unwrap(),
MarginEffect::Borrow,
)
.with_margin_interest(Arc::new(hourly_interest(base())));
let outcome = venue.open_order(market_order("held", Side::Sell, "0.5", gtc()));
assert_eq!(
balance_of(sans_debt(&outcome.events)[0]).balance.margin,
Some(MarginDetails::new(d("0.5"), d("0.00005"))),
"the loan's first charge takes what is owed past the 0.5 cap"
);
let events = feed_bar(&mut venue, 2, ohlc("98", "99", "97", "98"));
let trade = events
.iter()
.find_map(|event| match &event.kind {
AccountEventKind::Trade(trade) => Some(trade),
_ => None,
})
.expect("the held sell fills at its bar");
assert_eq!(trade.quantity, d("0.5"));
}
#[test]
fn a_held_buy_whose_bar_needs_a_loan_past_the_cap_is_cancelled() {
let mut venue = margin_venue_holding_with(
|config| config.with_max_borrow(quote(), d("50")).unwrap(),
MarginEffect::Borrow,
);
venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
let events = feed_bar(&mut venue, 2, ohlc("120", "121", "119", "120"));
assert_eq!(
balance_of(sans_debt(&events)[0]).balance,
Balance::new_margin(d("100"), d("100"), Decimal::ZERO, Decimal::ZERO),
"the cancel repays the 50 the order borrowed when it was accepted"
);
assert!(matches!(
order_of(sans_debt(&events)[1]).state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
));
}
const HOURLY_BASIS_POINT: &str = "0.876";
fn hourly_interest(asset: AssetNameExchange) -> PeriodicMarginInterest {
PeriodicMarginInterest::new(TimeDelta::hours(1), DayCount::Act365)
.with_rates(asset, RateSeries::constant(d(HOURLY_BASIS_POINT)))
}
fn interest_flow(
asset: AssetNameExchange,
principal: &str,
interest: &str,
time: DateTime<Utc>,
timing: InterestTiming,
id: &str,
) -> CashFlow<AssetNameExchange, InstrumentNameExchange> {
CashFlow::new(
CashFlowKind::MarginInterest {
rate: Some(d(HOURLY_BASIS_POINT)),
principal: Some(d(principal)),
timing: Some(timing),
},
asset,
-d(interest),
None,
time,
Some(CashFlowId::new(id)),
)
}
fn cash_flow_of(
event: &UnindexedAccountEvent,
) -> &CashFlow<AssetNameExchange, InstrumentNameExchange> {
match &event.kind {
AccountEventKind::CashFlow(flow) => flow,
other => panic!("expected a cash flow, got: {other:?}"),
}
}
fn margin_venue_short_btc() -> SimulatedVenue {
let mut venue = margin_venue("0.25", "100000", "3")
.with_margin_interest(Arc::new(hourly_interest(base())));
advance(&mut venue, time(1));
venue.open_order(sell_request("1", market_prices("50000")));
venue
}
#[test]
fn a_sell_that_borrows_is_charged_a_period_of_interest_reported_after_its_trade() {
let mut venue = margin_venue("0.25", "100000", "3")
.with_margin_interest(Arc::new(hourly_interest(base())));
advance(&mut venue, time(1));
let outcome = venue.open_order(sell_request("1", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.000075")),
"the interest is added to the loan, leaving total and free as they were"
);
assert_eq!(trade_of(sans_debt(&outcome.events)[2]).quantity, d("1"));
assert_eq!(
*cash_flow_of(sans_debt(&outcome.events)[3]),
interest_flow(
base(),
"0.75",
"0.000075",
time(1),
InterestTiming::OnBorrow,
"0"
),
"charged on the amount lent, stamped at the borrow, after the trade that opened the \
short, so the position it belongs to exists when it is read"
);
}
#[test]
fn a_loan_is_charged_on_its_principal_at_each_boundary_and_never_on_its_interest() {
let mut venue = margin_venue_short_btc();
assert_eq!(venue.next_deadline(), Some(boundary(0)));
let events = venue.advance_time(boundary(1) + TimeDelta::seconds(1));
let charged = charges(&events);
assert_eq!(charged.len(), 2);
assert_eq!(
charged[0].0.balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.00015"))
);
assert_eq!(charged[0].0.time_exchange, boundary(0));
assert_eq!(
*charged[0].1,
interest_flow(
base(),
"0.75",
"0.000075",
boundary(0),
InterestTiming::Periodic,
"1"
)
);
assert_eq!(
charged[1].0.balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.75"), d("0.000225"))
);
assert_eq!(
*charged[1].1,
interest_flow(
base(),
"0.75",
"0.000075",
boundary(1),
InterestTiming::Periodic,
"2"
)
);
assert_eq!(venue.next_deadline(), Some(boundary(2)));
}
fn debt_updates(
events: &[UnindexedAccountEvent],
) -> Vec<(AssetNameExchange, MarginDebtKind, MarginDetails)> {
events
.iter()
.enumerate()
.filter_map(|(at, event)| {
let AccountEventKind::MarginDebt(update) = &event.kind else {
return None;
};
let restated = at
.checked_sub(1)
.map(|before| balance_of(&events[before]))
.expect("a debt update follows a restatement");
assert_eq!(update.asset, restated.asset);
assert_eq!(Some(update.debt), restated.balance.margin);
assert_eq!(update.time_exchange, restated.time_exchange);
assert_eq!(update.instrument, None);
assert_eq!(update.venue_kind, update.kind.to_smolstr());
Some((update.asset.clone(), update.kind, update.debt))
})
.collect()
}
#[test]
fn a_margin_venue_reports_each_debt_change_after_its_restatement() {
let mut venue = margin_venue("0.25", "100000", "3")
.with_margin_interest(Arc::new(hourly_interest(base())));
advance(&mut venue, time(1));
let sold = venue.open_order(sell_request("1", market_prices("50000")));
assert_eq!(
debt_updates(&sold.events),
[(
base(),
MarginDebtKind::Borrow,
MarginDetails::new(d("0.75"), d("0.000075"))
)]
);
let charged = venue.advance_time(boundary(0));
assert_eq!(
debt_updates(&charged),
[(
base(),
MarginDebtKind::Interest,
MarginDetails::new(d("0.75"), d("0.00015"))
)]
);
let repaid = venue.open_order(with_effect(
buy_request("1", market_prices("50000")),
MarginEffect::Repay,
));
assert_eq!(
debt_updates(&repaid.events),
[(base(), MarginDebtKind::Repay, MarginDetails::default())]
);
let traded = venue.open_order(sell_request("0.1", market_prices("50000")));
assert!(debt_updates(&traded.events).is_empty(), "{traded:?}");
}
#[test]
fn a_repay_of_either_part_of_the_debt_reports_a_repay() {
let mut venue = margin_venue_short_btc();
let repaid = venue.open_order(with_effect(
buy_request("0.00005", market_prices("50000")),
MarginEffect::Repay,
));
assert_eq!(
debt_updates(&repaid.events),
[(
base(),
MarginDebtKind::Repay,
MarginDetails::new(d("0.75"), d("0.000025"))
)]
);
let mut venue = margin_venue("0.25", "100000", "3");
advance(&mut venue, time(1));
venue.open_order(sell_request("1", market_prices("50000")));
let repaid = venue.open_order(with_effect(
buy_request("0.5", market_prices("50000")),
MarginEffect::Repay,
));
assert_eq!(
debt_updates(&repaid.events),
[(
base(),
MarginDebtKind::Repay,
MarginDetails::new(d("0.25"), Decimal::ZERO)
)]
);
}
#[test]
fn only_a_change_on_a_margin_venue_reports_debt() {
let config = config_owing_base(MarginEffect::Borrow);
let mut venue = market_venue_for(&config);
let bought = venue.open_order(market_order("buy", Side::Buy, "0.1", gtc()));
assert!(debt_updates(&bought.events).is_empty(), "{bought:?}");
let mut venue = make_venue("10", "1000000");
let sold = venue.open_order(sell_request("1", market_prices("50000")));
assert!(
!sold
.events
.iter()
.any(|event| matches!(event.kind, AccountEventKind::MarginDebt(_))),
"{sold:?}"
);
}
#[test]
fn a_loan_repaid_before_a_boundary_is_not_charged_there() {
let mut venue = margin_venue_short_btc();
let outcome = venue.open_order(with_effect(
buy_request("0.76", market_prices("50000")),
MarginEffect::Repay,
));
assert_eq!(
filled_received(&outcome).balance,
Balance::new_margin(d("0.009925"), d("0.009925"), Decimal::ZERO, Decimal::ZERO)
);
assert_eq!(venue.next_deadline(), None);
assert!(venue.advance_time(boundary(1)).is_empty());
}
#[test]
fn a_boundary_charges_the_principal_left_after_a_partial_repay() {
let mut venue = margin_venue_short_btc();
venue.open_order(with_effect(
buy_request("0.5", market_prices("50000")),
MarginEffect::Repay,
));
let events = venue.advance_time(boundary(0));
assert_eq!(
*charges(&events)[0].1,
interest_flow(
base(),
"0.250075",
"0.00002501",
boundary(0),
InterestTiming::Periodic,
"1"
),
"0.250075 at 0.01% is 0.0000250075, rounded up to the satoshi"
);
}
#[test]
fn a_loan_taken_at_a_boundary_s_instant_is_charged_there_only_at_borrow() {
let mut venue = margin_venue("0.25", "100000", "3")
.with_margin_interest(Arc::new(hourly_interest(base())));
advance(&mut venue, time(1));
advance(&mut venue, boundary(0));
let outcome = venue.open_order(sell_request("1", market_prices("50000")));
assert_eq!(
*cash_flow_of(sans_debt(&outcome.events)[3]),
interest_flow(
base(),
"0.75",
"0.000075",
boundary(0),
InterestTiming::OnBorrow,
"0"
)
);
assert!(
venue
.advance_time(boundary(0) + TimeDelta::seconds(1))
.is_empty()
);
assert_eq!(venue.next_deadline(), Some(boundary(1)));
}
#[test]
fn a_resting_sell_is_charged_when_placed_reported_at_its_cancel_which_repays_interest_first() {
let mut venue = margin_market_venue("0", "100000")
.with_margin_interest(Arc::new(hourly_interest(base())));
let outcome = venue.open_order(limit_request("rest", Side::Sell, "1", "50000", gtc()));
assert_eq!(
sans_debt(&outcome.events).len(),
1,
"the hold's balance alone: nothing traded, so the interest has no position to belong \
to yet: {:?}",
outcome.events
);
assert_eq!(
balance_of(sans_debt(&outcome.events)[0]).balance,
Balance::new_margin(d("1"), Decimal::ZERO, d("1"), d("0.0001")),
"the balance already carries the charge"
);
advance(&mut venue, time(5));
let cancelled = venue.cancel_order(OrderRequestCancel {
key: limit_request("rest", Side::Sell, "1", "50000", gtc()).key,
state: RequestCancel { id: None },
});
assert_eq!(
balance_of(sans_debt(&cancelled.events)[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("0.0001"), Decimal::ZERO),
"the released base repays the interest first, so as much principal stays owed"
);
assert_eq!(
*cash_flow_of(sans_debt(&cancelled.events)[1]),
interest_flow(
base(),
"1",
"0.0001",
time(5),
InterestTiming::OnBorrow,
"0"
),
"reported with the cancel, the order's end, stamped then"
);
assert_eq!(sans_debt(&cancelled.events).len(), 2);
assert_eq!(venue.next_deadline(), Some(boundary(0)));
}
#[test]
fn a_held_buy_reports_the_interest_of_its_hold_and_of_its_bar_after_its_fill() {
let mut venue =
margin_venue_holding().with_margin_interest(Arc::new(hourly_interest(quote())));
let outcome = venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
assert_eq!(
sans_debt(&outcome.events).len(),
1,
"the hold's balance alone: the interest waits for the fill: {:?}",
outcome.events
);
assert_eq!(
balance_of(sans_debt(&outcome.events)[0]).balance,
Balance::new_margin(d("150"), Decimal::ZERO, d("50"), d("0.005"))
);
let events = feed_bar(&mut venue, 2, ohlc("120", "121", "119", "120"));
let events = sans_debt(&events);
assert_eq!(
balance_of(events[0]).balance,
Balance::new_margin(Decimal::ZERO, Decimal::ZERO, d("80"), d("0.008"))
);
assert_eq!(trade_of(events[2]).price, d("120"));
assert_eq!(
*cash_flow_of(events[3]),
interest_flow(
quote(),
"50",
"0.005",
time(2),
InterestTiming::OnBorrow,
"0"
),
"the hold's charge after the fill, stamped at the fill, so a position entered then \
takes it"
);
assert_eq!(
cash_flow_of(events[4]).amount,
d("-0.003"),
"then what the bar borrowed"
);
assert_eq!(cash_flow_of(events[4]).time_exchange, time(2));
assert_eq!(events.len(), 6, "and the filled order last: {events:?}");
}
#[test]
fn a_resting_sell_that_borrowed_reports_its_interest_after_its_fill() {
let mut venue = margin_market_venue("0", "100000")
.with_margin_interest(Arc::new(hourly_interest(base())));
venue.open_order(limit_request("rest", Side::Sell, "1", "50000", gtc()));
advance(&mut venue, time(7));
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("50100", "50200")),
time(7),
);
let events = sans_debt(&events);
assert_eq!(trade_of(events[2]).quantity, d("1"));
assert_eq!(
*cash_flow_of(events[3]),
interest_flow(
base(),
"1",
"0.0001",
time(7),
InterestTiming::OnBorrow,
"0"
),
"after the fill that opened the short, stamped at it"
);
assert_eq!(events.len(), 5, "and the filled order last: {events:?}");
}
#[test]
fn a_held_buy_unaffordable_at_its_bar_reports_its_interest_with_its_cancel() {
let mut venue =
margin_venue_holding().with_margin_interest(Arc::new(hourly_interest(quote())));
venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
let events = feed_bar(&mut venue, 2, ohlc("250", "251", "249", "250"));
let events = sans_debt(&events);
assert_eq!(
*cash_flow_of(events[1]),
interest_flow(
quote(),
"50",
"0.005",
time(2),
InterestTiming::OnBorrow,
"0"
),
"the order never traded, so its end reports the charge, stamped then"
);
assert!(matches!(
order_of(events[2]).state,
OrderState::Inactive(InactiveOrderState::Cancelled(_))
));
}
#[test]
fn a_held_sell_that_expires_reports_its_interest_before_its_end() {
let mut venue = margin_venue_holding_with(|config| config, MarginEffect::Borrow)
.with_margin_interest(Arc::new(hourly_interest(base())));
venue.open_order(market_order("held", Side::Sell, "1", gtc()));
let events = venue.expire_held(time(2));
let events = sans_debt(&events);
assert_eq!(
*cash_flow_of(events[1]),
interest_flow(
base(),
"1",
"0.0001",
time(2),
InterestTiming::OnBorrow,
"0"
)
);
assert!(matches!(
order_of(events[2]).state,
OrderState::Inactive(InactiveOrderState::Expired(_))
));
}
#[test]
fn a_held_buy_whose_bar_repays_its_whole_loan_owes_no_more_boundaries() {
let free_loans = PeriodicMarginInterest::new(TimeDelta::hours(1), DayCount::Act365)
.with_rates(quote(), RateSeries::constant(Decimal::ZERO));
let mut venue = margin_venue_holding().with_margin_interest(Arc::new(free_loans));
venue.open_order(market_order("held", Side::Buy, "1.5", gtc()));
assert_eq!(venue.next_deadline(), Some(boundary(0)));
let events = feed_bar(&mut venue, 2, ohlc("60", "61", "59", "60"));
assert_eq!(
balance_of(&events[0]).balance,
Balance::new_margin(d("10"), d("10"), Decimal::ZERO, Decimal::ZERO)
);
assert_eq!(venue.next_deadline(), None);
}
#[test]
fn a_seeded_loan_is_charged_at_boundaries_from_the_clock_s_first_move_only() {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
),
..config_from_balances(
vec![
AssetBalance::new(
base(),
Balance::new_margin(d("0.5"), d("0.5"), d("0.5"), Decimal::ZERO),
Utc::now(),
),
funded(quote(), d("100000")),
],
FeeModelConfig::default(),
)
};
let mut venue = SimulatedVenue::new(&config, spot_instruments())
.with_margin_interest(Arc::new(hourly_interest(base())));
assert_eq!(
venue.next_deadline(),
None,
"charging starts when the clock moves"
);
advance(&mut venue, time(1));
assert_eq!(venue.next_deadline(), Some(boundary(0)));
let events = venue.advance_time(boundary(0));
assert_eq!(
*charges(&events)[0].1,
interest_flow(
base(),
"0.5",
"0.00005",
boundary(0),
InterestTiming::Periodic,
"0"
)
);
}
#[test]
fn at_borrow_only_charges_the_borrow_and_no_boundary() {
let mut venue = margin_venue("0.25", "100000", "3").with_margin_interest(Arc::new(
hourly_interest(base()).with_schedule(InterestSchedule::AtBorrowOnly),
));
advance(&mut venue, time(1));
let outcome = venue.open_order(sell_request("1", market_prices("50000")));
assert_eq!(
filled_balance(&outcome).balance.margin.unwrap().interest,
d("0.000075")
);
assert_eq!(venue.next_deadline(), None);
assert!(venue.advance_time(boundary(3)).is_empty());
}
#[test]
#[should_panic(expected = "without MockExecutionConfig::spot_margin")]
fn a_cash_venue_given_an_interest_model_panics() {
let _ = make_venue("1", "100000").with_margin_interest(Arc::new(hourly_interest(base())));
}
#[derive(Debug)]
struct Misbehaving;
impl MarginInterestModel for Misbehaving {
fn next_boundary(
&self,
_: &AssetNameExchange,
after: DateTime<Utc>,
) -> Option<DateTime<Utc>> {
Some(after)
}
fn charge(
&self,
_: &AssetNameExchange,
_: Decimal,
_: InterestDue,
) -> Option<InterestCharge> {
Some(InterestCharge::new(d("-0.00000001"), None))
}
}
#[derive(Debug)]
struct FlatAtBorrow;
impl MarginInterestModel for FlatAtBorrow {
fn next_boundary(&self, _: &AssetNameExchange, _: DateTime<Utc>) -> Option<DateTime<Utc>> {
None
}
fn charge(
&self,
_: &AssetNameExchange,
_: Decimal,
due: InterestDue,
) -> Option<InterestCharge> {
matches!(due, InterestDue::Borrow(_))
.then(|| InterestCharge::new(d("0.00000001"), None))
}
}
#[test]
fn an_order_that_borrows_nothing_is_not_asked_about_interest() {
let mut venue =
margin_venue("0.25", "100000", "3").with_margin_interest(Arc::new(FlatAtBorrow));
advance(&mut venue, time(1));
let outcome = venue.open_order(sell_request("0.1", market_prices("50000")));
assert_eq!(filled_trade(&outcome).quantity, d("0.1"));
assert!(
!outcome
.events
.iter()
.any(|event| matches!(event.kind, AccountEventKind::CashFlow(_))),
"0.1 of the 0.25 held lends nothing, so nothing is charged"
);
}
#[test]
#[should_panic(expected = "charged negative interest")]
fn a_model_charging_negative_interest_panics() {
let mut venue =
margin_venue("0.25", "100000", "3").with_margin_interest(Arc::new(Misbehaving));
advance(&mut venue, time(1));
let _ = venue.open_order(sell_request("1", market_prices("50000")));
}
#[test]
#[should_panic(expected = "which is not after")]
fn a_model_whose_boundary_is_not_after_the_instant_asked_panics() {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
),
..config_from_balances(
vec![
AssetBalance::new(
base(),
Balance::new_margin(d("0.5"), d("0.5"), d("0.5"), Decimal::ZERO),
Utc::now(),
),
funded(quote(), d("100000")),
],
FeeModelConfig::default(),
)
};
let mut venue = SimulatedVenue::new(&config, spot_instruments())
.with_margin_interest(Arc::new(Misbehaving));
let _ = venue.advance_time(time(1));
}
fn bnb() -> AssetNameExchange {
AssetNameExchange::new("BNB")
}
fn spot_instrument(
name: &str,
base: &str,
quote: &str,
) -> Instrument<ExchangeId, AssetNameExchange> {
Instrument {
exchange: EXCHANGE,
name_internal: InstrumentNameInternal::new(name.to_lowercase()),
name_exchange: InstrumentNameExchange::new(name),
underlying: Underlying {
base: AssetNameExchange::new(base),
quote: AssetNameExchange::new(quote),
},
quote: InstrumentQuoteAsset::UnderlyingQuote,
kind: InstrumentKind::Spot,
spec: None,
data_venue: None,
}
}
fn fee_asset_instruments()
-> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
let mut instruments = spot_instruments();
let bnb_usdt = spot_instrument("BNBUSDT", "BNB", "USDT");
instruments.insert(bnb_usdt.name_exchange.clone(), bnb_usdt);
instruments
}
fn fee_asset_config(bnb: &str) -> MockExecutionConfig {
MockExecutionConfig {
fee_asset: Some(FeeAssetConfig::new(self::bnb(), d("0.25")).unwrap()),
..config_from_balances(
vec![
funded(base(), d("10")),
funded(quote(), d("100000")),
funded(self::bnb(), d(bnb)),
],
maker_taker_fee(),
)
}
}
fn fee_asset_venue_from(config: &MockExecutionConfig, priced: bool) -> SimulatedVenue {
let mut venue = SimulatedVenue::new_market_driven(config, fee_asset_instruments());
advance(&mut venue, time(1));
if priced {
assert!(
venue
.apply_market(
&InstrumentNameExchange::new("BNBUSDT"),
MarketObservation::new(book("599", "601")),
time(1)
)
.is_empty()
);
}
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(traded("50000")),
time(1)
)
.is_empty()
);
venue
}
fn fee_asset_venue(bnb: &str) -> SimulatedVenue {
fee_asset_venue_from(&fee_asset_config(bnb), true)
}
fn restated(events: &[UnindexedAccountEvent]) -> Vec<(AssetNameExchange, Balance)> {
events
.iter()
.filter_map(|event| match &event.kind {
AccountEventKind::BalanceSnapshot(balance) => {
Some((balance.0.asset.clone(), balance.0.balance))
}
_ => None,
})
.collect()
}
#[test]
fn a_fill_pays_its_fee_in_the_fee_asset_at_a_discount() {
let mut venue = fee_asset_venue("1");
let outcome = venue.open_order(buy_request("1", None));
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(bnb(), d("0.0625"), Some(d("37.5")))
);
assert_eq!(
restated(&outcome.events),
vec![
(quote(), Balance::new(d("50000"), d("50000"))),
(base(), Balance::new(d("11"), d("11"))),
(bnb(), Balance::new(d("0.9375"), d("0.9375"))),
]
);
}
#[test]
fn a_sell_receives_its_whole_proceeds_and_pays_its_fee_in_the_fee_asset() {
let mut venue = fee_asset_venue("1");
let outcome = venue.open_order(sell_request("1", None));
assert_eq!(filled_trade(&outcome).fees.asset, bnb());
assert_eq!(
restated(&outcome.events),
vec![
(base(), Balance::new(d("9"), d("9"))),
(quote(), Balance::new(d("150000"), d("150000"))),
(bnb(), Balance::new(d("0.9375"), d("0.9375"))),
]
);
}
#[test]
fn a_fee_asset_balance_short_of_the_fee_falls_back_whole() {
for bnb in ["0.0624", "0"] {
let mut venue = fee_asset_venue(bnb);
let outcome = venue.open_order(buy_request("1", None));
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(quote(), d("50"), Some(d("50"))),
"{bnb} BNB"
);
assert_eq!(
restated(&outcome.events),
vec![
(quote(), Balance::new(d("49950"), d("49950"))),
(base(), Balance::new(d("11"), d("11"))),
],
"{bnb} BNB"
);
}
}
#[test]
fn a_fee_asset_balance_of_exactly_the_fee_pays_it() {
let mut venue = fee_asset_venue("0.0625");
let outcome = venue.open_order(buy_request("1", None));
assert_eq!(filled_trade(&outcome).fees.asset, bnb());
assert_eq!(balance_now(&venue, bnb()), Balance::new(d("0"), d("0")));
}
#[test]
fn a_fee_asset_the_venue_cannot_price_falls_back() {
let mut venue = fee_asset_venue_from(&fee_asset_config("1"), false);
let outcome = venue.open_order(buy_request("1", None));
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(quote(), d("50"), Some(d("50")))
);
assert_eq!(balance_now(&venue, bnb()), Balance::new(d("1"), d("1")));
}
#[test]
fn a_fill_on_the_fee_assets_own_instrument_falls_back() {
let mut venue = fee_asset_venue("1");
assert!(
venue
.apply_market(
&InstrumentNameExchange::new("BNBUSDT"),
MarketObservation::new(traded("600")),
time(1)
)
.is_empty()
);
let mut request = buy_request("1", None);
request.key.instrument = InstrumentNameExchange::new("BNBUSDT");
let outcome = venue.open_order(request);
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(quote(), d("0.6"), Some(d("0.6")))
);
assert_eq!(balance_now(&venue, bnb()), Balance::new(d("2"), d("2")));
}
#[test]
fn a_resting_fill_paid_in_the_fee_asset_releases_the_fee_it_held() {
let mut venue = fee_asset_venue("1");
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("49000", "49100")),
time(1)
)
.is_empty()
);
let outcome = venue.open_order(limit_request("resting", Side::Buy, "1", "49000", gtc()));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new(d("100000"), d("50990.2"))
);
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("48900", "48950")),
time(2),
);
let trade = events
.iter()
.find_map(|event| match &event.kind {
AccountEventKind::Trade(trade) => Some(trade),
_ => None,
})
.unwrap();
assert_eq!(
trade.fees,
AssetFees::new(bnb(), d("0.01225"), Some(d("7.35")))
);
assert_eq!(
restated(&events),
vec![
(quote(), Balance::new(d("51000"), d("51000"))),
(base(), Balance::new(d("11"), d("11"))),
(bnb(), Balance::new(d("0.98775"), d("0.98775"))),
]
);
}
#[test]
fn under_received_a_buy_paying_in_the_fee_asset_receives_its_whole_quantity() {
let config = MockExecutionConfig {
spot_fee_asset: SpotFeeAsset::Received,
..fee_asset_config("1")
};
let mut venue = fee_asset_venue_from(&config, true);
let outcome = venue.open_order(buy_request("1", None));
assert_eq!(filled_trade(&outcome).fees.asset, bnb());
assert_eq!(balance_now(&venue, base()).total, d("11"));
assert_eq!(balance_now(&venue, quote()).total, d("50000"));
}
#[test]
fn a_zero_fee_is_charged_without_the_fee_asset() {
let config = MockExecutionConfig {
fee_model: FeeModelConfig::default(),
..fee_asset_config("1")
};
let mut venue = fee_asset_venue_from(&config, true);
let outcome = venue.open_order(buy_request("1", None));
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(quote(), Decimal::ZERO, Some(Decimal::ZERO))
);
assert_eq!(balance_now(&venue, bnb()), Balance::new(d("1"), d("1")));
}
#[test]
fn a_full_discount_charges_nothing_and_needs_no_fee_asset_balance() {
let config = MockExecutionConfig {
fee_asset: Some(FeeAssetConfig::new(bnb(), Decimal::ONE).unwrap()),
..config_from_balances(
vec![funded(base(), d("10")), funded(quote(), d("100000"))],
maker_taker_fee(),
)
};
let mut venue = fee_asset_venue_from(&config, true);
let outcome = venue.open_order(buy_request("1", None));
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(bnb(), Decimal::ZERO, Some(Decimal::ZERO))
);
assert_eq!(
restated(&outcome.events),
vec![
(quote(), Balance::new(d("50000"), d("50000"))),
(base(), Balance::new(d("11"), d("11"))),
]
);
}
#[test]
fn a_held_market_order_pays_its_fee_in_the_fee_asset() {
let config = MockExecutionConfig {
market_order_timing: MarketOrderTiming::NextBar(NextBarPrice::Open),
..fee_asset_config("1")
};
let mut venue = SimulatedVenue::new_market_driven(&config, fee_asset_instruments());
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&InstrumentNameExchange::new("BNBUSDT"),
MarketObservation::new(book("599", "601")),
time(1)
)
.is_empty()
);
assert!(feed_bar(&mut venue, 1, ohlc("95", "105", "90", "100")).is_empty());
let held = venue.open_order(ioc_buy("held", "1"));
assert!(held.response.state.is_accepted());
advance(&mut venue, time(2));
let events = feed_bar(&mut venue, 2, ohlc("200", "210", "190", "205"));
assert_eq!(
restated(&events),
vec![
(quote(), Balance::new(d("99800"), d("99800"))),
(base(), Balance::new(d("11"), d("11"))),
(bnb(), Balance::new(d("0.99975"), d("0.99975"))),
]
);
}
#[test]
fn a_fill_on_an_instrument_quoted_in_the_fee_asset_falls_back() {
let mut instruments = fee_asset_instruments();
let eth_bnb = spot_instrument("ETHBNB", "ETH", "BNB");
instruments.insert(eth_bnb.name_exchange.clone(), eth_bnb);
let eth = || AssetNameExchange::new("ETH");
let config = MockExecutionConfig {
fee_asset: Some(FeeAssetConfig::new(bnb(), d("0.25")).unwrap()),
..config_from_balances(
vec![
funded(base(), d("10")),
funded(quote(), d("100000")),
funded(bnb(), d("10")),
funded(eth(), d("1")),
],
maker_taker_fee(),
)
};
for (side, eth_after, bnb_after) in [
(Side::Buy, "2", "9.4995"),
(Side::Sell, "0", "10.4995"),
] {
let mut venue = SimulatedVenue::new_market_driven(&config, instruments.clone());
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&InstrumentNameExchange::new("ETHBNB"),
MarketObservation::new(traded("0.5")),
time(1)
)
.is_empty()
);
let mut request = match side {
Side::Buy => buy_request("1", None),
Side::Sell => sell_request("1", None),
};
request.key.instrument = InstrumentNameExchange::new("ETHBNB");
let outcome = venue.open_order(request);
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(bnb(), d("0.0005"), Some(d("0.0005"))),
"{side:?}"
);
assert_eq!(balance_now(&venue, eth()).total, d(eth_after), "{side:?}");
assert_eq!(balance_now(&venue, bnb()).total, d(bnb_after), "{side:?}");
}
}
#[test]
fn a_negative_fee_is_paid_without_the_fee_asset() {
let rebate =
FeeModelConfig::Percentage(PercentageFeeModel::maker_taker(d("-0.0001"), d("-0.0001")));
let config = MockExecutionConfig {
fee_model: rebate,
..fee_asset_config("1")
};
let mut venue = fee_asset_venue_from(&config, true);
let outcome = venue.open_order(buy_request("1", None));
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(quote(), d("-5"), Some(d("-5")))
);
assert_eq!(balance_now(&venue, quote()).total, d("50005"));
assert_eq!(balance_now(&venue, bnb()), Balance::new(d("1"), d("1")));
}
#[test]
fn the_fee_asset_is_priced_by_a_reciprocal_pair_or_the_pair_first_by_name() {
let usdt_bnb = spot_instrument("USDTBNB", "USDT", "BNB");
let feed = |venue: &mut SimulatedVenue, name: &str, price: &str| {
assert!(
venue
.apply_market(
&InstrumentNameExchange::new(name),
MarketObservation::new(traded(price)),
time(1)
)
.is_empty()
);
};
let mut instruments = spot_instruments();
instruments.insert(usdt_bnb.name_exchange.clone(), usdt_bnb.clone());
let mut venue = SimulatedVenue::new_market_driven(&fee_asset_config("1"), instruments);
advance(&mut venue, time(1));
feed(&mut venue, "USDTBNB", "0.0016");
feed(&mut venue, "BTCUSDT", "50000");
let outcome = venue.open_order(buy_request("1", None));
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(bnb(), d("0.06"), Some(d("37.5")))
);
let mut instruments = fee_asset_instruments();
instruments.insert(usdt_bnb.name_exchange.clone(), usdt_bnb);
let mut venue = SimulatedVenue::new_market_driven(&fee_asset_config("1"), instruments);
advance(&mut venue, time(1));
feed(&mut venue, "BNBUSDT", "600");
feed(&mut venue, "USDTBNB", "0.0016");
feed(&mut venue, "BTCUSDT", "50000");
let outcome = venue.open_order(buy_request("1", None));
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(bnb(), d("0.0625"), Some(d("37.5")))
);
}
#[test]
fn a_partly_filled_order_pays_its_fill_in_the_fee_asset_and_holds_the_rest_in_quote() {
let mut venue = fee_asset_venue("1");
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("49000", "49100"))
.with_depth(MarketDepth::new(None, Some(d("0.5")))),
time(1),
)
.is_empty()
);
let outcome = venue.open_order(limit_request("part", Side::Buy, "1", "49100", gtc()));
assert_eq!(rested_open(&outcome).filled_quantity, d("0.5"));
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(bnb(), d("0.0306875"), Some(d("18.4125")))
);
assert_eq!(
balance_now(&venue, quote()),
Balance::new(d("75450"), d("50895.09"))
);
assert_eq!(
balance_now(&venue, bnb()),
Balance::new(d("0.9693125"), d("0.9693125"))
);
}
fn fee_asset_margin_venue(bnb: &str) -> SimulatedVenue {
let config = MockExecutionConfig {
spot_margin: Some(
SpotMarginConfig::new(quote(), d("3"), MarginEffect::Borrow).unwrap(),
),
fee_asset: Some(FeeAssetConfig::new(self::bnb(), d("0.25")).unwrap()),
..config_from_balances(
vec![
funded(base(), Decimal::ZERO),
funded(quote(), d("10000")),
funded(self::bnb(), d(bnb)),
],
maker_taker_fee(),
)
};
fee_asset_venue_from(&config, true).with_margin_interest(Arc::new(hourly_interest(quote())))
}
#[test]
fn a_margin_buy_paying_in_the_fee_asset_borrows_only_its_notional() {
let mut venue = fee_asset_margin_venue("1");
let outcome = venue.open_order(buy_request("0.3", None));
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(bnb(), d("0.01875"), Some(d("11.25")))
);
let paid = balance_of(sans_debt(&outcome.events)[0]);
assert_eq!(paid.asset, quote());
assert_eq!(
paid.balance.margin.map(|margin| margin.borrowed),
Some(d("5000"))
);
assert_eq!(balance_of(sans_debt(&outcome.events)[1]).asset, base());
assert_eq!(
*balance_of(sans_debt(&outcome.events)[2]),
AssetBalance::new(
bnb(),
Balance::new_margin(d("0.98125"), d("0.98125"), d("0"), d("0")),
time(1)
)
);
assert_eq!(trade_of(sans_debt(&outcome.events)[3]).quantity, d("0.3"));
assert_eq!(
*cash_flow_of(sans_debt(&outcome.events)[4]),
interest_flow(
quote(),
"5000",
"0.5",
time(1),
InterestTiming::OnBorrow,
"0"
)
);
}
#[test]
fn a_margin_account_short_of_the_fee_asset_borrows_the_fallback_fee_not_the_fee_asset() {
let mut venue = fee_asset_margin_venue("0");
let outcome = venue.open_order(buy_request("0.3", None));
assert_eq!(
filled_trade(&outcome).fees,
AssetFees::new(quote(), d("15"), Some(d("15")))
);
assert_eq!(
balance_now(&venue, quote())
.margin
.map(|margin| margin.borrowed),
Some(d("5015"))
);
assert_eq!(
balance_now(&venue, bnb()),
Balance::new_margin(d("0"), d("0"), d("0"), d("0"))
);
}
#[test]
fn a_resting_margin_fill_paying_in_the_fee_asset_repays_the_fee_it_borrowed() {
let mut venue = fee_asset_margin_venue("1");
assert!(
venue
.apply_market(
&instrument_name(),
MarketObservation::new(book("49000", "49100")),
time(1)
)
.is_empty()
);
let outcome = venue.open_order(limit_request("resting", Side::Buy, "0.3", "49000", gtc()));
assert_eq!(
filled_balance(&outcome).balance,
Balance::new_margin(d("14702.94"), d("0"), d("4702.94"), d("0.470294"))
);
let events = venue.apply_market(
&instrument_name(),
MarketObservation::new(book("48900", "48950")),
time(2),
);
let trade = events
.iter()
.find_map(|event| match &event.kind {
AccountEventKind::Trade(trade) => Some(trade),
_ => None,
})
.unwrap();
assert_eq!(
trade.fees,
AssetFees::new(bnb(), d("0.003675"), Some(d("2.205")))
);
assert_eq!(
balance_now(&venue, quote()),
Balance::new_margin(d("0"), d("0"), d("4700.470294"), d("0"))
);
assert_eq!(balance_now(&venue, bnb()).total, d("0.996325"));
}
#[test]
fn a_cfd_fill_is_charged_without_the_fee_asset() {
let mut instruments = instruments_of(cfd_instrument());
let bnb_usd = spot_instrument("BNBUSD", "BNB", "usd");
instruments.insert(bnb_usd.name_exchange.clone(), bnb_usd);
let config = MockExecutionConfig {
fee_asset: Some(FeeAssetConfig::new(bnb(), d("0.25")).unwrap()),
..config_from_balances(
vec![funded(usd(), d("1000000")), funded(bnb(), d("10"))],
maker_taker_fee(),
)
};
let mut venue = SimulatedVenue::new_market_driven(&config, instruments);
advance(&mut venue, time(1));
assert!(
venue
.apply_market(
&InstrumentNameExchange::new("BNBUSD"),
MarketObservation::new(traded("600")),
time(1)
)
.is_empty()
);
assert!(cfd_market(&mut venue, "4999", "5001", "5000").is_empty());
let outcome = venue.open_order(cfd_request(Side::Buy, "1", None));
assert_eq!(filled_trade(&outcome).fees.asset, usd());
assert_eq!(balance_now(&venue, bnb()).total, d("10"));
}
fn eth() -> AssetNameExchange {
AssetNameExchange::new("ETH")
}
fn eth_instrument_name() -> InstrumentNameExchange {
InstrumentNameExchange::new("ETHUSDT")
}
fn btc_and_eth_instruments()
-> FnvHashMap<InstrumentNameExchange, Instrument<ExchangeId, AssetNameExchange>> {
let mut instruments = spot_instruments();
instruments.insert(
eth_instrument_name(),
Instrument {
exchange: EXCHANGE,
name_internal: InstrumentNameInternal::new("ethusdt"),
name_exchange: eth_instrument_name(),
underlying: Underlying {
base: eth(),
quote: quote(),
},
quote: InstrumentQuoteAsset::UnderlyingQuote,
kind: InstrumentKind::Spot,
spec: None,
data_venue: None,
},
);
instruments
}
mod borrow_capacity;
mod collateral_ratio;
mod liquidation;
}