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