nautilus_model/python/account/
margin.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 nautilus_core::python::{IntoPyObjectNautilusExt, to_pyvalue_err};
17use pyo3::{IntoPyObjectExt, basic::CompareOp, prelude::*, types::PyDict};
18use rust_decimal::Decimal;
19
20use crate::{
21    accounts::MarginAccount,
22    events::AccountState,
23    identifiers::{AccountId, InstrumentId},
24    instruments::InstrumentAny,
25    python::instruments::pyobject_to_instrument_any,
26    types::{Money, Price, Quantity},
27};
28
29#[pymethods]
30impl MarginAccount {
31    #[new]
32    fn py_new(event: AccountState, calculate_account_state: bool) -> Self {
33        Self::new(event, calculate_account_state)
34    }
35
36    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
37        match op {
38            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
39            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
40            _ => py.NotImplemented(),
41        }
42    }
43
44    #[getter]
45    fn id(&self) -> AccountId {
46        self.id
47    }
48
49    #[getter]
50    fn default_leverage(&self) -> Decimal {
51        self.default_leverage
52    }
53
54    #[getter]
55    #[pyo3(name = "calculate_account_state")]
56    fn py_calculate_account_state(&self) -> bool {
57        self.calculate_account_state
58    }
59
60    fn __repr__(&self) -> String {
61        format!(
62            "{}(id={}, type={}, base={})",
63            stringify!(MarginAccount),
64            self.id,
65            self.account_type,
66            self.base_currency.map_or_else(
67                || "None".to_string(),
68                |base_currency| format!("{}", base_currency.code)
69            ),
70        )
71    }
72
73    #[pyo3(name = "set_default_leverage")]
74    fn py_set_default_leverage(&mut self, default_leverage: Decimal) {
75        self.set_default_leverage(default_leverage);
76    }
77
78    #[pyo3(name = "leverages")]
79    fn py_leverages(&self, py: Python) -> PyResult<Py<PyAny>> {
80        let leverages = PyDict::new(py);
81        for (key, &value) in &self.leverages {
82            leverages
83                .set_item(key.into_py_any_unwrap(py), value)
84                .unwrap();
85        }
86        leverages.into_py_any(py)
87    }
88
89    #[pyo3(name = "leverage")]
90    fn py_leverage(&self, instrument_id: &InstrumentId) -> Decimal {
91        self.get_leverage(instrument_id)
92    }
93
94    #[pyo3(name = "set_leverage")]
95    fn py_set_leverage(&mut self, instrument_id: InstrumentId, leverage: Decimal) {
96        self.set_leverage(instrument_id, leverage);
97    }
98
99    #[pyo3(name = "is_unleveraged")]
100    fn py_is_unleveraged(&self, instrument_id: InstrumentId) -> PyResult<bool> {
101        Ok(self.is_unleveraged(instrument_id))
102    }
103
104    #[pyo3(name = "initial_margins")]
105    fn py_initial_margins(&self, py: Python) -> PyResult<Py<PyAny>> {
106        let initial_margins = PyDict::new(py);
107        for (key, &value) in &self.initial_margins() {
108            initial_margins
109                .set_item(key.into_py_any_unwrap(py), value.into_py_any_unwrap(py))
110                .unwrap();
111        }
112        initial_margins.into_py_any(py)
113    }
114
115    #[pyo3(name = "maintenance_margins")]
116    fn py_maintenance_margins(&self, py: Python) -> PyResult<Py<PyAny>> {
117        let maintenance_margins = PyDict::new(py);
118        for (key, &value) in &self.maintenance_margins() {
119            maintenance_margins
120                .set_item(key.into_py_any_unwrap(py), value.into_py_any_unwrap(py))
121                .unwrap();
122        }
123        maintenance_margins.into_py_any(py)
124    }
125
126    #[pyo3(name = "update_initial_margin")]
127    fn py_update_initial_margin(
128        &mut self,
129        instrument_id: InstrumentId,
130        initial_margin: Money,
131    ) -> PyResult<()> {
132        self.update_initial_margin(instrument_id, initial_margin);
133        Ok(())
134    }
135
136    #[pyo3(name = "initial_margin")]
137    fn py_initial_margin(&self, instrument_id: InstrumentId) -> PyResult<Money> {
138        Ok(self.initial_margin(instrument_id))
139    }
140
141    #[pyo3(name = "update_maintenance_margin")]
142    fn py_update_maintenance_margin(
143        &mut self,
144        instrument_id: InstrumentId,
145        maintenance_margin: Money,
146    ) -> PyResult<()> {
147        self.update_maintenance_margin(instrument_id, maintenance_margin);
148        Ok(())
149    }
150
151    #[pyo3(name = "maintenance_margin")]
152    fn py_maintenance_margin(&self, instrument_id: InstrumentId) -> PyResult<Money> {
153        Ok(self.maintenance_margin(instrument_id))
154    }
155
156    #[pyo3(name = "calculate_initial_margin")]
157    #[pyo3(signature = (instrument, quantity, price, use_quote_for_inverse=None))]
158    /// Calculates the initial margin for a given instrument and quantity at the specified price.
159    ///
160    /// # Errors
161    ///
162    /// Returns a `PyErr` if the Python `instrument` object cannot be converted to a supported internal instrument,
163    /// or if the instrument type is unsupported.
164    pub fn py_calculate_initial_margin(
165        &mut self,
166        instrument: Py<PyAny>,
167        quantity: Quantity,
168        price: Price,
169        use_quote_for_inverse: Option<bool>,
170        py: Python,
171    ) -> PyResult<Money> {
172        let instrument_type = pyobject_to_instrument_any(py, instrument)?;
173        match instrument_type {
174            InstrumentAny::CryptoPerpetual(inst) => self
175                .calculate_initial_margin(inst, quantity, price, use_quote_for_inverse)
176                .map_err(to_pyvalue_err),
177            InstrumentAny::CryptoFuture(inst) => self
178                .calculate_initial_margin(inst, quantity, price, use_quote_for_inverse)
179                .map_err(to_pyvalue_err),
180            InstrumentAny::CryptoOption(inst) => self
181                .calculate_initial_margin(inst, quantity, price, use_quote_for_inverse)
182                .map_err(to_pyvalue_err),
183            InstrumentAny::CurrencyPair(inst) => self
184                .calculate_initial_margin(inst, quantity, price, use_quote_for_inverse)
185                .map_err(to_pyvalue_err),
186            InstrumentAny::Equity(inst) => self
187                .calculate_initial_margin(inst, quantity, price, use_quote_for_inverse)
188                .map_err(to_pyvalue_err),
189            InstrumentAny::FuturesContract(inst) => self
190                .calculate_initial_margin(inst, quantity, price, use_quote_for_inverse)
191                .map_err(to_pyvalue_err),
192            InstrumentAny::OptionContract(inst) => self
193                .calculate_initial_margin(inst, quantity, price, use_quote_for_inverse)
194                .map_err(to_pyvalue_err),
195            _ => Err(to_pyvalue_err("Unsupported instrument type")),
196        }
197    }
198
199    /// Calculates the maintenance margin for a given instrument and quantity at the specified price.
200    ///
201    /// # Errors
202    ///
203    /// Returns a `PyErr` if the Python `instrument` object cannot be converted to a supported internal instrument,
204    /// or if the instrument type is unsupported.
205    #[pyo3(name = "calculate_maintenance_margin")]
206    #[pyo3(signature = (instrument, quantity, price, use_quote_for_inverse=None))]
207    pub fn py_calculate_maintenance_margin(
208        &mut self,
209        instrument: Py<PyAny>,
210        quantity: Quantity,
211        price: Price,
212        use_quote_for_inverse: Option<bool>,
213        py: Python,
214    ) -> PyResult<Money> {
215        let instrument_type = pyobject_to_instrument_any(py, instrument)?;
216        match instrument_type {
217            InstrumentAny::CryptoFuture(inst) => self
218                .calculate_maintenance_margin(inst, quantity, price, use_quote_for_inverse)
219                .map_err(to_pyvalue_err),
220            InstrumentAny::CryptoPerpetual(inst) => self
221                .calculate_maintenance_margin(inst, quantity, price, use_quote_for_inverse)
222                .map_err(to_pyvalue_err),
223            InstrumentAny::CryptoOption(inst) => self
224                .calculate_maintenance_margin(inst, quantity, price, use_quote_for_inverse)
225                .map_err(to_pyvalue_err),
226            InstrumentAny::CurrencyPair(inst) => self
227                .calculate_maintenance_margin(inst, quantity, price, use_quote_for_inverse)
228                .map_err(to_pyvalue_err),
229            InstrumentAny::Equity(inst) => self
230                .calculate_maintenance_margin(inst, quantity, price, use_quote_for_inverse)
231                .map_err(to_pyvalue_err),
232            InstrumentAny::FuturesContract(inst) => self
233                .calculate_maintenance_margin(inst, quantity, price, use_quote_for_inverse)
234                .map_err(to_pyvalue_err),
235            InstrumentAny::OptionContract(inst) => self
236                .calculate_maintenance_margin(inst, quantity, price, use_quote_for_inverse)
237                .map_err(to_pyvalue_err),
238            _ => Err(to_pyvalue_err("Unsupported instrument type")),
239        }
240    }
241
242    #[pyo3(name = "to_dict")]
243    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
244        let dict = PyDict::new(py);
245        dict.set_item("calculate_account_state", self.calculate_account_state)?;
246        let events_list: PyResult<Vec<Py<PyAny>>> =
247            self.events.iter().map(|item| item.py_to_dict(py)).collect();
248        dict.set_item("events", events_list.unwrap())?;
249        Ok(dict.into())
250    }
251}