Skip to main content

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    str::FromStr,
20};
21
22use nautilus_core::python::{get_pytype_name, to_pytype_err, to_pyvalue_err};
23use pyo3::{IntoPyObjectExt, basic::CompareOp, prelude::*, types::PyFloat};
24use rust_decimal::{Decimal, RoundingStrategy};
25
26use crate::types::{Currency, Money, money::MoneyRaw};
27
28#[pymethods]
29#[pyo3_stub_gen::derive::gen_stub_pymethods]
30impl Money {
31    /// Represents an amount of money in a specified currency denomination.
32    ///
33    /// - `MONEY_MAX` - Maximum representable money amount
34    /// - `MONEY_MIN` - Minimum representable money amount
35    #[new]
36    fn py_new(amount: f64, currency: Currency) -> PyResult<Self> {
37        Self::new_checked(amount, currency).map_err(to_pyvalue_err)
38    }
39
40    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
41        let from_raw = py.get_type::<Self>().getattr("from_raw")?;
42        let args = (self.raw, self.currency).into_py_any(py)?;
43        (from_raw, args).into_py_any(py)
44    }
45
46    fn __richcmp__(
47        &self,
48        other: &Bound<'_, PyAny>,
49        op: CompareOp,
50        py: Python<'_>,
51    ) -> PyResult<Py<PyAny>> {
52        if let Ok(other_money) = other.extract::<Self>() {
53            if self.currency != other_money.currency {
54                return Err(to_pyvalue_err(format!(
55                    "Cannot compare Money with different currencies: {} vs {}",
56                    self.currency.code, other_money.currency.code
57                )));
58            }
59            let result = match op {
60                CompareOp::Eq => self.eq(&other_money),
61                CompareOp::Ne => self.ne(&other_money),
62                CompareOp::Ge => self.ge(&other_money),
63                CompareOp::Gt => self.gt(&other_money),
64                CompareOp::Le => self.le(&other_money),
65                CompareOp::Lt => self.lt(&other_money),
66            };
67            result.into_py_any(py)
68        } else {
69            Ok(py.NotImplemented())
70        }
71    }
72
73    fn __hash__(&self) -> isize {
74        let mut h = DefaultHasher::new();
75        self.hash(&mut h);
76        h.finish() as isize
77    }
78
79    fn __add__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
80        if other.is_instance_of::<PyFloat>() {
81            let other_float: f64 = other.extract::<f64>()?;
82            (self.as_f64() + other_float).into_py_any(py)
83        } else if let Ok(other_money) = other.extract::<Self>() {
84            if self.currency != other_money.currency {
85                return Err(to_pyvalue_err(format!(
86                    "Currency mismatch: cannot add {} to {}",
87                    other_money.currency.code, self.currency.code
88                )));
89            }
90            (*self + other_money).into_py_any(py)
91        } else if let Ok(other_dec) = other.extract::<Decimal>() {
92            (self.as_decimal() + other_dec).into_py_any(py)
93        } else {
94            let pytype_name = get_pytype_name(other)?;
95            Err(to_pytype_err(format!(
96                "Unsupported type for __add__, was `{pytype_name}`"
97            )))
98        }
99    }
100
101    fn __radd__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
102        if other.is_instance_of::<PyFloat>() {
103            let other_float: f64 = other.extract()?;
104            (other_float + self.as_f64()).into_py_any(py)
105        } else if let Ok(other_money) = other.extract::<Self>() {
106            if self.currency != other_money.currency {
107                return Err(to_pyvalue_err(format!(
108                    "Currency mismatch: cannot add {} to {}",
109                    self.currency.code, other_money.currency.code
110                )));
111            }
112            (other_money + *self).into_py_any(py)
113        } else if let Ok(other_dec) = other.extract::<Decimal>() {
114            (other_dec + self.as_decimal()).into_py_any(py)
115        } else {
116            let pytype_name = get_pytype_name(other)?;
117            Err(to_pytype_err(format!(
118                "Unsupported type for __radd__, was `{pytype_name}`"
119            )))
120        }
121    }
122
123    fn __sub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
124        if other.is_instance_of::<PyFloat>() {
125            let other_float: f64 = other.extract()?;
126            (self.as_f64() - other_float).into_py_any(py)
127        } else if let Ok(other_money) = other.extract::<Self>() {
128            if self.currency != other_money.currency {
129                return Err(to_pyvalue_err(format!(
130                    "Currency mismatch: cannot subtract {} from {}",
131                    other_money.currency.code, self.currency.code
132                )));
133            }
134            (*self - other_money).into_py_any(py)
135        } else if let Ok(other_dec) = other.extract::<Decimal>() {
136            (self.as_decimal() - other_dec).into_py_any(py)
137        } else {
138            let pytype_name = get_pytype_name(other)?;
139            Err(to_pytype_err(format!(
140                "Unsupported type for __sub__, was `{pytype_name}`"
141            )))
142        }
143    }
144
145    fn __rsub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
146        if other.is_instance_of::<PyFloat>() {
147            let other_float: f64 = other.extract()?;
148            (other_float - self.as_f64()).into_py_any(py)
149        } else if let Ok(other_money) = other.extract::<Self>() {
150            if self.currency != other_money.currency {
151                return Err(to_pyvalue_err(format!(
152                    "Currency mismatch: cannot subtract {} from {}",
153                    self.currency.code, other_money.currency.code
154                )));
155            }
156            (other_money - *self).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 __rsub__, was `{pytype_name}`"
163            )))
164        }
165    }
166
167    fn __mul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
168        if other.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 __mul__, was `{pytype_name}`"
179            )))
180        }
181    }
182
183    fn __rmul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
184        if other.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 __rmul__, was `{pytype_name}`"
195            )))
196        }
197    }
198
199    fn __truediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
200        if other.is_instance_of::<PyFloat>() {
201            let other_float: f64 = other.extract()?;
202            (self.as_f64() / other_float).into_py_any(py)
203        } else if let Ok(other_qty) = other.extract::<Self>() {
204            (self.as_decimal() / other_qty.as_decimal()).into_py_any(py)
205        } else if let Ok(other_dec) = other.extract::<Decimal>() {
206            (self.as_decimal() / other_dec).into_py_any(py)
207        } else {
208            let pytype_name = get_pytype_name(other)?;
209            Err(to_pytype_err(format!(
210                "Unsupported type for __truediv__, was `{pytype_name}`"
211            )))
212        }
213    }
214
215    fn __rtruediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
216        if other.is_instance_of::<PyFloat>() {
217            let other_float: f64 = other.extract()?;
218            (other_float / self.as_f64()).into_py_any(py)
219        } else if let Ok(other_qty) = other.extract::<Self>() {
220            (other_qty.as_decimal() / self.as_decimal()).into_py_any(py)
221        } else if let Ok(other_dec) = other.extract::<Decimal>() {
222            (other_dec / self.as_decimal()).into_py_any(py)
223        } else {
224            let pytype_name = get_pytype_name(other)?;
225            Err(to_pytype_err(format!(
226                "Unsupported type for __rtruediv__, was `{pytype_name}`"
227            )))
228        }
229    }
230
231    fn __floordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
232        if other.is_instance_of::<PyFloat>() {
233            let other_float: f64 = other.extract()?;
234            (self.as_f64() / other_float).floor().into_py_any(py)
235        } else if let Ok(other_qty) = other.extract::<Self>() {
236            (self.as_decimal() / other_qty.as_decimal())
237                .floor()
238                .into_py_any(py)
239        } else if let Ok(other_dec) = other.extract::<Decimal>() {
240            (self.as_decimal() / other_dec).floor().into_py_any(py)
241        } else {
242            let pytype_name = get_pytype_name(other)?;
243            Err(to_pytype_err(format!(
244                "Unsupported type for __floordiv__, was `{pytype_name}`"
245            )))
246        }
247    }
248
249    fn __rfloordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
250        if other.is_instance_of::<PyFloat>() {
251            let other_float: f64 = other.extract()?;
252            (other_float / self.as_f64()).floor().into_py_any(py)
253        } else if let Ok(other_qty) = other.extract::<Self>() {
254            (other_qty.as_decimal() / self.as_decimal())
255                .floor()
256                .into_py_any(py)
257        } else if let Ok(other_dec) = other.extract::<Decimal>() {
258            (other_dec / self.as_decimal()).floor().into_py_any(py)
259        } else {
260            let pytype_name = get_pytype_name(other)?;
261            Err(to_pytype_err(format!(
262                "Unsupported type for __rfloordiv__, was `{pytype_name}`"
263            )))
264        }
265    }
266
267    fn __mod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
268        if other.is_instance_of::<PyFloat>() {
269            let other_float: f64 = other.extract()?;
270            (self.as_f64() % other_float).into_py_any(py)
271        } else if let Ok(other_qty) = other.extract::<Self>() {
272            (self.as_decimal() % other_qty.as_decimal()).into_py_any(py)
273        } else if let Ok(other_dec) = other.extract::<Decimal>() {
274            (self.as_decimal() % other_dec).into_py_any(py)
275        } else {
276            let pytype_name = get_pytype_name(other)?;
277            Err(to_pytype_err(format!(
278                "Unsupported type for __mod__, was `{pytype_name}`"
279            )))
280        }
281    }
282
283    fn __rmod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
284        if other.is_instance_of::<PyFloat>() {
285            let other_float: f64 = other.extract()?;
286            (other_float % self.as_f64()).into_py_any(py)
287        } else if let Ok(other_qty) = other.extract::<Self>() {
288            (other_qty.as_decimal() % self.as_decimal()).into_py_any(py)
289        } else if let Ok(other_dec) = other.extract::<Decimal>() {
290            (other_dec % self.as_decimal()).into_py_any(py)
291        } else {
292            let pytype_name = get_pytype_name(other)?;
293            Err(to_pytype_err(format!(
294                "Unsupported type for __rmod__, was `{pytype_name}`"
295            )))
296        }
297    }
298
299    fn __neg__(&self) -> Self {
300        -*self
301    }
302
303    fn __pos__(&self) -> Self {
304        *self
305    }
306
307    fn __abs__(&self) -> Self {
308        if self.raw < 0 { -*self } else { *self }
309    }
310
311    fn __int__(&self) -> i64 {
312        self.as_f64() as i64
313    }
314
315    fn __float__(&self) -> f64 {
316        self.as_f64()
317    }
318
319    #[pyo3(signature = (ndigits=None))]
320    fn __round__(&self, ndigits: Option<u32>) -> Decimal {
321        self.as_decimal()
322            .round_dp_with_strategy(ndigits.unwrap_or(0), RoundingStrategy::MidpointNearestEven)
323    }
324
325    fn __repr__(&self) -> String {
326        format!("{self:?}")
327    }
328
329    fn __str__(&self) -> String {
330        self.to_string()
331    }
332
333    #[getter]
334    fn raw(&self) -> MoneyRaw {
335        self.raw
336    }
337
338    #[getter]
339    fn currency(&self) -> Currency {
340        self.currency
341    }
342
343    /// Creates a new `Money` instance with a value of zero with the given `Currency`.
344    #[staticmethod]
345    #[pyo3(name = "zero")]
346    fn py_zero(currency: Currency) -> Self {
347        Self::new(0.0, currency)
348    }
349
350    /// Creates a new `Money` instance from the given `raw` fixed-point value and the specified `currency`.
351    #[staticmethod]
352    #[pyo3(name = "from_raw")]
353    fn py_from_raw(raw: MoneyRaw, currency: Currency) -> Self {
354        Self::from_raw(raw, currency)
355    }
356
357    /// Creates a new `Money` from a `Decimal` value with specified currency.
358    ///
359    /// This method provides more reliable parsing by using Decimal arithmetic
360    /// to avoid floating-point precision issues during conversion.
361    ///
362    /// # Errors
363    ///
364    /// Returns an error if:
365    /// - The decimal value cannot be converted to the raw representation.
366    /// - Overflow occurs during scaling.
367    #[staticmethod]
368    #[pyo3(name = "from_decimal")]
369    fn py_from_decimal(value: Decimal, currency: Currency) -> PyResult<Self> {
370        Self::from_decimal(value, currency).map_err(to_pyvalue_err)
371    }
372
373    #[staticmethod]
374    #[pyo3(name = "from_str")]
375    fn py_from_str(value: &str) -> PyResult<Self> {
376        Self::from_str(value).map_err(to_pyvalue_err)
377    }
378
379    /// Returns `true` if the value of this instance is zero.
380    #[pyo3(name = "is_zero")]
381    fn py_is_zero(&self) -> bool {
382        self.is_zero()
383    }
384
385    /// Returns `true` if the value of this instance is positive (> 0).
386    #[pyo3(name = "is_positive")]
387    fn py_is_positive(&self) -> bool {
388        self.is_positive()
389    }
390
391    /// Returns the value of this instance as a `Decimal`.
392    #[pyo3(name = "as_decimal")]
393    fn py_as_decimal(&self) -> Decimal {
394        self.as_decimal()
395    }
396
397    #[pyo3(name = "as_double")]
398    fn py_as_double(&self) -> f64 {
399        self.as_f64()
400    }
401
402    #[pyo3(name = "to_formatted_str")]
403    fn py_to_formatted_str(&self) -> String {
404        self.to_formatted_string()
405    }
406
407    /// Performs a checked addition, returning `None` on raw integer overflow, when
408    /// the result falls outside `[MONEY_RAW_MIN, MONEY_RAW_MAX]`, or when the operands
409    /// have mixed raw scales (e.g. a wei-scaled `Money` and a `FIXED_SCALAR`-scaled
410    /// `Money`, even if their currency codes match).
411    #[pyo3(name = "checked_add")]
412    fn py_checked_add(&self, other: Self) -> PyResult<Option<Self>> {
413        if self.currency != other.currency {
414            return Err(to_pyvalue_err(format!(
415                "Currency mismatch: cannot add {} to {}",
416                other.currency.code, self.currency.code
417            )));
418        }
419        Ok(self.checked_add(other))
420    }
421
422    /// Performs a checked subtraction, returning `None` on raw integer underflow, when
423    /// the result falls outside `[MONEY_RAW_MIN, MONEY_RAW_MAX]`, or when the operands
424    /// have mixed raw scales (e.g. a wei-scaled `Money` and a `FIXED_SCALAR`-scaled
425    /// `Money`, even if their currency codes match).
426    #[pyo3(name = "checked_sub")]
427    fn py_checked_sub(&self, other: Self) -> PyResult<Option<Self>> {
428        if self.currency != other.currency {
429            return Err(to_pyvalue_err(format!(
430                "Currency mismatch: cannot subtract {} from {}",
431                other.currency.code, self.currency.code
432            )));
433        }
434        Ok(self.checked_sub(other))
435    }
436}