rustrade_execution/exchange/mock/account.rs
1use crate::{
2 UnindexedAccountSnapshot,
3 balance::AssetBalance,
4 exchange::mock::orders::{OpenOrders, as_open},
5 order::{
6 Order,
7 id::ClientOrderId,
8 state::{Cancelled, Expired, InactiveOrderState, Open, OrderState},
9 },
10 trade::Trade,
11};
12use chrono::{DateTime, Utc};
13use fnv::{FnvHashMap, FnvHashSet};
14use rust_decimal::Decimal;
15use rustrade_instrument::{
16 asset::name::AssetNameExchange, exchange::ExchangeId, instrument::name::InstrumentNameExchange,
17};
18
19#[derive(Debug)]
20pub struct AccountState {
21 balances: FnvHashMap<AssetNameExchange, AssetBalance<AssetNameExchange>>,
22 orders_open: OpenOrders,
23 orders_cancelled:
24 FnvHashMap<ClientOrderId, Order<ExchangeId, InstrumentNameExchange, Cancelled>>,
25 /// Orders that filled, kept only so a cancel arriving after one can say *why* it failed.
26 ///
27 /// Ids alone: nothing reads the order back, and the fill itself is already in
28 /// [`trades`](Self::trades). Grows with the run, like `trades` and `orders_cancelled` — a
29 /// simulated venue's ledgers are bounded by the dataset, not reaped.
30 orders_filled: FnvHashSet<ClientOrderId>,
31 /// Orders retired by their own deadline, kept so they reach a later account snapshot and so a
32 /// cancel arriving after one can say *why* it failed.
33 ///
34 /// Whole orders rather than ids alone, unlike [`orders_filled`](Self::orders_filled): nothing
35 /// else records an expiry, so a snapshot has no other source for it — whereas a fill is already
36 /// in [`trades`](Self::trades). Grows with the run, like `trades` and `orders_cancelled`.
37 orders_expired: FnvHashMap<ClientOrderId, Order<ExchangeId, InstrumentNameExchange, Expired>>,
38 trades: Vec<Trade<AssetNameExchange, InstrumentNameExchange>>,
39}
40
41impl AccountState {
42 pub fn new(
43 balances: FnvHashMap<AssetNameExchange, AssetBalance<AssetNameExchange>>,
44 orders_open: OpenOrders,
45 orders_cancelled: FnvHashMap<
46 ClientOrderId,
47 Order<ExchangeId, InstrumentNameExchange, Cancelled>,
48 >,
49 trades: Vec<Trade<AssetNameExchange, InstrumentNameExchange>>,
50 ) -> Self {
51 Self {
52 balances,
53 orders_open,
54 orders_cancelled,
55 orders_filled: FnvHashSet::default(),
56 orders_expired: FnvHashMap::default(),
57 trades,
58 }
59 }
60
61 /// This account's open orders, indexed for lookup and ordered for matching.
62 pub fn orders(&self) -> &OpenOrders {
63 &self.orders_open
64 }
65
66 /// This account's open orders, for a venue booking, matching or cancelling one.
67 pub fn orders_mut(&mut self) -> &mut OpenOrders {
68 &mut self.orders_open
69 }
70
71 /// Records that `order` was cancelled, so it is reported by a later account snapshot and a
72 /// second cancel for it can be told apart from a cancel for an order that never existed.
73 pub fn ack_cancelled(&mut self, order: Order<ExchangeId, InstrumentNameExchange, Cancelled>) {
74 self.orders_cancelled.insert(order.key.cid.clone(), order);
75 }
76
77 /// Whether `cid` names an order this account cancelled.
78 pub fn is_cancelled(&self, cid: &ClientOrderId) -> bool {
79 self.orders_cancelled.contains_key(cid)
80 }
81
82 /// Records that `cid` filled, so a cancel that loses the race to it can say so.
83 pub fn ack_filled(&mut self, cid: ClientOrderId) {
84 self.orders_filled.insert(cid);
85 }
86
87 /// Whether `cid` names an order this account filled.
88 pub fn is_filled(&self, cid: &ClientOrderId) -> bool {
89 self.orders_filled.contains(cid)
90 }
91
92 /// Records that `order` reached its own deadline, so it is reported by a later account snapshot
93 /// and a cancel arriving after it can be told apart from a cancel for an unknown order.
94 pub fn ack_expired(&mut self, order: Order<ExchangeId, InstrumentNameExchange, Expired>) {
95 self.orders_expired.insert(order.key.cid.clone(), order);
96 }
97
98 /// Whether `cid` names an order that reached its deadline while this account held it.
99 pub fn is_expired(&self, cid: &ClientOrderId) -> bool {
100 self.orders_expired.contains_key(cid)
101 }
102
103 /// Restates every balance as of `time_exchange`.
104 ///
105 /// # Open orders keep their own stamps
106 /// [`Open::time_exchange`](crate::order::state::Open::time_exchange) is the instant the venue
107 /// accepted the order, and an order does not become a different order because time passed. This
108 /// used to rewrite it on every advance, which was invisible only because nothing rested: the
109 /// venue filled every order on arrival, so `orders_open` held at most what an `initial_state`
110 /// seeded.
111 ///
112 /// It is not invisible once orders rest. Arrival order is half of price-time priority, so
113 /// rewriting it would make a matching engine's tie-break depend on when the clock last moved
114 /// rather than on when each order arrived — and since every order would be rewritten to the
115 /// same instant, there would be no tie-break left at all. It would also make every open order
116 /// look newer than the engine's copy on each advance, which is exactly what the `rustrade`
117 /// engine's `OrderManager` recency guard reads to decide whether an update is stale.
118 pub fn update_time_exchange(&mut self, time_exchange: DateTime<Utc>) {
119 for balance in self.balances.values_mut() {
120 balance.time_exchange = time_exchange;
121 }
122 }
123
124 pub fn balances(&self) -> impl Iterator<Item = &AssetBalance<AssetNameExchange>> + '_ {
125 self.balances.values()
126 }
127
128 /// Every open order, in no particular order — see [`OpenOrders::resting`] for the order a
129 /// venue matches in.
130 pub fn orders_open(
131 &self,
132 ) -> impl Iterator<Item = &Order<ExchangeId, InstrumentNameExchange, Open>> + '_ {
133 self.orders_open.iter()
134 }
135
136 pub fn orders_cancelled(
137 &self,
138 ) -> impl Iterator<Item = &Order<ExchangeId, InstrumentNameExchange, Cancelled>> + '_ {
139 self.orders_cancelled.values()
140 }
141
142 pub fn orders_expired(
143 &self,
144 ) -> impl Iterator<Item = &Order<ExchangeId, InstrumentNameExchange, Expired>> + '_ {
145 self.orders_expired.values()
146 }
147
148 pub fn trades(
149 &self,
150 time_since: DateTime<Utc>,
151 ) -> impl Iterator<Item = &Trade<AssetNameExchange, InstrumentNameExchange>> + '_ {
152 self.trades
153 .iter()
154 .filter(move |trade| trade.time_exchange >= time_since)
155 }
156
157 pub fn balance_mut(
158 &mut self,
159 asset: &AssetNameExchange,
160 ) -> Option<&mut AssetBalance<AssetNameExchange>> {
161 self.balances.get_mut(asset)
162 }
163
164 pub fn ack_trade(&mut self, trade: Trade<AssetNameExchange, InstrumentNameExchange>) {
165 self.trades.push(trade);
166 }
167
168 /// Holds `amount` of `asset` against an order, moving it out of `free`.
169 ///
170 /// `total` is untouched: the asset is still held, it is merely no longer spendable. The held
171 /// portion is `total - free`, which is what [`Balance::used`](crate::balance::Balance::used)
172 /// reports and what a venue restates to its client.
173 ///
174 /// # Errors
175 /// [`BalanceInsufficient`] if `free` does not cover `amount`. The ledger is left untouched, so
176 /// a caller may reject the order and carry on.
177 ///
178 /// # Panics
179 /// Panics if `asset` has no balance. The balances are the venue's own fixture, so an absent one
180 /// is a mis-specified account rather than a runtime condition — see [`SimulatedVenue`]'s
181 /// caller obligations.
182 ///
183 /// [`SimulatedVenue`]: crate::exchange::mock::SimulatedVenue
184 pub fn reserve(
185 &mut self,
186 asset: &AssetNameExchange,
187 amount: Decimal,
188 time_exchange: DateTime<Utc>,
189 ) -> Result<AssetBalance<AssetNameExchange>, BalanceInsufficient> {
190 let balance = self.balance_expect(asset);
191
192 if balance.balance.free < amount {
193 return Err(BalanceInsufficient {
194 free: balance.balance.free,
195 required: amount,
196 });
197 }
198
199 balance.balance.free -= amount;
200 balance.time_exchange = time_exchange;
201
202 Ok(balance.clone())
203 }
204
205 /// Settles `amount` of `asset` already held by [`reserve`](Self::reserve) out of the account.
206 ///
207 /// Only `total` moves: `free` was reduced when the amount was reserved. Reserving and then
208 /// settling the same amount therefore lowers both by it, which is what an order that fills on
209 /// arrival does.
210 ///
211 /// # Panics
212 /// Panics if `asset` has no balance — see [`reserve`](Self::reserve). Settling more than is
213 /// held would take `total` below `free` and break the ledger's invariant, so it panics in debug
214 /// builds; callers settle an amount they reserved.
215 pub fn settle(
216 &mut self,
217 asset: &AssetNameExchange,
218 amount: Decimal,
219 time_exchange: DateTime<Utc>,
220 ) -> AssetBalance<AssetNameExchange> {
221 let balance = self.balance_expect(asset);
222
223 debug_assert!(
224 balance.balance.total - amount >= balance.balance.free,
225 "settling {amount} of {asset} would take total {} below free {}: only a reserved \
226 amount may be settled",
227 balance.balance.total,
228 balance.balance.free
229 );
230
231 balance.balance.total -= amount;
232 balance.time_exchange = time_exchange;
233
234 balance.clone()
235 }
236
237 /// Gives `amount` of `asset` back to `free`, undoing a [`reserve`](Self::reserve) that will
238 /// never be settled.
239 ///
240 /// Only `free` moves: nothing left the account, so `total` is unchanged. This is what a
241 /// cancelled, expired or rejected order does to the balance held against it.
242 ///
243 /// # Panics
244 /// Panics if `asset` has no balance — see [`reserve`](Self::reserve). Releasing more than is
245 /// held would take `free` above `total` and break the ledger's invariant, so it panics in debug
246 /// builds; callers release an amount they reserved.
247 pub fn release(
248 &mut self,
249 asset: &AssetNameExchange,
250 amount: Decimal,
251 time_exchange: DateTime<Utc>,
252 ) -> AssetBalance<AssetNameExchange> {
253 let balance = self.balance_expect(asset);
254
255 debug_assert!(
256 balance.balance.free + amount <= balance.balance.total,
257 "releasing {amount} of {asset} would take free {} above total {}: only a reserved \
258 amount may be released",
259 balance.balance.free,
260 balance.balance.total
261 );
262
263 balance.balance.free += amount;
264 balance.time_exchange = time_exchange;
265
266 balance.clone()
267 }
268
269 /// Commits one arriving order's whole ledger effect, or none of it.
270 ///
271 /// One order's arrival can both settle a fill and take a hold: a taker that the book could
272 /// only partly fill settles what traded and reserves against the remainder it leaves resting.
273 /// Those are one event at the venue, so they are one operation here — and because
274 /// [`reserve`](Self::reserve) tests `free` before it moves anything, an arrival this account
275 /// cannot afford leaves the ledger exactly as it found it.
276 ///
277 /// # Why this is not two calls
278 /// Settling first and reserving second is the same two moves in the order that can fail
279 /// halfway: the settle commits, the reserve is refused, and nothing puts back what `total`
280 /// has already lost — [`release`](Self::release) moves only `free`, and its own assertion
281 /// fires if it is used as a rollback. The venue would then hold a balance its client never
282 /// hears about, silently and for the rest of the run. Asking for the whole requirement up
283 /// front is what makes that state unreachable rather than merely avoided.
284 ///
285 /// # One arrival, one restatement
286 /// The returned balance is the only one an arrival owes. A balance is an absolute restatement
287 /// rather than a delta, and a fill is atomic at the venue, so emitting the state between the
288 /// settle and the hold would report a balance the account never held — for the same reason a
289 /// conservative reservation that had to be released and re-debited would.
290 ///
291 /// # Errors
292 /// [`BalanceInsufficient`] if `free` does not cover
293 /// [`settled`](Debit::settled) + [`reserved`](Debit::reserved), leaving the ledger untouched.
294 ///
295 /// # Panics
296 /// Panics if the asset has no balance — see [`reserve`](Self::reserve).
297 pub fn commit(
298 &mut self,
299 debit: &Debit,
300 time_exchange: DateTime<Utc>,
301 ) -> Result<AssetBalance<AssetNameExchange>, BalanceInsufficient> {
302 // Asked for as one requirement, so a refusal refuses the arrival rather than half of it.
303 self.reserve(&debit.asset, debit.settled + debit.reserved, time_exchange)?;
304
305 // Infallible: `total` cannot fall below `free`, which the reserve above has already taken
306 // the whole requirement out of. Whatever was reserved and not settled stays held.
307 Ok(self.settle(&debit.asset, debit.settled, time_exchange))
308 }
309
310 #[allow(clippy::expect_used)] // Documented panic: an absent balance is a mis-specified fixture.
311 fn balance_expect(
312 &mut self,
313 asset: &AssetNameExchange,
314 ) -> &mut AssetBalance<AssetNameExchange> {
315 self.balances
316 .get_mut(asset)
317 .expect("SimulatedVenue has Balance for all configured Instrument assets")
318 }
319}
320
321/// What one arriving order does to one asset's balance, as a single commitment.
322///
323/// Both fields are denominated in [`asset`](Self::asset) and inclusive of fees. They are one
324/// asset, not two, because they come from one order: which asset an order pays with is decided by
325/// its instrument and its side, and an order has one of each. That is what makes an arrival a
326/// single ledger operation rather than two that must be kept in step.
327///
328/// The three arrivals a [`SimulatedVenue`] can have are the three shapes of this type:
329///
330/// | arrival | `settled` | `reserved` |
331/// |---|---|---|
332/// | fills in full | the fill | zero |
333/// | rests, having traded nothing | zero | the reservation |
334/// | fills in part and rests the remainder | the fill | the remainder's reservation |
335///
336/// [`SimulatedVenue`]: crate::exchange::mock::SimulatedVenue
337#[derive(Debug, Clone, PartialEq, Eq)]
338pub struct Debit {
339 /// Which asset the order pays with: quote for a buy or a CFD, base for a spot sell.
340 pub asset: AssetNameExchange,
341
342 /// Left the account outright, because it traded. Lowers `free` and `total` alike.
343 pub settled: Decimal,
344
345 /// Held against a remainder that is still working. Lowers `free` and leaves `total`, so it is
346 /// still the account's until whatever it is held against settles or is released.
347 pub reserved: Decimal,
348}
349
350/// A ledger operation asked for more of an asset than `free` covers.
351///
352/// Carries both sides of the comparison so the caller can report them without re-reading the
353/// ledger it has just been refused by.
354#[derive(Debug, Copy, Clone, PartialEq, Eq)]
355pub struct BalanceInsufficient {
356 /// Freely-spendable amount at the moment of refusal.
357 pub free: Decimal,
358 /// Amount the operation asked for.
359 pub required: Decimal,
360}
361
362impl From<UnindexedAccountSnapshot> for AccountState {
363 fn from(value: UnindexedAccountSnapshot) -> Self {
364 let UnindexedAccountSnapshot {
365 exchange: _,
366 balances,
367 instruments,
368 } = value;
369
370 let balances = balances
371 .into_iter()
372 .map(|asset_balance| (asset_balance.asset.clone(), asset_balance))
373 .collect();
374
375 let (orders_open, orders_cancelled) = instruments.into_iter().fold(
376 (OpenOrders::default(), FnvHashMap::default()),
377 |(mut orders_open, mut orders_cancelled), snapshot| {
378 for order in snapshot.orders {
379 match &order.state {
380 OrderState::Active(_) => {
381 // `as_open` yields `None` for an active order that is not yet open —
382 // an `OpenInFlight`, which has no venue-side existence to record.
383 if let Some(open) = as_open(order) {
384 // Seeded, not booked here: the venue holds nothing against it, so
385 // a displaced entry can leak nothing. A snapshot listing one
386 // `ClientOrderId` twice is a mis-specified fixture rather than a
387 // runtime condition, and the later order silently replacing the
388 // earlier is what it would otherwise get.
389 let cid = open.key.cid.clone();
390 let displaced = orders_open.insert(open, None);
391 debug_assert!(
392 displaced.is_none(),
393 "account snapshot lists {cid} as open more than once, so only \
394 the last of them reaches the book"
395 );
396 }
397 }
398 OrderState::Inactive(InactiveOrderState::Cancelled(cancelled)) => {
399 let cancelled = cancelled.clone();
400 orders_cancelled.insert(
401 order.key.cid.clone(),
402 Order {
403 key: order.key,
404 side: order.side,
405 price: order.price,
406 quantity: order.quantity,
407 kind: order.kind,
408 time_in_force: order.time_in_force,
409 state: cancelled,
410 },
411 );
412 }
413 _ => {}
414 }
415 }
416
417 (orders_open, orders_cancelled)
418 },
419 );
420
421 Self {
422 balances,
423 orders_open,
424 orders_cancelled,
425 orders_filled: FnvHashSet::default(),
426 orders_expired: FnvHashMap::default(),
427 trades: vec![],
428 }
429 }
430}