1use indexmap::IndexMap;
17use nautilus_core::{
18 UnixNanos,
19 python::{IntoPyObjectNautilusExt, to_pyruntime_err, to_pyvalue_err},
20};
21use pyo3::{IntoPyObjectExt, basic::CompareOp, prelude::*, types::PyDict};
22use rust_decimal::Decimal;
23
24use crate::{
25 accounts::{Account, MarginAccount},
26 enums::{AccountType, LiquiditySide, OrderSide},
27 events::{AccountState, OrderFilled},
28 identifiers::{AccountId, InstrumentId},
29 instruments::InstrumentAny,
30 position::Position,
31 python::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 MarginAccount {
38 #[new]
40 fn py_new(event: AccountState, calculate_account_state: bool) -> Self {
41 Self::new(event, calculate_account_state)
42 }
43
44 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
45 match op {
46 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
47 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
48 _ => py.NotImplemented(),
49 }
50 }
51
52 #[getter]
53 fn id(&self) -> AccountId {
54 self.id
55 }
56
57 #[getter]
58 #[pyo3(name = "account_type")]
59 fn py_account_type(&self) -> AccountType {
60 self.account_type
61 }
62
63 #[getter]
64 #[pyo3(name = "base_currency")]
65 fn py_base_currency(&self) -> Option<Currency> {
66 self.base_currency
67 }
68
69 #[getter]
70 fn default_leverage(&self) -> Decimal {
71 self.default_leverage
72 }
73
74 #[getter]
75 #[pyo3(name = "calculate_account_state")]
76 fn py_calculate_account_state(&self) -> bool {
77 self.calculate_account_state
78 }
79
80 #[getter]
81 #[pyo3(name = "last_event")]
82 fn py_last_event(&self) -> Option<AccountState> {
83 Account::last_event(self)
84 }
85
86 #[getter]
87 #[pyo3(name = "event_count")]
88 fn py_event_count(&self) -> usize {
89 Account::event_count(self)
90 }
91
92 #[getter]
93 #[pyo3(name = "events")]
94 fn py_events(&self) -> Vec<AccountState> {
95 Account::events(self)
96 }
97
98 #[pyo3(name = "balance_total")]
99 #[pyo3(signature = (currency=None))]
100 fn py_balance_total(&self, currency: Option<Currency>) -> Option<Money> {
101 Account::balance_total(self, currency)
102 }
103
104 #[pyo3(name = "balances_total")]
105 fn py_balances_total(&self) -> IndexMap<Currency, Money> {
106 Account::balances_total(self)
107 }
108
109 #[pyo3(name = "balance_free")]
110 #[pyo3(signature = (currency=None))]
111 fn py_balance_free(&self, currency: Option<Currency>) -> Option<Money> {
112 Account::balance_free(self, currency)
113 }
114
115 #[pyo3(name = "balances_free")]
116 fn py_balances_free(&self) -> IndexMap<Currency, Money> {
117 Account::balances_free(self)
118 }
119
120 #[pyo3(name = "balance_locked")]
121 #[pyo3(signature = (currency=None))]
122 fn py_balance_locked(&self, currency: Option<Currency>) -> Option<Money> {
123 Account::balance_locked(self, currency)
124 }
125
126 #[pyo3(name = "balances_locked")]
127 fn py_balances_locked(&self) -> IndexMap<Currency, Money> {
128 Account::balances_locked(self)
129 }
130
131 #[pyo3(name = "balance")]
132 #[pyo3(signature = (currency=None))]
133 fn py_balance(&self, currency: Option<Currency>) -> Option<AccountBalance> {
134 Account::balance(self, currency).copied()
135 }
136
137 #[pyo3(name = "balances")]
138 fn py_balances(&self) -> IndexMap<Currency, AccountBalance> {
139 Account::balances(self)
140 }
141
142 #[pyo3(name = "starting_balances")]
143 fn py_starting_balances(&self) -> IndexMap<Currency, Money> {
144 Account::starting_balances(self)
145 }
146
147 #[pyo3(name = "currencies")]
148 fn py_currencies(&self) -> Vec<Currency> {
149 Account::currencies(self)
150 }
151
152 #[pyo3(name = "is_cash_account")]
153 fn py_is_cash_account(&self) -> bool {
154 Account::is_cash_account(self)
155 }
156
157 #[pyo3(name = "is_margin_account")]
158 fn py_is_margin_account(&self) -> bool {
159 Account::is_margin_account(self)
160 }
161
162 #[pyo3(name = "apply")]
163 fn py_apply(&mut self, event: AccountState) -> PyResult<()> {
164 Account::apply(self, event).map_err(to_pyruntime_err)
165 }
166
167 #[pyo3(name = "purge_account_events")]
168 fn py_purge_account_events(&mut self, ts_now: u64, lookback_secs: u64) {
169 Account::purge_account_events(self, UnixNanos::from(ts_now), lookback_secs);
170 }
171
172 #[pyo3(name = "calculate_balance_locked")]
173 #[pyo3(signature = (instrument, side, quantity, price, use_quote_for_inverse=None))]
174 fn py_calculate_balance_locked(
175 &mut self,
176 instrument: Py<PyAny>,
177 side: OrderSide,
178 quantity: Quantity,
179 price: Price,
180 use_quote_for_inverse: Option<bool>,
181 py: Python,
182 ) -> PyResult<Money> {
183 let instrument = pyobject_to_instrument_any(py, instrument)?;
184 Account::calculate_balance_locked(
185 self,
186 &instrument,
187 side,
188 quantity,
189 price,
190 use_quote_for_inverse,
191 )
192 .map_err(to_pyvalue_err)
193 }
194
195 #[pyo3(name = "calculate_commission")]
196 #[pyo3(signature = (instrument, last_qty, last_px, liquidity_side, use_quote_for_inverse=None))]
197 fn py_calculate_commission(
198 &self,
199 instrument: Py<PyAny>,
200 last_qty: Quantity,
201 last_px: Price,
202 liquidity_side: LiquiditySide,
203 use_quote_for_inverse: Option<bool>,
204 py: Python,
205 ) -> PyResult<Money> {
206 if liquidity_side == LiquiditySide::NoLiquiditySide {
207 return Err(to_pyvalue_err("Invalid liquidity side"));
208 }
209 let instrument = pyobject_to_instrument_any(py, instrument)?;
210 Account::calculate_commission(
211 self,
212 &instrument,
213 last_qty,
214 last_px,
215 liquidity_side,
216 use_quote_for_inverse,
217 )
218 .map_err(to_pyvalue_err)
219 }
220
221 #[pyo3(name = "calculate_pnls")]
222 #[pyo3(signature = (instrument, fill, position=None))]
223 fn py_calculate_pnls(
224 &self,
225 instrument: Py<PyAny>,
226 fill: OrderFilled,
227 position: Option<Position>,
228 py: Python,
229 ) -> PyResult<Vec<Money>> {
230 let instrument = pyobject_to_instrument_any(py, instrument)?;
231 Account::calculate_pnls(self, &instrument, &fill, position).map_err(to_pyvalue_err)
232 }
233
234 fn __repr__(&self) -> String {
235 format!(
236 "{}(id={}, type={}, base={})",
237 stringify!(MarginAccount),
238 self.id,
239 self.account_type,
240 self.base_currency.map_or_else(
241 || "None".to_string(),
242 |base_currency| format!("{}", base_currency.code)
243 ),
244 )
245 }
246
247 #[pyo3(name = "set_default_leverage")]
249 fn py_set_default_leverage(&mut self, default_leverage: Decimal) {
250 self.set_default_leverage(default_leverage);
251 }
252
253 #[pyo3(name = "leverages")]
254 fn py_leverages(&self, py: Python) -> PyResult<Py<PyAny>> {
255 let leverages = PyDict::new(py);
256 for (key, &value) in &self.leverages {
257 leverages
258 .set_item(key.into_py_any_unwrap(py), value)
259 .unwrap();
260 }
261 leverages.into_py_any(py)
262 }
263
264 #[pyo3(name = "leverage")]
265 fn py_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
266 self.get_leverage(instrument_id)
267 }
268
269 #[pyo3(name = "set_leverage")]
271 fn py_set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) {
272 self.set_leverage(instrument_id, leverage);
273 }
274
275 #[pyo3(name = "is_unleveraged")]
276 fn py_is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
277 self.is_unleveraged(instrument_id)
278 }
279
280 #[pyo3(name = "initial_margins")]
281 fn py_initial_margins(&self, py: Python) -> PyResult<Py<PyAny>> {
282 let initial_margins = PyDict::new(py);
283 for (key, &value) in &self.initial_margins() {
284 initial_margins
285 .set_item(key.into_py_any_unwrap(py), value.into_py_any_unwrap(py))
286 .unwrap();
287 }
288 initial_margins.into_py_any(py)
289 }
290
291 #[pyo3(name = "maintenance_margins")]
292 fn py_maintenance_margins(&self, py: Python) -> PyResult<Py<PyAny>> {
293 let maintenance_margins = PyDict::new(py);
294 for (key, &value) in &self.maintenance_margins() {
295 maintenance_margins
296 .set_item(key.into_py_any_unwrap(py), value.into_py_any_unwrap(py))
297 .unwrap();
298 }
299 maintenance_margins.into_py_any(py)
300 }
301
302 #[pyo3(name = "update_initial_margin")]
304 fn py_update_initial_margin(&mut self, instrument_id: InstrumentId, initial_margin: Money) {
305 self.update_initial_margin(instrument_id, initial_margin);
306 }
307
308 #[pyo3(name = "initial_margin")]
310 fn py_initial_margin(&self, instrument_id: InstrumentId) -> Money {
311 self.initial_margin(instrument_id)
312 }
313
314 #[pyo3(name = "update_maintenance_margin")]
316 fn py_update_maintenance_margin(
317 &mut self,
318 instrument_id: InstrumentId,
319 maintenance_margin: Money,
320 ) {
321 self.update_maintenance_margin(instrument_id, maintenance_margin);
322 }
323
324 #[pyo3(name = "maintenance_margin")]
326 fn py_maintenance_margin(&self, instrument_id: InstrumentId) -> Money {
327 self.maintenance_margin(instrument_id)
328 }
329
330 #[pyo3(name = "calculate_initial_margin")]
331 #[pyo3(signature = (instrument, quantity, price, use_quote_for_inverse=None))]
332 pub fn py_calculate_initial_margin(
336 &mut self,
337 instrument: Py<PyAny>,
338 quantity: Quantity,
339 price: Price,
340 use_quote_for_inverse: Option<bool>,
341 py: Python,
342 ) -> PyResult<Money> {
343 let instrument_type = pyobject_to_instrument_any(py, instrument)?;
344 match instrument_type {
345 InstrumentAny::Betting(inst) => self
346 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
347 .map_err(to_pyvalue_err),
348 InstrumentAny::BinaryOption(inst) => self
349 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
350 .map_err(to_pyvalue_err),
351 InstrumentAny::Cfd(inst) => self
352 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
353 .map_err(to_pyvalue_err),
354 InstrumentAny::Commodity(inst) => self
355 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
356 .map_err(to_pyvalue_err),
357 InstrumentAny::CryptoFuture(inst) => self
358 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
359 .map_err(to_pyvalue_err),
360 InstrumentAny::CryptoOption(inst) => self
361 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
362 .map_err(to_pyvalue_err),
363 InstrumentAny::CryptoPerpetual(inst) => self
364 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
365 .map_err(to_pyvalue_err),
366 InstrumentAny::CurrencyPair(inst) => self
367 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
368 .map_err(to_pyvalue_err),
369 InstrumentAny::Equity(inst) => self
370 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
371 .map_err(to_pyvalue_err),
372 InstrumentAny::FuturesContract(inst) => self
373 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
374 .map_err(to_pyvalue_err),
375 InstrumentAny::FuturesSpread(inst) => self
376 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
377 .map_err(to_pyvalue_err),
378 InstrumentAny::IndexInstrument(inst) => self
379 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
380 .map_err(to_pyvalue_err),
381 InstrumentAny::OptionContract(inst) => self
382 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
383 .map_err(to_pyvalue_err),
384 InstrumentAny::OptionSpread(inst) => self
385 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
386 .map_err(to_pyvalue_err),
387 InstrumentAny::PerpetualContract(inst) => self
388 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
389 .map_err(to_pyvalue_err),
390 InstrumentAny::TokenizedAsset(inst) => self
391 .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
392 .map_err(to_pyvalue_err),
393 }
394 }
395
396 #[pyo3(name = "calculate_maintenance_margin")]
400 #[pyo3(signature = (instrument, quantity, price, use_quote_for_inverse=None))]
401 pub fn py_calculate_maintenance_margin(
402 &mut self,
403 instrument: Py<PyAny>,
404 quantity: Quantity,
405 price: Price,
406 use_quote_for_inverse: Option<bool>,
407 py: Python,
408 ) -> PyResult<Money> {
409 let instrument_type = pyobject_to_instrument_any(py, instrument)?;
410 match instrument_type {
411 InstrumentAny::Betting(inst) => self
412 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
413 .map_err(to_pyvalue_err),
414 InstrumentAny::BinaryOption(inst) => self
415 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
416 .map_err(to_pyvalue_err),
417 InstrumentAny::Cfd(inst) => self
418 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
419 .map_err(to_pyvalue_err),
420 InstrumentAny::Commodity(inst) => self
421 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
422 .map_err(to_pyvalue_err),
423 InstrumentAny::CryptoFuture(inst) => self
424 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
425 .map_err(to_pyvalue_err),
426 InstrumentAny::CryptoOption(inst) => self
427 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
428 .map_err(to_pyvalue_err),
429 InstrumentAny::CryptoPerpetual(inst) => self
430 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
431 .map_err(to_pyvalue_err),
432 InstrumentAny::CurrencyPair(inst) => self
433 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
434 .map_err(to_pyvalue_err),
435 InstrumentAny::Equity(inst) => self
436 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
437 .map_err(to_pyvalue_err),
438 InstrumentAny::FuturesContract(inst) => self
439 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
440 .map_err(to_pyvalue_err),
441 InstrumentAny::FuturesSpread(inst) => self
442 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
443 .map_err(to_pyvalue_err),
444 InstrumentAny::IndexInstrument(inst) => self
445 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
446 .map_err(to_pyvalue_err),
447 InstrumentAny::OptionContract(inst) => self
448 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
449 .map_err(to_pyvalue_err),
450 InstrumentAny::OptionSpread(inst) => self
451 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
452 .map_err(to_pyvalue_err),
453 InstrumentAny::PerpetualContract(inst) => self
454 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
455 .map_err(to_pyvalue_err),
456 InstrumentAny::TokenizedAsset(inst) => self
457 .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
458 .map_err(to_pyvalue_err),
459 }
460 }
461
462 #[pyo3(name = "to_dict")]
463 fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
464 let dict = PyDict::new(py);
465 dict.set_item("calculate_account_state", self.calculate_account_state)?;
466 let events_list: PyResult<Vec<Py<PyAny>>> =
467 self.events.iter().map(|item| item.py_to_dict(py)).collect();
468 dict.set_item("events", events_list.unwrap())?;
469 Ok(dict.into())
470 }
471}