use std::fmt::Display;
use nautilus_core::{UUID4, UnixNanos};
use serde::{Deserialize, Serialize};
use crate::{
enums::AccountType,
identifiers::{AccountId, InstrumentId},
types::{AccountBalance, Currency, MarginBalance, Money},
};
#[repr(C)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct PortfolioSnapshot {
pub account_id: AccountId,
pub account_type: AccountType,
pub base_currency: Option<Currency>,
pub balances: Vec<AccountBalance>,
pub margins: Vec<MarginBalance>,
pub unrealized_pnls: Vec<Money>,
pub realized_pnls: Vec<Money>,
pub total_equity: Vec<Money>,
#[serde(default)]
pub base_currency_equity: Option<Money>,
#[serde(default)]
pub is_stale: bool,
#[serde(default)]
pub stale_instruments: Vec<InstrumentId>,
#[serde(default)]
pub stale_currencies: Vec<Currency>,
#[serde(default)]
pub unpriced_instruments: Vec<InstrumentId>,
pub event_id: UUID4,
pub ts_event: UnixNanos,
pub ts_init: UnixNanos,
}
impl PortfolioSnapshot {
#[expect(clippy::too_many_arguments)]
#[must_use]
pub fn new(
account_id: AccountId,
account_type: AccountType,
base_currency: Option<Currency>,
balances: Vec<AccountBalance>,
margins: Vec<MarginBalance>,
unrealized_pnls: Vec<Money>,
realized_pnls: Vec<Money>,
total_equity: Vec<Money>,
base_currency_equity: Option<Money>,
is_stale: bool,
stale_instruments: Vec<InstrumentId>,
stale_currencies: Vec<Currency>,
unpriced_instruments: Vec<InstrumentId>,
event_id: UUID4,
ts_event: UnixNanos,
ts_init: UnixNanos,
) -> Self {
Self {
account_id,
account_type,
base_currency,
balances,
margins,
unrealized_pnls,
realized_pnls,
total_equity,
base_currency_equity,
is_stale,
stale_instruments,
stale_currencies,
unpriced_instruments,
event_id,
ts_event,
ts_init,
}
}
}
impl Display for PortfolioSnapshot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}(account_id={}, account_type={}, total_equity=[{}], unrealized_pnls=[{}], realized_pnls=[{}], event_id={})",
stringify!(PortfolioSnapshot),
self.account_id,
self.account_type,
self.total_equity
.iter()
.map(|m| format!("{m}"))
.collect::<Vec<_>>()
.join(", "),
self.unrealized_pnls
.iter()
.map(|m| format!("{m}"))
.collect::<Vec<_>>()
.join(", "),
self.realized_pnls
.iter()
.map(|m| format!("{m}"))
.collect::<Vec<_>>()
.join(", "),
self.event_id,
)
}
}
impl PartialEq for PortfolioSnapshot {
fn eq(&self, other: &Self) -> bool {
self.account_id == other.account_id && self.event_id == other.event_id
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn test_deserialize_legacy_snapshot_defaults_valuation_metadata() {
let snapshot = PortfolioSnapshot::new(
AccountId::new("SIM-001"),
AccountType::Cash,
Some(Currency::USD()),
Vec::new(),
Vec::new(),
Vec::new(),
Vec::new(),
vec![Money::from("100.00 USD")],
Some(Money::from("100.00 USD")),
true,
vec![InstrumentId::from("AUDUSD.SIM")],
vec![Currency::AUD()],
vec![InstrumentId::from("GBPUSD.SIM")],
UUID4::new(),
UnixNanos::from(1),
UnixNanos::from(2),
);
let mut value = serde_json::to_value(snapshot).unwrap();
let object = value.as_object_mut().unwrap();
object.remove("base_currency_equity");
object.remove("is_stale");
object.remove("stale_instruments");
object.remove("stale_currencies");
object.remove("unpriced_instruments");
let decoded: PortfolioSnapshot = serde_json::from_value(value).unwrap();
assert_eq!(decoded.base_currency_equity, None);
assert!(!decoded.is_stale);
assert!(decoded.stale_instruments.is_empty());
assert!(decoded.stale_currencies.is_empty());
assert!(decoded.unpriced_instruments.is_empty());
}
}