Skip to main content

nautilus_model/python/types/
balance.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 std::{
17    collections::hash_map::DefaultHasher,
18    hash::{Hash, Hasher},
19    str::FromStr,
20};
21
22use nautilus_core::python::{
23    parsing::{get_optional_parsed, get_required_string},
24    to_pyvalue_err,
25};
26use pyo3::{prelude::*, types::PyDict};
27
28use crate::{
29    identifiers::InstrumentId,
30    types::{AccountBalance, Currency, MarginBalance, Money},
31};
32
33#[pymethods]
34#[pyo3_stub_gen::derive::gen_stub_pymethods]
35impl AccountBalance {
36    /// Represents an account balance denominated in a particular currency.
37    #[new]
38    fn py_new(total: Money, locked: Money, free: Money) -> PyResult<Self> {
39        Self::new_checked(total, locked, free).map_err(to_pyvalue_err)
40    }
41
42    fn __repr__(&self) -> String {
43        format!("{self:?}")
44    }
45
46    fn __str__(&self) -> String {
47        self.to_string()
48    }
49
50    fn __hash__(&self) -> isize {
51        let mut h = DefaultHasher::new();
52        self.total.raw.hash(&mut h);
53        self.locked.raw.hash(&mut h);
54        self.free.raw.hash(&mut h);
55        self.currency.code.hash(&mut h);
56        h.finish() as isize
57    }
58
59    /// Returns a copy of this balance.
60    #[pyo3(name = "copy")]
61    fn py_copy(&self) -> Self {
62        *self
63    }
64
65    /// Constructs an [`AccountBalance`] from a Python dict.
66    ///
67    /// # Errors
68    ///
69    /// Returns a `PyErr` if parsing or conversion fails.
70    ///
71    /// # Panics
72    ///
73    /// Panics if parsing numeric values (`unwrap()`) fails due to invalid format.
74    #[staticmethod]
75    #[pyo3(name = "from_dict")]
76    pub fn py_from_dict(values: &Bound<'_, PyDict>) -> PyResult<Self> {
77        let currency_str = get_required_string(values, "currency")?;
78        let total_str = get_required_string(values, "total")?;
79        let total: f64 = total_str.parse::<f64>().unwrap();
80        let free_str = get_required_string(values, "free")?;
81        let free: f64 = free_str.parse::<f64>().unwrap();
82        let locked_str = get_required_string(values, "locked")?;
83        let locked: f64 = locked_str.parse::<f64>().unwrap();
84        let currency = Currency::from_str(currency_str.as_str()).map_err(to_pyvalue_err)?;
85        Self::new_checked(
86            Money::new(total, currency),
87            Money::new(locked, currency),
88            Money::new(free, currency),
89        )
90        .map_err(to_pyvalue_err)
91    }
92
93    /// Converts this [`AccountBalance`] into a Python dict.
94    ///
95    /// # Errors
96    ///
97    /// Returns a `PyErr` if serialization fails.
98    #[pyo3(name = "to_dict")]
99    pub fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
100        let dict = PyDict::new(py);
101        dict.set_item("type", stringify!(AccountBalance))?;
102        dict.set_item(
103            "total",
104            format!(
105                "{:.*}",
106                self.total.currency.precision as usize,
107                self.total.as_f64()
108            ),
109        )?;
110        dict.set_item(
111            "locked",
112            format!(
113                "{:.*}",
114                self.locked.currency.precision as usize,
115                self.locked.as_f64()
116            ),
117        )?;
118        dict.set_item(
119            "free",
120            format!(
121                "{:.*}",
122                self.free.currency.precision as usize,
123                self.free.as_f64()
124            ),
125        )?;
126        dict.set_item("currency", self.currency.code.to_string())?;
127        Ok(dict.into())
128    }
129}
130
131#[pymethods]
132#[pyo3_stub_gen::derive::gen_stub_pymethods]
133impl MarginBalance {
134    /// Represents a margin balance.
135    ///
136    /// Margin entries have two mutually exclusive scopes:
137    ///
138    /// - Per-instrument: `instrument_id = Some(id)`. Used for isolated margin and
139    ///   for calculated margin in backtest mode where each instrument carries its
140    ///   own reserve.
141    /// - Account-wide (cross margin): `instrument_id = None`. Used for venues that
142    ///   report a single aggregate margin per collateral currency (most derivatives
143    ///   venues in cross-margin mode).
144    #[new]
145    #[pyo3(signature = (initial, maintenance, instrument_id=None))]
146    fn py_new(
147        initial: Money,
148        maintenance: Money,
149        instrument_id: Option<InstrumentId>,
150    ) -> PyResult<Self> {
151        Self::new_checked(initial, maintenance, instrument_id).map_err(to_pyvalue_err)
152    }
153
154    fn __repr__(&self) -> String {
155        format!("{self:?}")
156    }
157
158    fn __str__(&self) -> String {
159        self.to_string()
160    }
161
162    fn __hash__(&self) -> isize {
163        let mut h = DefaultHasher::new();
164        self.initial.raw.hash(&mut h);
165        self.maintenance.raw.hash(&mut h);
166        self.currency.code.hash(&mut h);
167        self.instrument_id.hash(&mut h);
168        h.finish() as isize
169    }
170
171    /// Returns a copy of this margin balance.
172    #[pyo3(name = "copy")]
173    fn py_copy(&self) -> Self {
174        *self
175    }
176
177    /// Constructs a [`MarginBalance`] from a Python dict.
178    ///
179    /// # Errors
180    ///
181    /// Returns a `PyErr` if parsing or conversion fails.
182    ///
183    /// # Panics
184    ///
185    /// Panics if parsing numeric values (`unwrap()`) fails due to invalid format.
186    #[staticmethod]
187    #[pyo3(name = "from_dict")]
188    pub fn py_from_dict(values: &Bound<'_, PyDict>) -> PyResult<Self> {
189        let currency_str = get_required_string(values, "currency")?;
190        let initial_str = get_required_string(values, "initial")?;
191        let initial: f64 = initial_str.parse::<f64>().unwrap();
192        let maintenance_str = get_required_string(values, "maintenance")?;
193        let maintenance: f64 = maintenance_str.parse::<f64>().unwrap();
194        let instrument_id = get_optional_parsed(values, "instrument_id", |s| {
195            Ok::<InstrumentId, String>(InstrumentId::from(s))
196        })?;
197        let currency = Currency::from_str(currency_str.as_str()).map_err(to_pyvalue_err)?;
198        Self::new_checked(
199            Money::new(initial, currency),
200            Money::new(maintenance, currency),
201            instrument_id,
202        )
203        .map_err(to_pyvalue_err)
204    }
205
206    /// Converts this [`MarginBalance`] into a Python dict.
207    ///
208    /// # Errors
209    ///
210    /// Returns a `PyErr` if serialization fails.
211    ///
212    #[pyo3(name = "to_dict")]
213    pub fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
214        let dict = PyDict::new(py);
215        dict.set_item("type", stringify!(MarginBalance))?;
216        dict.set_item(
217            "initial",
218            format!(
219                "{:.*}",
220                self.initial.currency.precision as usize,
221                self.initial.as_f64()
222            ),
223        )?;
224        dict.set_item(
225            "maintenance",
226            format!(
227                "{:.*}",
228                self.maintenance.currency.precision as usize,
229                self.maintenance.as_f64()
230            ),
231        )?;
232        dict.set_item("currency", self.currency.code.to_string())?;
233        match self.instrument_id {
234            Some(id) => dict.set_item("instrument_id", id.to_string())?,
235            None => dict.set_item("instrument_id", py.None())?,
236        }
237        Ok(dict.into())
238    }
239}