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