Skip to main content

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}