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.as_decimal() + other_qty.as_decimal()).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.as_decimal() + self.as_decimal()).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            (self.as_decimal() - other_qty.as_decimal()).into_py_any(py)
116        } else if let Ok(other_dec) = other.extract::<Decimal>() {
117            (self.as_decimal() - other_dec).into_py_any(py)
118        } else {
119            let pytype_name = get_pytype_name(other)?;
120            Err(to_pytype_err(format!(
121                "Unsupported type for __sub__, was `{pytype_name}`"
122            )))
123        }
124    }
125
126    fn __rsub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
127        if other.is_instance_of::<PyFloat>() {
128            let other_float: f64 = other.extract()?;
129            (other_float - self.as_f64()).into_py_any(py)
130        } else if let Ok(other_qty) = other.extract::<Self>() {
131            (other_qty.as_decimal() - self.as_decimal()).into_py_any(py)
132        } else if let Ok(other_dec) = other.extract::<Decimal>() {
133            (other_dec - self.as_decimal()).into_py_any(py)
134        } else {
135            let pytype_name = get_pytype_name(other)?;
136            Err(to_pytype_err(format!(
137                "Unsupported type for __rsub__, was `{pytype_name}`"
138            )))
139        }
140    }
141
142    fn __mul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
143        if other.is_instance_of::<PyFloat>() {
144            let other_float: f64 = other.extract()?;
145            (self.as_f64() * other_float).into_py_any(py)
146        } else if let Ok(other_qty) = other.extract::<Self>() {
147            (self.as_decimal() * other_qty.as_decimal()).into_py_any(py)
148        } else if let Ok(other_dec) = other.extract::<Decimal>() {
149            (self.as_decimal() * other_dec).into_py_any(py)
150        } else {
151            let pytype_name = get_pytype_name(other)?;
152            Err(to_pytype_err(format!(
153                "Unsupported type for __mul__, was `{pytype_name}`"
154            )))
155        }
156    }
157
158    fn __rmul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
159        if other.is_instance_of::<PyFloat>() {
160            let other_float: f64 = other.extract()?;
161            (other_float * self.as_f64()).into_py_any(py)
162        } else if let Ok(other_qty) = other.extract::<Self>() {
163            (other_qty.as_decimal() * self.as_decimal()).into_py_any(py)
164        } else if let Ok(other_dec) = other.extract::<Decimal>() {
165            (other_dec * self.as_decimal()).into_py_any(py)
166        } else {
167            let pytype_name = get_pytype_name(other)?;
168            Err(to_pytype_err(format!(
169                "Unsupported type for __rmul__, was `{pytype_name}`"
170            )))
171        }
172    }
173
174    fn __truediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
175        if other.is_instance_of::<PyFloat>() {
176            let other_float: f64 = other.extract()?;
177            (self.as_f64() / other_float).into_py_any(py)
178        } else if let Ok(other_qty) = other.extract::<Self>() {
179            (self.as_decimal() / other_qty.as_decimal()).into_py_any(py)
180        } else if let Ok(other_dec) = other.extract::<Decimal>() {
181            (self.as_decimal() / other_dec).into_py_any(py)
182        } else {
183            let pytype_name = get_pytype_name(other)?;
184            Err(to_pytype_err(format!(
185                "Unsupported type for __truediv__, was `{pytype_name}`"
186            )))
187        }
188    }
189
190    fn __rtruediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
191        if other.is_instance_of::<PyFloat>() {
192            let other_float: f64 = other.extract()?;
193            (other_float / self.as_f64()).into_py_any(py)
194        } else if let Ok(other_qty) = other.extract::<Self>() {
195            (other_qty.as_decimal() / self.as_decimal()).into_py_any(py)
196        } else if let Ok(other_dec) = other.extract::<Decimal>() {
197            (other_dec / self.as_decimal()).into_py_any(py)
198        } else {
199            let pytype_name = get_pytype_name(other)?;
200            Err(to_pytype_err(format!(
201                "Unsupported type for __rtruediv__, was `{pytype_name}`"
202            )))
203        }
204    }
205
206    fn __floordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
207        if other.is_instance_of::<PyFloat>() {
208            let other_float: f64 = other.extract()?;
209            (self.as_f64() / other_float).floor().into_py_any(py)
210        } else if let Ok(other_qty) = other.extract::<Self>() {
211            (self.as_decimal() / other_qty.as_decimal())
212                .floor()
213                .into_py_any(py)
214        } else if let Ok(other_dec) = other.extract::<Decimal>() {
215            (self.as_decimal() / other_dec).floor().into_py_any(py)
216        } else {
217            let pytype_name = get_pytype_name(other)?;
218            Err(to_pytype_err(format!(
219                "Unsupported type for __floordiv__, was `{pytype_name}`"
220            )))
221        }
222    }
223
224    fn __rfloordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
225        if other.is_instance_of::<PyFloat>() {
226            let other_float: f64 = other.extract()?;
227            (other_float / self.as_f64()).floor().into_py_any(py)
228        } else if let Ok(other_qty) = other.extract::<Self>() {
229            (other_qty.as_decimal() / self.as_decimal())
230                .floor()
231                .into_py_any(py)
232        } else if let Ok(other_dec) = other.extract::<Decimal>() {
233            (other_dec / self.as_decimal()).floor().into_py_any(py)
234        } else {
235            let pytype_name = get_pytype_name(other)?;
236            Err(to_pytype_err(format!(
237                "Unsupported type for __rfloordiv__, was `{pytype_name}`"
238            )))
239        }
240    }
241
242    fn __mod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
243        if other.is_instance_of::<PyFloat>() {
244            let other_float: f64 = other.extract()?;
245            (self.as_f64() % other_float).into_py_any(py)
246        } else if let Ok(other_qty) = other.extract::<Self>() {
247            (self.as_decimal() % other_qty.as_decimal()).into_py_any(py)
248        } else if let Ok(other_dec) = other.extract::<Decimal>() {
249            (self.as_decimal() % other_dec).into_py_any(py)
250        } else {
251            let pytype_name = get_pytype_name(other)?;
252            Err(to_pytype_err(format!(
253                "Unsupported type for __mod__, was `{pytype_name}`"
254            )))
255        }
256    }
257
258    fn __rmod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
259        if other.is_instance_of::<PyFloat>() {
260            let other_float: f64 = other.extract()?;
261            (other_float % self.as_f64()).into_py_any(py)
262        } else if let Ok(other_qty) = other.extract::<Self>() {
263            (other_qty.as_decimal() % self.as_decimal()).into_py_any(py)
264        } else if let Ok(other_dec) = other.extract::<Decimal>() {
265            (other_dec % self.as_decimal()).into_py_any(py)
266        } else {
267            let pytype_name = get_pytype_name(other)?;
268            Err(to_pytype_err(format!(
269                "Unsupported type for __rmod__, was `{pytype_name}`"
270            )))
271        }
272    }
273
274    fn __neg__(&self) -> Decimal {
275        self.as_decimal().neg()
276    }
277
278    fn __pos__(&self) -> Decimal {
279        let mut value = self.as_decimal();
280        value.set_sign_positive(true);
281        value
282    }
283
284    fn __abs__(&self) -> Decimal {
285        self.as_decimal().abs()
286    }
287
288    fn __int__(&self) -> u64 {
289        self.as_f64() as u64
290    }
291
292    fn __float__(&self) -> f64 {
293        self.as_f64()
294    }
295
296    #[pyo3(signature = (ndigits=None))]
297    fn __round__(&self, ndigits: Option<u32>) -> Decimal {
298        self.as_decimal()
299            .round_dp_with_strategy(ndigits.unwrap_or(0), RoundingStrategy::MidpointNearestEven)
300    }
301
302    fn __repr__(&self) -> String {
303        format!("{self:?}")
304    }
305
306    fn __str__(&self) -> String {
307        self.to_string()
308    }
309
310    #[getter]
311    fn raw(&self) -> QuantityRaw {
312        self.raw
313    }
314
315    #[getter]
316    fn precision(&self) -> u8 {
317        self.precision
318    }
319
320    #[staticmethod]
321    #[pyo3(name = "from_raw")]
322    fn py_from_raw(raw: QuantityRaw, precision: u8) -> Self {
323        Self::from_raw(raw, precision)
324    }
325
326    #[staticmethod]
327    #[pyo3(name = "zero")]
328    #[pyo3(signature = (precision = 0))]
329    fn py_zero(precision: u8) -> PyResult<Self> {
330        Self::new_checked(0.0, precision).map_err(to_pyvalue_err)
331    }
332
333    #[staticmethod]
334    #[pyo3(name = "from_int")]
335    fn py_from_int(value: u64) -> PyResult<Self> {
336        Self::new_checked(value as f64, 0).map_err(to_pyvalue_err)
337    }
338
339    #[staticmethod]
340    #[pyo3(name = "from_str")]
341    fn py_from_str(value: &str) -> Self {
342        Self::from(value)
343    }
344
345    #[staticmethod]
346    #[pyo3(name = "from_decimal")]
347    fn py_from_decimal(decimal: Decimal) -> PyResult<Self> {
348        Self::from_decimal(decimal).map_err(to_pyvalue_err)
349    }
350
351    #[staticmethod]
352    #[pyo3(name = "from_decimal_dp")]
353    fn py_from_decimal_dp(decimal: Decimal, precision: u8) -> PyResult<Self> {
354        Self::from_decimal_dp(decimal, precision).map_err(to_pyvalue_err)
355    }
356
357    #[pyo3(name = "is_zero")]
358    fn py_is_zero(&self) -> bool {
359        self.is_zero()
360    }
361
362    #[pyo3(name = "is_positive")]
363    fn py_is_positive(&self) -> bool {
364        self.is_positive()
365    }
366
367    #[pyo3(name = "as_decimal")]
368    fn py_as_decimal(&self) -> Decimal {
369        self.as_decimal()
370    }
371
372    #[pyo3(name = "as_double")]
373    fn py_as_double(&self) -> f64 {
374        self.as_f64()
375    }
376
377    #[pyo3(name = "to_formatted_str")]
378    fn py_to_formatted_str(&self) -> String {
379        self.to_formatted_string()
380    }
381}