Skip to main content

nautilus_model/python/types/
price.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::{basic::CompareOp, conversion::IntoPyObjectExt, prelude::*, types::PyFloat};
24use rust_decimal::{Decimal, RoundingStrategy};
25
26#[cfg(not(feature = "high-precision"))]
27use crate::types::fixed::fixed_i64_to_f64;
28#[cfg(feature = "high-precision")]
29use crate::types::fixed::fixed_i128_to_f64;
30use crate::types::price::{Price, PriceRaw};
31
32#[pymethods]
33#[pyo3_stub_gen::derive::gen_stub_pymethods]
34impl Price {
35    /// Represents a price in a market with a specified precision.
36    ///
37    /// The number of decimal places may vary. For certain asset classes, prices may
38    /// have negative values. For example, prices for options instruments can be
39    /// negative under certain conditions.
40    ///
41    /// Handles up to `FIXED_PRECISION` decimals of precision.
42    ///
43    /// - `PRICE_MAX` - Maximum representable price value.
44    /// - `PRICE_MIN` - Minimum representable price value.
45    #[new]
46    fn py_new(value: f64, precision: u8) -> PyResult<Self> {
47        Self::new_checked(value, precision).map_err(to_pyvalue_err)
48    }
49
50    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
51        let from_raw = py.get_type::<Self>().getattr("from_raw")?;
52        let args = (self.raw, self.precision).into_py_any(py)?;
53        (from_raw, args).into_py_any(py)
54    }
55
56    fn __richcmp__(
57        &self,
58        other: &Bound<'_, PyAny>,
59        op: CompareOp,
60        py: Python<'_>,
61    ) -> PyResult<Py<PyAny>> {
62        if let Ok(other_price) = other.extract::<Self>() {
63            let result = match op {
64                CompareOp::Eq => self.eq(&other_price),
65                CompareOp::Ne => self.ne(&other_price),
66                CompareOp::Ge => self.ge(&other_price),
67                CompareOp::Gt => self.gt(&other_price),
68                CompareOp::Le => self.le(&other_price),
69                CompareOp::Lt => self.lt(&other_price),
70            };
71            result.into_py_any(py)
72        } else if let Ok(other_dec) = other.extract::<Decimal>() {
73            let result = match op {
74                CompareOp::Eq => self.as_decimal() == other_dec,
75                CompareOp::Ne => self.as_decimal() != other_dec,
76                CompareOp::Ge => self.as_decimal() >= other_dec,
77                CompareOp::Gt => self.as_decimal() > other_dec,
78                CompareOp::Le => self.as_decimal() <= other_dec,
79                CompareOp::Lt => self.as_decimal() < other_dec,
80            };
81            result.into_py_any(py)
82        } else {
83            Ok(py.NotImplemented())
84        }
85    }
86
87    fn __hash__(&self) -> isize {
88        let mut h = DefaultHasher::new();
89        self.hash(&mut h);
90        h.finish() as isize
91    }
92
93    fn __add__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
94        if other.is_instance_of::<PyFloat>() {
95            let other_float: f64 = other.extract()?;
96            (self.as_f64() + other_float).into_py_any(py)
97        } else if let Ok(other_price) = other.extract::<Self>() {
98            (*self + other_price).into_py_any(py)
99        } else if let Ok(other_dec) = other.extract::<Decimal>() {
100            (self.as_decimal() + other_dec).into_py_any(py)
101        } else {
102            let pytype_name = get_pytype_name(other)?;
103            Err(to_pytype_err(format!(
104                "Unsupported type for __add__, was `{pytype_name}`"
105            )))
106        }
107    }
108
109    fn __radd__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
110        if other.is_instance_of::<PyFloat>() {
111            let other_float: f64 = other.extract()?;
112            (other_float + self.as_f64()).into_py_any(py)
113        } else if let Ok(other_price) = other.extract::<Self>() {
114            (other_price + *self).into_py_any(py)
115        } else if let Ok(other_dec) = other.extract::<Decimal>() {
116            (other_dec + self.as_decimal()).into_py_any(py)
117        } else {
118            let pytype_name = get_pytype_name(other)?;
119            Err(to_pytype_err(format!(
120                "Unsupported type for __radd__, was `{pytype_name}`"
121            )))
122        }
123    }
124
125    fn __sub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
126        if other.is_instance_of::<PyFloat>() {
127            let other_float: f64 = other.extract()?;
128            (self.as_f64() - other_float).into_py_any(py)
129        } else if let Ok(other_price) = other.extract::<Self>() {
130            (*self - other_price).into_py_any(py)
131        } else if let Ok(other_dec) = other.extract::<Decimal>() {
132            (self.as_decimal() - other_dec).into_py_any(py)
133        } else {
134            let pytype_name = get_pytype_name(other)?;
135            Err(to_pytype_err(format!(
136                "Unsupported type for __sub__, was `{pytype_name}`"
137            )))
138        }
139    }
140
141    fn __rsub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
142        if other.is_instance_of::<PyFloat>() {
143            let other_float: f64 = other.extract()?;
144            (other_float - self.as_f64()).into_py_any(py)
145        } else if let Ok(other_price) = other.extract::<Self>() {
146            (other_price - *self).into_py_any(py)
147        } else if let Ok(other_dec) = other.extract::<Decimal>() {
148            (other_dec - self.as_decimal()).into_py_any(py)
149        } else {
150            let pytype_name = get_pytype_name(other)?;
151            Err(to_pytype_err(format!(
152                "Unsupported type for __rsub__, was `{pytype_name}`"
153            )))
154        }
155    }
156
157    fn __mul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
158        if other.is_instance_of::<PyFloat>() {
159            let other_float: f64 = other.extract()?;
160            (self.as_f64() * other_float).into_py_any(py)
161        } else if let Ok(other_price) = other.extract::<Self>() {
162            (self.as_decimal() * other_price.as_decimal()).into_py_any(py)
163        } else if let Ok(other_dec) = other.extract::<Decimal>() {
164            (self.as_decimal() * other_dec).into_py_any(py)
165        } else {
166            let pytype_name = get_pytype_name(other)?;
167            Err(to_pytype_err(format!(
168                "Unsupported type for __mul__, was `{pytype_name}`"
169            )))
170        }
171    }
172
173    fn __rmul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
174        if other.is_instance_of::<PyFloat>() {
175            let other_float: f64 = other.extract()?;
176            (other_float * self.as_f64()).into_py_any(py)
177        } else if let Ok(other_price) = other.extract::<Self>() {
178            (other_price.as_decimal() * self.as_decimal()).into_py_any(py)
179        } else if let Ok(other_dec) = other.extract::<Decimal>() {
180            (other_dec * self.as_decimal()).into_py_any(py)
181        } else {
182            let pytype_name = get_pytype_name(other)?;
183            Err(to_pytype_err(format!(
184                "Unsupported type for __rmul__, was `{pytype_name}`"
185            )))
186        }
187    }
188
189    fn __truediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
190        if other.is_instance_of::<PyFloat>() {
191            let other_float: f64 = other.extract()?;
192            (self.as_f64() / other_float).into_py_any(py)
193        } else if let Ok(other_price) = other.extract::<Self>() {
194            (self.as_decimal() / other_price.as_decimal()).into_py_any(py)
195        } else if let Ok(other_dec) = other.extract::<Decimal>() {
196            (self.as_decimal() / other_dec).into_py_any(py)
197        } else {
198            let pytype_name = get_pytype_name(other)?;
199            Err(to_pytype_err(format!(
200                "Unsupported type for __truediv__, was `{pytype_name}`"
201            )))
202        }
203    }
204
205    fn __rtruediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
206        if other.is_instance_of::<PyFloat>() {
207            let other_float: f64 = other.extract()?;
208            (other_float / self.as_f64()).into_py_any(py)
209        } else if let Ok(other_price) = other.extract::<Self>() {
210            (other_price.as_decimal() / self.as_decimal()).into_py_any(py)
211        } else if let Ok(other_dec) = other.extract::<Decimal>() {
212            (other_dec / self.as_decimal()).into_py_any(py)
213        } else {
214            let pytype_name = get_pytype_name(other)?;
215            Err(to_pytype_err(format!(
216                "Unsupported type for __rtruediv__, was `{pytype_name}`"
217            )))
218        }
219    }
220
221    fn __floordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
222        if other.is_instance_of::<PyFloat>() {
223            let other_float: f64 = other.extract()?;
224            (self.as_f64() / other_float).floor().into_py_any(py)
225        } else if let Ok(other_price) = other.extract::<Self>() {
226            (self.as_decimal() / other_price.as_decimal())
227                .floor()
228                .into_py_any(py)
229        } else if let Ok(other_dec) = other.extract::<Decimal>() {
230            (self.as_decimal() / other_dec).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 __floordiv__, was `{pytype_name}`"
235            )))
236        }
237    }
238
239    fn __rfloordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
240        if other.is_instance_of::<PyFloat>() {
241            let other_float: f64 = other.extract()?;
242            (other_float / self.as_f64()).floor().into_py_any(py)
243        } else if let Ok(other_price) = other.extract::<Self>() {
244            (other_price.as_decimal() / self.as_decimal())
245                .floor()
246                .into_py_any(py)
247        } else if let Ok(other_dec) = other.extract::<Decimal>() {
248            (other_dec / self.as_decimal()).floor().into_py_any(py)
249        } else {
250            let pytype_name = get_pytype_name(other)?;
251            Err(to_pytype_err(format!(
252                "Unsupported type for __rfloordiv__, was `{pytype_name}`"
253            )))
254        }
255    }
256
257    fn __mod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
258        if other.is_instance_of::<PyFloat>() {
259            let other_float: f64 = other.extract()?;
260            (self.as_f64() % other_float).into_py_any(py)
261        } else if let Ok(other_price) = other.extract::<Self>() {
262            (self.as_decimal() % other_price.as_decimal()).into_py_any(py)
263        } else if let Ok(other_dec) = other.extract::<Decimal>() {
264            (self.as_decimal() % other_dec).into_py_any(py)
265        } else {
266            let pytype_name = get_pytype_name(other)?;
267            Err(to_pytype_err(format!(
268                "Unsupported type for __mod__, was `{pytype_name}`"
269            )))
270        }
271    }
272
273    fn __rmod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
274        if other.is_instance_of::<PyFloat>() {
275            let other_float: f64 = other.extract()?;
276            (other_float % self.as_f64()).into_py_any(py)
277        } else if let Ok(other_price) = other.extract::<Self>() {
278            (other_price.as_decimal() % self.as_decimal()).into_py_any(py)
279        } else if let Ok(other_dec) = other.extract::<Decimal>() {
280            (other_dec % self.as_decimal()).into_py_any(py)
281        } else {
282            let pytype_name = get_pytype_name(other)?;
283            Err(to_pytype_err(format!(
284                "Unsupported type for __rmod__, was `{pytype_name}`"
285            )))
286        }
287    }
288
289    fn __neg__(&self) -> Self {
290        -*self
291    }
292
293    fn __pos__(&self) -> Self {
294        *self
295    }
296
297    fn __abs__(&self) -> Self {
298        if self.raw < 0 { -*self } else { *self }
299    }
300
301    fn __int__(&self) -> i64 {
302        self.as_f64() as i64
303    }
304
305    fn __float__(&self) -> f64 {
306        self.as_f64()
307    }
308
309    #[pyo3(signature = (ndigits=None))]
310    fn __round__(&self, ndigits: Option<u32>) -> Decimal {
311        self.as_decimal()
312            .round_dp_with_strategy(ndigits.unwrap_or(0), RoundingStrategy::MidpointNearestEven)
313    }
314
315    fn __repr__(&self) -> String {
316        format!("{self:?}")
317    }
318
319    fn __str__(&self) -> String {
320        self.to_string()
321    }
322
323    #[getter]
324    fn raw(&self) -> PriceRaw {
325        self.raw
326    }
327
328    #[getter]
329    fn precision(&self) -> u8 {
330        self.precision
331    }
332
333    /// Creates a new `Price` instance from the given `raw` fixed-point value and `precision`.
334    #[staticmethod]
335    #[pyo3(name = "from_raw")]
336    fn py_from_raw(raw: PriceRaw, precision: u8) -> Self {
337        Self::from_raw(raw, precision)
338    }
339
340    /// Creates a new `Price` instance with a value of zero with the given `precision`.
341    #[staticmethod]
342    #[pyo3(name = "zero")]
343    #[pyo3(signature = (precision = 0))]
344    fn py_zero(precision: u8) -> PyResult<Self> {
345        Self::new_checked(0.0, precision).map_err(to_pyvalue_err)
346    }
347
348    #[staticmethod]
349    #[pyo3(name = "from_int")]
350    fn py_from_int(value: u64) -> PyResult<Self> {
351        Self::new_checked(value as f64, 0).map_err(to_pyvalue_err)
352    }
353
354    #[staticmethod]
355    #[pyo3(name = "from_str")]
356    fn py_from_str(value: &str) -> PyResult<Self> {
357        Self::from_str(value).map_err(to_pyvalue_err)
358    }
359
360    /// Creates a new `Price` from a `Decimal` value with precision inferred from the decimal's scale.
361    ///
362    /// The precision is determined by the scale of the decimal (number of decimal places).
363    /// The value is rounded to the inferred precision using banker's rounding (round half to even).
364    ///
365    /// # Errors
366    ///
367    /// Returns an error if:
368    /// - The inferred precision exceeds `FIXED_PRECISION`.
369    /// - The decimal value cannot be converted to the raw representation.
370    /// - Overflow occurs during scaling.
371    #[staticmethod]
372    #[pyo3(name = "from_decimal")]
373    fn py_from_decimal(decimal: Decimal) -> PyResult<Self> {
374        Self::from_decimal(decimal).map_err(to_pyvalue_err)
375    }
376
377    /// Creates a new `Price` from a `Decimal` value with specified precision.
378    ///
379    /// Uses pure integer arithmetic on the Decimal's mantissa and scale for fast conversion.
380    /// The value is rounded to the specified precision using banker's rounding (round half to even).
381    ///
382    /// # Errors
383    ///
384    /// Returns an error if:
385    /// - `precision` exceeds `FIXED_PRECISION`.
386    /// - The decimal value cannot be converted to the raw representation.
387    /// - Overflow occurs during scaling.
388    #[staticmethod]
389    #[pyo3(name = "from_decimal_dp")]
390    fn py_from_decimal_dp(decimal: Decimal, precision: u8) -> PyResult<Self> {
391        Self::from_decimal_dp(decimal, precision).map_err(to_pyvalue_err)
392    }
393
394    /// Creates a new `Price` from a mantissa/exponent pair using pure integer arithmetic.
395    ///
396    /// The value is `mantissa * 10^exponent`. This avoids all floating-point and Decimal
397    /// operations, making it ideal for exchange data that arrives as mantissa/exponent pairs.
398    #[staticmethod]
399    #[pyo3(name = "from_mantissa_exponent")]
400    fn py_from_mantissa_exponent(mantissa: i64, exponent: i8, precision: u8) -> Self {
401        Self::from_mantissa_exponent(mantissa, exponent, precision)
402    }
403
404    /// Returns `true` if the value of this instance is zero.
405    #[pyo3(name = "is_zero")]
406    fn py_is_zero(&self) -> bool {
407        self.is_zero()
408    }
409
410    /// Returns `true` if the value of this instance is position (> 0).
411    #[pyo3(name = "is_positive")]
412    fn py_is_positive(&self) -> bool {
413        self.is_positive()
414    }
415
416    /// Returns the value of this instance as a `Decimal`.
417    #[pyo3(name = "as_decimal")]
418    fn py_as_decimal(&self) -> Decimal {
419        self.as_decimal()
420    }
421
422    #[pyo3(name = "to_formatted_str")]
423    fn py_to_formatted_str(&self) -> String {
424        self.to_formatted_string()
425    }
426
427    /// Performs a checked addition, returning `None` on raw integer overflow, when the
428    /// result falls outside `[PRICE_RAW_MIN, PRICE_RAW_MAX]`, when either operand is a
429    /// sentinel (`PRICE_UNDEF`, `PRICE_ERROR`, or `ERROR_PRICE`), or when the operands
430    /// have mixed raw scales (one at `FIXED_PRECISION` scale, the other at a defi
431    /// `WEI_PRECISION` scale).
432    ///
433    /// Precision follows the `Add` implementation: uses the maximum precision of both operands.
434    #[pyo3(name = "checked_add")]
435    fn py_checked_add(&self, other: Self) -> Option<Self> {
436        self.checked_add(other)
437    }
438
439    /// Performs a checked subtraction, returning `None` on raw integer underflow, when
440    /// the result falls outside `[PRICE_RAW_MIN, PRICE_RAW_MAX]`, when either operand
441    /// is a sentinel (`PRICE_UNDEF`, `PRICE_ERROR`, or `ERROR_PRICE`), or when the
442    /// operands have mixed raw scales (one at `FIXED_PRECISION` scale, the other at a
443    /// defi `WEI_PRECISION` scale).
444    ///
445    /// Precision follows the `Sub` implementation: uses the maximum precision of both operands.
446    #[pyo3(name = "checked_sub")]
447    fn py_checked_sub(&self, other: Self) -> Option<Self> {
448        self.checked_sub(other)
449    }
450}
451
452#[pymethods]
453impl Price {
454    #[cfg(feature = "high-precision")]
455    #[pyo3(name = "as_double")]
456    fn py_as_double(&self) -> f64 {
457        fixed_i128_to_f64(self.raw)
458    }
459
460    #[cfg(not(feature = "high-precision"))]
461    #[pyo3(name = "as_double")]
462    fn py_as_double(&self) -> f64 {
463        fixed_i64_to_f64(self.raw)
464    }
465}