nautilus_model/python/types/
money.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 std::{
17    collections::hash_map::DefaultHasher,
18    hash::{Hash, Hasher},
19    ops::Neg,
20    str::FromStr,
21};
22
23use nautilus_core::python::{get_pytype_name, to_pytype_err, to_pyvalue_err};
24use pyo3::{
25    IntoPyObjectExt,
26    exceptions::PyValueError,
27    prelude::*,
28    pyclass::CompareOp,
29    types::{PyFloat, PyInt, PyString, PyTuple},
30};
31use rust_decimal::{Decimal, RoundingStrategy};
32
33use crate::types::{Currency, Money, money::MoneyRaw};
34
35#[pymethods]
36impl Money {
37    #[new]
38    fn py_new(amount: f64, currency: Currency) -> PyResult<Self> {
39        Self::new_checked(amount, currency).map_err(to_pyvalue_err)
40    }
41
42    fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
43        let py_tuple: &Bound<'_, PyTuple> = state.downcast::<PyTuple>()?;
44        self.raw = py_tuple
45            .get_item(0)?
46            .downcast::<PyInt>()?
47            .extract::<MoneyRaw>()?;
48        let currency_code: String = py_tuple
49            .get_item(1)?
50            .downcast::<PyString>()?
51            .extract::<String>()?;
52        self.currency = Currency::from_str(currency_code.as_str()).map_err(to_pyvalue_err)?;
53        Ok(())
54    }
55
56    fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
57        (self.raw, self.currency.code.to_string()).into_py_any(py)
58    }
59
60    fn __reduce__(&self, py: Python) -> PyResult<PyObject> {
61        let safe_constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
62        let state = self.__getstate__(py)?;
63        (safe_constructor, PyTuple::empty(py), state).into_py_any(py)
64    }
65
66    #[staticmethod]
67    fn _safe_constructor() -> Self {
68        Self::new(0.0, Currency::AUD())
69    }
70
71    fn __add__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
72        if other.as_ref().is_instance_of::<PyFloat>() {
73            let other_float: f64 = other.extract::<f64>()?;
74            (self.as_f64() + other_float).into_py_any(py)
75        } else if let Ok(other_qty) = other.extract::<Self>() {
76            (self.as_decimal() + other_qty.as_decimal()).into_py_any(py)
77        } else if let Ok(other_dec) = other.extract::<Decimal>() {
78            (self.as_decimal() + other_dec).into_py_any(py)
79        } else {
80            let pytype_name = get_pytype_name(other)?;
81            Err(to_pytype_err(format!(
82                "Unsupported type for __add__, was `{pytype_name}`"
83            )))
84        }
85    }
86
87    fn __radd__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
88        if other.as_ref().is_instance_of::<PyFloat>() {
89            let other_float: f64 = other.extract()?;
90            (other_float + self.as_f64()).into_py_any(py)
91        } else if let Ok(other_qty) = other.extract::<Self>() {
92            (other_qty.as_decimal() + self.as_decimal()).into_py_any(py)
93        } else if let Ok(other_dec) = other.extract::<Decimal>() {
94            (other_dec + self.as_decimal()).into_py_any(py)
95        } else {
96            let pytype_name = get_pytype_name(other)?;
97            Err(to_pytype_err(format!(
98                "Unsupported type for __radd__, was `{pytype_name}`"
99            )))
100        }
101    }
102
103    fn __sub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
104        if other.as_ref().is_instance_of::<PyFloat>() {
105            let other_float: f64 = other.extract()?;
106            (self.as_f64() - other_float).into_py_any(py)
107        } else if let Ok(other_qty) = other.extract::<Self>() {
108            (self.as_decimal() - other_qty.as_decimal()).into_py_any(py)
109        } else if let Ok(other_dec) = other.extract::<Decimal>() {
110            (self.as_decimal() - other_dec).into_py_any(py)
111        } else {
112            let pytype_name = get_pytype_name(other)?;
113            Err(to_pytype_err(format!(
114                "Unsupported type for __sub__, was `{pytype_name}`"
115            )))
116        }
117    }
118
119    fn __rsub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
120        if other.as_ref().is_instance_of::<PyFloat>() {
121            let other_float: f64 = other.extract()?;
122            (other_float - self.as_f64()).into_py_any(py)
123        } else if let Ok(other_qty) = other.extract::<Self>() {
124            (other_qty.as_decimal() - self.as_decimal()).into_py_any(py)
125        } else if let Ok(other_dec) = other.extract::<Decimal>() {
126            (other_dec - self.as_decimal()).into_py_any(py)
127        } else {
128            let pytype_name = get_pytype_name(other)?;
129            Err(to_pytype_err(format!(
130                "Unsupported type for __rsub__, was `{pytype_name}`"
131            )))
132        }
133    }
134
135    fn __mul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
136        if other.as_ref().is_instance_of::<PyFloat>() {
137            let other_float: f64 = other.extract()?;
138            (self.as_f64() * other_float).into_py_any(py)
139        } else if let Ok(other_qty) = other.extract::<Self>() {
140            (self.as_decimal() * other_qty.as_decimal()).into_py_any(py)
141        } else if let Ok(other_dec) = other.extract::<Decimal>() {
142            (self.as_decimal() * other_dec).into_py_any(py)
143        } else {
144            let pytype_name = get_pytype_name(other)?;
145            Err(to_pytype_err(format!(
146                "Unsupported type for __mul__, was `{pytype_name}`"
147            )))
148        }
149    }
150
151    fn __rmul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
152        if other.as_ref().is_instance_of::<PyFloat>() {
153            let other_float: f64 = other.extract()?;
154            (other_float * self.as_f64()).into_py_any(py)
155        } else if let Ok(other_qty) = other.extract::<Self>() {
156            (other_qty.as_decimal() * self.as_decimal()).into_py_any(py)
157        } else if let Ok(other_dec) = other.extract::<Decimal>() {
158            (other_dec * self.as_decimal()).into_py_any(py)
159        } else {
160            let pytype_name = get_pytype_name(other)?;
161            Err(to_pytype_err(format!(
162                "Unsupported type for __rmul__, was `{pytype_name}`"
163            )))
164        }
165    }
166
167    fn __truediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
168        if other.as_ref().is_instance_of::<PyFloat>() {
169            let other_float: f64 = other.extract()?;
170            (self.as_f64() / other_float).into_py_any(py)
171        } else if let Ok(other_qty) = other.extract::<Self>() {
172            (self.as_decimal() / other_qty.as_decimal()).into_py_any(py)
173        } else if let Ok(other_dec) = other.extract::<Decimal>() {
174            (self.as_decimal() / other_dec).into_py_any(py)
175        } else {
176            let pytype_name = get_pytype_name(other)?;
177            Err(to_pytype_err(format!(
178                "Unsupported type for __truediv__, was `{pytype_name}`"
179            )))
180        }
181    }
182
183    fn __rtruediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
184        if other.as_ref().is_instance_of::<PyFloat>() {
185            let other_float: f64 = other.extract()?;
186            (other_float / self.as_f64()).into_py_any(py)
187        } else if let Ok(other_qty) = other.extract::<Self>() {
188            (other_qty.as_decimal() / self.as_decimal()).into_py_any(py)
189        } else if let Ok(other_dec) = other.extract::<Decimal>() {
190            (other_dec / self.as_decimal()).into_py_any(py)
191        } else {
192            let pytype_name = get_pytype_name(other)?;
193            Err(to_pytype_err(format!(
194                "Unsupported type for __rtruediv__, was `{pytype_name}`"
195            )))
196        }
197    }
198
199    fn __floordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
200        if other.as_ref().is_instance_of::<PyFloat>() {
201            let other_float: f64 = other.extract()?;
202            (self.as_f64() / other_float).floor().into_py_any(py)
203        } else if let Ok(other_qty) = other.extract::<Self>() {
204            (self.as_decimal() / other_qty.as_decimal())
205                .floor()
206                .into_py_any(py)
207        } else if let Ok(other_dec) = other.extract::<Decimal>() {
208            (self.as_decimal() / other_dec).floor().into_py_any(py)
209        } else {
210            let pytype_name = get_pytype_name(other)?;
211            Err(to_pytype_err(format!(
212                "Unsupported type for __floordiv__, was `{pytype_name}`"
213            )))
214        }
215    }
216
217    fn __rfloordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
218        if other.as_ref().is_instance_of::<PyFloat>() {
219            let other_float: f64 = other.extract()?;
220            (other_float / self.as_f64()).floor().into_py_any(py)
221        } else if let Ok(other_qty) = other.extract::<Self>() {
222            (other_qty.as_decimal() / self.as_decimal())
223                .floor()
224                .into_py_any(py)
225        } else if let Ok(other_dec) = other.extract::<Decimal>() {
226            (other_dec / self.as_decimal()).floor().into_py_any(py)
227        } else {
228            let pytype_name = get_pytype_name(other)?;
229            Err(to_pytype_err(format!(
230                "Unsupported type for __rfloordiv__, was `{pytype_name}`"
231            )))
232        }
233    }
234
235    fn __mod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
236        if other.as_ref().is_instance_of::<PyFloat>() {
237            let other_float: f64 = other.extract()?;
238            (self.as_f64() % other_float).into_py_any(py)
239        } else if let Ok(other_qty) = other.extract::<Self>() {
240            (self.as_decimal() % other_qty.as_decimal()).into_py_any(py)
241        } else if let Ok(other_dec) = other.extract::<Decimal>() {
242            (self.as_decimal() % other_dec).into_py_any(py)
243        } else {
244            let pytype_name = get_pytype_name(other)?;
245            Err(to_pytype_err(format!(
246                "Unsupported type for __mod__, was `{pytype_name}`"
247            )))
248        }
249    }
250
251    fn __rmod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<PyObject> {
252        if other.as_ref().is_instance_of::<PyFloat>() {
253            let other_float: f64 = other.extract()?;
254            (other_float % self.as_f64()).into_py_any(py)
255        } else if let Ok(other_qty) = other.extract::<Self>() {
256            (other_qty.as_decimal() % self.as_decimal()).into_py_any(py)
257        } else if let Ok(other_dec) = other.extract::<Decimal>() {
258            (other_dec % self.as_decimal()).into_py_any(py)
259        } else {
260            let pytype_name = get_pytype_name(other)?;
261            Err(to_pytype_err(format!(
262                "Unsupported type for __rmod__, was `{pytype_name}`"
263            )))
264        }
265    }
266
267    fn __neg__(&self) -> Decimal {
268        self.as_decimal().neg()
269    }
270
271    fn __pos__(&self) -> Decimal {
272        let mut value = self.as_decimal();
273        value.set_sign_positive(true);
274        value
275    }
276
277    fn __abs__(&self) -> Decimal {
278        self.as_decimal().abs()
279    }
280
281    fn __int__(&self) -> u64 {
282        self.as_f64() as u64
283    }
284
285    fn __float__(&self) -> f64 {
286        self.as_f64()
287    }
288
289    #[pyo3(signature = (ndigits=None))]
290    fn __round__(&self, ndigits: Option<u32>) -> Decimal {
291        self.as_decimal()
292            .round_dp_with_strategy(ndigits.unwrap_or(0), RoundingStrategy::MidpointNearestEven)
293    }
294
295    fn __richcmp__(&self, other: PyObject, op: CompareOp, py: Python<'_>) -> PyResult<Py<PyAny>> {
296        if let Ok(other_money) = other.extract::<Self>(py) {
297            if self.currency != other_money.currency {
298                return Err(PyErr::new::<PyValueError, _>(
299                    "Cannot compare `Money` with different currencies",
300                ));
301            }
302
303            let result = match op {
304                CompareOp::Eq => self.eq(&other_money),
305                CompareOp::Ne => self.ne(&other_money),
306                CompareOp::Ge => self.ge(&other_money),
307                CompareOp::Gt => self.gt(&other_money),
308                CompareOp::Le => self.le(&other_money),
309                CompareOp::Lt => self.lt(&other_money),
310            };
311            result.into_py_any(py)
312        } else {
313            Ok(py.NotImplemented())
314        }
315    }
316
317    fn __hash__(&self) -> isize {
318        let mut h = DefaultHasher::new();
319        self.hash(&mut h);
320        h.finish() as isize
321    }
322
323    fn __repr__(&self) -> String {
324        format!("{self:?}")
325    }
326
327    fn __str__(&self) -> String {
328        self.to_string()
329    }
330
331    #[getter]
332    fn raw(&self) -> MoneyRaw {
333        self.raw
334    }
335
336    #[getter]
337    fn currency(&self) -> Currency {
338        self.currency
339    }
340
341    #[staticmethod]
342    #[pyo3(name = "zero")]
343    fn py_zero(currency: Currency) -> Self {
344        Self::new(0.0, currency)
345    }
346
347    #[staticmethod]
348    #[pyo3(name = "from_raw")]
349    fn py_from_raw(raw: MoneyRaw, currency: Currency) -> PyResult<Self> {
350        Ok(Self::from_raw(raw, currency))
351    }
352
353    #[staticmethod]
354    #[pyo3(name = "from_str")]
355    fn py_from_str(value: &str) -> PyResult<Self> {
356        Self::from_str(value).map_err(to_pyvalue_err)
357    }
358
359    #[pyo3(name = "is_zero")]
360    fn py_is_zero(&self) -> bool {
361        self.is_zero()
362    }
363
364    #[pyo3(name = "as_decimal")]
365    fn py_as_decimal(&self) -> Decimal {
366        self.as_decimal()
367    }
368
369    #[pyo3(name = "as_double")]
370    fn py_as_double(&self) -> f64 {
371        self.as_f64()
372    }
373
374    #[pyo3(name = "to_formatted_str")]
375    fn py_to_formatted_str(&self) -> String {
376        self.to_formatted_string()
377    }
378}