1use indexmap::IndexMap;
17use nautilus_core::{
18 UnixNanos,
19 python::{
20 IntoPyObjectNautilusExt, correctness_error_to_pyvalue_err, to_pyruntime_err, to_pyvalue_err,
21 },
22};
23use pyo3::{IntoPyObjectExt, basic::CompareOp, prelude::*, types::PyDict};
24use rust_decimal::Decimal;
25
26use crate::{
27 accounts::{Account, MarginAccount},
28 enums::{AccountType, LiquiditySide, OrderSide},
29 events::{AccountState, OrderFilled},
30 fees::MakerTakerFeeRates,
31 identifiers::{AccountId, InstrumentId},
32 instruments::InstrumentAny,
33 position::Position,
34 python::{account::resolve_balance_currency, instruments::pyobject_to_instrument_any},
35 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
36};
37
38#[pymethods]
39#[pyo3_stub_gen::derive::gen_stub_pymethods]
40impl MarginAccount {
41 #[new]
43 fn py_new(event: AccountState, calculate_account_state: bool) -> Self {
44 Self::new(event, calculate_account_state)
45 }
46
47 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
48 match op {
49 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
50 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
51 _ => py.NotImplemented(),
52 }
53 }
54
55 #[getter]
56 fn id(&self) -> AccountId {
57 self.id
58 }
59
60 #[getter]
61 #[pyo3(name = "account_type")]
62 fn py_account_type(&self) -> AccountType {
63 self.account_type
64 }
65
66 #[getter]
67 #[pyo3(name = "base_currency")]
68 fn py_base_currency(&self) -> Option<Currency> {
69 self.base_currency
70 }
71
72 #[getter]
73 fn default_leverage(&self) -> Decimal {
74 self.default_leverage
75 }
76
77 #[getter]
78 #[pyo3(name = "calculate_account_state")]
79 fn py_calculate_account_state(&self) -> bool {
80 self.calculate_account_state
81 }
82
83 #[getter]
84 #[pyo3(name = "last_event")]
85 fn py_last_event(&self) -> Option<AccountState> {
86 Account::last_event(self)
87 }
88
89 #[getter]
90 #[pyo3(name = "event_count")]
91 fn py_event_count(&self) -> usize {
92 Account::event_count(self)
93 }
94
95 #[getter]
96 #[pyo3(name = "events")]
97 fn py_events(&self) -> Vec<AccountState> {
98 Account::events(self)
99 }
100
101 #[pyo3(name = "balance_total")]
102 #[pyo3(signature = (currency=None))]
103 fn py_balance_total(&self, currency: Option<Currency>) -> PyResult<Option<Money>> {
104 let currency = resolve_balance_currency(currency, self.base_currency)?;
105 Ok(Account::balance_total(self, Some(currency)))
106 }
107
108 #[pyo3(name = "balances_total")]
109 fn py_balances_total(&self) -> IndexMap<Currency, Money> {
110 Account::balances_total(self)
111 }
112
113 #[pyo3(name = "balance_free")]
114 #[pyo3(signature = (currency=None))]
115 fn py_balance_free(&self, currency: Option<Currency>) -> PyResult<Option<Money>> {
116 let currency = resolve_balance_currency(currency, self.base_currency)?;
117 Ok(Account::balance_free(self, Some(currency)))
118 }
119
120 #[pyo3(name = "balances_free")]
121 fn py_balances_free(&self) -> IndexMap<Currency, Money> {
122 Account::balances_free(self)
123 }
124
125 #[pyo3(name = "balance_locked")]
126 #[pyo3(signature = (currency=None))]
127 fn py_balance_locked(&self, currency: Option<Currency>) -> PyResult<Option<Money>> {
128 let currency = resolve_balance_currency(currency, self.base_currency)?;
129 Ok(Account::balance_locked(self, Some(currency)))
130 }
131
132 #[pyo3(name = "balances_locked")]
133 fn py_balances_locked(&self) -> IndexMap<Currency, Money> {
134 Account::balances_locked(self)
135 }
136
137 #[pyo3(name = "balance")]
138 #[pyo3(signature = (currency=None))]
139 fn py_balance(&self, currency: Option<Currency>) -> PyResult<Option<AccountBalance>> {
140 let currency = resolve_balance_currency(currency, self.base_currency)?;
141 Ok(Account::balance(self, Some(currency)).copied())
142 }
143
144 #[pyo3(name = "balances")]
145 fn py_balances(&self) -> IndexMap<Currency, AccountBalance> {
146 Account::balances(self)
147 }
148
149 #[pyo3(name = "starting_balances")]
150 fn py_starting_balances(&self) -> IndexMap<Currency, Money> {
151 Account::starting_balances(self)
152 }
153
154 #[pyo3(name = "currencies")]
155 fn py_currencies(&self) -> Vec<Currency> {
156 Account::currencies(self)
157 }
158
159 #[pyo3(name = "is_cash_account")]
160 fn py_is_cash_account(&self) -> bool {
161 Account::is_cash_account(self)
162 }
163
164 #[pyo3(name = "is_margin_account")]
165 fn py_is_margin_account(&self) -> bool {
166 Account::is_margin_account(self)
167 }
168
169 #[pyo3(name = "apply")]
170 fn py_apply(&mut self, event: AccountState) -> PyResult<()> {
171 Account::apply(self, event).map_err(to_pyruntime_err)
172 }
173
174 #[pyo3(name = "purge_account_events")]
175 fn py_purge_account_events(&mut self, ts_now: u64, lookback_secs: u64) {
176 Account::purge_account_events(self, UnixNanos::from(ts_now), lookback_secs);
177 }
178
179 #[pyo3(name = "calculate_balance_locked")]
180 #[pyo3(signature = (instrument, side, quantity, price, use_quote_for_inverse=None))]
181 fn py_calculate_balance_locked(
182 &self,
183 instrument: Py<PyAny>,
184 side: OrderSide,
185 quantity: Quantity,
186 price: Price,
187 use_quote_for_inverse: Option<bool>,
188 py: Python,
189 ) -> PyResult<Money> {
190 let instrument = pyobject_to_instrument_any(py, instrument)?;
191 Account::calculate_balance_locked(
192 self,
193 &instrument,
194 side,
195 quantity,
196 price,
197 use_quote_for_inverse,
198 )
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 Account::calculate_commission(
222 self,
223 &instrument,
224 last_qty,
225 last_px,
226 liquidity_side,
227 fee_rates,
228 use_quote_for_inverse,
229 )
230 .map_err(to_pyvalue_err)
231 }
232
233 #[pyo3(name = "calculate_pnls")]
234 #[pyo3(signature = (instrument, fill, position=None))]
235 fn py_calculate_pnls(
236 &self,
237 instrument: Py<PyAny>,
238 fill: &OrderFilled,
239 position: Option<Position>,
240 py: Python,
241 ) -> PyResult<Vec<Money>> {
242 let instrument = pyobject_to_instrument_any(py, instrument)?;
243 Account::calculate_pnls(self, &instrument, fill, position).map_err(to_pyvalue_err)
244 }
245
246 fn __repr__(&self) -> String {
247 format!(
248 "{}(id={}, type={}, base={})",
249 stringify!(MarginAccount),
250 self.id,
251 self.account_type,
252 self.base_currency.map_or_else(
253 || "None".to_string(),
254 |base_currency| format!("{}", base_currency.code)
255 ),
256 )
257 }
258
259 #[pyo3(name = "set_default_leverage")]
261 fn py_set_default_leverage(&mut self, default_leverage: Decimal) -> PyResult<()> {
262 self.try_set_default_leverage(default_leverage)
263 .map_err(correctness_error_to_pyvalue_err)
264 }
265
266 #[pyo3(name = "leverages")]
267 fn py_leverages(&self, py: Python) -> PyResult<Py<PyAny>> {
268 let leverages = PyDict::new(py);
269 for (key, &value) in &self.leverages {
270 leverages.set_item(key.into_py_any(py)?, value)?;
271 }
272 leverages.into_py_any(py)
273 }
274
275 #[pyo3(name = "leverage")]
276 fn py_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
277 self.get_leverage(instrument_id)
278 }
279
280 #[pyo3(name = "set_leverage")]
282 fn py_set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) -> PyResult<()> {
283 self.try_set_leverage(instrument_id, leverage)
284 .map_err(correctness_error_to_pyvalue_err)
285 }
286
287 #[pyo3(name = "is_unleveraged")]
288 fn py_is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
289 self.is_unleveraged(instrument_id)
290 }
291
292 #[pyo3(name = "margin")]
294 fn py_margin(&self, instrument_id: InstrumentId) -> Option<MarginBalance> {
295 self.margin(&instrument_id)
296 }
297
298 #[pyo3(name = "margins")]
299 fn py_margins(&self) -> IndexMap<InstrumentId, MarginBalance> {
300 self.margins.clone()
301 }
302
303 #[pyo3(name = "initial_margins")]
304 fn py_initial_margins(&self) -> IndexMap<InstrumentId, Money> {
305 self.initial_margins()
306 }
307
308 #[pyo3(name = "maintenance_margins")]
309 fn py_maintenance_margins(&self) -> IndexMap<InstrumentId, Money> {
310 self.maintenance_margins()
311 }
312
313 #[pyo3(name = "account_margin")]
315 fn py_account_margin(&self, currency: Currency) -> Option<MarginBalance> {
316 self.account_margin(¤cy)
317 }
318
319 #[pyo3(name = "account_margins")]
320 fn py_account_margins(&self) -> IndexMap<Currency, MarginBalance> {
321 self.account_margins.clone()
322 }
323
324 #[pyo3(name = "account_initial_margin")]
326 fn py_account_initial_margin(&self, currency: Currency) -> Option<Money> {
327 self.account_initial_margin(¤cy)
328 }
329
330 #[pyo3(name = "account_initial_margins")]
332 fn py_account_initial_margins(&self) -> IndexMap<Currency, Money> {
333 self.account_initial_margins()
334 }
335
336 #[pyo3(name = "account_maintenance_margin")]
338 fn py_account_maintenance_margin(&self, currency: Currency) -> Option<Money> {
339 self.account_maintenance_margin(¤cy)
340 }
341
342 #[pyo3(name = "account_maintenance_margins")]
344 fn py_account_maintenance_margins(&self) -> IndexMap<Currency, Money> {
345 self.account_maintenance_margins()
346 }
347
348 #[pyo3(name = "total_initial_margin")]
351 fn py_total_initial_margin(&self, currency: Currency) -> Money {
352 self.total_initial_margin(currency)
353 }
354
355 #[pyo3(name = "total_maintenance_margin")]
358 fn py_total_maintenance_margin(&self, currency: Currency) -> Money {
359 self.total_maintenance_margin(currency)
360 }
361
362 #[pyo3(name = "update_initial_margin")]
364 fn py_update_initial_margin(&mut self, instrument_id: InstrumentId, initial_margin: Money) {
365 self.update_initial_margin(instrument_id, initial_margin);
366 }
367
368 #[pyo3(name = "initial_margin")]
370 fn py_initial_margin(&self, instrument_id: InstrumentId) -> Option<Money> {
371 self.margin(&instrument_id).map(|margin| margin.initial)
372 }
373
374 #[pyo3(name = "update_maintenance_margin")]
376 fn py_update_maintenance_margin(
377 &mut self,
378 instrument_id: InstrumentId,
379 maintenance_margin: Money,
380 ) {
381 self.update_maintenance_margin(instrument_id, maintenance_margin);
382 }
383
384 #[pyo3(name = "maintenance_margin")]
386 fn py_maintenance_margin(&self, instrument_id: InstrumentId) -> Option<Money> {
387 self.margin(&instrument_id).map(|margin| margin.maintenance)
388 }
389
390 #[pyo3(name = "calculate_initial_margin")]
391 #[pyo3(signature = (instrument, quantity, price, use_quote_for_inverse=None))]
392 pub fn py_calculate_initial_margin(
401 &self,
402 instrument: Py<PyAny>,
403 quantity: Quantity,
404 price: Price,
405 use_quote_for_inverse: Option<bool>,
406 py: Python,
407 ) -> PyResult<Money> {
408 let instrument_type = pyobject_to_instrument_any(py, instrument)?;
409 match instrument_type {
410 InstrumentAny::Betting(inst) => self
411 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
412 .map_err(to_pyvalue_err),
413 InstrumentAny::BinaryOption(inst) => self
414 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
415 .map_err(to_pyvalue_err),
416 InstrumentAny::Cfd(inst) => self
417 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
418 .map_err(to_pyvalue_err),
419 InstrumentAny::Commodity(inst) => self
420 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
421 .map_err(to_pyvalue_err),
422 InstrumentAny::CryptoFuture(inst) => self
423 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
424 .map_err(to_pyvalue_err),
425 InstrumentAny::CryptoFuturesSpread(inst) => self
426 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
427 .map_err(to_pyvalue_err),
428 InstrumentAny::CryptoOption(inst) => self
429 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
430 .map_err(to_pyvalue_err),
431 InstrumentAny::CryptoOptionSpread(inst) => self
432 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
433 .map_err(to_pyvalue_err),
434 InstrumentAny::CryptoPerpetual(inst) => self
435 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
436 .map_err(to_pyvalue_err),
437 InstrumentAny::CurrencyPair(inst) => self
438 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
439 .map_err(to_pyvalue_err),
440 InstrumentAny::Equity(inst) => self
441 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
442 .map_err(to_pyvalue_err),
443 InstrumentAny::FuturesContract(inst) => self
444 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
445 .map_err(to_pyvalue_err),
446 InstrumentAny::FuturesSpread(inst) => self
447 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
448 .map_err(to_pyvalue_err),
449 InstrumentAny::IndexInstrument(inst) => self
450 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
451 .map_err(to_pyvalue_err),
452 InstrumentAny::OptionContract(inst) => self
453 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
454 .map_err(to_pyvalue_err),
455 InstrumentAny::OptionSpread(inst) => self
456 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
457 .map_err(to_pyvalue_err),
458 InstrumentAny::PerpetualContract(inst) => self
459 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
460 .map_err(to_pyvalue_err),
461 InstrumentAny::TokenizedAsset(inst) => self
462 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
463 .map_err(to_pyvalue_err),
464 }
465 }
466
467 #[pyo3(name = "calculate_maintenance_margin")]
475 #[pyo3(signature = (instrument, quantity, price, use_quote_for_inverse=None))]
476 pub fn py_calculate_maintenance_margin(
477 &self,
478 instrument: Py<PyAny>,
479 quantity: Quantity,
480 price: Price,
481 use_quote_for_inverse: Option<bool>,
482 py: Python,
483 ) -> PyResult<Money> {
484 let instrument_type = pyobject_to_instrument_any(py, instrument)?;
485 match instrument_type {
486 InstrumentAny::Betting(inst) => self
487 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
488 .map_err(to_pyvalue_err),
489 InstrumentAny::BinaryOption(inst) => self
490 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
491 .map_err(to_pyvalue_err),
492 InstrumentAny::Cfd(inst) => self
493 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
494 .map_err(to_pyvalue_err),
495 InstrumentAny::Commodity(inst) => self
496 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
497 .map_err(to_pyvalue_err),
498 InstrumentAny::CryptoFuture(inst) => self
499 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
500 .map_err(to_pyvalue_err),
501 InstrumentAny::CryptoFuturesSpread(inst) => self
502 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
503 .map_err(to_pyvalue_err),
504 InstrumentAny::CryptoOption(inst) => self
505 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
506 .map_err(to_pyvalue_err),
507 InstrumentAny::CryptoOptionSpread(inst) => self
508 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
509 .map_err(to_pyvalue_err),
510 InstrumentAny::CryptoPerpetual(inst) => self
511 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
512 .map_err(to_pyvalue_err),
513 InstrumentAny::CurrencyPair(inst) => self
514 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
515 .map_err(to_pyvalue_err),
516 InstrumentAny::Equity(inst) => self
517 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
518 .map_err(to_pyvalue_err),
519 InstrumentAny::FuturesContract(inst) => self
520 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
521 .map_err(to_pyvalue_err),
522 InstrumentAny::FuturesSpread(inst) => self
523 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
524 .map_err(to_pyvalue_err),
525 InstrumentAny::IndexInstrument(inst) => self
526 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
527 .map_err(to_pyvalue_err),
528 InstrumentAny::OptionContract(inst) => self
529 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
530 .map_err(to_pyvalue_err),
531 InstrumentAny::OptionSpread(inst) => self
532 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
533 .map_err(to_pyvalue_err),
534 InstrumentAny::PerpetualContract(inst) => self
535 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
536 .map_err(to_pyvalue_err),
537 InstrumentAny::TokenizedAsset(inst) => self
538 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
539 .map_err(to_pyvalue_err),
540 }
541 }
542
543 #[pyo3(name = "to_dict")]
544 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
545 let dict = PyDict::new(py);
546 dict.set_item("calculate_account_state", self.calculate_account_state)?;
547 let events_list: PyResult<Vec<Py<PyAny>>> =
548 self.events.iter().map(|item| item.py_to_dict(py)).collect();
549 dict.set_item("events", events_list?)?;
550 Ok(dict.into())
551 }
552}