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