use chrono::{DateTime, Utc};
use derive_more::Constructor;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
)]
pub struct AssetBalance<AssetKey> {
pub asset: AssetKey,
pub balance: Balance,
pub time_exchange: DateTime<Utc>,
}
#[derive(
Debug,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Default,
Deserialize,
Serialize,
Constructor,
)]
pub struct MarginDetails {
pub borrowed: Decimal,
pub interest: Decimal,
}
#[derive(
Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Deserialize, Serialize,
)]
pub struct Balance {
pub total: Decimal,
pub free: Decimal,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub margin: Option<MarginDetails>,
}
impl Balance {
pub const fn new(total: Decimal, free: Decimal) -> Self {
Self {
total,
free,
margin: None,
}
}
pub const fn new_margin(
total: Decimal,
free: Decimal,
borrowed: Decimal,
interest: Decimal,
) -> Self {
Self {
total,
free,
margin: Some(MarginDetails { borrowed, interest }),
}
}
pub fn used(&self) -> Decimal {
self.total - self.free
}
pub fn net_asset(&self) -> Decimal {
match self.margin {
Some(margin) => self.total - margin.borrowed,
None => self.total,
}
}
pub fn apply_stream_update(prior: Option<Balance>, update: &BalanceUpdate) -> Balance {
Balance {
total: update.total(),
free: update.free,
margin: prior.and_then(|b| b.margin),
}
}
}
#[derive(
Debug,
Copy,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Default,
Deserialize,
Serialize,
Constructor,
)]
pub struct BalanceUpdate {
pub free: Decimal,
pub locked: Decimal,
}
impl BalanceUpdate {
pub fn total(&self) -> Decimal {
self.free + self.locked
}
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
)]
pub struct AssetBalanceUpdate<AssetKey> {
pub asset: AssetKey,
pub update: BalanceUpdate,
pub time_exchange: DateTime<Utc>,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn net_asset_cash_returns_total() {
let balance = Balance::new(dec!(100), dec!(80));
assert_eq!(balance.net_asset(), dec!(100));
assert_eq!(balance.used(), dec!(20));
}
#[test]
fn net_asset_margin_deducts_borrowed() {
let balance = Balance::new_margin(dec!(100), dec!(100), dec!(30), dec!(2));
assert_eq!(balance.net_asset(), dec!(70));
}
#[test]
fn net_asset_short_is_negative() {
let balance = Balance::new_margin(dec!(0), dec!(0), dec!(1.5), dec!(0.001));
assert_eq!(balance.net_asset(), dec!(-1.5));
}
#[test]
fn balance_update_total() {
let update = BalanceUpdate::new(dec!(1.5), dec!(0.5));
assert_eq!(update.total(), dec!(2.0));
assert_eq!(update.locked, dec!(0.5));
}
#[test]
fn balance_default_has_no_margin() {
assert_eq!(Balance::default().margin, None);
}
#[test]
fn balance_serde_omits_margin_when_none() {
let json = serde_json::to_string(&Balance::new(dec!(1), dec!(1))).unwrap();
assert!(
!json.contains("margin"),
"cash balance must not serialise a margin field"
);
let margin = Balance::new_margin(dec!(1), dec!(1), dec!(0.5), dec!(0));
let round_trip: Balance =
serde_json::from_str(&serde_json::to_string(&margin).unwrap()).unwrap();
assert_eq!(round_trip, margin);
}
}