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