1use std::{collections::HashMap, fmt::Display};
17
18use nautilus_core::{UUID4, UnixNanos};
19use serde::{Deserialize, Serialize};
20
21use crate::{
22 enums::AccountType,
23 identifiers::{AccountId, InstrumentId},
24 types::{AccountBalance, Currency, MarginBalance},
25};
26
27#[repr(C)]
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[cfg_attr(
31 feature = "python",
32 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
33)]
34#[cfg_attr(
35 feature = "python",
36 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
37)]
38pub struct AccountState {
39 pub account_id: AccountId,
41 pub account_type: AccountType,
43 pub base_currency: Option<Currency>,
45 pub balances: Vec<AccountBalance>,
47 pub margins: Vec<MarginBalance>,
49 pub is_reported: bool,
52 pub event_id: UUID4,
54 pub ts_event: UnixNanos,
56 pub ts_init: UnixNanos,
58}
59
60impl AccountState {
61 #[expect(clippy::too_many_arguments)]
63 #[must_use]
64 pub fn new(
65 account_id: AccountId,
66 account_type: AccountType,
67 balances: Vec<AccountBalance>,
68 margins: Vec<MarginBalance>,
69 is_reported: bool,
70 event_id: UUID4,
71 ts_event: UnixNanos,
72 ts_init: UnixNanos,
73 base_currency: Option<Currency>,
74 ) -> Self {
75 Self {
76 account_id,
77 account_type,
78 base_currency,
79 balances,
80 margins,
81 is_reported,
82 event_id,
83 ts_event,
84 ts_init,
85 }
86 }
87
88 #[must_use]
99 pub fn has_same_balances_and_margins(&self, other: &Self) -> bool {
100 if self.balances.len() != other.balances.len() || self.margins.len() != other.margins.len()
102 {
103 return false;
104 }
105
106 let self_balances: HashMap<Currency, &AccountBalance> = self
108 .balances
109 .iter()
110 .map(|balance| (balance.currency, balance))
111 .collect();
112
113 let other_balances: HashMap<Currency, &AccountBalance> = other
114 .balances
115 .iter()
116 .map(|balance| (balance.currency, balance))
117 .collect();
118
119 for (currency, self_balance) in &self_balances {
121 match other_balances.get(currency) {
122 Some(other_balance) => {
123 if self_balance != other_balance {
124 return false;
125 }
126 }
127 None => return false, }
129 }
130
131 let self_margins: HashMap<(Option<InstrumentId>, Currency), &MarginBalance> = self
135 .margins
136 .iter()
137 .map(|margin| ((margin.instrument_id, margin.currency), margin))
138 .collect();
139
140 let other_margins: HashMap<(Option<InstrumentId>, Currency), &MarginBalance> = other
141 .margins
142 .iter()
143 .map(|margin| ((margin.instrument_id, margin.currency), margin))
144 .collect();
145
146 for (key, self_margin) in &self_margins {
148 match other_margins.get(key) {
149 Some(other_margin) => {
150 if self_margin != other_margin {
151 return false;
152 }
153 }
154 None => return false, }
156 }
157
158 true
159 }
160}
161
162impl Display for AccountState {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 write!(
165 f,
166 "{}(account_id={}, account_type={}, base_currency={}, is_reported={}, balances=[{}], margins=[{}], event_id={})",
167 stringify!(AccountState),
168 self.account_id,
169 self.account_type,
170 self.base_currency.map_or_else(
171 || "None".to_string(),
172 |base_currency| format!("{}", base_currency.code)
173 ),
174 self.is_reported,
175 self.balances
176 .iter()
177 .map(|b| format!("{b}"))
178 .collect::<Vec<String>>()
179 .join(", "),
180 self.margins
181 .iter()
182 .map(|m| format!("{m}"))
183 .collect::<Vec<String>>()
184 .join(", "),
185 self.event_id
186 )
187 }
188}
189
190impl PartialEq for AccountState {
191 fn eq(&self, other: &Self) -> bool {
192 self.account_id == other.account_id
193 && self.account_type == other.account_type
194 && self.event_id == other.event_id
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use nautilus_core::{UUID4, UnixNanos};
201 use rstest::rstest;
202
203 use crate::{
204 enums::AccountType,
205 events::{
206 AccountState,
207 account::stubs::{cash_account_state, margin_account_state},
208 },
209 identifiers::{AccountId, InstrumentId},
210 types::{AccountBalance, Currency, MarginBalance, Money},
211 };
212
213 #[rstest]
214 fn test_equality() {
215 let cash_account_state_1 = cash_account_state();
216 let cash_account_state_2 = cash_account_state();
217 assert_eq!(cash_account_state_1, cash_account_state_2);
218 }
219
220 #[rstest]
221 fn test_display_cash_account_state(cash_account_state: AccountState) {
222 let display = format!("{cash_account_state}");
223 assert_eq!(
224 display,
225 "AccountState(account_id=SIM-001, account_type=CASH, base_currency=USD, is_reported=true, \
226 balances=[AccountBalance(total=1525000.00 USD, locked=25000.00 USD, free=1500000.00 USD)], \
227 margins=[], event_id=16578139-a945-4b65-b46c-bc131a15d8e7)"
228 );
229 }
230
231 #[rstest]
232 fn test_display_margin_account_state(margin_account_state: AccountState) {
233 let display = format!("{margin_account_state}");
234 assert_eq!(
235 display,
236 "AccountState(account_id=SIM-001, account_type=MARGIN, base_currency=USD, is_reported=true, \
237 balances=[AccountBalance(total=1525000.00 USD, locked=25000.00 USD, free=1500000.00 USD)], \
238 margins=[MarginBalance(initial=5000.00 USD, maintenance=20000.00 USD, instrument_id=BTCUSDT.COINBASE)], \
239 event_id=16578139-a945-4b65-b46c-bc131a15d8e7)"
240 );
241 }
242
243 #[rstest]
244 fn test_has_same_balances_and_margins_when_identical() {
245 let state1 = cash_account_state();
246 let state2 = cash_account_state();
247 assert!(state1.has_same_balances_and_margins(&state2));
248 }
249
250 #[rstest]
251 fn test_has_same_balances_and_margins_when_different_balance_amounts() {
252 let state1 = cash_account_state();
253 let mut state2 = cash_account_state();
254 let usd = Currency::USD();
256 let different_balance = AccountBalance::new(
257 Money::new(2_000_000.0, usd),
258 Money::new(50000.0, usd),
259 Money::new(1_950_000.0, usd),
260 );
261 state2.balances = vec![different_balance];
262 assert!(!state1.has_same_balances_and_margins(&state2));
263 }
264
265 #[rstest]
266 fn test_has_same_balances_and_margins_when_different_balance_currencies() {
267 let state1 = cash_account_state();
268 let mut state2 = cash_account_state();
269 let eur = Currency::EUR();
271 let different_balance = AccountBalance::new(
272 Money::new(1_525_000.0, eur),
273 Money::new(25000.0, eur),
274 Money::new(1_500_000.0, eur),
275 );
276 state2.balances = vec![different_balance];
277 assert!(!state1.has_same_balances_and_margins(&state2));
278 }
279
280 #[rstest]
281 fn test_has_same_balances_and_margins_when_missing_balance() {
282 let state1 = cash_account_state();
283 let mut state2 = cash_account_state();
284 let eur = Currency::EUR();
286 let additional_balance = AccountBalance::new(
287 Money::new(1_000_000.0, eur),
288 Money::new(0.0, eur),
289 Money::new(1_000_000.0, eur),
290 );
291 state2.balances.push(additional_balance);
292 assert!(!state1.has_same_balances_and_margins(&state2));
293 }
294
295 #[rstest]
296 fn test_has_same_balances_and_margins_when_different_margin_amounts() {
297 let state1 = margin_account_state();
298 let mut state2 = margin_account_state();
299 let usd = Currency::USD();
301 let instrument_id = InstrumentId::from("BTCUSDT.COINBASE");
302 let different_margin = MarginBalance::new(
303 Money::new(10000.0, usd),
304 Money::new(40000.0, usd),
305 Some(instrument_id),
306 );
307 state2.margins = vec![different_margin];
308 assert!(!state1.has_same_balances_and_margins(&state2));
309 }
310
311 #[rstest]
312 fn test_has_same_balances_and_margins_when_different_margin_instruments() {
313 let state1 = margin_account_state();
314 let mut state2 = margin_account_state();
315 let usd = Currency::USD();
317 let different_instrument_id = InstrumentId::from("ETHUSDT.BINANCE");
318 let different_margin = MarginBalance::new(
319 Money::new(5000.0, usd),
320 Money::new(20000.0, usd),
321 Some(different_instrument_id),
322 );
323 state2.margins = vec![different_margin];
324 assert!(!state1.has_same_balances_and_margins(&state2));
325 }
326
327 #[rstest]
328 fn test_has_same_balances_and_margins_when_missing_margin() {
329 let state1 = margin_account_state();
330 let mut state2 = margin_account_state();
331 let usd = Currency::USD();
333 let additional_instrument_id = InstrumentId::from("ETHUSDT.BINANCE");
334 let additional_margin = MarginBalance::new(
335 Money::new(3000.0, usd),
336 Money::new(15000.0, usd),
337 Some(additional_instrument_id),
338 );
339 state2.margins.push(additional_margin);
340 assert!(!state1.has_same_balances_and_margins(&state2));
341 }
342
343 #[rstest]
344 fn test_has_same_balances_and_margins_with_empty_collections() {
345 let account_id = AccountId::new("TEST-001");
346 let event_id = UUID4::new();
347 let ts_event = UnixNanos::from(1);
348 let ts_init = UnixNanos::from(2);
349
350 let state1 = AccountState::new(
351 account_id,
352 AccountType::Cash,
353 vec![], vec![], true,
356 event_id,
357 ts_event,
358 ts_init,
359 Some(Currency::USD()),
360 );
361
362 let state2 = AccountState::new(
363 account_id,
364 AccountType::Cash,
365 vec![], vec![], true,
368 UUID4::new(), UnixNanos::from(3), UnixNanos::from(4),
371 Some(Currency::USD()),
372 );
373
374 assert!(state1.has_same_balances_and_margins(&state2));
375 }
376
377 #[rstest]
378 fn test_has_same_balances_and_margins_with_multiple_balances_and_margins() {
379 let account_id = AccountId::new("TEST-001");
380 let event_id = UUID4::new();
381 let ts_event = UnixNanos::from(1);
382 let ts_init = UnixNanos::from(2);
383
384 let usd = Currency::USD();
385 let eur = Currency::EUR();
386 let btc_instrument = InstrumentId::from("BTCUSDT.COINBASE");
387 let eth_instrument = InstrumentId::from("ETHUSDT.BINANCE");
388
389 let balances = vec![
390 AccountBalance::new(
391 Money::new(1_000_000.0, usd),
392 Money::new(0.0, usd),
393 Money::new(1_000_000.0, usd),
394 ),
395 AccountBalance::new(
396 Money::new(500_000.0, eur),
397 Money::new(10000.0, eur),
398 Money::new(490_000.0, eur),
399 ),
400 ];
401
402 let margins = vec![
403 MarginBalance::new(
404 Money::new(5000.0, usd),
405 Money::new(20000.0, usd),
406 Some(btc_instrument),
407 ),
408 MarginBalance::new(
409 Money::new(3000.0, usd),
410 Money::new(15000.0, usd),
411 Some(eth_instrument),
412 ),
413 ];
414
415 let state1 = AccountState::new(
416 account_id,
417 AccountType::Margin,
418 balances.clone(),
419 margins.clone(),
420 true,
421 event_id,
422 ts_event,
423 ts_init,
424 Some(usd),
425 );
426
427 let state2 = AccountState::new(
428 account_id,
429 AccountType::Margin,
430 balances,
431 margins,
432 true,
433 UUID4::new(), UnixNanos::from(3), UnixNanos::from(4),
436 Some(usd),
437 );
438
439 assert!(state1.has_same_balances_and_margins(&state2));
440 }
441}