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 #[must_use]
87 pub fn balances(&self) -> IndexMap<Currency, AccountBalance> {
88 match self {
89 Self::Margin(margin) => margin.balances(),
90 Self::Cash(cash) => cash.balances(),
91 Self::Betting(betting) => betting.balances(),
92 }
93 }
94
95 #[must_use]
96 pub fn balances_locked(&self) -> IndexMap<Currency, Money> {
97 match self {
98 Self::Margin(margin) => margin.balances_locked(),
99 Self::Cash(cash) => cash.balances_locked(),
100 Self::Betting(betting) => betting.balances_locked(),
101 }
102 }
103
104 #[must_use]
105 pub fn base_currency(&self) -> Option<Currency> {
106 match self {
107 Self::Margin(margin) => margin.base_currency(),
108 Self::Cash(cash) => cash.base_currency(),
109 Self::Betting(betting) => betting.base_currency(),
110 }
111 }
112
113 #[expect(clippy::missing_panics_doc)] pub fn from_events(events: &[AccountState]) -> anyhow::Result<Self> {
118 if events.is_empty() {
119 anyhow::bail!("No order events provided to create `AccountAny`");
120 }
121
122 let init_event = events.first().unwrap();
123 let mut account = Self::from(init_event.clone());
124 for event in events.iter().skip(1) {
125 account.apply(event.clone())?;
126 }
127 Ok(account)
128 }
129
130 pub fn calculate_pnls(
134 &self,
135 instrument: &InstrumentAny,
136 fill: &OrderFilled,
137 position: Option<Position>,
138 ) -> anyhow::Result<Vec<Money>> {
139 match self {
140 Self::Margin(margin) => margin.calculate_pnls(instrument, fill, position),
141 Self::Cash(cash) => cash.calculate_pnls(instrument, fill, position),
142 Self::Betting(betting) => betting.calculate_pnls(instrument, fill, position),
143 }
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 match self {
158 Self::Margin(margin) => margin.calculate_commission(
159 instrument,
160 last_qty,
161 last_px,
162 liquidity_side,
163 use_quote_for_inverse,
164 ),
165 Self::Cash(cash) => cash.calculate_commission(
166 instrument,
167 last_qty,
168 last_px,
169 liquidity_side,
170 use_quote_for_inverse,
171 ),
172 Self::Betting(betting) => betting.calculate_commission(
173 instrument,
174 last_qty,
175 last_px,
176 liquidity_side,
177 use_quote_for_inverse,
178 ),
179 }
180 }
181
182 #[must_use]
183 pub fn balance(&self, currency: Option<Currency>) -> Option<&AccountBalance> {
184 match self {
185 Self::Margin(margin) => margin.balance(currency),
186 Self::Cash(cash) => cash.balance(currency),
187 Self::Betting(betting) => betting.balance(currency),
188 }
189 }
190}
191
192impl AccountAny {
193 pub fn try_from_state(event: AccountState) -> Result<Self, &'static str> {
199 match event.account_type {
200 AccountType::Margin => Ok(Self::Margin(MarginAccount::new(event, false))),
201 AccountType::Cash => Ok(Self::Cash(CashAccount::new(event, false, false))),
202 AccountType::Betting => Ok(Self::Betting(BettingAccount::new(event, false))),
203 AccountType::Wallet => Err("Wallet accounts are not yet implemented in Rust"),
204 }
205 }
206}
207
208impl From<AccountState> for AccountAny {
209 fn from(event: AccountState) -> Self {
216 Self::try_from_state(event).expect("Unsupported account type")
217 }
218}
219
220impl PartialEq for AccountAny {
221 fn eq(&self, other: &Self) -> bool {
222 self.id() == other.id()
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use nautilus_core::UUID4;
229 use rstest::rstest;
230
231 use crate::{
232 accounts::AccountAny,
233 enums::AccountType,
234 events::{AccountState, account::stubs::*},
235 identifiers::AccountId,
236 };
237
238 #[rstest]
239 fn test_from_events_empty_returns_error() {
240 let events: Vec<AccountState> = vec![];
241 let result = AccountAny::from_events(&events);
242 assert!(result.is_err());
243 }
244
245 #[rstest]
246 fn test_from_events_single_cash_event(cash_account_state: AccountState) {
247 let result = AccountAny::from_events(&[cash_account_state]);
248 assert!(result.is_ok());
249 assert!(matches!(result.unwrap(), AccountAny::Cash(_)));
250 }
251
252 #[rstest]
253 fn test_from_events_single_margin_event(margin_account_state: AccountState) {
254 let result = AccountAny::from_events(&[margin_account_state]);
255 assert!(result.is_ok());
256 assert!(matches!(result.unwrap(), AccountAny::Margin(_)));
257 }
258
259 #[rstest]
260 fn test_try_from_state_cash(cash_account_state: AccountState) {
261 let result = AccountAny::try_from_state(cash_account_state);
262 assert!(result.is_ok());
263 assert!(matches!(result.unwrap(), AccountAny::Cash(_)));
264 }
265
266 #[rstest]
267 fn test_try_from_state_margin(margin_account_state: AccountState) {
268 let result = AccountAny::try_from_state(margin_account_state);
269 assert!(result.is_ok());
270 assert!(matches!(result.unwrap(), AccountAny::Margin(_)));
271 }
272
273 #[rstest]
274 fn test_try_from_state_betting(betting_account_state: AccountState) {
275 let result = AccountAny::try_from_state(betting_account_state);
276 assert!(result.is_ok());
277 assert!(matches!(result.unwrap(), AccountAny::Betting(_)));
278 }
279
280 #[rstest]
281 fn test_try_from_state_wallet_returns_error() {
282 let state = AccountState::new(
283 AccountId::from("WALLET-001"),
284 AccountType::Wallet,
285 vec![],
286 vec![],
287 true,
288 UUID4::default(),
289 0.into(),
290 0.into(),
291 None,
292 );
293 let result = AccountAny::try_from_state(state);
294 assert!(result.is_err());
295 }
296}