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_from_events_single_margin_event(margin_account_state: AccountState) {
280 let result = AccountAny::from_events(&[margin_account_state]);
281 assert!(result.is_ok());
282 assert!(matches!(result.unwrap(), AccountAny::Margin(_)));
283 }
284
285 #[rstest]
286 #[case::cash(cash_account_state())]
287 #[case::margin(margin_account_state())]
288 #[case::betting(betting_account_state())]
289 #[case::wallet(wallet_account_state())]
290 fn test_clone_without_events_preserves_current_state(#[case] state: AccountState) {
291 let currency = state.balances[0].currency;
292 let instrument_id = InstrumentId::from("CLONE-TEST.SIM");
293 let locked = Money::from_decimal(Decimal::new(725, 2), currency).unwrap();
294 let commission = Money::from_decimal(Decimal::new(135, 2), currency).unwrap();
295 let mut account = AccountAny::try_from_state(state.clone()).unwrap();
296 account.apply(state).unwrap();
297
298 let base = match &mut account {
299 AccountAny::Margin(account) => &mut account.base,
300 AccountAny::Cash(account) => &mut account.base,
301 AccountAny::Betting(account) => &mut account.base,
302 AccountAny::Wallet(account) => &mut account.base,
303 };
304 base.calculate_account_state = true;
305 base.commissions.insert(currency, commission);
306
307 match &mut account {
308 AccountAny::Margin(account) => {
309 account.set_default_leverage(Decimal::new(7, 0));
310 account.set_leverage(instrument_id, Decimal::new(3, 0));
311 account.set_margin_model(MarginModelAny::Standard(StandardMarginModel).into());
312 }
313 AccountAny::Cash(account) => {
314 account.allow_borrowing = true;
315 account
316 .balances_locked
317 .insert((instrument_id, currency), locked);
318 }
319 AccountAny::Betting(account) => {
320 account
321 .balances_locked
322 .insert((instrument_id, currency), locked);
323 }
324 AccountAny::Wallet(account) => {
325 account
326 .balances_locked
327 .insert((instrument_id, currency), locked);
328 }
329 }
330
331 let cloned = account.clone_without_events();
332 let mut expected = account.clone();
333
334 match &mut expected {
335 AccountAny::Margin(account) => account.base.events.clear(),
336 AccountAny::Cash(account) => account.base.events.clear(),
337 AccountAny::Betting(account) => account.base.events.clear(),
338 AccountAny::Wallet(account) => account.base.events.clear(),
339 }
340
341 assert_eq!(account.event_count(), 2);
342 assert_eq!(cloned.event_count(), 0);
343 match (&account, &cloned) {
344 (AccountAny::Margin(source), AccountAny::Margin(cloned)) => {
345 assert_eq!(cloned.margin_model().name(), source.margin_model().name());
346 assert_eq!(cloned.margin_model().name(), "standard");
347 }
348 (AccountAny::Cash(source), AccountAny::Cash(cloned)) => {
349 assert_eq!(cloned.balances_locked, source.balances_locked);
350 assert!(cloned.allow_borrowing);
351 }
352 (AccountAny::Betting(source), AccountAny::Betting(cloned)) => {
353 assert_eq!(cloned.balances_locked, source.balances_locked);
354 }
355 (AccountAny::Wallet(source), AccountAny::Wallet(cloned)) => {
356 assert_eq!(cloned.balances_locked, source.balances_locked);
357 }
358 _ => panic!("cloned account variant changed"),
359 }
360 assert_eq!(
361 serde_json::to_value(&cloned).unwrap(),
362 serde_json::to_value(&expected).unwrap()
363 );
364 }
365
366 #[rstest]
367 fn test_try_from_state_cash(cash_account_state: AccountState) {
368 let result: Result<AccountAny, &'static str> =
369 AccountAny::try_from_state(cash_account_state);
370 assert!(result.is_ok());
371 assert!(matches!(result.unwrap(), AccountAny::Cash(_)));
372 }
373
374 #[rstest]
375 fn test_try_from_state_margin(margin_account_state: AccountState) {
376 let result = AccountAny::try_from_state(margin_account_state);
377 assert!(result.is_ok());
378 assert!(matches!(result.unwrap(), AccountAny::Margin(_)));
379 }
380
381 #[rstest]
382 fn test_try_from_state_betting(betting_account_state: AccountState) {
383 let result = AccountAny::try_from_state(betting_account_state);
384 assert!(result.is_ok());
385 assert!(matches!(result.unwrap(), AccountAny::Betting(_)));
386 }
387
388 #[rstest]
389 fn test_try_from_state_wallet(wallet_account_state: AccountState) {
390 let result = AccountAny::try_from_state(wallet_account_state);
391 assert!(result.is_ok());
392 assert!(matches!(result.unwrap(), AccountAny::Wallet(_)));
393 }
394
395 #[rstest]
396 fn test_try_from_state_invalid_wallet_returns_static_error() {
397 let result: Result<AccountAny, &'static str> =
398 AccountAny::try_from_state(invalid_wallet_state());
399
400 assert_eq!(result.unwrap_err(), "Invalid wallet account state");
401 }
402
403 #[rstest]
404 fn test_from_events_wallet_applies_sequence(
405 wallet_account_state: AccountState,
406 wallet_account_state_changed: AccountState,
407 ) {
408 let result = AccountAny::from_events(&[wallet_account_state, wallet_account_state_changed]);
409 assert!(result.is_ok());
410 let account = result.unwrap();
411 assert!(matches!(account, AccountAny::Wallet(_)));
412 assert_eq!(account.event_count(), 2);
413 }
414
415 #[rstest]
416 fn test_from_events_wallet_rejects_negative_initial_balance() {
417 let result = AccountAny::from_events(&[invalid_wallet_state()]);
418
419 assert!(result.is_err());
420 assert_eq!(
421 result.unwrap_err().to_string(),
422 "Wallet account balance total was negative"
423 );
424 }
425
426 #[rstest]
427 #[case::cash(cash_account_state(), "Cash")]
428 #[case::margin(margin_account_state(), "Margin")]
429 #[case::betting(betting_account_state(), "Betting")]
430 #[case::wallet(wallet_account_state(), "Wallet")]
431 fn test_serde_round_trip_preserves_variant_payload(
432 #[case] state: AccountState,
433 #[case] expected_variant: &str,
434 ) {
435 let account = AccountAny::try_from_state(state).unwrap();
436
437 let value = serde_json::to_value(&account).unwrap();
438 let object = value.as_object().unwrap();
439 assert_eq!(object.len(), 1);
440 assert!(object.contains_key(expected_variant));
441
442 let deserialized: AccountAny = serde_json::from_value(value).unwrap();
443 assert_eq!(deserialized.id(), account.id());
444 assert_eq!(deserialized.events(), account.events());
445 assert_eq!(deserialized.balances(), account.balances());
446 }
447
448 #[rstest]
449 #[case::cash(include_str!("../../test_data/account_legacy_cash.json"), "Cash")]
450 #[case::margin(include_str!("../../test_data/account_legacy_margin.json"), "Margin")]
451 #[case::betting(include_str!("../../test_data/account_legacy_betting.json"), "Betting")]
452 fn test_deserializes_legacy_payload(#[case] json: &str, #[case] expected_variant: &str) {
453 let account: AccountAny = serde_json::from_str(json).unwrap();
454 let variant = match &account {
455 AccountAny::Cash(_) => "Cash",
456 AccountAny::Margin(_) => "Margin",
457 AccountAny::Betting(_) => "Betting",
458 AccountAny::Wallet(_) => "Wallet",
459 };
460 assert_eq!(variant, expected_variant);
461 assert_eq!(account.event_count(), 1);
462 }
463
464 fn invalid_wallet_state() -> AccountState {
465 AccountState::new(
466 AccountId::from("WALLET-001"),
467 crate::enums::AccountType::Wallet,
468 vec![crate::types::AccountBalance::new(
469 crate::types::Money::from("-1 ETH"),
470 crate::types::Money::from("0 ETH"),
471 crate::types::Money::from("-1 ETH"),
472 )],
473 vec![],
474 true,
475 crate::identifiers::stubs::uuid4(),
476 0.into(),
477 0.into(),
478 None,
479 )
480 }
481}