1use enum_dispatch::enum_dispatch;
23use indexmap::IndexMap;
24use serde::{Deserialize, Serialize};
25
26use crate::{
27 accounts::{Account, BettingAccount, CashAccount, MarginAccount},
28 enums::{AccountType, LiquiditySide},
29 events::{AccountState, OrderFilled},
30 identifiers::AccountId,
31 instruments::InstrumentAny,
32 position::Position,
33 types::{AccountBalance, Currency, Money, Price, Quantity},
34};
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[enum_dispatch(Account)]
38pub enum AccountAny {
39 Margin(MarginAccount),
40 Cash(CashAccount),
41 Betting(BettingAccount),
42}
43
44impl AccountAny {
45 #[must_use]
46 pub fn id(&self) -> AccountId {
47 match self {
48 Self::Margin(margin) => margin.id,
49 Self::Cash(cash) => cash.id,
50 Self::Betting(betting) => betting.id,
51 }
52 }
53
54 #[must_use]
55 pub fn last_event(&self) -> Option<AccountState> {
56 match self {
57 Self::Margin(margin) => margin.last_event(),
58 Self::Cash(cash) => cash.last_event(),
59 Self::Betting(betting) => betting.last_event(),
60 }
61 }
62
63 #[must_use]
64 pub fn events(&self) -> Vec<AccountState> {
65 match self {
66 Self::Margin(margin) => margin.events(),
67 Self::Cash(cash) => cash.events(),
68 Self::Betting(betting) => betting.events(),
69 }
70 }
71
72 pub fn apply(&mut self, event: AccountState) -> anyhow::Result<()> {
79 match self {
80 Self::Margin(margin) => margin.apply(event),
81 Self::Cash(cash) => cash.apply(event),
82 Self::Betting(betting) => betting.apply(event),
83 }
84 }
85
86 pub fn set_calculate_account_state(&mut self, calculate_account_state: bool) {
88 match self {
89 Self::Margin(margin) => margin.base.calculate_account_state = calculate_account_state,
90 Self::Cash(cash) => cash.base.calculate_account_state = calculate_account_state,
91 Self::Betting(betting) => {
92 betting.base.calculate_account_state = calculate_account_state;
93 }
94 }
95 }
96
97 #[must_use]
98 pub fn balances(&self) -> IndexMap<Currency, AccountBalance> {
99 match self {
100 Self::Margin(margin) => margin.balances(),
101 Self::Cash(cash) => cash.balances(),
102 Self::Betting(betting) => betting.balances(),
103 }
104 }
105
106 #[must_use]
107 pub fn balances_locked(&self) -> IndexMap<Currency, Money> {
108 match self {
109 Self::Margin(margin) => margin.balances_locked(),
110 Self::Cash(cash) => cash.balances_locked(),
111 Self::Betting(betting) => betting.balances_locked(),
112 }
113 }
114
115 #[must_use]
116 pub fn base_currency(&self) -> Option<Currency> {
117 match self {
118 Self::Margin(margin) => margin.base_currency(),
119 Self::Cash(cash) => cash.base_currency(),
120 Self::Betting(betting) => betting.base_currency(),
121 }
122 }
123
124 #[expect(clippy::missing_panics_doc)] pub fn from_events(events: &[AccountState]) -> anyhow::Result<Self> {
129 if events.is_empty() {
130 anyhow::bail!("No order events provided to create `AccountAny`");
131 }
132
133 let init_event = events.first().unwrap();
134 let mut account = Self::from(init_event.clone());
135 for event in events.iter().skip(1) {
136 account.apply(event.clone())?;
137 }
138 Ok(account)
139 }
140
141 pub fn calculate_pnls(
145 &self,
146 instrument: &InstrumentAny,
147 fill: &OrderFilled,
148 position: Option<Position>,
149 ) -> anyhow::Result<Vec<Money>> {
150 match self {
151 Self::Margin(margin) => margin.calculate_pnls(instrument, fill, position),
152 Self::Cash(cash) => cash.calculate_pnls(instrument, fill, position),
153 Self::Betting(betting) => betting.calculate_pnls(instrument, fill, position),
154 }
155 }
156
157 pub fn calculate_commission(
161 &self,
162 instrument: &InstrumentAny,
163 last_qty: Quantity,
164 last_px: Price,
165 liquidity_side: LiquiditySide,
166 use_quote_for_inverse: Option<bool>,
167 ) -> anyhow::Result<Money> {
168 match self {
169 Self::Margin(margin) => margin.calculate_commission(
170 instrument,
171 last_qty,
172 last_px,
173 liquidity_side,
174 use_quote_for_inverse,
175 ),
176 Self::Cash(cash) => cash.calculate_commission(
177 instrument,
178 last_qty,
179 last_px,
180 liquidity_side,
181 use_quote_for_inverse,
182 ),
183 Self::Betting(betting) => betting.calculate_commission(
184 instrument,
185 last_qty,
186 last_px,
187 liquidity_side,
188 use_quote_for_inverse,
189 ),
190 }
191 }
192
193 #[must_use]
194 pub fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
195 match self {
196 Self::Margin(margin) => margin.balance(currency),
197 Self::Cash(cash) => cash.balance(currency),
198 Self::Betting(betting) => betting.balance(currency),
199 }
200 }
201}
202
203impl AccountAny {
204 pub fn try_from_state(event: AccountState) -> Result<Self, &'static str> {
210 match event.account_type {
211 AccountType::Margin => Ok(Self::Margin(MarginAccount::new(event, false))),
212 AccountType::Cash => Ok(Self::Cash(CashAccount::new(event, false, false))),
213 AccountType::Betting => Ok(Self::Betting(BettingAccount::new(event, false))),
214 AccountType::Wallet => Err("Wallet accounts are not yet implemented in Rust"),
215 }
216 }
217}
218
219impl From<AccountState> for AccountAny {
220 fn from(event: AccountState) -> Self {
227 Self::try_from_state(event).expect("Unsupported account type")
228 }
229}
230
231impl PartialEq for AccountAny {
232 fn eq(&self, other: &Self) -> bool {
233 self.id() == other.id()
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use nautilus_core::UUID4;
240 use rstest::rstest;
241
242 use crate::{
243 accounts::AccountAny,
244 enums::AccountType,
245 events::{AccountState, account::stubs::*},
246 identifiers::AccountId,
247 };
248
249 #[rstest]
250 fn test_from_events_empty_returns_error() {
251 let events: Vec<AccountState> = vec![];
252 let result = AccountAny::from_events(&events);
253 assert!(result.is_err());
254 }
255
256 #[rstest]
257 fn test_from_events_single_cash_event(cash_account_state: AccountState) {
258 let result = AccountAny::from_events(&[cash_account_state]);
259 assert!(result.is_ok());
260 assert!(matches!(result.unwrap(), AccountAny::Cash(_)));
261 }
262
263 #[rstest]
264 fn test_from_events_single_margin_event(margin_account_state: AccountState) {
265 let result = AccountAny::from_events(&[margin_account_state]);
266 assert!(result.is_ok());
267 assert!(matches!(result.unwrap(), AccountAny::Margin(_)));
268 }
269
270 #[rstest]
271 fn test_try_from_state_cash(cash_account_state: AccountState) {
272 let result = AccountAny::try_from_state(cash_account_state);
273 assert!(result.is_ok());
274 assert!(matches!(result.unwrap(), AccountAny::Cash(_)));
275 }
276
277 #[rstest]
278 fn test_try_from_state_margin(margin_account_state: AccountState) {
279 let result = AccountAny::try_from_state(margin_account_state);
280 assert!(result.is_ok());
281 assert!(matches!(result.unwrap(), AccountAny::Margin(_)));
282 }
283
284 #[rstest]
285 fn test_try_from_state_betting(betting_account_state: AccountState) {
286 let result = AccountAny::try_from_state(betting_account_state);
287 assert!(result.is_ok());
288 assert!(matches!(result.unwrap(), AccountAny::Betting(_)));
289 }
290
291 #[rstest]
292 fn test_try_from_state_wallet_returns_error() {
293 let state = AccountState::new(
294 AccountId::from("WALLET-001"),
295 AccountType::Wallet,
296 vec![],
297 vec![],
298 true,
299 UUID4::default(),
300 0.into(),
301 0.into(),
302 None,
303 );
304 let result = AccountAny::try_from_state(state);
305 assert!(result.is_err());
306 }
307}