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    /// Sets whether account state should be recalculated from order fills.
87    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    /// # Errors
125    ///
126    /// Returns an error if `events` is empty.
127    #[expect(clippy::missing_panics_doc)] // Guarded by empty check above
128    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    /// # Errors
142    ///
143    /// Returns an error if calculating P&Ls fails for the underlying account.
144    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    /// # Errors
158    ///
159    /// Returns an error if calculating commission fails for the underlying account.
160    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    /// Creates an `AccountAny` from an `AccountState`, returning an error for unsupported types.
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if the account type is `Wallet` (unsupported in Rust).
209    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    /// Creates an `AccountAny` from an `AccountState`.
221    ///
222    /// # Panics
223    ///
224    /// Panics if the account type is `Wallet` (unsupported in Rust).
225    /// Use [`AccountAny::try_from_state`] for fallible conversion.
226    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}