Skip to main content

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