Skip to main content

nautilus_model/python/account/
wallet.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
16use indexmap::IndexMap;
17use nautilus_core::{
18    UnixNanos,
19    python::{IntoPyObjectNautilusExt, to_pyruntime_err, to_pyvalue_err},
20};
21use pyo3::{basic::CompareOp, prelude::*, types::PyDict};
22use rust_decimal::Decimal;
23
24use crate::{
25    accounts::{Account, WalletAccount},
26    enums::{AccountType, LiquiditySide, OrderSide},
27    events::{AccountState, OrderFilled},
28    fees::MakerTakerFeeRates,
29    identifiers::AccountId,
30    position::Position,
31    python::{account::resolve_balance_currency, instruments::pyobject_to_instrument_any},
32    types::{AccountBalance, Currency, Money, Price, Quantity},
33};
34
35#[pymethods]
36#[pyo3_stub_gen::derive::gen_stub_pymethods]
37impl WalletAccount {
38    /// Represents a blockchain wallet account holding native and ERC-20 token balances.
39    #[new]
40    #[pyo3(signature = (event, calculate_account_state))]
41    pub fn py_new(event: AccountState, calculate_account_state: bool) -> PyResult<Self> {
42        Self::new_checked(event, calculate_account_state).map_err(to_pyvalue_err)
43    }
44
45    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
46        match op {
47            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
48            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
49            _ => py.NotImplemented(),
50        }
51    }
52
53    fn __repr__(&self) -> String {
54        format!(
55            "{}(id={}, type={}, base={})",
56            stringify!(WalletAccount),
57            self.id,
58            self.account_type,
59            self.base_currency.map_or_else(
60                || "None".to_string(),
61                |base_currency| format!("{}", base_currency.code)
62            ),
63        )
64    }
65
66    #[getter]
67    #[pyo3(name = "id")]
68    fn py_id(&self) -> AccountId {
69        self.id
70    }
71
72    #[getter]
73    #[pyo3(name = "account_type")]
74    fn py_account_type(&self) -> AccountType {
75        self.account_type
76    }
77
78    #[getter]
79    #[pyo3(name = "base_currency")]
80    fn py_base_currency(&self) -> Option<Currency> {
81        self.base_currency
82    }
83
84    #[getter]
85    #[pyo3(name = "last_event")]
86    fn py_last_event(&self) -> Option<AccountState> {
87        self.last_event()
88    }
89
90    #[getter]
91    #[pyo3(name = "event_count")]
92    fn py_event_count(&self) -> usize {
93        self.event_count()
94    }
95
96    #[getter]
97    #[pyo3(name = "events")]
98    fn py_events(&self) -> Vec<AccountState> {
99        self.events()
100    }
101
102    #[getter]
103    #[pyo3(name = "calculate_account_state")]
104    fn py_calculate_account_state(&self) -> bool {
105        self.calculate_account_state
106    }
107
108    #[pyo3(name = "balance_total")]
109    #[pyo3(signature = (currency=None))]
110    fn py_balance_total(&self, currency: Option<Currency>) -> PyResult<Option<Money>> {
111        let currency = resolve_balance_currency(currency, self.base_currency)?;
112        Ok(self.balance_total(Some(currency)))
113    }
114
115    #[pyo3(name = "balances_total")]
116    fn py_balances_total(&self) -> IndexMap<Currency, Money> {
117        self.balances_total()
118    }
119
120    #[pyo3(name = "balance_free")]
121    #[pyo3(signature = (currency=None))]
122    fn py_balance_free(&self, currency: Option<Currency>) -> PyResult<Option<Money>> {
123        let currency = resolve_balance_currency(currency, self.base_currency)?;
124        Ok(self.balance_free(Some(currency)))
125    }
126
127    #[pyo3(name = "balances_free")]
128    fn py_balances_free(&self) -> IndexMap<Currency, Money> {
129        self.balances_free()
130    }
131
132    #[pyo3(name = "balance_locked")]
133    #[pyo3(signature = (currency=None))]
134    fn py_balance_locked(&self, currency: Option<Currency>) -> PyResult<Option<Money>> {
135        let currency = resolve_balance_currency(currency, self.base_currency)?;
136        Ok(self.balance_locked(Some(currency)))
137    }
138
139    #[pyo3(name = "balances_locked")]
140    fn py_balances_locked(&self) -> IndexMap<Currency, Money> {
141        self.balances_locked()
142    }
143
144    #[pyo3(name = "balance")]
145    #[pyo3(signature = (currency=None))]
146    fn py_balance(&self, currency: Option<Currency>) -> PyResult<Option<AccountBalance>> {
147        let currency = resolve_balance_currency(currency, self.base_currency)?;
148        Ok(Account::balance(self, Some(currency)).copied())
149    }
150
151    #[pyo3(name = "balances")]
152    fn py_balances(&self) -> IndexMap<Currency, AccountBalance> {
153        Account::balances(self)
154    }
155
156    #[pyo3(name = "starting_balances")]
157    fn py_starting_balances(&self) -> IndexMap<Currency, Money> {
158        Account::starting_balances(self)
159    }
160
161    #[pyo3(name = "currencies")]
162    fn py_currencies(&self) -> Vec<Currency> {
163        Account::currencies(self)
164    }
165
166    #[pyo3(name = "is_cash_account")]
167    fn py_is_cash_account(&self) -> bool {
168        Account::is_cash_account(self)
169    }
170
171    #[pyo3(name = "is_margin_account")]
172    fn py_is_margin_account(&self) -> bool {
173        Account::is_margin_account(self)
174    }
175
176    #[pyo3(name = "purge_account_events")]
177    fn py_purge_account_events(&mut self, ts_now: u64, lookback_secs: u64) {
178        Account::purge_account_events(self, UnixNanos::from(ts_now), lookback_secs);
179    }
180
181    #[pyo3(name = "apply")]
182    fn py_apply(&mut self, event: AccountState) -> PyResult<()> {
183        self.apply(event).map_err(to_pyruntime_err)
184    }
185
186    #[pyo3(name = "calculate_balance_locked")]
187    #[pyo3(signature = (instrument, side, quantity, price, use_quote_for_inverse=None))]
188    fn py_calculate_balance_locked(
189        &self,
190        instrument: Py<PyAny>,
191        side: OrderSide,
192        quantity: Quantity,
193        price: Price,
194        use_quote_for_inverse: Option<bool>,
195        py: Python,
196    ) -> PyResult<Money> {
197        let instrument = pyobject_to_instrument_any(py, instrument)?;
198        self.calculate_balance_locked(&instrument, side, quantity, price, use_quote_for_inverse)
199            .map_err(to_pyvalue_err)
200    }
201
202    #[pyo3(name = "calculate_commission")]
203    #[pyo3(signature = (instrument, last_qty, last_px, liquidity_side, maker_rate, taker_rate, use_quote_for_inverse=None))]
204    #[expect(clippy::too_many_arguments)]
205    fn py_calculate_commission(
206        &self,
207        instrument: Py<PyAny>,
208        last_qty: Quantity,
209        last_px: Price,
210        liquidity_side: LiquiditySide,
211        maker_rate: Decimal,
212        taker_rate: Decimal,
213        use_quote_for_inverse: Option<bool>,
214        py: Python,
215    ) -> PyResult<Money> {
216        if liquidity_side == LiquiditySide::NoLiquiditySide {
217            return Err(to_pyvalue_err("Invalid liquidity side"));
218        }
219        let instrument = pyobject_to_instrument_any(py, instrument)?;
220        let fee_rates = MakerTakerFeeRates::new(maker_rate, taker_rate);
221        self.calculate_commission(
222            &instrument,
223            last_qty,
224            last_px,
225            liquidity_side,
226            fee_rates,
227            use_quote_for_inverse,
228        )
229        .map_err(to_pyvalue_err)
230    }
231
232    #[pyo3(name = "calculate_pnls")]
233    #[pyo3(signature = (instrument, fill, position=None))]
234    fn py_calculate_pnls(
235        &self,
236        instrument: Py<PyAny>,
237        fill: &OrderFilled,
238        position: Option<Position>,
239        py: Python,
240    ) -> PyResult<Vec<Money>> {
241        let instrument = pyobject_to_instrument_any(py, instrument)?;
242        self.calculate_pnls(&instrument, fill, position)
243            .map_err(to_pyvalue_err)
244    }
245
246    #[pyo3(name = "to_dict")]
247    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
248        let dict = PyDict::new(py);
249        dict.set_item("type", "WalletAccount")?;
250        dict.set_item("calculate_account_state", self.calculate_account_state)?;
251        let events_list: PyResult<Vec<Py<PyAny>>> =
252            self.events.iter().map(|item| item.py_to_dict(py)).collect();
253        dict.set_item("events", events_list?)?;
254        Ok(dict.into())
255    }
256}