Skip to main content

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