Skip to main content

nautilus_model/python/account/
margin.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::{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, MarginBalance, Money, Price, Quantity},
33};
34
35#[pymethods]
36#[pyo3_stub_gen::derive::gen_stub_pymethods]
37impl MarginAccount {
38    /// Creates a new `MarginAccount` instance.
39    #[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    /// Sets the default leverage for the account.
248    #[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.set_item(key.into_py_any(py)?, value)?;
258        }
259        leverages.into_py_any(py)
260    }
261
262    #[pyo3(name = "leverage")]
263    fn py_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
264        self.get_leverage(instrument_id)
265    }
266
267    /// Sets the leverage for a specific instrument.
268    #[pyo3(name = "set_leverage")]
269    fn py_set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) {
270        self.set_leverage(instrument_id, leverage);
271    }
272
273    #[pyo3(name = "is_unleveraged")]
274    fn py_is_unleveraged(&self, instrument_id: InstrumentId) -> bool {
275        self.is_unleveraged(instrument_id)
276    }
277
278    /// Returns the margin balance for the specified instrument.
279    #[pyo3(name = "margin")]
280    fn py_margin(&self, instrument_id: InstrumentId) -> Option<MarginBalance> {
281        self.margin(&instrument_id)
282    }
283
284    #[pyo3(name = "margins")]
285    fn py_margins(&self) -> IndexMap<InstrumentId, MarginBalance> {
286        self.margins.clone()
287    }
288
289    #[pyo3(name = "initial_margins")]
290    fn py_initial_margins(&self) -> IndexMap<InstrumentId, Money> {
291        self.initial_margins()
292    }
293
294    #[pyo3(name = "maintenance_margins")]
295    fn py_maintenance_margins(&self) -> IndexMap<InstrumentId, Money> {
296        self.maintenance_margins()
297    }
298
299    /// Returns the account-wide margin balance for the specified collateral currency.
300    #[pyo3(name = "account_margin")]
301    fn py_account_margin(&self, currency: Currency) -> Option<MarginBalance> {
302        self.account_margin(&currency)
303    }
304
305    #[pyo3(name = "account_margins")]
306    fn py_account_margins(&self) -> IndexMap<Currency, MarginBalance> {
307        self.account_margins.clone()
308    }
309
310    /// Returns the account-wide initial margin for the specified collateral currency.
311    #[pyo3(name = "account_initial_margin")]
312    fn py_account_initial_margin(&self, currency: Currency) -> Option<Money> {
313        self.account_initial_margin(&currency)
314    }
315
316    /// Returns all account-wide initial margins keyed by currency.
317    #[pyo3(name = "account_initial_margins")]
318    fn py_account_initial_margins(&self) -> IndexMap<Currency, Money> {
319        self.account_initial_margins()
320    }
321
322    /// Returns the account-wide maintenance margin for the specified collateral currency.
323    #[pyo3(name = "account_maintenance_margin")]
324    fn py_account_maintenance_margin(&self, currency: Currency) -> Option<Money> {
325        self.account_maintenance_margin(&currency)
326    }
327
328    /// Returns all account-wide maintenance margins keyed by currency.
329    #[pyo3(name = "account_maintenance_margins")]
330    fn py_account_maintenance_margins(&self) -> IndexMap<Currency, Money> {
331        self.account_maintenance_margins()
332    }
333
334    /// Returns the total initial margin reserved in the specified currency,
335    /// summing per-instrument and account-wide entries.
336    #[pyo3(name = "total_initial_margin")]
337    fn py_total_initial_margin(&self, currency: Currency) -> Money {
338        self.total_initial_margin(currency)
339    }
340
341    /// Returns the total maintenance margin reserved in the specified currency,
342    /// summing per-instrument and account-wide entries.
343    #[pyo3(name = "total_maintenance_margin")]
344    fn py_total_maintenance_margin(&self, currency: Currency) -> Money {
345        self.total_maintenance_margin(currency)
346    }
347
348    /// Updates the initial margin for the specified instrument.
349    #[pyo3(name = "update_initial_margin")]
350    fn py_update_initial_margin(&mut self, instrument_id: InstrumentId, initial_margin: Money) {
351        self.update_initial_margin(instrument_id, initial_margin);
352    }
353
354    /// Returns the initial margin amount for the specified instrument.
355    #[pyo3(name = "initial_margin")]
356    fn py_initial_margin(&self, instrument_id: InstrumentId) -> Option<Money> {
357        self.margin(&instrument_id).map(|margin| margin.initial)
358    }
359
360    /// Updates the maintenance margin for the specified instrument.
361    #[pyo3(name = "update_maintenance_margin")]
362    fn py_update_maintenance_margin(
363        &mut self,
364        instrument_id: InstrumentId,
365        maintenance_margin: Money,
366    ) {
367        self.update_maintenance_margin(instrument_id, maintenance_margin);
368    }
369
370    /// Returns the maintenance margin amount for the specified instrument.
371    #[pyo3(name = "maintenance_margin")]
372    fn py_maintenance_margin(&self, instrument_id: InstrumentId) -> Option<Money> {
373        self.margin(&instrument_id).map(|margin| margin.maintenance)
374    }
375
376    #[pyo3(name = "calculate_initial_margin")]
377    #[pyo3(signature = (instrument, quantity, price, use_quote_for_inverse=None))]
378    /// Calculates the initial margin amount for the specified instrument and quantity.
379    ///
380    /// Delegates to the configured `MarginModel`.
381    ///
382    /// # Errors
383    ///
384    /// Returns an error if leverage is not positive, or if the result cannot be represented
385    /// as `Money`.
386    pub fn py_calculate_initial_margin(
387        &mut self,
388        instrument: Py<PyAny>,
389        quantity: Quantity,
390        price: Price,
391        use_quote_for_inverse: Option<bool>,
392        py: Python,
393    ) -> PyResult<Money> {
394        let instrument_type = pyobject_to_instrument_any(py, instrument)?;
395        match instrument_type {
396            InstrumentAny::Betting(inst) => self
397                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
398                .map_err(to_pyvalue_err),
399            InstrumentAny::BinaryOption(inst) => self
400                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
401                .map_err(to_pyvalue_err),
402            InstrumentAny::Cfd(inst) => self
403                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
404                .map_err(to_pyvalue_err),
405            InstrumentAny::Commodity(inst) => self
406                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
407                .map_err(to_pyvalue_err),
408            InstrumentAny::CryptoFuture(inst) => self
409                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
410                .map_err(to_pyvalue_err),
411            InstrumentAny::CryptoFuturesSpread(inst) => self
412                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
413                .map_err(to_pyvalue_err),
414            InstrumentAny::CryptoOption(inst) => self
415                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
416                .map_err(to_pyvalue_err),
417            InstrumentAny::CryptoOptionSpread(inst) => self
418                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
419                .map_err(to_pyvalue_err),
420            InstrumentAny::CryptoPerpetual(inst) => self
421                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
422                .map_err(to_pyvalue_err),
423            InstrumentAny::CurrencyPair(inst) => self
424                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
425                .map_err(to_pyvalue_err),
426            InstrumentAny::Equity(inst) => self
427                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
428                .map_err(to_pyvalue_err),
429            InstrumentAny::FuturesContract(inst) => self
430                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
431                .map_err(to_pyvalue_err),
432            InstrumentAny::FuturesSpread(inst) => self
433                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
434                .map_err(to_pyvalue_err),
435            InstrumentAny::IndexInstrument(inst) => self
436                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
437                .map_err(to_pyvalue_err),
438            InstrumentAny::OptionContract(inst) => self
439                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
440                .map_err(to_pyvalue_err),
441            InstrumentAny::OptionSpread(inst) => self
442                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
443                .map_err(to_pyvalue_err),
444            InstrumentAny::PerpetualContract(inst) => self
445                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
446                .map_err(to_pyvalue_err),
447            InstrumentAny::TokenizedAsset(inst) => self
448                .calculate_initial_margin(&inst, quantity, price, use_quote_for_inverse)
449                .map_err(to_pyvalue_err),
450        }
451    }
452
453    /// Calculates the maintenance margin amount for the specified instrument and quantity.
454    ///
455    /// Delegates to the configured `MarginModel`.
456    ///
457    /// # Errors
458    ///
459    /// Returns an error if the result cannot be represented as `Money`.
460    #[pyo3(name = "calculate_maintenance_margin")]
461    #[pyo3(signature = (instrument, quantity, price, use_quote_for_inverse=None))]
462    pub fn py_calculate_maintenance_margin(
463        &mut self,
464        instrument: Py<PyAny>,
465        quantity: Quantity,
466        price: Price,
467        use_quote_for_inverse: Option<bool>,
468        py: Python,
469    ) -> PyResult<Money> {
470        let instrument_type = pyobject_to_instrument_any(py, instrument)?;
471        match instrument_type {
472            InstrumentAny::Betting(inst) => self
473                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
474                .map_err(to_pyvalue_err),
475            InstrumentAny::BinaryOption(inst) => self
476                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
477                .map_err(to_pyvalue_err),
478            InstrumentAny::Cfd(inst) => self
479                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
480                .map_err(to_pyvalue_err),
481            InstrumentAny::Commodity(inst) => self
482                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
483                .map_err(to_pyvalue_err),
484            InstrumentAny::CryptoFuture(inst) => self
485                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
486                .map_err(to_pyvalue_err),
487            InstrumentAny::CryptoFuturesSpread(inst) => self
488                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
489                .map_err(to_pyvalue_err),
490            InstrumentAny::CryptoOption(inst) => self
491                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
492                .map_err(to_pyvalue_err),
493            InstrumentAny::CryptoOptionSpread(inst) => self
494                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
495                .map_err(to_pyvalue_err),
496            InstrumentAny::CryptoPerpetual(inst) => self
497                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
498                .map_err(to_pyvalue_err),
499            InstrumentAny::CurrencyPair(inst) => self
500                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
501                .map_err(to_pyvalue_err),
502            InstrumentAny::Equity(inst) => self
503                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
504                .map_err(to_pyvalue_err),
505            InstrumentAny::FuturesContract(inst) => self
506                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
507                .map_err(to_pyvalue_err),
508            InstrumentAny::FuturesSpread(inst) => self
509                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
510                .map_err(to_pyvalue_err),
511            InstrumentAny::IndexInstrument(inst) => self
512                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
513                .map_err(to_pyvalue_err),
514            InstrumentAny::OptionContract(inst) => self
515                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
516                .map_err(to_pyvalue_err),
517            InstrumentAny::OptionSpread(inst) => self
518                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
519                .map_err(to_pyvalue_err),
520            InstrumentAny::PerpetualContract(inst) => self
521                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
522                .map_err(to_pyvalue_err),
523            InstrumentAny::TokenizedAsset(inst) => self
524                .calculate_maintenance_margin(&inst, quantity, price, use_quote_for_inverse)
525                .map_err(to_pyvalue_err),
526        }
527    }
528
529    #[pyo3(name = "to_dict")]
530    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
531        let dict = PyDict::new(py);
532        dict.set_item("calculate_account_state", self.calculate_account_state)?;
533        let events_list: PyResult<Vec<Py<PyAny>>> =
534            self.events.iter().map(|item| item.py_to_dict(py)).collect();
535        dict.set_item("events", events_list?)?;
536        Ok(dict.into())
537    }
538}