use std::fmt::{Debug, Display};
use nautilus_core::correctness::{FAILED, check_predicate_true};
use serde::{Deserialize, Serialize};
use crate::{
identifiers::InstrumentId,
types::{Currency, Money},
};
#[derive(Copy, Clone, Serialize, Deserialize)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
module = "nautilus_trader.core.nautilus_pyo3.model",
frozen,
eq,
from_py_object
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct AccountBalance {
pub currency: Currency,
pub total: Money,
pub locked: Money,
pub free: Money,
}
impl AccountBalance {
pub fn new_checked(total: Money, locked: Money, free: Money) -> anyhow::Result<Self> {
check_predicate_true(
total.currency == locked.currency,
&format!(
"`total` currency ({}) != `locked` currency ({})",
total.currency, locked.currency
),
)?;
check_predicate_true(
total.currency == free.currency,
&format!(
"`total` currency ({}) != `free` currency ({})",
total.currency, free.currency
),
)?;
check_predicate_true(
total == locked + free,
&format!("`total` ({total}) - `locked` ({locked}) != `free` ({free})"),
)?;
Ok(Self {
currency: total.currency,
total,
locked,
free,
})
}
pub fn new(total: Money, locked: Money, free: Money) -> Self {
Self::new_checked(total, locked, free).expect(FAILED)
}
}
impl PartialEq for AccountBalance {
fn eq(&self, other: &Self) -> bool {
self.total == other.total && self.locked == other.locked && self.free == other.free
}
}
impl Debug for AccountBalance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}(total={}, locked={}, free={})",
stringify!(AccountBalance),
self.total,
self.locked,
self.free,
)
}
}
impl Display for AccountBalance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Copy, Clone, Serialize, Deserialize)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
module = "nautilus_trader.core.nautilus_pyo3.model",
frozen,
eq,
from_py_object
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct MarginBalance {
pub initial: Money,
pub maintenance: Money,
pub currency: Currency,
pub instrument_id: InstrumentId,
}
impl MarginBalance {
pub fn new_checked(
initial: Money,
maintenance: Money,
instrument_id: InstrumentId,
) -> anyhow::Result<Self> {
check_predicate_true(
initial.currency == maintenance.currency,
&format!(
"`initial` currency ({}) != `maintenance` currency ({})",
initial.currency, maintenance.currency
),
)?;
Ok(Self {
initial,
maintenance,
currency: initial.currency,
instrument_id,
})
}
pub fn new(initial: Money, maintenance: Money, instrument_id: InstrumentId) -> Self {
Self::new_checked(initial, maintenance, instrument_id).expect(FAILED)
}
}
impl PartialEq for MarginBalance {
fn eq(&self, other: &Self) -> bool {
self.initial == other.initial
&& self.maintenance == other.maintenance
&& self.instrument_id == other.instrument_id
}
}
impl Debug for MarginBalance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}(initial={}, maintenance={}, instrument_id={})",
stringify!(MarginBalance),
self.initial,
self.maintenance,
self.instrument_id,
)
}
}
impl Display for MarginBalance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use crate::{
identifiers::InstrumentId,
types::{
AccountBalance, Currency, MarginBalance, Money,
stubs::{stub_account_balance, stub_margin_balance},
},
};
#[rstest]
fn test_account_balance_equality() {
let account_balance_1 = stub_account_balance();
let account_balance_2 = stub_account_balance();
assert_eq!(account_balance_1, account_balance_2);
}
#[rstest]
fn test_account_balance_debug(stub_account_balance: AccountBalance) {
let result = format!("{stub_account_balance:?}");
let expected =
"AccountBalance(total=1525000.00 USD, locked=25000.00 USD, free=1500000.00 USD)";
assert_eq!(result, expected);
}
#[rstest]
fn test_account_balance_display(stub_account_balance: AccountBalance) {
let result = format!("{stub_account_balance}");
let expected =
"AccountBalance(total=1525000.00 USD, locked=25000.00 USD, free=1500000.00 USD)";
assert_eq!(result, expected);
}
#[rstest]
fn test_margin_balance_equality() {
let margin_balance_1 = stub_margin_balance();
let margin_balance_2 = stub_margin_balance();
assert_eq!(margin_balance_1, margin_balance_2);
}
#[rstest]
fn test_margin_balance_debug(stub_margin_balance: MarginBalance) {
let display = format!("{stub_margin_balance:?}");
assert_eq!(
"MarginBalance(initial=5000.00 USD, maintenance=20000.00 USD, instrument_id=BTCUSDT.COINBASE)",
display
);
}
#[rstest]
fn test_margin_balance_display(stub_margin_balance: MarginBalance) {
let display = format!("{stub_margin_balance}");
assert_eq!(
"MarginBalance(initial=5000.00 USD, maintenance=20000.00 USD, instrument_id=BTCUSDT.COINBASE)",
display
);
}
#[rstest]
fn test_margin_balance_new_checked_with_currency_mismatch_returns_error() {
let usd = Currency::USD();
let eur = Currency::EUR();
let instrument_id = InstrumentId::from("BTCUSDT.COINBASE");
let result = MarginBalance::new_checked(
Money::new(5000.0, usd),
Money::new(20000.0, eur),
instrument_id,
);
assert!(result.is_err());
}
#[rstest]
#[should_panic(expected = "`initial` currency (USD) != `maintenance` currency (EUR)")]
fn test_margin_balance_new_with_currency_mismatch_panics() {
let usd = Currency::USD();
let eur = Currency::EUR();
let instrument_id = InstrumentId::from("BTCUSDT.COINBASE");
let _ = MarginBalance::new(
Money::new(5000.0, usd),
Money::new(20000.0, eur),
instrument_id,
);
}
}