Skip to main content

nautilus_model/python/account/
mod.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
16pub mod betting;
17pub mod cash;
18pub mod margin;
19pub mod margin_model;
20pub mod transformer;
21pub mod wallet;
22
23use nautilus_core::python::to_pyvalue_err;
24use pyo3::{Py, PyAny, PyResult, Python, conversion::IntoPyObjectExt};
25
26use crate::{
27    accounts::{AccountAny, BettingAccount, CashAccount, MarginAccount, WalletAccount},
28    enums::AccountType,
29    types::Currency,
30};
31
32/// Converts a Python account object into a Rust `AccountAny` enum.
33///
34/// # Errors
35///
36/// Returns an error if:
37/// - Retrieving the `account_type` attribute fails.
38/// - Extracting the object into the account type's concrete class fails.
39/// - The `account_type` is unsupported.
40#[expect(clippy::needless_pass_by_value)]
41pub fn pyobject_to_account_any(py: Python, account: Py<PyAny>) -> PyResult<AccountAny> {
42    let account_type = account
43        .getattr(py, "account_type")?
44        .extract::<AccountType>(py)?;
45    if account_type == AccountType::Margin {
46        let margin = account.extract::<MarginAccount>(py)?;
47        Ok(AccountAny::Margin(margin))
48    } else if account_type == AccountType::Cash {
49        let cash = account.extract::<CashAccount>(py)?;
50        Ok(AccountAny::Cash(cash))
51    } else if account_type == AccountType::Betting {
52        let betting = account.extract::<BettingAccount>(py)?;
53        Ok(AccountAny::Betting(betting))
54    } else if account_type == AccountType::Wallet {
55        let wallet = account.extract::<WalletAccount>(py)?;
56        Ok(AccountAny::Wallet(wallet))
57    } else {
58        Err(to_pyvalue_err("Unsupported account type"))
59    }
60}
61
62/// Converts a Rust `AccountAny` into a Python account object.
63///
64/// # Errors
65///
66/// Returns an error if converting the underlying account into a Python object fails.
67pub fn account_any_to_pyobject(py: Python, account: AccountAny) -> PyResult<Py<PyAny>> {
68    match account {
69        AccountAny::Margin(account) => account.into_py_any(py),
70        AccountAny::Cash(account) => account.into_py_any(py),
71        AccountAny::Betting(account) => account.into_py_any(py),
72        AccountAny::Wallet(account) => account.into_py_any(py),
73    }
74}
75
76fn resolve_balance_currency(
77    currency: Option<Currency>,
78    base_currency: Option<Currency>,
79) -> PyResult<Currency> {
80    currency.or(base_currency).ok_or_else(|| {
81        to_pyvalue_err("`currency` must be specified for an account with no base currency")
82    })
83}
84
85#[cfg(test)]
86mod tests {
87    use pyo3::Python;
88    use rstest::rstest;
89
90    use super::resolve_balance_currency;
91    use crate::types::Currency;
92
93    #[rstest]
94    #[case(Some(Currency::USD()), None, Currency::USD())]
95    #[case(None, Some(Currency::EUR()), Currency::EUR())]
96    #[case(Some(Currency::USD()), Some(Currency::EUR()), Currency::USD())]
97    fn test_resolve_balance_currency(
98        #[case] currency: Option<Currency>,
99        #[case] base_currency: Option<Currency>,
100        #[case] expected: Currency,
101    ) {
102        let resolved = resolve_balance_currency(currency, base_currency).unwrap();
103
104        assert_eq!(resolved, expected);
105    }
106
107    #[rstest]
108    fn test_resolve_balance_currency_rejects_missing_currency() {
109        Python::initialize();
110        Python::attach(|_| {
111            let error = resolve_balance_currency(None, None).unwrap_err();
112
113            assert_eq!(
114                error.to_string(),
115                "ValueError: `currency` must be specified for an account with no base currency"
116            );
117        });
118    }
119}