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::{
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    /// Represents a margin account that can hold leveraged positions.
42    #[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    /// Sets the default leverage for the account.
260    #[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    /// Sets the leverage for a specific instrument.
281    #[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    /// Returns the margin balance for the specified instrument.
293    #[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    /// Returns the account-wide margin balance for the specified collateral currency.
314    #[pyo3(name = "account_margin")]
315    fn py_account_margin(&self, currency: Currency) -> Option<MarginBalance> {
316        self.account_margin(&currency)
317    }
318
319    #[pyo3(name = "account_margins")]
320    fn py_account_margins(&self) -> IndexMap<Currency, MarginBalance> {
321        self.account_margins.clone()
322    }
323
324    /// Returns the account-wide initial margin for the specified collateral currency.
325    #[pyo3(name = "account_initial_margin")]
326    fn py_account_initial_margin(&self, currency: Currency) -> Option<Money> {
327        self.account_initial_margin(&currency)
328    }
329
330    /// Returns all account-wide initial margins keyed by currency.
331    #[pyo3(name = "account_initial_margins")]
332    fn py_account_initial_margins(&self) -> IndexMap<Currency, Money> {
333        self.account_initial_margins()
334    }
335
336    /// Returns the account-wide maintenance margin for the specified collateral currency.
337    #[pyo3(name = "account_maintenance_margin")]
338    fn py_account_maintenance_margin(&self, currency: Currency) -> Option<Money> {
339        self.account_maintenance_margin(&currency)
340    }
341
342    /// Returns all account-wide maintenance margins keyed by currency.
343    #[pyo3(name = "account_maintenance_margins")]
344    fn py_account_maintenance_margins(&self) -> IndexMap<Currency, Money> {
345        self.account_maintenance_margins()
346    }
347
348    /// Returns the total initial margin reserved in the specified currency,
349    /// summing per-instrument and account-wide entries.
350    #[pyo3(name = "total_initial_margin")]
351    fn py_total_initial_margin(&self, currency: Currency) -> Money {
352        self.total_initial_margin(currency)
353    }
354
355    /// Returns the total maintenance margin reserved in the specified currency,
356    /// summing per-instrument and account-wide entries.
357    #[pyo3(name = "total_maintenance_margin")]
358    fn py_total_maintenance_margin(&self, currency: Currency) -> Money {
359        self.total_maintenance_margin(currency)
360    }
361
362    /// Updates the initial margin for the specified instrument.
363    #[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    /// Returns the initial margin amount for the specified instrument.
369    #[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    /// Updates the maintenance margin for the specified instrument.
375    #[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    /// Returns the maintenance margin amount for the specified instrument.
385    #[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    /// Calculates the initial margin amount for the specified instrument and quantity.
393    ///
394    /// Delegates to the configured `MarginModel`.
395    ///
396    /// # Errors
397    ///
398    /// Returns an error if leverage is not positive, or if the result cannot be represented
399    /// as `Money`.
400    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    /// Calculates the maintenance margin amount for the specified instrument and quantity.
468    ///
469    /// Delegates to the configured `MarginModel`.
470    ///
471    /// # Errors
472    ///
473    /// Returns an error if the result cannot be represented as `Money`.
474    #[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}