Skip to main content

rustrade_execution/
margin_debt.rs

1//! A change in what a margin account owes in one asset, as the venue streams it: a borrow, a repay
2//! or an interest charge.
3//!
4//! An account stream reports each one as
5//! [`AccountEventKind::MarginDebt`](crate::AccountEventKind::MarginDebt), carrying a
6//! [`MarginDebtUpdate`]. The engine applies one that names no instrument to the asset's
7//! [`Balance::margin`](crate::balance::Balance::margin), so debt stays current between REST
8//! account snapshots.
9use crate::balance::MarginDetails;
10use chrono::{DateTime, Utc};
11use derive_more::Constructor;
12use serde::{Deserialize, Serialize};
13use smol_str::SmolStr;
14use std::fmt::{Display, Formatter};
15
16/// What a margin account owes in one asset after a change, as the venue reported it.
17///
18/// Sent as [`AccountEventKind::MarginDebt`](crate::AccountEventKind::MarginDebt).
19///
20/// # Totals, not deltas
21///
22/// [`debt`](Self::debt) is everything owed in [`asset`](Self::asset) once the change is applied:
23/// the whole principal and the whole unpaid interest, not the amount the change borrowed, repaid or
24/// charged. Applying an update therefore replaces the debt the consumer held, and applying the same
25/// update twice changes nothing. A debt repaid in full is reported as zero, not left out.
26///
27/// # Ordering
28///
29/// A venue may send these on a channel of their own, unordered against the account's fills and
30/// balance updates. Order them by [`time_exchange`](Self::time_exchange), not by arrival, and
31/// against debt alone: a newer free/locked balance update says nothing about debt. An update can
32/// be missed while a stream is disconnected; a REST account snapshot restates the debt, as does a
33/// stream that restates its balances after reconnecting by itself (Binance's do).
34///
35/// # Producers
36///
37/// Known producers, as of writing:
38/// - Binance margin (`BinanceMargin`): `liabilityChange`, one update per asset it lists. See the
39///   client's rustdoc for how its type maps to a [`MarginDebtKind`], and why an isolated pair's
40///   debt is not known to be streamed.
41/// - The simulated venue (`SimulatedVenue`) on a spot-margin account, after each borrow, repay and
42///   interest charge.
43#[non_exhaustive]
44#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Constructor)]
45pub struct MarginDebtUpdate<AssetKey, InstrumentKey> {
46    /// The asset owed.
47    pub asset: AssetKey,
48    /// The isolated margin pair the debt belongs to, or `None` for a debt of the account as a whole,
49    /// as on a cross margin account.
50    ///
51    /// The engine applies only an update with no instrument to its asset balances: an isolated
52    /// pair's balances are per pair, and the engine stores none of them (see
53    /// [`InstrumentBalanceUpdate`](crate::InstrumentBalanceUpdate)).
54    pub instrument: Option<InstrumentKey>,
55    /// The principal and interest owed in [`asset`](Self::asset) after the change, both totals.
56    pub debt: MarginDetails,
57    /// What changed the debt, independent of venue.
58    pub kind: MarginDebtKind,
59    /// The venue's own name for the change, verbatim, such as Binance's `BORROW`.
60    pub venue_kind: SmolStr,
61    /// When the venue made the change.
62    pub time_exchange: DateTime<Utc>,
63}
64
65/// What changed a [`MarginDebtUpdate`]'s debt, independent of venue.
66///
67/// Each producer documents how its venue's names map onto these. A name that maps to none is sent
68/// as [`Other`](Self::Other), with the venue's name in [`MarginDebtUpdate::venue_kind`].
69#[non_exhaustive]
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
71pub enum MarginDebtKind {
72    /// A loan was taken, or added to.
73    Borrow,
74    /// Principal or interest was repaid.
75    Repay,
76    /// Interest was charged on the principal owed.
77    Interest,
78    /// A change no other variant names. [`MarginDebtUpdate::venue_kind`] carries the venue's name
79    /// for it.
80    Other,
81}
82
83impl Display for MarginDebtKind {
84    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
85        f.write_str(match self {
86            Self::Borrow => "borrow",
87            Self::Repay => "repay",
88            Self::Interest => "interest",
89            Self::Other => "other",
90        })
91    }
92}
93
94#[cfg(test)]
95#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
96mod tests {
97    use super::*;
98    use chrono::TimeZone;
99    use rust_decimal_macros::dec;
100
101    #[test]
102    fn an_update_round_trips_through_serde() {
103        let update = MarginDebtUpdate::new(
104            SmolStr::new("BTC"),
105            Some(SmolStr::new("BTCUSDT")),
106            MarginDetails {
107                borrowed: dec!(0.00006),
108                interest: dec!(0.00000002),
109            },
110            MarginDebtKind::Interest,
111            SmolStr::new("INTEREST"),
112            Utc.timestamp_millis_opt(1_700_000_000_000).unwrap(),
113        );
114        let json = serde_json::to_string(&update).unwrap();
115        assert_eq!(
116            serde_json::from_str::<MarginDebtUpdate<SmolStr, SmolStr>>(&json).unwrap(),
117            update
118        );
119    }
120
121    #[test]
122    fn a_kind_displays_as_its_lowercase_name() {
123        let names = [
124            MarginDebtKind::Borrow,
125            MarginDebtKind::Repay,
126            MarginDebtKind::Interest,
127            MarginDebtKind::Other,
128        ]
129        .map(|kind| kind.to_string());
130        assert_eq!(names, ["borrow", "repay", "interest", "other"]);
131    }
132}