use crate::{
UnindexedAccountSnapshot,
balance::AssetBalance,
exchange::mock::orders::{
AlreadyResting, OpenOrder, OpenOrders, Reservation, RestingOrder, as_open,
},
order::{
Order, UnindexedInactiveOrder,
id::ClientOrderId,
state::{Cancelled, Expired, Filled, InactiveOrderState, Open, OrderState},
},
trade::Trade,
};
use chrono::{DateTime, Utc};
use fnv::FnvHashMap;
use rust_decimal::Decimal;
use rustrade_instrument::{
asset::name::AssetNameExchange, exchange::ExchangeId, instrument::name::InstrumentNameExchange,
};
use thiserror::Error;
#[derive(Debug)]
pub struct AccountState {
balances: FnvHashMap<AssetNameExchange, AssetBalance<AssetNameExchange>>,
orders_open: OpenOrders,
orders_cancelled:
FnvHashMap<ClientOrderId, Order<ExchangeId, InstrumentNameExchange, Cancelled>>,
orders_filled: FnvHashMap<ClientOrderId, Order<ExchangeId, InstrumentNameExchange, Filled>>,
orders_expired: FnvHashMap<ClientOrderId, Order<ExchangeId, InstrumentNameExchange, Expired>>,
trades: Vec<Trade<AssetNameExchange, InstrumentNameExchange>>,
}
impl AccountState {
pub fn new(
balances: FnvHashMap<AssetNameExchange, AssetBalance<AssetNameExchange>>,
orders_open: OpenOrders,
orders_cancelled: FnvHashMap<
ClientOrderId,
Order<ExchangeId, InstrumentNameExchange, Cancelled>,
>,
trades: Vec<Trade<AssetNameExchange, InstrumentNameExchange>>,
) -> Self {
Self {
balances,
orders_open,
orders_cancelled,
orders_filled: FnvHashMap::default(),
orders_expired: FnvHashMap::default(),
trades,
}
}
pub fn orders(&self) -> &OpenOrders {
&self.orders_open
}
pub fn remove_order(&mut self, cid: &ClientOrderId) -> Option<RestingOrder> {
self.orders_open.remove(cid)
}
pub fn book(
&mut self,
order: OpenOrder,
reservation: Option<Reservation>,
) -> Result<(), AlreadyResting> {
let cid = order.key.cid.clone();
self.orders_open.insert(order, reservation)?;
self.forget_ended(&cid);
Ok(())
}
fn forget_ended(&mut self, cid: &ClientOrderId) {
self.orders_cancelled.remove(cid);
self.orders_filled.remove(cid);
self.orders_expired.remove(cid);
}
pub fn ack_cancelled(&mut self, order: Order<ExchangeId, InstrumentNameExchange, Cancelled>) {
self.forget_ended(&order.key.cid);
self.orders_cancelled.insert(order.key.cid.clone(), order);
}
pub fn is_cancelled(&self, cid: &ClientOrderId) -> bool {
self.orders_cancelled.contains_key(cid)
}
pub fn ack_filled(&mut self, order: Order<ExchangeId, InstrumentNameExchange, Filled>) {
self.forget_ended(&order.key.cid);
self.orders_filled.insert(order.key.cid.clone(), order);
}
pub fn is_filled(&self, cid: &ClientOrderId) -> bool {
self.orders_filled.contains_key(cid)
}
pub fn order_ended(&self, cid: &ClientOrderId) -> Option<UnindexedInactiveOrder> {
if self.orders_open.contains(cid) {
None
} else if let Some(filled) = self.orders_filled.get(cid) {
Some(filled.clone().map_state(InactiveOrderState::FullyFilled))
} else if let Some(cancelled) = self.orders_cancelled.get(cid) {
Some(cancelled.clone().map_state(InactiveOrderState::Cancelled))
} else {
self.orders_expired
.get(cid)
.map(|expired| expired.clone().map_state(InactiveOrderState::Expired))
}
}
pub fn ack_expired(&mut self, order: Order<ExchangeId, InstrumentNameExchange, Expired>) {
self.forget_ended(&order.key.cid);
self.orders_expired.insert(order.key.cid.clone(), order);
}
pub fn is_expired(&self, cid: &ClientOrderId) -> bool {
self.orders_expired.contains_key(cid)
}
pub fn update_time_exchange(&mut self, time_exchange: DateTime<Utc>) {
for balance in self.balances.values_mut() {
balance.time_exchange = time_exchange;
}
}
pub fn balances(&self) -> impl Iterator<Item = &AssetBalance<AssetNameExchange>> + '_ {
self.balances.values()
}
pub fn orders_open(
&self,
) -> impl Iterator<Item = &Order<ExchangeId, InstrumentNameExchange, Open>> + '_ {
self.orders_open.iter()
}
pub fn orders_cancelled(
&self,
) -> impl Iterator<Item = &Order<ExchangeId, InstrumentNameExchange, Cancelled>> + '_ {
self.orders_cancelled.values()
}
pub fn orders_expired(
&self,
) -> impl Iterator<Item = &Order<ExchangeId, InstrumentNameExchange, Expired>> + '_ {
self.orders_expired.values()
}
pub fn trades(
&self,
time_since: DateTime<Utc>,
) -> impl Iterator<Item = &Trade<AssetNameExchange, InstrumentNameExchange>> + '_ {
self.trades
.iter()
.filter(move |trade| trade.time_exchange >= time_since)
}
pub fn balance_mut(
&mut self,
asset: &AssetNameExchange,
) -> Option<&mut AssetBalance<AssetNameExchange>> {
self.balances.get_mut(asset)
}
pub fn ack_trade(&mut self, trade: Trade<AssetNameExchange, InstrumentNameExchange>) {
self.trades.push(trade);
}
pub fn reserve(
&mut self,
asset: &AssetNameExchange,
amount: Decimal,
time_exchange: DateTime<Utc>,
) -> Result<AssetBalance<AssetNameExchange>, BalanceInsufficient> {
let balance = self.balance_expect(asset);
if balance.balance.free < amount {
return Err(BalanceInsufficient {
free: balance.balance.free,
required: amount,
});
}
balance.balance.free -= amount;
balance.time_exchange = time_exchange;
Ok(balance.clone())
}
pub fn settle(
&mut self,
asset: &AssetNameExchange,
amount: Decimal,
time_exchange: DateTime<Utc>,
) -> AssetBalance<AssetNameExchange> {
let balance = self.balance_expect(asset);
debug_assert!(
balance.balance.total - amount >= balance.balance.free,
"settling {amount} of {asset} would take total {} below free {}: only a reserved \
amount may be settled",
balance.balance.total,
balance.balance.free
);
balance.balance.total -= amount;
balance.time_exchange = time_exchange;
balance.clone()
}
pub fn release(
&mut self,
asset: &AssetNameExchange,
amount: Decimal,
time_exchange: DateTime<Utc>,
) -> AssetBalance<AssetNameExchange> {
let balance = self.balance_expect(asset);
debug_assert!(
balance.balance.free + amount <= balance.balance.total,
"releasing {amount} of {asset} would take free {} above total {}: only a reserved \
amount may be released",
balance.balance.free,
balance.balance.total
);
balance.balance.free += amount;
balance.time_exchange = time_exchange;
balance.clone()
}
pub fn commit(
&mut self,
debit: &Debit,
credit: Decimal,
time_exchange: DateTime<Utc>,
) -> Result<AssetBalance<AssetNameExchange>, BalanceInsufficient> {
debug_assert!(
!credit.is_sign_negative(),
"a credit of {credit} {} is negative: a loss beyond the margin is debited",
debit.asset
);
let required = debit.settled + debit.reserved;
let free = self.balance_expect(&debit.asset).balance.free;
if free + credit < required {
return Err(BalanceInsufficient {
free: free + credit,
required,
});
}
if !credit.is_zero() {
let balance = self.balance_expect(&debit.asset);
balance.balance.total += credit;
balance.balance.free += credit;
}
self.reserve(&debit.asset, required, time_exchange)?;
Ok(self.settle(&debit.asset, debit.settled, time_exchange))
}
#[allow(clippy::expect_used)] fn balance_expect(
&mut self,
asset: &AssetNameExchange,
) -> &mut AssetBalance<AssetNameExchange> {
self.balances
.get_mut(asset)
.expect("SimulatedVenue has Balance for all configured Instrument assets")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Debit {
pub asset: AssetNameExchange,
pub settled: Decimal,
pub reserved: Decimal,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct BalanceInsufficient {
pub free: Decimal,
pub required: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("account snapshot lists {0} more than once among its open and cancelled orders")]
pub struct DuplicateSeededOrder(pub ClientOrderId);
impl TryFrom<UnindexedAccountSnapshot> for AccountState {
type Error = DuplicateSeededOrder;
fn try_from(value: UnindexedAccountSnapshot) -> Result<Self, Self::Error> {
let UnindexedAccountSnapshot {
exchange: _,
balances,
instruments,
} = value;
let balances = balances
.into_iter()
.map(|asset_balance| (asset_balance.asset.clone(), asset_balance))
.collect();
let mut orders_open = OpenOrders::default();
let mut orders_cancelled = FnvHashMap::default();
for order in instruments.into_iter().flat_map(|snapshot| snapshot.orders) {
match &order.state {
OrderState::Active(_) => {
let Some(open) = as_open(order) else {
continue;
};
let cid = open.key.cid.clone();
if orders_cancelled.contains_key(&cid) {
return Err(DuplicateSeededOrder(cid));
}
orders_open
.insert(open, None)
.map_err(|_| DuplicateSeededOrder(cid))?;
}
OrderState::Inactive(InactiveOrderState::Cancelled(cancelled)) => {
let cid = order.key.cid.clone();
if orders_open.contains(&cid) || orders_cancelled.contains_key(&cid) {
return Err(DuplicateSeededOrder(cid));
}
let cancelled = cancelled.clone();
orders_cancelled.insert(
cid,
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,
},
);
}
OrderState::Inactive(_) => {}
}
}
Ok(Self {
balances,
orders_open,
orders_cancelled,
orders_filled: FnvHashMap::default(),
orders_expired: FnvHashMap::default(),
trades: vec![],
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use crate::{
balance::Balance,
exchange::mock::fixtures::{funded, seeded_gtd, spot_config_holding},
order::{
UnindexedOrder,
id::OrderId,
state::{Cancelled, Filled},
},
};
use rust_decimal_macros::dec;
fn usd() -> AssetNameExchange {
AssetNameExchange::new("usd")
}
fn account_with_usd(amount: Decimal) -> AccountState {
AccountState::new(
FnvHashMap::from_iter([(usd(), funded(usd(), amount))]),
OpenOrders::default(),
FnvHashMap::default(),
vec![],
)
}
fn usd_balance(account: &AccountState) -> Balance {
let Some(balance) = account.balances().find(|balance| balance.asset == usd()) else {
panic!("the account holds usd");
};
balance.balance
}
#[test]
fn a_credit_counts_towards_the_requirement_it_arrives_with() {
let mut account = account_with_usd(dec!(100));
let debit = Debit {
asset: usd(),
settled: dec!(250),
reserved: dec!(40),
};
let Ok(balance) = account.commit(&debit, dec!(200), DateTime::<Utc>::MIN_UTC) else {
panic!("100 free plus a 200 credit covers 290");
};
assert_eq!(balance.balance, Balance::new(dec!(50), dec!(10)));
assert_eq!(usd_balance(&account), Balance::new(dec!(50), dec!(10)));
}
#[test]
fn a_refused_commit_applies_none_of_its_credit() {
let mut account = account_with_usd(dec!(100));
let debit = Debit {
asset: usd(),
settled: dec!(250),
reserved: dec!(60),
};
let Err(insufficient) = account.commit(&debit, dec!(200), DateTime::<Utc>::MIN_UTC) else {
panic!("100 free plus a 200 credit does not cover 310");
};
assert_eq!(
insufficient,
BalanceInsufficient {
free: dec!(300),
required: dec!(310),
}
);
assert_eq!(usd_balance(&account), Balance::new(dec!(100), dec!(100)));
}
fn snapshot_of(orders: impl IntoIterator<Item = UnindexedOrder>) -> UnindexedAccountSnapshot {
let mut orders = orders.into_iter();
let first = orders.next().unwrap();
let mut snapshot = spot_config_holding("1", "1000", first).initial_state;
snapshot.instruments[0].orders.extend(orders);
snapshot
}
fn open(cid: &str) -> UnindexedOrder {
let at = DateTime::<Utc>::MIN_UTC;
seeded_gtd(cid, "100", at, at + chrono::TimeDelta::days(1))
}
fn ended(
cid: &str,
state: OrderState<AssetNameExchange, InstrumentNameExchange>,
) -> UnindexedOrder {
UnindexedOrder { state, ..open(cid) }
}
fn cancelled(cid: &str) -> UnindexedOrder {
let at = DateTime::<Utc>::MIN_UTC;
ended(
cid,
OrderState::inactive(Cancelled::new(OrderId::new(cid), at, None)),
)
}
#[test]
fn a_snapshot_seeding_one_id_twice_is_refused() {
for (orders, case) in [
([open("x"), open("x")], "open twice"),
([open("x"), cancelled("x")], "open, then cancelled"),
([cancelled("x"), open("x")], "cancelled, then open"),
([cancelled("x"), cancelled("x")], "cancelled twice"),
] {
assert_eq!(
AccountState::try_from(snapshot_of(orders)).err(),
Some(DuplicateSeededOrder(ClientOrderId::new("x"))),
"{case}"
);
}
}
#[test]
fn an_order_a_snapshot_does_not_seed_does_not_clash() {
let at = DateTime::<Utc>::MIN_UTC;
let filled = ended(
"x",
OrderState::fully_filled(Filled::new(OrderId::new("x"), at, dec!(1), None)),
);
let Ok(account) = AccountState::try_from(snapshot_of([filled, open("x")])) else {
panic!("a filled order is not seeded");
};
assert!(account.orders().contains(&ClientOrderId::new("x")));
}
}