Skip to main content

nautilus_model/accounts/
any.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Enum wrapper providing a type-erased view over the various concrete [`Account`] implementations.
17//!
18//! The `AccountAny` enum is primarily used when heterogeneous account types need to be stored in a
19//! single collection (e.g. `Vec<AccountAny>`).  Each variant simply embeds one of the concrete
20//! account structs defined in this module.
21
22use 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    /// Applies an account state event to update the account.
73    ///
74    /// # Errors
75    ///
76    /// Returns an error if the account state cannot be applied (e.g., negative balance
77    /// when borrowing is not allowed for a cash account).
78    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    /// # Errors
114    ///
115    /// Returns an error if `events` is empty.
116    #[expect(clippy::missing_panics_doc)] // Guarded by empty check above
117    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    /// # Errors
131    ///
132    /// Returns an error if calculating P&Ls fails for the underlying account.
133    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    /// # Errors
147    ///
148    /// Returns an error if calculating commission fails for the underlying account.
149    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    /// Creates an `AccountAny` from an `AccountState`, returning an error for unsupported types.
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if the account type is `Wallet` (unsupported in Rust).
198    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    /// Creates an `AccountAny` from an `AccountState`.
210    ///
211    /// # Panics
212    ///
213    /// Panics if the account type is `Wallet` (unsupported in Rust).
214    /// Use [`AccountAny::try_from_state`] for fallible conversion.
215    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}