1use enum_dispatch::enum_dispatch;
23use indexmap::IndexMap;
24use nautilus_core::correctness::{CorrectnessResult, CorrectnessResultExt, FAILED};
25use serde::{Deserialize, Serialize};
26
27use crate::{
28 accounts::{Account, BettingAccount, CashAccount, MarginAccount, WalletAccount},
29 enums::{AccountType, LiquiditySide},
30 events::{AccountState, OrderFilled},
31 identifiers::AccountId,
32 instruments::InstrumentAny,
33 position::Position,
34 types::{AccountBalance, Currency, Money, Price, Quantity},
35};
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39#[enum_dispatch(Account)]
40pub enum AccountAny {
41 Margin(MarginAccount),
43 Cash(CashAccount),
45 Betting(BettingAccount),
47 Wallet(WalletAccount),
49}
50
51impl AccountAny {
52 #[must_use]
54 pub fn clone_without_events(&self) -> Self {
55 match self {
56 Self::Margin(margin) => Self::Margin(margin.clone_without_events()),
57 Self::Cash(cash) => Self::Cash(cash.clone_without_events()),
58 Self::Betting(betting) => Self::Betting(betting.clone_without_events()),
59 Self::Wallet(wallet) => Self::Wallet(wallet.clone_without_events()),
60 }
61 }
62
63 #[must_use]
64 pub fn id(&self) -> AccountId {
65 Account::id(self)
66 }
67
68 #[must_use]
69 pub fn last_event(&self) -> Option<AccountState> {
70 Account::last_event(self)
71 }
72
73 #[must_use]
74 pub fn events(&self) -> Vec<AccountState> {
75 Account::events(self)
76 }
77
78 pub fn apply(&mut self, event: AccountState) -> anyhow::Result<()> {
85 Account::apply(self, event)
86 }
87
88 pub fn set_calculate_account_state(&mut self, calculate_account_state: bool) {
90 match self {
91 Self::Margin(margin) => margin.base.calculate_account_state = calculate_account_state,
92 Self::Cash(cash) => cash.base.calculate_account_state = calculate_account_state,
93 Self::Betting(betting) => {
94 betting.base.calculate_account_state = calculate_account_state;
95 }
96 Self::Wallet(wallet) => {
97 wallet.base.calculate_account_state = calculate_account_state;
98 }
99 }
100 }
101
102 #[must_use]
103 pub fn balances(&self) -> IndexMap<Currency, AccountBalance> {
104 Account::balances(self)
105 }
106
107 #[must_use]
108 pub fn balances_locked(&self) -> IndexMap<Currency, Money> {
109 Account::balances_locked(self)
110 }
111
112 #[must_use]
113 pub fn base_currency(&self) -> Option<Currency> {
114 Account::base_currency(self)
115 }
116
117 pub fn from_events(events: &[AccountState]) -> anyhow::Result<Self> {
121 let Some((init_event, remaining_events)) = events.split_first() else {
122 anyhow::bail!("No account events provided to create `AccountAny`");
123 };
124
125 let mut account = Self::from_state_checked(init_event.clone())?;
126
127 for event in remaining_events {
128 account.apply(event.clone())?;
129 }
130
131 Ok(account)
132 }
133
134 pub fn calculate_pnls(
138 &self,
139 instrument: &InstrumentAny,
140 fill: &OrderFilled,
141 position: Option<Position>,
142 ) -> anyhow::Result<Vec<Money>> {
143 Account::calculate_pnls(self, instrument, fill, position)
144 }
145
146 pub fn calculate_commission(
150 &self,
151 instrument: &InstrumentAny,
152 last_qty: Quantity,
153 last_px: Price,
154 liquidity_side: LiquiditySide,
155 use_quote_for_inverse: Option<bool>,
156 ) -> anyhow::Result<Money> {
157 Account::calculate_commission(
158 self,
159 instrument,
160 last_qty,
161 last_px,
162 liquidity_side,
163 use_quote_for_inverse,
164 )
165 }
166
167 #[must_use]
168 pub fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
169 Account::balance(self, currency)
170 }
171}
172
173impl AccountAny {
174 pub fn try_from_state(event: AccountState) -> Result<Self, &'static str> {
180 Self::from_state_checked(event).map_err(|_| "Invalid wallet account state")
181 }
182
183 fn from_state_checked(event: AccountState) -> CorrectnessResult<Self> {
184 match event.account_type {
185 AccountType::Margin => Ok(Self::Margin(MarginAccount::new(event, false))),
186 AccountType::Cash => Ok(Self::Cash(CashAccount::new(event, false, false))),
187 AccountType::Betting => Ok(Self::Betting(BettingAccount::new(event, false))),
188 AccountType::Wallet => Ok(Self::Wallet(WalletAccount::new_checked(event, false)?)),
189 }
190 }
191}
192
193impl From<AccountState> for AccountAny {
194 fn from(event: AccountState) -> Self {
201 Self::from_state_checked(event).expect_display(FAILED)
202 }
203}
204
205impl PartialEq for AccountAny {
206 fn eq(&self, other: &Self) -> bool {
207 self.id() == other.id()
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use rstest::rstest;
214 use rust_decimal::Decimal;
215
216 use crate::{
217 accounts::{
218 Account, AccountAny,
219 margin_model::{MarginModel, MarginModelAny, StandardMarginModel},
220 },
221 events::{AccountState, account::stubs::*},
222 identifiers::{AccountId, InstrumentId},
223 types::Money,
224 };
225
226 #[rstest]
227 fn test_from_events_empty_returns_error() {
228 let events: Vec<AccountState> = vec![];
229 let result = AccountAny::from_events(&events);
230
231 assert_eq!(
232 result.unwrap_err().to_string(),
233 "No account events provided to create `AccountAny`"
234 );
235 }
236
237 #[rstest]
238 fn test_from_events_single_cash_event(cash_account_state: AccountState) {
239 let result = AccountAny::from_events(&[cash_account_state]);
240 assert!(result.is_ok());
241 assert!(matches!(result.unwrap(), AccountAny::Cash(_)));
242 }
243
244 #[rstest]
245 fn test_from_events_rejects_different_account(cash_account_state: AccountState) {
246 let mut different_account = cash_account_state.clone();
247 different_account.account_id = AccountId::from("OTHER-001");
248
249 let result = AccountAny::from_events(&[cash_account_state, different_account]);
250
251 assert_eq!(
252 result.unwrap_err().to_string(),
253 "Account event had a different account ID: expected SIM-001, received OTHER-001"
254 );
255 }
256
257 #[rstest]
258 #[case::cash(cash_account_state())]
259 #[case::margin(margin_account_state())]
260 #[case::betting(betting_account_state())]
261 #[case::wallet(wallet_account_state())]
262 fn test_apply_rejects_different_account_without_mutation(#[case] state: AccountState) {
263 let mut account = AccountAny::try_from_state(state.clone()).unwrap();
264 let balances_before = account.balances();
265 let mut foreign = state;
266 foreign.account_id = AccountId::from("OTHER-001");
267
268 let error = account.apply(foreign).unwrap_err();
269
270 assert_eq!(
271 error.to_string(),
272 "Account event had a different account ID: expected SIM-001, received OTHER-001"
273 );
274 assert_eq!(account.event_count(), 1);
275 assert_eq!(account.balances(), balances_before);
276 }
277
278 #[rstest]
279 fn test_delegated_state_accessors(cash_account_state: AccountState) {
280 let balance = cash_account_state.balances[0];
281 let account = AccountAny::try_from_state(cash_account_state.clone()).unwrap();
282
283 assert_eq!(account.last_event(), Some(cash_account_state.clone()));
284 assert_eq!(account.events(), vec![cash_account_state.clone()]);
285 assert_eq!(account.base_currency(), cash_account_state.base_currency);
286 assert_eq!(
287 account.balances_locked().get(&balance.currency),
288 Some(&balance.locked)
289 );
290 assert_eq!(account.balances().get(&balance.currency), Some(&balance));
291 }
292
293 #[rstest]
294 fn test_equality_compares_account_ids(cash_account_state: AccountState) {
295 let account = AccountAny::try_from_state(cash_account_state.clone()).unwrap();
296 let same = AccountAny::try_from_state(cash_account_state.clone()).unwrap();
297 let mut other_state = cash_account_state;
298 other_state.account_id = AccountId::from("OTHER-001");
299 let other = AccountAny::try_from_state(other_state).unwrap();
300
301 assert_eq!(account, same);
302 assert_ne!(account, other);
303 }
304
305 #[rstest]
306 fn test_from_events_single_margin_event(margin_account_state: AccountState) {
307 let result = AccountAny::from_events(&[margin_account_state]);
308 assert!(result.is_ok());
309 assert!(matches!(result.unwrap(), AccountAny::Margin(_)));
310 }
311
312 #[rstest]
313 #[case::cash(cash_account_state())]
314 #[case::margin(margin_account_state())]
315 #[case::betting(betting_account_state())]
316 #[case::wallet(wallet_account_state())]
317 fn test_clone_without_events_preserves_current_state(#[case] state: AccountState) {
318 let currency = state.balances[0].currency;
319 let instrument_id = InstrumentId::from("CLONE-TEST.SIM");
320 let locked = Money::from_decimal(Decimal::new(725, 2), currency).unwrap();
321 let commission = Money::from_decimal(Decimal::new(135, 2), currency).unwrap();
322 let mut account = AccountAny::try_from_state(state.clone()).unwrap();
323 account.apply(state).unwrap();
324
325 let base = match &mut account {
326 AccountAny::Margin(account) => &mut account.base,
327 AccountAny::Cash(account) => &mut account.base,
328 AccountAny::Betting(account) => &mut account.base,
329 AccountAny::Wallet(account) => &mut account.base,
330 };
331 base.calculate_account_state = true;
332 base.commissions.insert(currency, commission);
333
334 match &mut account {
335 AccountAny::Margin(account) => {
336 account.set_default_leverage(Decimal::new(7, 0));
337 account.set_leverage(instrument_id, Decimal::new(3, 0));
338 account.set_margin_model(MarginModelAny::Standard(StandardMarginModel).into());
339 }
340 AccountAny::Cash(account) => {
341 account.allow_borrowing = true;
342 account
343 .balances_locked
344 .insert((instrument_id, currency), locked);
345 }
346 AccountAny::Betting(account) => {
347 account
348 .balances_locked
349 .insert((instrument_id, currency), locked);
350 }
351 AccountAny::Wallet(account) => {
352 account
353 .balances_locked
354 .insert((instrument_id, currency), locked);
355 }
356 }
357
358 let cloned = account.clone_without_events();
359 let mut expected = account.clone();
360
361 match &mut expected {
362 AccountAny::Margin(account) => account.base.events.clear(),
363 AccountAny::Cash(account) => account.base.events.clear(),
364 AccountAny::Betting(account) => account.base.events.clear(),
365 AccountAny::Wallet(account) => account.base.events.clear(),
366 }
367
368 assert_eq!(account.event_count(), 2);
369 assert_eq!(cloned.event_count(), 0);
370 match (&account, &cloned) {
371 (AccountAny::Margin(source), AccountAny::Margin(cloned)) => {
372 assert_eq!(cloned.margin_model().name(), source.margin_model().name());
373 assert_eq!(cloned.margin_model().name(), "standard");
374 }
375 (AccountAny::Cash(source), AccountAny::Cash(cloned)) => {
376 assert_eq!(cloned.balances_locked, source.balances_locked);
377 assert!(cloned.allow_borrowing);
378 }
379 (AccountAny::Betting(source), AccountAny::Betting(cloned)) => {
380 assert_eq!(cloned.balances_locked, source.balances_locked);
381 }
382 (AccountAny::Wallet(source), AccountAny::Wallet(cloned)) => {
383 assert_eq!(cloned.balances_locked, source.balances_locked);
384 }
385 _ => panic!("cloned account variant changed"),
386 }
387 assert_eq!(
388 serde_json::to_value(&cloned).unwrap(),
389 serde_json::to_value(&expected).unwrap()
390 );
391 }
392
393 #[rstest]
394 fn test_try_from_state_cash(cash_account_state: AccountState) {
395 let result: Result<AccountAny, &'static str> =
396 AccountAny::try_from_state(cash_account_state);
397 assert!(result.is_ok());
398 assert!(matches!(result.unwrap(), AccountAny::Cash(_)));
399 }
400
401 #[rstest]
402 fn test_try_from_state_margin(margin_account_state: AccountState) {
403 let result = AccountAny::try_from_state(margin_account_state);
404 assert!(result.is_ok());
405 assert!(matches!(result.unwrap(), AccountAny::Margin(_)));
406 }
407
408 #[rstest]
409 fn test_try_from_state_betting(betting_account_state: AccountState) {
410 let result = AccountAny::try_from_state(betting_account_state);
411 assert!(result.is_ok());
412 assert!(matches!(result.unwrap(), AccountAny::Betting(_)));
413 }
414
415 #[rstest]
416 fn test_try_from_state_wallet(wallet_account_state: AccountState) {
417 let result = AccountAny::try_from_state(wallet_account_state);
418 assert!(result.is_ok());
419 assert!(matches!(result.unwrap(), AccountAny::Wallet(_)));
420 }
421
422 #[rstest]
423 fn test_try_from_state_invalid_wallet_returns_static_error() {
424 let result: Result<AccountAny, &'static str> =
425 AccountAny::try_from_state(invalid_wallet_state());
426
427 assert_eq!(result.unwrap_err(), "Invalid wallet account state");
428 }
429
430 #[rstest]
431 fn test_from_events_wallet_applies_sequence(
432 wallet_account_state: AccountState,
433 wallet_account_state_changed: AccountState,
434 ) {
435 let result = AccountAny::from_events(&[wallet_account_state, wallet_account_state_changed]);
436 assert!(result.is_ok());
437 let account = result.unwrap();
438 assert!(matches!(account, AccountAny::Wallet(_)));
439 assert_eq!(account.event_count(), 2);
440 }
441
442 #[rstest]
443 fn test_from_events_wallet_rejects_negative_initial_balance() {
444 let result = AccountAny::from_events(&[invalid_wallet_state()]);
445
446 assert!(result.is_err());
447 assert_eq!(
448 result.unwrap_err().to_string(),
449 "Wallet account balance total was negative"
450 );
451 }
452
453 #[rstest]
454 #[case::cash(cash_account_state(), "Cash")]
455 #[case::margin(margin_account_state(), "Margin")]
456 #[case::betting(betting_account_state(), "Betting")]
457 #[case::wallet(wallet_account_state(), "Wallet")]
458 fn test_serde_round_trip_preserves_variant_payload(
459 #[case] state: AccountState,
460 #[case] expected_variant: &str,
461 ) {
462 let account = AccountAny::try_from_state(state).unwrap();
463
464 let value = serde_json::to_value(&account).unwrap();
465 let object = value.as_object().unwrap();
466 assert_eq!(object.len(), 1);
467 assert!(object.contains_key(expected_variant));
468
469 let deserialized: AccountAny = serde_json::from_value(value).unwrap();
470 assert_eq!(deserialized.id(), account.id());
471 assert_eq!(deserialized.events(), account.events());
472 assert_eq!(deserialized.balances(), account.balances());
473 }
474
475 #[rstest]
476 #[case::cash(include_str!("../../test_data/account_legacy_cash.json"), "Cash")]
477 #[case::margin(include_str!("../../test_data/account_legacy_margin.json"), "Margin")]
478 #[case::betting(include_str!("../../test_data/account_legacy_betting.json"), "Betting")]
479 fn test_deserializes_legacy_payload(#[case] json: &str, #[case] expected_variant: &str) {
480 let account: AccountAny = serde_json::from_str(json).unwrap();
481 let variant = match &account {
482 AccountAny::Cash(_) => "Cash",
483 AccountAny::Margin(_) => "Margin",
484 AccountAny::Betting(_) => "Betting",
485 AccountAny::Wallet(_) => "Wallet",
486 };
487 assert_eq!(variant, expected_variant);
488 assert_eq!(account.event_count(), 1);
489 }
490
491 fn invalid_wallet_state() -> AccountState {
492 AccountState::new(
493 AccountId::from("WALLET-001"),
494 crate::enums::AccountType::Wallet,
495 vec![crate::types::AccountBalance::new(
496 crate::types::Money::from("-1 ETH"),
497 crate::types::Money::from("0 ETH"),
498 crate::types::Money::from("-1 ETH"),
499 )],
500 vec![],
501 true,
502 crate::identifiers::stubs::uuid4(),
503 0.into(),
504 0.into(),
505 None,
506 )
507 }
508}