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