nautilus_model/python/account/
cash.rs1use 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 #[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}