Skip to main content

rustrade_execution/exchange/mock/
account.rs

1use crate::{
2    UnindexedAccountSnapshot,
3    balance::{AssetBalance, Balance, MarginDetails},
4    exchange::mock::orders::{
5        AlreadyResting, OpenOrder, OpenOrderEntry, OpenOrders, Reservation, RestingOrder, as_open,
6    },
7    margin::MarginEffect,
8    order::{
9        Order, UnindexedInactiveOrder,
10        id::ClientOrderId,
11        state::{Cancelled, Expired, Filled, InactiveOrderState, Open, OrderState},
12    },
13    trade::Trade,
14};
15use chrono::{DateTime, Utc};
16use fnv::FnvHashMap;
17use itertools::Itertools;
18use rust_decimal::Decimal;
19use rustrade_instrument::{
20    asset::name::AssetNameExchange, exchange::ExchangeId, instrument::name::InstrumentNameExchange,
21};
22use serde::{Deserialize, Serialize};
23use thiserror::Error;
24
25#[derive(Debug)]
26pub struct AccountState {
27    balances: FnvHashMap<AssetNameExchange, AssetBalance<AssetNameExchange>>,
28    orders_open: OpenOrders,
29    orders_cancelled:
30        FnvHashMap<ClientOrderId, Order<ExchangeId, InstrumentNameExchange, Cancelled>>,
31    /// Orders that filled, kept so a cancel arriving after one can say *why* it failed, and so an
32    /// order-state lookup can report how it ended.
33    ///
34    /// Whole orders, like `orders_cancelled`: the fill's total and average price are not
35    /// recoverable from [`trades`](Self::trades) alone, which records each cross separately. Grows
36    /// with the run, like `trades` and `orders_cancelled` — a simulated venue's ledgers are bounded
37    /// by the dataset, not reaped.
38    orders_filled: FnvHashMap<ClientOrderId, Order<ExchangeId, InstrumentNameExchange, Filled>>,
39    /// Orders retired by their own deadline, kept so they reach a later account snapshot and so a
40    /// cancel arriving after one can say *why* it failed.
41    ///
42    /// Whole orders rather than ids alone, unlike [`orders_filled`](Self::orders_filled): nothing
43    /// else records an expiry, so a snapshot has no other source for it — whereas a fill is already
44    /// in [`trades`](Self::trades). Grows with the run, like `trades` and `orders_cancelled`.
45    orders_expired: FnvHashMap<ClientOrderId, Order<ExchangeId, InstrumentNameExchange, Expired>>,
46    trades: Vec<Trade<AssetNameExchange, InstrumentNameExchange>>,
47    /// Whether this is a margin account, which reports every balance's loan, or a cash account,
48    /// which reports none and borrows nothing. Set by [`enable_margin`](Self::enable_margin).
49    margin: bool,
50    /// Counts each time an asset's principal opened from zero or was repaid to zero. Read through
51    /// [`loans_changed`](Self::loans_changed).
52    loans_changed: u64,
53}
54
55impl AccountState {
56    pub fn new(
57        balances: FnvHashMap<AssetNameExchange, AssetBalance<AssetNameExchange>>,
58        orders_open: OpenOrders,
59        orders_cancelled: FnvHashMap<
60            ClientOrderId,
61            Order<ExchangeId, InstrumentNameExchange, Cancelled>,
62        >,
63        trades: Vec<Trade<AssetNameExchange, InstrumentNameExchange>>,
64    ) -> Self {
65        Self {
66            balances,
67            orders_open,
68            orders_cancelled,
69            orders_filled: FnvHashMap::default(),
70            orders_expired: FnvHashMap::default(),
71            trades,
72            margin: false,
73            loans_changed: 0,
74        }
75    }
76
77    /// Makes this a margin account: every balance reports its loan, and an order whose
78    /// [`MarginEffect`] says so borrows what an arrival lacks or repays loans from what it
79    /// receives — see [`SpotMarginConfig`](crate::margin::SpotMarginConfig).
80    ///
81    /// A balance that already reports a loan, as one seeded from a live margin account does, keeps
82    /// it; every other balance starts owing nothing.
83    pub fn enable_margin(&mut self) {
84        self.margin = true;
85        for balance in self.balances.values_mut() {
86            balance
87                .balance
88                .margin
89                .get_or_insert_with(MarginDetails::default);
90        }
91    }
92
93    /// The free balance of `asset`, zero for an asset the account has never held.
94    pub fn free(&self, asset: &AssetNameExchange) -> Decimal {
95        self.balances
96            .get(asset)
97            .map_or(Decimal::ZERO, |balance| balance.balance.free)
98    }
99
100    /// What this account owes of `asset`, principal plus interest, zero for an asset it has never
101    /// held or owes nothing in.
102    pub fn owed(&self, asset: &AssetNameExchange) -> Decimal {
103        self.balances
104            .get(asset)
105            .and_then(|balance| balance.balance.margin)
106            .map_or(Decimal::ZERO, |margin| margin.owed())
107    }
108
109    /// What a borrow cap of `cap` leaves to lend of `asset`, once what this account already owes
110    /// of it, principal plus interest, is counted. Never negative: a debt past the cap leaves zero.
111    pub fn cap_room(&self, asset: &AssetNameExchange, cap: Decimal) -> Decimal {
112        (cap - self.owed(asset)).max(Decimal::ZERO)
113    }
114
115    /// Each asset this account owes principal in, with the principal owed.
116    pub fn loans(&self) -> impl Iterator<Item = (&AssetNameExchange, Decimal)> + '_ {
117        self.balances.iter().filter_map(|(asset, balance)| {
118            let borrowed = balance.balance.margin?.borrowed;
119            (borrowed > Decimal::ZERO).then_some((asset, borrowed))
120        })
121    }
122
123    /// A count that changes whenever the set of assets in [`loans`](Self::loans) may have: each
124    /// time an asset's principal opens from zero or is repaid to zero.
125    ///
126    /// For a caller that caches something derived from that set, such as when interest next falls
127    /// due, and would otherwise recompute it after every operation.
128    pub fn loans_changed(&self) -> u64 {
129        self.loans_changed
130    }
131
132    /// Adds `interest` to the interest `asset`'s loan owes, leaving `total` and `free` as they
133    /// are: interest is owed to the lender, and repaid before principal.
134    ///
135    /// # Panics
136    /// Panics if `asset` has no balance — see [`reserve`](Self::reserve). A loan opens its
137    /// balance, so an asset with a loan to charge always has one.
138    pub fn charge_interest(
139        &mut self,
140        asset: &AssetNameExchange,
141        interest: Decimal,
142        time_exchange: DateTime<Utc>,
143    ) -> AssetBalance<AssetNameExchange> {
144        let balance = self.balance_expect(asset);
145        balance
146            .balance
147            .margin
148            .get_or_insert_with(MarginDetails::default)
149            .interest += interest;
150        balance.time_exchange = time_exchange;
151
152        balance.clone()
153    }
154
155    /// This account's open orders, indexed for lookup and ordered for matching.
156    pub fn orders(&self) -> &OpenOrders {
157        &self.orders_open
158    }
159
160    /// Takes the order resting under `cid` off the book, with whatever is held against it.
161    ///
162    /// The caller records how it ended, with [`ack_cancelled`](Self::ack_cancelled),
163    /// [`ack_filled`](Self::ack_filled) or [`ack_expired`](Self::ack_expired). Booking is only
164    /// through [`book`](Self::book), which keeps an id in at most one place. The caller also
165    /// reports the reservation's [`unreported_interest`](Reservation::unreported_interest), or
166    /// carries it into the order's next reservation: dropped, that charge is never reported.
167    pub fn remove_order(&mut self, cid: &ClientOrderId) -> Option<RestingOrder> {
168        self.orders_open.remove(cid)
169    }
170
171    /// Puts `order` on the book holding `reservation`, unless an order is already resting under
172    /// its [`ClientOrderId`].
173    ///
174    /// A client order id names one order at a time, not one for good: once an order has ended its
175    /// id may name a new one. So booking forgets how an earlier order under the same id ended,
176    /// which keeps an id in at most one place, the book or one ledger, and a snapshot from listing
177    /// it twice.
178    ///
179    /// # Errors
180    /// [`AlreadyResting`], with `order` and `reservation` handed back and nothing changed, if an
181    /// order is already resting under its id. See [`OpenOrders::insert`].
182    pub fn book(
183        &mut self,
184        order: OpenOrder,
185        reservation: Option<Reservation>,
186    ) -> Result<(), AlreadyResting> {
187        let cid = order.key.cid.clone();
188        self.orders_open.insert(order, reservation)?;
189        self.forget_ended(&cid);
190        Ok(())
191    }
192
193    /// Holds `order` until its venue releases it, with `reservation` held against it, unless an
194    /// order is already resting under its [`ClientOrderId`]. Forgets how an earlier order under
195    /// the id ended, as [`book`](Self::book) does.
196    ///
197    /// # Errors
198    /// [`AlreadyResting`], as for [`book`](Self::book). See [`OpenOrders::hold`].
199    pub fn hold(
200        &mut self,
201        order: OpenOrder,
202        reservation: Option<Reservation>,
203    ) -> Result<(), AlreadyResting> {
204        let cid = order.key.cid.clone();
205        self.orders_open.hold(order, reservation)?;
206        self.forget_ended(&cid);
207        Ok(())
208    }
209
210    /// The orders held on `instrument`, earliest arrival first, no longer held. See
211    /// [`OpenOrders::take_held`].
212    pub fn take_held(&mut self, instrument: &InstrumentNameExchange) -> Vec<ClientOrderId> {
213        self.orders_open.take_held(instrument)
214    }
215
216    /// Drops what the ledgers remember of how an order under `cid` ended, so that the order about
217    /// to be recorded under it is the only one they hold.
218    fn forget_ended(&mut self, cid: &ClientOrderId) {
219        self.orders_cancelled.remove(cid);
220        self.orders_filled.remove(cid);
221        self.orders_expired.remove(cid);
222    }
223
224    /// Records that `order` was cancelled, so it is reported by a later account snapshot and a
225    /// second cancel for it can be told apart from a cancel for an order that never existed.
226    ///
227    /// Replaces whatever the ledgers held for an earlier order under the same id.
228    pub fn ack_cancelled(&mut self, order: Order<ExchangeId, InstrumentNameExchange, Cancelled>) {
229        self.forget_ended(&order.key.cid);
230        self.orders_cancelled.insert(order.key.cid.clone(), order);
231    }
232
233    /// Whether `cid` names an order this account cancelled.
234    pub fn is_cancelled(&self, cid: &ClientOrderId) -> bool {
235        self.orders_cancelled.contains_key(cid)
236    }
237
238    /// Records that `order` filled, so a cancel that loses the race to it can say so and an
239    /// order-state lookup can report it.
240    ///
241    /// Replaces whatever the ledgers held for an earlier order under the same id.
242    pub fn ack_filled(&mut self, order: Order<ExchangeId, InstrumentNameExchange, Filled>) {
243        self.forget_ended(&order.key.cid);
244        self.orders_filled.insert(order.key.cid.clone(), order);
245    }
246
247    /// Whether `cid` names an order this account filled.
248    pub fn is_filled(&self, cid: &ClientOrderId) -> bool {
249        self.orders_filled.contains_key(cid)
250    }
251
252    /// How the order `cid` ended, or `None` if it is still open or this account never held it.
253    ///
254    /// An order a configured `initial_state` reported as cancelled is found too; one it reported
255    /// in any other inactive state is not, because a snapshot seeds only open and cancelled orders.
256    ///
257    /// An id names at most one order at a time, and an id is in at most one place: the book, or
258    /// one of the ledgers an order can end in, holding the latest order under it (see
259    /// [`book`](Self::book)). So the answer is how the latest order under `cid` ended.
260    pub fn order_ended(&self, cid: &ClientOrderId) -> Option<UnindexedInactiveOrder> {
261        if self.orders_open.contains(cid) {
262            None
263        } else if let Some(filled) = self.orders_filled.get(cid) {
264            Some(filled.clone().map_state(InactiveOrderState::FullyFilled))
265        } else if let Some(cancelled) = self.orders_cancelled.get(cid) {
266            Some(cancelled.clone().map_state(InactiveOrderState::Cancelled))
267        } else {
268            self.orders_expired
269                .get(cid)
270                .map(|expired| expired.clone().map_state(InactiveOrderState::Expired))
271        }
272    }
273
274    /// Records that `order` reached its own deadline, so it is reported by a later account snapshot
275    /// and a cancel arriving after it can be told apart from a cancel for an unknown order.
276    ///
277    /// Replaces whatever the ledgers held for an earlier order under the same id.
278    pub fn ack_expired(&mut self, order: Order<ExchangeId, InstrumentNameExchange, Expired>) {
279        self.forget_ended(&order.key.cid);
280        self.orders_expired.insert(order.key.cid.clone(), order);
281    }
282
283    /// Whether `cid` names an order that reached its deadline while this account held it.
284    pub fn is_expired(&self, cid: &ClientOrderId) -> bool {
285        self.orders_expired.contains_key(cid)
286    }
287
288    /// Restates every balance as of `time_exchange`.
289    ///
290    /// # Open orders keep their own stamps
291    /// [`Open::time_exchange`](crate::order::state::Open::time_exchange) is the instant the venue
292    /// accepted the order, and an order does not become a different order because time passed. This
293    /// used to rewrite it on every advance, which was invisible only because nothing rested: the
294    /// venue filled every order on arrival, so `orders_open` held at most what an `initial_state`
295    /// seeded.
296    ///
297    /// It is not invisible once orders rest. Arrival order is half of price-time priority, so
298    /// rewriting it would make a matching engine's tie-break depend on when the clock last moved
299    /// rather than on when each order arrived — and since every order would be rewritten to the
300    /// same instant, there would be no tie-break left at all. It would also make every open order
301    /// look newer than the engine's copy on each advance, which is exactly what the `rustrade`
302    /// engine's `OrderManager` recency guard reads to decide whether an update is stale.
303    pub fn update_time_exchange(&mut self, time_exchange: DateTime<Utc>) {
304        for balance in self.balances.values_mut() {
305            balance.time_exchange = time_exchange;
306        }
307    }
308
309    pub fn balances(&self) -> impl Iterator<Item = &AssetBalance<AssetNameExchange>> + '_ {
310        self.balances.values()
311    }
312
313    /// Every open order, in no particular order — see [`OpenOrders::resting`] for the order a
314    /// venue matches in.
315    pub fn orders_open(
316        &self,
317    ) -> impl Iterator<Item = &Order<ExchangeId, InstrumentNameExchange, Open>> + '_ {
318        self.orders_open.iter()
319    }
320
321    pub fn orders_cancelled(
322        &self,
323    ) -> impl Iterator<Item = &Order<ExchangeId, InstrumentNameExchange, Cancelled>> + '_ {
324        self.orders_cancelled.values()
325    }
326
327    pub fn orders_expired(
328        &self,
329    ) -> impl Iterator<Item = &Order<ExchangeId, InstrumentNameExchange, Expired>> + '_ {
330        self.orders_expired.values()
331    }
332
333    pub fn trades(
334        &self,
335        time_since: DateTime<Utc>,
336    ) -> impl Iterator<Item = &Trade<AssetNameExchange, InstrumentNameExchange>> + '_ {
337        self.trades
338            .iter()
339            .filter(move |trade| trade.time_exchange >= time_since)
340    }
341
342    pub fn balance_mut(
343        &mut self,
344        asset: &AssetNameExchange,
345    ) -> Option<&mut AssetBalance<AssetNameExchange>> {
346        self.balances.get_mut(asset)
347    }
348
349    pub fn ack_trade(&mut self, trade: Trade<AssetNameExchange, InstrumentNameExchange>) {
350        self.trades.push(trade);
351    }
352
353    /// Holds `amount` of `asset` against an order, moving it out of `free`.
354    ///
355    /// `total` is untouched: the asset is still held, it is merely no longer spendable. The held
356    /// portion is `total - free`, which is what [`Balance::used`](crate::balance::Balance::used)
357    /// reports and what a venue restates to its client.
358    ///
359    /// # Errors
360    /// [`BalanceInsufficient`] if `free` does not cover `amount`. The ledger is left untouched, so
361    /// a caller may reject the order and carry on.
362    ///
363    /// # Panics
364    /// Panics if `asset` has no balance. The balances are the venue's own fixture, so an absent one
365    /// is a mis-specified account rather than a runtime condition — see [`SimulatedVenue`]'s
366    /// caller obligations.
367    ///
368    /// [`SimulatedVenue`]: crate::exchange::mock::SimulatedVenue
369    pub fn reserve(
370        &mut self,
371        asset: &AssetNameExchange,
372        amount: Decimal,
373        time_exchange: DateTime<Utc>,
374    ) -> Result<AssetBalance<AssetNameExchange>, BalanceInsufficient> {
375        let balance = self.balance_expect(asset);
376
377        if balance.balance.free < amount {
378            return Err(BalanceInsufficient {
379                asset: asset.clone(),
380                free: balance.balance.free,
381                required: amount,
382                borrowable: None,
383                min_borrow: None,
384                max_borrow: None,
385            });
386        }
387
388        balance.balance.free -= amount;
389        balance.time_exchange = time_exchange;
390
391        Ok(balance.clone())
392    }
393
394    /// Settles `amount` of `asset` already held by [`reserve`](Self::reserve) out of the account.
395    ///
396    /// Only `total` moves: `free` was reduced when the amount was reserved. Reserving and then
397    /// settling the same amount therefore lowers both by it, which is what an order that fills on
398    /// arrival does.
399    ///
400    /// # Panics
401    /// Panics if `asset` has no balance — see [`reserve`](Self::reserve). Settling more than is
402    /// held would take `total` below `free` and break the ledger's invariant, so it panics in debug
403    /// builds; callers settle an amount they reserved.
404    pub fn settle(
405        &mut self,
406        asset: &AssetNameExchange,
407        amount: Decimal,
408        time_exchange: DateTime<Utc>,
409    ) -> AssetBalance<AssetNameExchange> {
410        let balance = self.balance_expect(asset);
411
412        debug_assert!(
413            balance.balance.total - amount >= balance.balance.free,
414            "settling {amount} of {asset} would take total {} below free {}: only a reserved \
415             amount may be settled",
416            balance.balance.total,
417            balance.balance.free
418        );
419
420        balance.balance.total -= amount;
421        balance.time_exchange = time_exchange;
422
423        balance.clone()
424    }
425
426    /// Gives `amount` of `asset` back to `free`, undoing a [`reserve`](Self::reserve) that will
427    /// never be settled.
428    ///
429    /// Only `free` moves: nothing left the account, so `total` is unchanged. This is what a
430    /// cancelled, expired or rejected order does to the balance held against it.
431    ///
432    /// # Panics
433    /// Panics if `asset` has no balance — see [`reserve`](Self::reserve). Releasing more than is
434    /// held would take `free` above `total` and break the ledger's invariant, so it panics in debug
435    /// builds; callers release an amount they reserved.
436    pub fn release(
437        &mut self,
438        asset: &AssetNameExchange,
439        amount: Decimal,
440        time_exchange: DateTime<Utc>,
441    ) -> AssetBalance<AssetNameExchange> {
442        let balance = self.balance_expect(asset);
443
444        debug_assert!(
445            balance.balance.free + amount <= balance.balance.total,
446            "releasing {amount} of {asset} would take free {} above total {}: only a reserved \
447             amount may be released",
448            balance.balance.free,
449            balance.balance.total
450        );
451
452        balance.balance.free += amount;
453        balance.time_exchange = time_exchange;
454
455        balance.clone()
456    }
457
458    /// Moves `amount` of `asset` into the account, or out of it when negative, whatever `free`
459    /// holds.
460    ///
461    /// For a charge the account cannot refuse, such as a holding cost: a venue posts carry whether
462    /// or not the account can cover it. `total` and `free` move together, so what is held stays
463    /// held, and a debit larger than `free` takes `free` below zero rather than being refused. The
464    /// caller reports that, since only it knows what the charge was for.
465    ///
466    /// # Panics
467    /// Panics if `asset` has no balance — see [`reserve`](Self::reserve).
468    pub fn charge(
469        &mut self,
470        asset: &AssetNameExchange,
471        amount: Decimal,
472        time_exchange: DateTime<Utc>,
473    ) -> AssetBalance<AssetNameExchange> {
474        let balance = self.balance_expect(asset);
475
476        balance.balance.total += amount;
477        balance.balance.free += amount;
478        balance.time_exchange = time_exchange;
479
480        balance.clone()
481    }
482
483    /// Moves a [`Delivery`] into the account, or out of it when negative, creating the balance at
484    /// zero if the account has never held its asset.
485    ///
486    /// What a fill delivers: the base a spot buy receives, or the proceeds of a spot sell. A
487    /// venue credits the asset an order receives whether or not the account held it before, so
488    /// unlike every debit, an absent balance here is not a mis-specified fixture. `total` and
489    /// `free` move together, as for [`charge`](Self::charge).
490    ///
491    /// **Unchecked**, like [`charge`](Self::charge): a negative delivery larger than `free` takes
492    /// `free` below zero rather than being refused. [`commit`](Self::commit) is the checked path,
493    /// and a fill that can be refused goes through it.
494    ///
495    /// On a margin account, for an order whose `effect` is [`Repay`](MarginEffect::Repay), what is
496    /// delivered into an asset the account owes repays that loan first, interest then principal,
497    /// and only the rest is added to `free`. Under any other effect, or on a cash account, all of
498    /// it is added to `free`.
499    pub fn deliver(
500        &mut self,
501        delivery: &Delivery,
502        effect: MarginEffect,
503        time_exchange: DateTime<Utc>,
504    ) -> AssetBalance<AssetNameExchange> {
505        let repays = self.margin && effect == MarginEffect::Repay;
506        let balance = self.balance_or_open(&delivery.asset, time_exchange);
507
508        balance.balance.total += delivery.amount;
509        balance.balance.free += delivery.amount;
510        let cleared = repays && repay(&mut balance.balance, delivery.amount);
511        balance.time_exchange = time_exchange;
512
513        let balance = balance.clone();
514        self.loans_changed += u64::from(cleared);
515        balance
516    }
517
518    /// Gives `reservation` back to `free`, as [`release`](Self::release) does, then, with
519    /// `auto_repay_at_cancel`, repays the loan its order took when it was placed
520    /// ([`Reservation::loan`]) out of the free balance, interest first.
521    ///
522    /// What a cancelled, expired or rejected order does to the balance held against it on a margin
523    /// account, which repays an unused loan when the order it was taken for ends. Without
524    /// `auto_repay_at_cancel` the loan stays owed. A reservation that borrowed nothing is simply
525    /// released.
526    ///
527    /// # Panics
528    /// As [`release`](Self::release).
529    pub fn release_reservation(
530        &mut self,
531        reservation: &Reservation,
532        auto_repay_at_cancel: bool,
533        time_exchange: DateTime<Utc>,
534    ) -> AssetBalance<AssetNameExchange> {
535        let released = self.release(&reservation.asset, reservation.amount, time_exchange);
536        match auto_repay_at_cancel && !reservation.loan().is_zero() {
537            false => released,
538            true => self.repay_loan(&reservation.asset, reservation.loan(), time_exchange),
539        }
540    }
541
542    /// Repays up to `amount` of `asset`'s loan out of its free balance, interest first, and never
543    /// more than `free` holds.
544    ///
545    /// # Panics
546    /// Panics if `asset` has no balance — see [`reserve`](Self::reserve).
547    pub fn repay_loan(
548        &mut self,
549        asset: &AssetNameExchange,
550        amount: Decimal,
551        time_exchange: DateTime<Utc>,
552    ) -> AssetBalance<AssetNameExchange> {
553        let balance = self.balance_expect(asset);
554        let available = amount.min(balance.balance.free);
555        let cleared = repay(&mut balance.balance, available);
556        balance.time_exchange = time_exchange;
557
558        let balance = balance.clone();
559        self.loans_changed += u64::from(cleared);
560        balance
561    }
562
563    /// Lends `amount` of `asset` whatever any limit says, and returns the restated balance:
564    /// `total`, `free` and the loan rise by it.
565    ///
566    /// For a delivery the account cannot refuse, such as the underlying an exercised option
567    /// delivers; the caller decides how much and reports any limit passed. An asset the account
568    /// has never held is opened at zero first.
569    ///
570    /// # Panics
571    /// The caller must call it only on a margin account (see
572    /// [`enable_margin`](Self::enable_margin)): debug builds assert it, and a cash account would
573    /// otherwise be given a loan it cannot report.
574    pub fn lend(
575        &mut self,
576        asset: &AssetNameExchange,
577        amount: Decimal,
578        time_exchange: DateTime<Utc>,
579    ) -> AssetBalance<AssetNameExchange> {
580        debug_assert!(self.margin, "AccountState::lend on a cash account");
581        self.borrow(asset, amount, time_exchange);
582        let balance = self.balance_expect(asset);
583        balance.time_exchange = time_exchange;
584        balance.clone()
585    }
586
587    /// Lends `amount` of `asset`: `total` and `free` rise by it, and so does the loan. An asset the
588    /// account has never held is opened at zero first, as a delivery opens one.
589    fn borrow(&mut self, asset: &AssetNameExchange, amount: Decimal, time_exchange: DateTime<Utc>) {
590        let balance = &mut self.balance_or_open(asset, time_exchange).balance;
591        balance.total += amount;
592        balance.free += amount;
593        let margin = balance.margin.get_or_insert_with(MarginDetails::default);
594        let opened = margin.borrowed.is_zero();
595        margin.borrowed += amount;
596
597        self.loans_changed += u64::from(opened);
598    }
599
600    /// `asset`'s balance, opened at zero if the account has never held it, reporting a loan from
601    /// the start on a margin account.
602    fn balance_or_open(
603        &mut self,
604        asset: &AssetNameExchange,
605        time_exchange: DateTime<Utc>,
606    ) -> &mut AssetBalance<AssetNameExchange> {
607        let margin = self.margin.then(MarginDetails::default);
608        // `SmolStr` clones without allocating for an asset name, so the entry API costs nothing
609        // on the common path where the balance exists.
610        self.balances
611            .entry(asset.clone())
612            .or_insert_with(|| AssetBalance {
613                asset: asset.clone(),
614                balance: Balance {
615                    margin,
616                    ..Balance::new(Decimal::ZERO, Decimal::ZERO)
617                },
618                time_exchange,
619            })
620    }
621
622    /// Commits one arriving order's whole ledger effect, or none of it.
623    ///
624    /// One order's arrival can both settle a fill and take a hold: a taker that the book could
625    /// only partly fill settles what traded and reserves against the remainder it leaves resting.
626    /// Those are one event at the venue, so they are one operation here — and because every
627    /// requirement is tested before anything moves, an arrival this account cannot afford leaves
628    /// the ledger exactly as it found it.
629    ///
630    /// # Why this is not two calls
631    /// Settling first and reserving second is the same two moves in the order that can fail
632    /// halfway: the settle commits, the reserve is refused, and nothing puts back what `total`
633    /// has already lost — [`release`](Self::release) moves only `free`, and its own assertion
634    /// fires if it is used as a rollback. The venue would then hold a balance its client never
635    /// hears about, silently and for the rest of the run. Asking for the whole requirement up
636    /// front is what makes that state unreachable rather than merely avoided.
637    ///
638    /// # One restatement per asset moved
639    /// A balance is an absolute restatement rather than a delta, and a fill is atomic at the
640    /// venue, so emitting the state between the settle and the hold would report a balance the
641    /// account never held — for the same reason a conservative reservation that had to be released
642    /// and re-debited would. [`Committed`] therefore carries one balance per asset the arrival
643    /// moved: the one it paid with, and the one it received when that is another asset.
644    ///
645    /// # What the fill pays back
646    /// `credit` is what the fill gives the account, and which [`Credit`] it is decides what it
647    /// does:
648    /// - **[`PaidBack`](Credit::PaidBack)**, into the paying asset — the margin and realised PnL
649    ///   of the part of a CFD fill that closes a position. It counts towards what `free` must
650    ///   cover, because one fill can carry both: a CFD flip, closing a long and opening a short in
651    ///   one order, is paid back for the long and posts margin for the short, so the flip is
652    ///   funded by the long it closes.
653    /// - **[`Delivery`](Credit::Delivery)**, into another asset — what a spot fill receives: base
654    ///   for a buy, the proceeds for a sell, each net of any fee charged in it. It funds nothing,
655    ///   since it is not the asset paid with, and it is [delivered](Self::deliver) whether or not
656    ///   the account held that asset before. It is negative only for a fill whose fee exceeds
657    ///   what it receives, and then that asset's `free` must cover the excess too.
658    ///
659    /// Nothing is credited until every requirement is known to be covered, so a refusal still
660    /// leaves the ledger exactly as it was. `None` is an arrival with nothing to pay back: an
661    /// order that rests having traded nothing.
662    ///
663    /// # What a margin account lends and repays
664    /// On a margin account, for an order whose `effect` is [`Borrow`](MarginEffect::Borrow), a
665    /// requirement `free` and the credit do not cover borrows the shortfall of the paying asset,
666    /// raised to [`lending.min_borrow`](Lending::min_borrow) if it is less, if that loan is within
667    /// both of `lending`'s limits: [`borrowable`](Lending::borrowable), and what
668    /// [`max_borrow`](Lending::max_borrow) leaves once what the account already owes of the asset,
669    /// principal plus interest, is counted. The loan is taken with the rest of the arrival, or not
670    /// at all. [`Committed::borrowed`] says how much was lent, and [`Committed::excess_borrowed`]
671    /// how much of it the minimum added, which stays free. A paying asset the account has never
672    /// held has nothing free, and is opened when the loan is taken. A cash account, and an order
673    /// with another effect, ignore `lending`, and a negative `borrowable` lends nothing.
674    ///
675    /// For an order whose `effect` is [`Repay`](MarginEffect::Repay), what the credit delivers
676    /// repays the loan of the asset it is delivered into, as [`deliver`](Self::deliver) does.
677    ///
678    /// # Errors
679    /// [`BalanceInsufficient`], naming the asset short, if `free` plus a credit into the paying
680    /// asset, plus what may be borrowed, does not cover [`settled`](Debit::settled) +
681    /// [`reserved`](Debit::reserved), or if a negative credit into another asset exceeds that
682    /// asset's `free`. The ledger is left untouched.
683    ///
684    /// # Panics
685    /// Panics if the paying asset has no balance and the arrival borrows none of it — see
686    /// [`reserve`](Self::reserve). Debug-asserts that a [`PaidBack`](Credit::PaidBack) credit is
687    /// not negative.
688    pub fn commit(
689        &mut self,
690        debit: &Debit,
691        credit: Option<&Credit>,
692        effect: MarginEffect,
693        lending: Lending,
694        time_exchange: DateTime<Utc>,
695    ) -> Result<Committed, BalanceInsufficient> {
696        let (funding, delivery) = match credit {
697            Some(Credit::PaidBack(amount)) => {
698                debug_assert!(
699                    !amount.is_sign_negative(),
700                    "a credit of {amount} {} is negative: a loss beyond the margin is debited",
701                    debit.asset
702                );
703                (*amount, None)
704            }
705            Some(Credit::Delivery(delivery)) => (Decimal::ZERO, Some(delivery)),
706            None => (Decimal::ZERO, None),
707        };
708
709        // Asked for as one requirement, so a refusal refuses the arrival rather than half of it.
710        // A margin account lends the shortfall, or the asset's minimum borrow if that is more,
711        // within what the venue allows. It never lends less than the minimum, so a minimum the
712        // venue cannot lend refuses the arrival.
713        // A margin account borrows even an asset it has never held, which it opens on lending.
714        let Lending {
715            borrowable,
716            min_borrow,
717            max_borrow,
718        } = lending;
719        let required = debit.settled + debit.reserved;
720        let lends = self.margin && effect == MarginEffect::Borrow;
721        let free = match self.margin {
722            true => self.free(&debit.asset),
723            false => self.balance_expect(&debit.asset).balance.free,
724        };
725        // What the cap leaves, when it leaves less than the leverage limit does: the cap is then
726        // the limit that decides, and a refusal names it. A tie leaves the leverage limit deciding.
727        let capped = max_borrow
728            .map(|cap| self.cap_room(&debit.asset, cap))
729            .filter(|room| *room < borrowable);
730        let lendable = capped.unwrap_or(borrowable);
731        let shortfall = required - (free + funding);
732        let loan = shortfall.max(min_borrow);
733        let (borrowed, excess_borrowed) = match shortfall > Decimal::ZERO {
734            false => (Decimal::ZERO, Decimal::ZERO),
735            true if lends && loan <= lendable => (loan, loan - shortfall),
736            true => {
737                return Err(BalanceInsufficient {
738                    asset: debit.asset.clone(),
739                    free: free + funding,
740                    required,
741                    borrowable: lends.then_some(lendable.max(Decimal::ZERO)),
742                    // Named only when it is what was refused: the shortfall alone was lendable, so
743                    // the loan refused is the minimum it was raised to.
744                    min_borrow: (lends && shortfall <= lendable).then_some(min_borrow),
745                    max_borrow: max_borrow.filter(|_| lends && capped.is_some()),
746                });
747            }
748        };
749
750        // A delivery that takes rather than gives -- a fill whose fee exceeds what it receives --
751        // must be covered before anything moves too. An asset never held has nothing free.
752        if let Some(delivery) = delivery.filter(|delivery| delivery.amount.is_sign_negative()) {
753            let free = self
754                .balances
755                .get(&delivery.asset)
756                .map_or(Decimal::ZERO, |balance| balance.balance.free);
757            if free + delivery.amount < Decimal::ZERO {
758                return Err(BalanceInsufficient {
759                    asset: delivery.asset.clone(),
760                    free,
761                    required: -delivery.amount,
762                    borrowable: None,
763                    min_borrow: None,
764                    max_borrow: None,
765                });
766            }
767        }
768
769        // Applied only now that every requirement is covered, so a refusal moves nothing.
770        if !borrowed.is_zero() {
771            self.borrow(&debit.asset, borrowed, time_exchange);
772        }
773        if !funding.is_zero() {
774            let balance = self.balance_expect(&debit.asset);
775            balance.balance.total += funding;
776            balance.balance.free += funding;
777        }
778
779        // Infallible: `free` now covers the whole requirement. `total` cannot fall below `free`,
780        // which the reserve takes the whole requirement out of, and whatever was reserved and not
781        // settled stays held.
782        self.reserve(&debit.asset, required, time_exchange)?;
783        let paid = self.settle(&debit.asset, debit.settled, time_exchange);
784        let received = delivery.map(|delivery| self.deliver(delivery, effect, time_exchange));
785
786        Ok(Committed {
787            paid,
788            received,
789            borrowed,
790            excess_borrowed,
791        })
792    }
793
794    #[allow(clippy::expect_used)] // Documented panic: an absent balance is a mis-specified fixture.
795    fn balance_expect(
796        &mut self,
797        asset: &AssetNameExchange,
798    ) -> &mut AssetBalance<AssetNameExchange> {
799        self.balances
800            .get_mut(asset)
801            .expect("SimulatedVenue has Balance for all configured Instrument assets")
802    }
803}
804
805/// Repays up to `available` of `balance`'s loan, interest first, then principal, out of its
806/// `total` and `free`, and returns whether that repaid the last of its principal. Repays nothing
807/// from a `available` that is not positive, or from a balance that owes nothing.
808fn repay(balance: &mut Balance, available: Decimal) -> bool {
809    let Some(margin) = balance.margin.as_mut() else {
810        return false;
811    };
812    let available = available.max(Decimal::ZERO);
813    let interest = margin.interest.min(available);
814    let principal = margin.borrowed.min(available - interest);
815    margin.interest -= interest;
816    margin.borrowed -= principal;
817
818    let repaid = interest + principal;
819    balance.total -= repaid;
820    balance.free -= repaid;
821    principal > Decimal::ZERO && margin.borrowed.is_zero()
822}
823
824/// What one arriving order does to one asset's balance, as a single commitment.
825///
826/// Both fields are denominated in [`asset`](Self::asset) and inclusive of fees. They are one
827/// asset, not two, because they come from one order: which asset an order pays with is decided by
828/// its instrument and its side, and an order has one of each. That is what makes an arrival a
829/// single ledger operation rather than two that must be kept in step.
830///
831/// The three arrivals a [`SimulatedVenue`] can have are the three shapes of this type:
832///
833/// | arrival | `settled` | `reserved` |
834/// |---|---|---|
835/// | fills in full | the fill | zero |
836/// | rests, having traded nothing | zero | the reservation |
837/// | fills in part and rests the remainder | the fill | the remainder's reservation |
838///
839/// [`SimulatedVenue`]: crate::exchange::mock::SimulatedVenue
840#[derive(Debug, Clone, PartialEq, Eq)]
841pub struct Debit {
842    /// Which asset the order pays with: quote for a buy or a CFD, base for a spot sell.
843    pub asset: AssetNameExchange,
844
845    /// Left the account outright, because it traded. Lowers `free` and `total` alike.
846    pub settled: Decimal,
847
848    /// Held against a remainder that is still working. Lowers `free` and leaves `total`, so it is
849    /// still the account's until whatever it is held against settles or is released.
850    pub reserved: Decimal,
851}
852
853/// What one fill gives the account. See [`AccountState::commit`].
854///
855/// Two kinds, because where a credit goes decides what it does: one paid back into the asset the
856/// order paid with funds that order, and one delivered in another asset does not.
857#[derive(Debug, Clone, PartialEq, Eq)]
858pub enum Credit {
859    /// Paid back into the asset the order paid with: the margin and realised PnL of the part of a
860    /// CFD fill that closes a position. Zero for a fill that closes nothing, and never negative,
861    /// since a loss beyond the margin is part of the debit.
862    PaidBack(Decimal),
863    /// Delivered in another asset: what a spot fill receives.
864    Delivery(Delivery),
865}
866
867/// What a spot fill delivers, in the asset it receives.
868#[derive(Debug, Clone, PartialEq, Eq)]
869pub struct Delivery {
870    /// The asset received: base for a buy, quote for a sell.
871    pub asset: AssetNameExchange,
872    /// Denominated in [`asset`](Self::asset), net of any fee charged in it. Negative only for a
873    /// fill whose fee exceeds what it receives.
874    pub amount: Decimal,
875}
876
877/// The balances one [`AccountState::commit`] restated, one per asset it moved.
878#[derive(Debug, Clone, PartialEq, Eq)]
879pub struct Committed {
880    /// The asset the order paid with, or held against it.
881    pub paid: AssetBalance<AssetNameExchange>,
882    /// The asset a spot fill delivered, which is never the one paid with. `None` for an arrival
883    /// that moved one asset: a CFD fill, or an order that rests having traded nothing.
884    pub received: Option<AssetBalance<AssetNameExchange>>,
885    /// How much of the paying asset a margin account lent the arrival. Zero when it lent nothing.
886    pub borrowed: Decimal,
887    /// How much of [`borrowed`](Self::borrowed) the paying asset's minimum borrow lent beyond the
888    /// arrival's shortfall. The arrival does not spend it, so it stays free. Zero when the loan
889    /// was the shortfall.
890    pub excess_borrowed: Decimal,
891}
892
893impl Committed {
894    /// How much of [`borrowed`](Self::borrowed) the arrival's shortfall took: the loan less what
895    /// the minimum borrow added.
896    pub fn shortfall_borrowed(&self) -> Decimal {
897        self.borrowed - self.excess_borrowed
898    }
899}
900
901/// A ledger operation asked for more of an asset than `free` covers.
902///
903/// Carries the asset and both sides of the comparison so the caller can report them without
904/// re-reading the ledger it has just been refused by.
905#[derive(Debug, Clone, PartialEq, Eq)]
906pub struct BalanceInsufficient {
907    /// The asset short of what was asked for.
908    pub asset: AssetNameExchange,
909    /// Freely-spendable amount at the moment of refusal.
910    pub free: Decimal,
911    /// Amount the operation asked for.
912    pub required: Decimal,
913    /// How much of the asset a margin account would have lent, or `None` when the account lends
914    /// nothing, as a cash account does.
915    pub borrowable: Option<Decimal>,
916    /// The asset's minimum borrow, when it is why a margin account refused: the venue would have
917    /// lent the shortfall, but not the minimum the loan was raised to. `None` otherwise.
918    pub min_borrow: Option<Decimal>,
919    /// The asset's borrow cap, when it is the limit that refused: it left less to lend than the
920    /// leverage limit, and less than the loan. [`borrowable`](Self::borrowable) is then what the
921    /// cap left. `None` otherwise.
922    pub max_borrow: Option<Decimal>,
923}
924
925/// What a margin account may lend one arrival of the asset it pays with, as its venue judged it.
926/// Passed to [`AccountState::commit`], which a cash account, or an order that does not borrow,
927/// leaves unread.
928///
929/// The default lends nothing, has no minimum and no cap.
930#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
931pub struct Lending {
932    /// The most the leverage limit lets the account borrow of the asset now.
933    pub borrowable: Decimal,
934    /// The least one loan of the asset may be: a smaller loan is raised to it.
935    pub min_borrow: Decimal,
936    /// The most the account may owe of the asset, principal plus interest, counting the new loan,
937    /// or `None` when the asset has no cap. The account counts what it already owes against it.
938    pub max_borrow: Option<Decimal>,
939}
940
941/// An account snapshot lists one [`ClientOrderId`] more than once among the orders it seeds.
942///
943/// A snapshot seeds open and cancelled orders, and a client order id names one order at a time,
944/// so it can be listed once among them. Two entries under one id would leave the venue to choose
945/// which order it holds, silently.
946///
947/// Also what restoring an [`AccountStateSnapshot`] that lists one id twice among its open orders,
948/// or twice among its cancelled, filled or expired ones, returns.
949#[derive(Debug, Clone, PartialEq, Eq, Error)]
950#[error("account snapshot lists {0} more than once among its open and cancelled orders")]
951pub struct DuplicateSeededOrder(pub ClientOrderId);
952
953/// An [`AccountState`] as plain data, which [`AccountState::from_snapshot`] rebuilds exactly: one
954/// part of a [`SimulatedVenueSnapshot`](crate::exchange::mock::venue::SimulatedVenueSnapshot).
955///
956/// Listed in a fixed order (balances by asset, ended orders by id, trades as booked), so two
957/// snapshots of one state are equal.
958#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
959pub struct AccountStateSnapshot {
960    balances: Vec<AssetBalance<AssetNameExchange>>,
961    orders_open: Vec<OpenOrderEntry>,
962    orders_cancelled: Vec<Order<ExchangeId, InstrumentNameExchange, Cancelled>>,
963    orders_filled: Vec<Order<ExchangeId, InstrumentNameExchange, Filled>>,
964    orders_expired: Vec<Order<ExchangeId, InstrumentNameExchange, Expired>>,
965    trades: Vec<Trade<AssetNameExchange, InstrumentNameExchange>>,
966    margin: bool,
967    loans_changed: u64,
968}
969
970impl AccountStateSnapshot {
971    /// Whether the account was a margin account.
972    pub fn margin(&self) -> bool {
973        self.margin
974    }
975
976    /// The asset of each balance, in order.
977    pub(crate) fn balance_assets(&self) -> impl Iterator<Item = &AssetNameExchange> + '_ {
978        self.balances.iter().map(|balance| &balance.asset)
979    }
980
981    /// Every instrument an open, ended or traded order names, possibly more than once.
982    pub(crate) fn instruments(&self) -> impl Iterator<Item = &InstrumentNameExchange> + '_ {
983        self.orders_open
984            .iter()
985            .map(|entry| &entry.resting.order.key.instrument)
986            .chain(
987                self.orders_cancelled
988                    .iter()
989                    .map(|order| &order.key.instrument),
990            )
991            .chain(self.orders_filled.iter().map(|order| &order.key.instrument))
992            .chain(
993                self.orders_expired
994                    .iter()
995                    .map(|order| &order.key.instrument),
996            )
997            .chain(self.trades.iter().map(|trade| &trade.instrument))
998    }
999}
1000
1001impl AccountState {
1002    /// This account as plain data: see [`AccountStateSnapshot`].
1003    pub fn snapshot(&self) -> AccountStateSnapshot {
1004        fn by_id<State: Clone>(
1005            orders: &FnvHashMap<ClientOrderId, Order<ExchangeId, InstrumentNameExchange, State>>,
1006        ) -> Vec<Order<ExchangeId, InstrumentNameExchange, State>> {
1007            orders
1008                .iter()
1009                .sorted_unstable_by(|(a, _), (b, _)| a.cmp(b))
1010                .map(|(_, order)| order.clone())
1011                .collect()
1012        }
1013
1014        AccountStateSnapshot {
1015            balances: self
1016                .balances
1017                .iter()
1018                .sorted_unstable_by(|(a, _), (b, _)| a.cmp(b))
1019                .map(|(_, balance)| balance.clone())
1020                .collect(),
1021            orders_open: self.orders_open.entries(),
1022            orders_cancelled: by_id(&self.orders_cancelled),
1023            orders_filled: by_id(&self.orders_filled),
1024            orders_expired: by_id(&self.orders_expired),
1025            trades: self.trades.clone(),
1026            margin: self.margin,
1027            loans_changed: self.loans_changed,
1028        }
1029    }
1030
1031    /// The account `snapshot` describes, exactly as it was when [`snapshot`](Self::snapshot) was
1032    /// taken.
1033    ///
1034    /// # Errors
1035    /// [`DuplicateSeededOrder`] if `snapshot` lists one [`ClientOrderId`] twice among its open
1036    /// orders, or twice among its cancelled, its filled or its expired orders.
1037    pub fn from_snapshot(snapshot: AccountStateSnapshot) -> Result<Self, DuplicateSeededOrder> {
1038        fn by_id<State>(
1039            orders: Vec<Order<ExchangeId, InstrumentNameExchange, State>>,
1040        ) -> Result<
1041            FnvHashMap<ClientOrderId, Order<ExchangeId, InstrumentNameExchange, State>>,
1042            DuplicateSeededOrder,
1043        > {
1044            let mut map = FnvHashMap::default();
1045            for order in orders {
1046                let cid = order.key.cid.clone();
1047                if map.insert(cid.clone(), order).is_some() {
1048                    return Err(DuplicateSeededOrder(cid));
1049                }
1050            }
1051            Ok(map)
1052        }
1053
1054        let AccountStateSnapshot {
1055            balances,
1056            orders_open,
1057            orders_cancelled,
1058            orders_filled,
1059            orders_expired,
1060            trades,
1061            margin,
1062            loans_changed,
1063        } = snapshot;
1064        Ok(Self {
1065            balances: balances
1066                .into_iter()
1067                .map(|balance| (balance.asset.clone(), balance))
1068                .collect(),
1069            orders_open: OpenOrders::from_entries(orders_open)
1070                .map_err(|AlreadyResting(resting)| DuplicateSeededOrder(resting.order.key.cid))?,
1071            orders_cancelled: by_id(orders_cancelled)?,
1072            orders_filled: by_id(orders_filled)?,
1073            orders_expired: by_id(orders_expired)?,
1074            trades,
1075            margin,
1076            loans_changed,
1077        })
1078    }
1079}
1080
1081/// Seeds an account from a snapshot, as a [`SimulatedVenue`] does from its `initial_state`.
1082///
1083/// # Errors
1084/// [`DuplicateSeededOrder`] if the snapshot lists one [`ClientOrderId`] more than once among its
1085/// open and cancelled orders.
1086///
1087/// [`SimulatedVenue`]: crate::exchange::mock::SimulatedVenue
1088impl TryFrom<UnindexedAccountSnapshot> for AccountState {
1089    type Error = DuplicateSeededOrder;
1090
1091    fn try_from(value: UnindexedAccountSnapshot) -> Result<Self, Self::Error> {
1092        let UnindexedAccountSnapshot {
1093            exchange: _,
1094            balances,
1095            instruments,
1096        } = value;
1097
1098        let balances = balances
1099            .into_iter()
1100            .map(|asset_balance| (asset_balance.asset.clone(), asset_balance))
1101            .collect();
1102
1103        let mut orders_open = OpenOrders::default();
1104        let mut orders_cancelled = FnvHashMap::default();
1105        for order in instruments.into_iter().flat_map(|snapshot| snapshot.orders) {
1106            match &order.state {
1107                OrderState::Active(_) => {
1108                    // `as_open` yields `None` for an active order that is not yet open — an
1109                    // `OpenInFlight`, which has no venue-side existence to record.
1110                    let Some(open) = as_open(order) else {
1111                        continue;
1112                    };
1113                    let cid = open.key.cid.clone();
1114                    if orders_cancelled.contains_key(&cid) {
1115                        return Err(DuplicateSeededOrder(cid));
1116                    }
1117                    // Seeded, not booked here: the venue holds nothing against it.
1118                    orders_open
1119                        .insert(open, None)
1120                        .map_err(|_| DuplicateSeededOrder(cid))?;
1121                }
1122                OrderState::Inactive(InactiveOrderState::Cancelled(cancelled)) => {
1123                    let cid = order.key.cid.clone();
1124                    if orders_open.contains(&cid) || orders_cancelled.contains_key(&cid) {
1125                        return Err(DuplicateSeededOrder(cid));
1126                    }
1127                    let cancelled = cancelled.clone();
1128                    orders_cancelled.insert(
1129                        cid,
1130                        Order {
1131                            key: order.key,
1132                            side: order.side,
1133                            price: order.price,
1134                            quantity: order.quantity,
1135                            kind: order.kind,
1136                            time_in_force: order.time_in_force,
1137                            state: cancelled,
1138                        },
1139                    );
1140                }
1141                // Not seeded: see `order_ended`.
1142                OrderState::Inactive(_) => {}
1143            }
1144        }
1145
1146        Ok(Self {
1147            balances,
1148            orders_open,
1149            orders_cancelled,
1150            orders_filled: FnvHashMap::default(),
1151            orders_expired: FnvHashMap::default(),
1152            trades: vec![],
1153            margin: false,
1154            loans_changed: 0,
1155        })
1156    }
1157}
1158
1159#[cfg(test)]
1160#[allow(clippy::unwrap_used)] // Test code: panics are the correct failure mode
1161mod tests {
1162    use super::*;
1163    use crate::{
1164        balance::Balance,
1165        exchange::mock::fixtures::{funded, seeded_gtd, spot_config_holding},
1166        order::{
1167            UnindexedOrder,
1168            id::OrderId,
1169            state::{Cancelled, Filled},
1170        },
1171    };
1172    use rust_decimal_macros::dec;
1173
1174    fn usd() -> AssetNameExchange {
1175        AssetNameExchange::new("usd")
1176    }
1177
1178    fn account_with_usd(amount: Decimal) -> AccountState {
1179        AccountState::new(
1180            FnvHashMap::from_iter([(usd(), funded(usd(), amount))]),
1181            OpenOrders::default(),
1182            FnvHashMap::default(),
1183            vec![],
1184        )
1185    }
1186
1187    fn usd_balance(account: &AccountState) -> Balance {
1188        let Some(balance) = account.balances().find(|balance| balance.asset == usd()) else {
1189            panic!("the account holds usd");
1190        };
1191        balance.balance
1192    }
1193
1194    /// A CFD flip: what closing the old position pays back funds the margin the new one posts,
1195    /// so a requirement above `free` alone is met, and the remainder's hold stays held.
1196    #[test]
1197    fn a_credit_counts_towards_the_requirement_it_arrives_with() {
1198        let mut account = account_with_usd(dec!(100));
1199        let debit = Debit {
1200            asset: usd(),
1201            settled: dec!(250),
1202            reserved: dec!(40),
1203        };
1204
1205        let credit = Credit::PaidBack(dec!(200));
1206
1207        let Ok(committed) = account.commit(
1208            &debit,
1209            Some(&credit),
1210            MarginEffect::NoSideEffect,
1211            Lending::default(),
1212            DateTime::<Utc>::MIN_UTC,
1213        ) else {
1214            panic!("100 free plus a 200 credit covers 290");
1215        };
1216
1217        // total: 100 + 200 credited - 250 settled; free: that, less the 40 still held.
1218        assert_eq!(committed.paid.balance, Balance::new(dec!(50), dec!(10)));
1219        assert_eq!(
1220            committed.received, None,
1221            "a CFD credit restates the asset it paid with"
1222        );
1223        assert_eq!(usd_balance(&account), Balance::new(dec!(50), dec!(10)));
1224    }
1225
1226    #[test]
1227    fn a_refused_commit_applies_none_of_its_credit() {
1228        let mut account = account_with_usd(dec!(100));
1229        let debit = Debit {
1230            asset: usd(),
1231            settled: dec!(250),
1232            reserved: dec!(60),
1233        };
1234
1235        let credit = Credit::PaidBack(dec!(200));
1236
1237        let Err(insufficient) = account.commit(
1238            &debit,
1239            Some(&credit),
1240            MarginEffect::NoSideEffect,
1241            Lending::default(),
1242            DateTime::<Utc>::MIN_UTC,
1243        ) else {
1244            panic!("100 free plus a 200 credit does not cover 310");
1245        };
1246
1247        assert_eq!(
1248            insufficient,
1249            BalanceInsufficient {
1250                asset: usd(),
1251                free: dec!(300),
1252                required: dec!(310),
1253                borrowable: None,
1254                min_borrow: None,
1255                max_borrow: None,
1256            }
1257        );
1258        assert_eq!(usd_balance(&account), Balance::new(dec!(100), dec!(100)));
1259    }
1260
1261    fn btc() -> AssetNameExchange {
1262        AssetNameExchange::new("btc")
1263    }
1264
1265    fn balance_of(account: &AccountState, asset: &AssetNameExchange) -> Option<Balance> {
1266        account
1267            .balances()
1268            .find(|balance| &balance.asset == asset)
1269            .map(|balance| balance.balance)
1270    }
1271
1272    /// A spot buy pays quote and receives base: the base funds nothing, is restated as its own
1273    /// balance, and is created at zero when the account never held it.
1274    #[test]
1275    fn a_credit_into_another_asset_is_delivered_and_restated_separately() {
1276        let mut account = account_with_usd(dec!(100));
1277        let debit = Debit {
1278            asset: usd(),
1279            settled: dec!(100),
1280            reserved: dec!(0),
1281        };
1282        let credit = Credit::Delivery(Delivery {
1283            asset: btc(),
1284            amount: dec!(2),
1285        });
1286
1287        let Ok(committed) = account.commit(
1288            &debit,
1289            Some(&credit),
1290            MarginEffect::NoSideEffect,
1291            Lending::default(),
1292            DateTime::<Utc>::MIN_UTC,
1293        ) else {
1294            panic!("100 free covers 100");
1295        };
1296
1297        assert_eq!(committed.paid.balance, Balance::new(dec!(0), dec!(0)));
1298        let Some(received) = committed.received else {
1299            panic!("a credit into another asset restates that asset");
1300        };
1301        assert_eq!(received.asset, btc());
1302        assert_eq!(received.balance, Balance::new(dec!(2), dec!(2)));
1303        assert_eq!(
1304            balance_of(&account, &btc()),
1305            Some(Balance::new(dec!(2), dec!(2)))
1306        );
1307    }
1308
1309    /// A credit into another asset is not the asset paid with, so it cannot fund the debit.
1310    #[test]
1311    fn a_credit_into_another_asset_does_not_fund_the_debit() {
1312        let mut account = account_with_usd(dec!(100));
1313        let debit = Debit {
1314            asset: usd(),
1315            settled: dec!(101),
1316            reserved: dec!(0),
1317        };
1318        let credit = Credit::Delivery(Delivery {
1319            asset: btc(),
1320            amount: dec!(1000),
1321        });
1322
1323        let Err(insufficient) = account.commit(
1324            &debit,
1325            Some(&credit),
1326            MarginEffect::NoSideEffect,
1327            Lending::default(),
1328            DateTime::<Utc>::MIN_UTC,
1329        ) else {
1330            panic!("100 free does not cover 101, whatever another asset is credited");
1331        };
1332
1333        assert_eq!(
1334            insufficient,
1335            BalanceInsufficient {
1336                asset: usd(),
1337                free: dec!(100),
1338                required: dec!(101),
1339                borrowable: None,
1340                min_borrow: None,
1341                max_borrow: None,
1342            }
1343        );
1344        assert_eq!(usd_balance(&account), Balance::new(dec!(100), dec!(100)));
1345        assert_eq!(
1346            balance_of(&account, &btc()),
1347            None,
1348            "a refusal creates nothing"
1349        );
1350    }
1351
1352    /// A fill whose fee exceeds what it receives takes the excess from the asset it receives, and
1353    /// is refused, moving nothing, when that asset cannot cover it.
1354    #[test]
1355    fn a_negative_delivery_must_be_covered_by_its_own_asset() {
1356        let mut account = AccountState::new(
1357            FnvHashMap::from_iter([
1358                (usd(), funded(usd(), dec!(3))),
1359                (btc(), funded(btc(), dec!(1))),
1360            ]),
1361            OpenOrders::default(),
1362            FnvHashMap::default(),
1363            vec![],
1364        );
1365        let debit = Debit {
1366            asset: btc(),
1367            settled: dec!(1),
1368            reserved: dec!(0),
1369        };
1370        let short = Credit::Delivery(Delivery {
1371            asset: usd(),
1372            amount: dec!(-4),
1373        });
1374
1375        let Err(insufficient) = account.commit(
1376            &debit,
1377            Some(&short),
1378            MarginEffect::NoSideEffect,
1379            Lending::default(),
1380            DateTime::<Utc>::MIN_UTC,
1381        ) else {
1382            panic!("3 usd free does not cover a 4 usd excess fee");
1383        };
1384        assert_eq!(
1385            insufficient,
1386            BalanceInsufficient {
1387                asset: usd(),
1388                free: dec!(3),
1389                required: dec!(4),
1390                borrowable: None,
1391                min_borrow: None,
1392                max_borrow: None,
1393            }
1394        );
1395        assert_eq!(
1396            balance_of(&account, &btc()),
1397            Some(Balance::new(dec!(1), dec!(1)))
1398        );
1399        assert_eq!(usd_balance(&account), Balance::new(dec!(3), dec!(3)));
1400
1401        let covered = Credit::Delivery(Delivery {
1402            asset: usd(),
1403            amount: dec!(-2),
1404        });
1405        let Ok(committed) = account.commit(
1406            &debit,
1407            Some(&covered),
1408            MarginEffect::NoSideEffect,
1409            Lending::default(),
1410            DateTime::<Utc>::MIN_UTC,
1411        ) else {
1412            panic!("3 usd free covers a 2 usd excess fee");
1413        };
1414        assert_eq!(committed.paid.balance, Balance::new(dec!(0), dec!(0)));
1415        assert_eq!(
1416            committed.received.map(|balance| balance.balance),
1417            Some(Balance::new(dec!(1), dec!(1)))
1418        );
1419    }
1420
1421    #[test]
1422    fn a_charge_moves_total_and_free_together_and_may_take_free_negative() {
1423        let mut account = account_with_usd(dec!(100));
1424        account
1425            .reserve(&usd(), dec!(60), DateTime::<Utc>::MIN_UTC)
1426            .unwrap();
1427
1428        let charged = account.charge(&usd(), dec!(-50), DateTime::<Utc>::MIN_UTC);
1429        assert_eq!(charged.balance, Balance::new(dec!(50), dec!(-10)));
1430        assert_eq!(usd_balance(&account), Balance::new(dec!(50), dec!(-10)));
1431
1432        let credited = account.charge(&usd(), dec!(15), DateTime::<Utc>::MIN_UTC);
1433        assert_eq!(credited.balance, Balance::new(dec!(65), dec!(5)));
1434    }
1435
1436    /// A snapshot holding `orders`, as a configured `initial_state` would.
1437    fn snapshot_of(orders: impl IntoIterator<Item = UnindexedOrder>) -> UnindexedAccountSnapshot {
1438        let mut orders = orders.into_iter();
1439        let first = orders.next().unwrap();
1440        let mut snapshot = spot_config_holding("1", "1000", first).initial_state;
1441        snapshot.instruments[0].orders.extend(orders);
1442        snapshot
1443    }
1444
1445    fn open(cid: &str) -> UnindexedOrder {
1446        let at = DateTime::<Utc>::MIN_UTC;
1447        seeded_gtd(cid, "100", at, at + chrono::TimeDelta::days(1))
1448    }
1449
1450    fn ended(
1451        cid: &str,
1452        state: OrderState<AssetNameExchange, InstrumentNameExchange>,
1453    ) -> UnindexedOrder {
1454        UnindexedOrder { state, ..open(cid) }
1455    }
1456
1457    fn cancelled(cid: &str) -> UnindexedOrder {
1458        let at = DateTime::<Utc>::MIN_UTC;
1459        ended(
1460            cid,
1461            OrderState::inactive(Cancelled::new(OrderId::new(cid), at, None)),
1462        )
1463    }
1464
1465    /// One id listed twice among the orders a snapshot seeds is refused, rather than one of them
1466    /// silently winning, whichever two of open and cancelled they are.
1467    #[test]
1468    fn a_snapshot_seeding_one_id_twice_is_refused() {
1469        for (orders, case) in [
1470            ([open("x"), open("x")], "open twice"),
1471            ([open("x"), cancelled("x")], "open, then cancelled"),
1472            ([cancelled("x"), open("x")], "cancelled, then open"),
1473            ([cancelled("x"), cancelled("x")], "cancelled twice"),
1474        ] {
1475            assert_eq!(
1476                AccountState::try_from(snapshot_of(orders)).err(),
1477                Some(DuplicateSeededOrder(ClientOrderId::new("x"))),
1478                "{case}"
1479            );
1480        }
1481    }
1482
1483    /// An order the snapshot does not seed holds no id, so it cannot clash with one it does.
1484    #[test]
1485    fn an_order_a_snapshot_does_not_seed_does_not_clash() {
1486        let at = DateTime::<Utc>::MIN_UTC;
1487        let filled = ended(
1488            "x",
1489            OrderState::fully_filled(Filled::new(OrderId::new("x"), at, dec!(1), None)),
1490        );
1491
1492        let Ok(account) = AccountState::try_from(snapshot_of([filled, open("x")])) else {
1493            panic!("a filled order is not seeded");
1494        };
1495        assert!(account.orders().contains(&ClientOrderId::new("x")));
1496    }
1497
1498    /// A margin account owing 30 usd of principal and 1 of interest out of 100 held, as one seeded
1499    /// from a live margin account does: no loan has opened through it, so its count starts at zero.
1500    fn account_owing_usd() -> AccountState {
1501        let owing = AssetBalance::new(
1502            usd(),
1503            Balance::new_margin(dec!(100), dec!(100), dec!(30), dec!(1)),
1504            DateTime::<Utc>::MIN_UTC,
1505        );
1506        let mut account = AccountState::new(
1507            FnvHashMap::from_iter([(usd(), owing)]),
1508            OpenOrders::default(),
1509            FnvHashMap::default(),
1510            vec![],
1511        );
1512        account.enable_margin();
1513        account
1514    }
1515
1516    /// Lending of up to 1,000 by the leverage limit, under a cap of 40, which leaves 9 to an
1517    /// account already owing 31 (30 principal and 1 interest).
1518    fn capped_at_40() -> Lending {
1519        Lending {
1520            borrowable: dec!(1000),
1521            min_borrow: Decimal::ZERO,
1522            max_borrow: Some(dec!(40)),
1523        }
1524    }
1525
1526    fn usd_debit(settled: Decimal) -> Debit {
1527        Debit {
1528            asset: usd(),
1529            settled,
1530            reserved: Decimal::ZERO,
1531        }
1532    }
1533
1534    #[test]
1535    fn the_cap_counts_the_principal_and_interest_already_owed() {
1536        let mut account = account_owing_usd();
1537        let at = DateTime::<Utc>::MIN_UTC;
1538
1539        let Err(insufficient) = account.commit(
1540            &usd_debit(dec!(110)),
1541            None,
1542            MarginEffect::Borrow,
1543            capped_at_40(),
1544            at,
1545        ) else {
1546            panic!("a shortfall of 10 is more than the 9 the cap leaves");
1547        };
1548        assert_eq!(
1549            insufficient,
1550            BalanceInsufficient {
1551                asset: usd(),
1552                free: dec!(100),
1553                required: dec!(110),
1554                borrowable: Some(dec!(9)),
1555                min_borrow: None,
1556                max_borrow: Some(dec!(40)),
1557            }
1558        );
1559
1560        let Ok(committed) = account.commit(
1561            &usd_debit(dec!(109)),
1562            None,
1563            MarginEffect::Borrow,
1564            capped_at_40(),
1565            at,
1566        ) else {
1567            panic!("a shortfall of 9 is what the cap leaves");
1568        };
1569        assert_eq!(committed.borrowed, dec!(9));
1570        assert_eq!(account.owed(&usd()), dec!(40));
1571    }
1572
1573    #[test]
1574    fn a_debt_past_the_cap_does_not_name_it_when_the_leverage_limit_lends_nothing_either() {
1575        let mut account = account_owing_usd();
1576
1577        // Owing 31 under a cap of 20 leaves nothing, the same as the leverage limit, so the cap
1578        // is not the tighter limit, however far past it the debt is.
1579        let Err(insufficient) = account.commit(
1580            &usd_debit(dec!(110)),
1581            None,
1582            MarginEffect::Borrow,
1583            Lending {
1584                borrowable: Decimal::ZERO,
1585                min_borrow: Decimal::ZERO,
1586                max_borrow: Some(dec!(20)),
1587            },
1588            DateTime::<Utc>::MIN_UTC,
1589        ) else {
1590            panic!("nothing may be lent");
1591        };
1592        assert_eq!(insufficient.borrowable, Some(Decimal::ZERO));
1593        assert_eq!(insufficient.max_borrow, None);
1594    }
1595
1596    #[test]
1597    fn an_order_that_does_not_borrow_is_refused_naming_neither_lending_nor_cap() {
1598        let mut account = account_owing_usd();
1599
1600        let Err(insufficient) = account.commit(
1601            &usd_debit(dec!(110)),
1602            None,
1603            MarginEffect::NoSideEffect,
1604            capped_at_40(),
1605            DateTime::<Utc>::MIN_UTC,
1606        ) else {
1607            panic!("an order that does not borrow pays with the 100 free alone");
1608        };
1609        assert_eq!(insufficient.borrowable, None);
1610        assert_eq!(insufficient.max_borrow, None);
1611    }
1612
1613    #[test]
1614    fn the_loan_count_changes_only_when_a_principal_is_repaid_to_zero() {
1615        let mut account = account_owing_usd();
1616        let at = DateTime::<Utc>::MIN_UTC;
1617
1618        account.deliver(
1619            &Delivery {
1620                asset: btc(),
1621                amount: dec!(1),
1622            },
1623            MarginEffect::Repay,
1624            at,
1625        );
1626        assert_eq!(
1627            account.loans_changed(),
1628            0,
1629            "a delivery into an asset owing nothing repays no loan"
1630        );
1631
1632        // The 1 interest, then 10 of the principal.
1633        account.repay_loan(&usd(), dec!(11), at);
1634        assert_eq!(
1635            account.loans_changed(),
1636            0,
1637            "20 of the principal is still owed"
1638        );
1639
1640        account.repay_loan(&usd(), dec!(20), at);
1641        assert_eq!(account.loans_changed(), 1);
1642        assert_eq!(account.loans().count(), 0);
1643    }
1644}