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::{
23    correctness_error_to_pyvalue_err, get_pytype_name, to_pytype_err, to_pyvalue_err,
24};
25use pyo3::{basic::CompareOp, conversion::IntoPyObjectExt, prelude::*, types::PyFloat};
26use rust_decimal::{Decimal, RoundingStrategy};
27
28use super::fixed::{
29    ArithmeticError, ArithmeticOperation, FloatArithmetic, check_raw_scales,
30    extract_arithmetic_decimal,
31};
32use crate::types::{
33    fixed::raw_scale,
34    price::{Price, PriceRaw},
35};
36
37#[pymethods]
38#[pyo3_stub_gen::derive::gen_stub_pymethods]
39impl Price {
40    /// Represents a price in a market with a specified precision.
41    ///
42    /// The number of decimal places may vary. For certain asset classes, prices may
43    /// have negative values. For example, prices for options instruments can be
44    /// negative under certain conditions.
45    ///
46    /// Handles up to `FIXED_PRECISION` decimals of precision.
47    ///
48    /// - `PRICE_MAX` - Maximum representable price value.
49    /// - `PRICE_MIN` - Minimum representable price value.
50    #[new]
51    fn py_new(value: f64, precision: u8) -> PyResult<Self> {
52        Self::new_checked(value, precision).map_err(to_pyvalue_err)
53    }
54
55    fn __reduce__(&self, py: Python) -> PyResult<Py<PyAny>> {
56        let from_raw = py.get_type::<Self>().getattr("from_raw")?;
57        let args = (self.raw(), self.precision).into_py_any(py)?;
58        (from_raw, args).into_py_any(py)
59    }
60
61    fn __richcmp__(
62        &self,
63        other: &Bound<'_, PyAny>,
64        op: CompareOp,
65        py: Python<'_>,
66    ) -> PyResult<Py<PyAny>> {
67        if let Ok(other_price) = other.extract::<Self>() {
68            let result = match op {
69                CompareOp::Eq => self.eq(&other_price),
70                CompareOp::Ne => self.ne(&other_price),
71                CompareOp::Ge => self.ge(&other_price),
72                CompareOp::Gt => self.gt(&other_price),
73                CompareOp::Le => self.le(&other_price),
74                CompareOp::Lt => self.lt(&other_price),
75            };
76            result.into_py_any(py)
77        } else if let Ok(other_dec) = other.extract::<Decimal>() {
78            let result = match op {
79                CompareOp::Eq => self.as_decimal() == other_dec,
80                CompareOp::Ne => self.as_decimal() != other_dec,
81                CompareOp::Ge => self.as_decimal() >= other_dec,
82                CompareOp::Gt => self.as_decimal() > other_dec,
83                CompareOp::Le => self.as_decimal() <= other_dec,
84                CompareOp::Lt => self.as_decimal() < other_dec,
85            };
86            result.into_py_any(py)
87        } else {
88            Ok(py.NotImplemented())
89        }
90    }
91
92    fn __hash__(&self) -> isize {
93        let mut h = DefaultHasher::new();
94        self.hash(&mut h);
95        h.finish() as isize
96    }
97
98    fn __add__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
99        if other.is_instance_of::<PyFloat>() {
100            let other_float: f64 = other.extract()?;
101            ArithmeticOperation::Add
102                .checked_f64(self.as_f64_checked()?, other_float)?
103                .into_py_any(py)
104        } else if let Ok(other_price) = other.extract::<Self>() {
105            check_raw_scales(self.precision, other_price.precision)?;
106            (*self)
107                .checked_add(other_price)
108                .ok_or(ArithmeticError::Overflow)?
109                .into_py_any(py)
110        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
111            ArithmeticOperation::Add
112                .checked_decimal(self.as_decimal(), other_dec)?
113                .into_py_any(py)
114        } else {
115            let pytype_name = get_pytype_name(other)?;
116            Err(to_pytype_err(format!(
117                "Unsupported type for __add__, was `{pytype_name}`"
118            )))
119        }
120    }
121
122    fn __radd__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
123        if other.is_instance_of::<PyFloat>() {
124            let other_float: f64 = other.extract()?;
125            ArithmeticOperation::Add
126                .checked_f64(other_float, self.as_f64_checked()?)?
127                .into_py_any(py)
128        } else if let Ok(other_price) = other.extract::<Self>() {
129            check_raw_scales(other_price.precision, self.precision)?;
130            other_price
131                .checked_add(*self)
132                .ok_or(ArithmeticError::Overflow)?
133                .into_py_any(py)
134        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
135            ArithmeticOperation::Add
136                .checked_decimal(other_dec, self.as_decimal())?
137                .into_py_any(py)
138        } else {
139            let pytype_name = get_pytype_name(other)?;
140            Err(to_pytype_err(format!(
141                "Unsupported type for __radd__, was `{pytype_name}`"
142            )))
143        }
144    }
145
146    fn __sub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
147        if other.is_instance_of::<PyFloat>() {
148            let other_float: f64 = other.extract()?;
149            ArithmeticOperation::Sub
150                .checked_f64(self.as_f64_checked()?, other_float)?
151                .into_py_any(py)
152        } else if let Ok(other_price) = other.extract::<Self>() {
153            check_raw_scales(self.precision, other_price.precision)?;
154            (*self)
155                .checked_sub(other_price)
156                .ok_or(ArithmeticError::Overflow)?
157                .into_py_any(py)
158        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
159            ArithmeticOperation::Sub
160                .checked_decimal(self.as_decimal(), other_dec)?
161                .into_py_any(py)
162        } else {
163            let pytype_name = get_pytype_name(other)?;
164            Err(to_pytype_err(format!(
165                "Unsupported type for __sub__, was `{pytype_name}`"
166            )))
167        }
168    }
169
170    fn __rsub__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
171        if other.is_instance_of::<PyFloat>() {
172            let other_float: f64 = other.extract()?;
173            ArithmeticOperation::Sub
174                .checked_f64(other_float, self.as_f64_checked()?)?
175                .into_py_any(py)
176        } else if let Ok(other_price) = other.extract::<Self>() {
177            check_raw_scales(other_price.precision, self.precision)?;
178            other_price
179                .checked_sub(*self)
180                .ok_or(ArithmeticError::Overflow)?
181                .into_py_any(py)
182        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
183            ArithmeticOperation::Sub
184                .checked_decimal(other_dec, self.as_decimal())?
185                .into_py_any(py)
186        } else {
187            let pytype_name = get_pytype_name(other)?;
188            Err(to_pytype_err(format!(
189                "Unsupported type for __rsub__, was `{pytype_name}`"
190            )))
191        }
192    }
193
194    fn __mul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
195        if other.is_instance_of::<PyFloat>() {
196            let other_float: f64 = other.extract()?;
197            ArithmeticOperation::Mul
198                .checked_f64(self.as_f64_checked()?, other_float)?
199                .into_py_any(py)
200        } else if let Ok(other_price) = other.extract::<Self>() {
201            ArithmeticOperation::Mul
202                .checked_decimal(self.as_decimal(), other_price.as_decimal())?
203                .into_py_any(py)
204        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
205            ArithmeticOperation::Mul
206                .checked_decimal(self.as_decimal(), other_dec)?
207                .into_py_any(py)
208        } else {
209            let pytype_name = get_pytype_name(other)?;
210            Err(to_pytype_err(format!(
211                "Unsupported type for __mul__, was `{pytype_name}`"
212            )))
213        }
214    }
215
216    fn __rmul__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
217        if other.is_instance_of::<PyFloat>() {
218            let other_float: f64 = other.extract()?;
219            ArithmeticOperation::Mul
220                .checked_f64(other_float, self.as_f64_checked()?)?
221                .into_py_any(py)
222        } else if let Ok(other_price) = other.extract::<Self>() {
223            ArithmeticOperation::Mul
224                .checked_decimal(other_price.as_decimal(), self.as_decimal())?
225                .into_py_any(py)
226        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
227            ArithmeticOperation::Mul
228                .checked_decimal(other_dec, self.as_decimal())?
229                .into_py_any(py)
230        } else {
231            let pytype_name = get_pytype_name(other)?;
232            Err(to_pytype_err(format!(
233                "Unsupported type for __rmul__, was `{pytype_name}`"
234            )))
235        }
236    }
237
238    fn __truediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
239        if other.is_instance_of::<PyFloat>() {
240            let other_float: f64 = other.extract()?;
241            ArithmeticOperation::Div
242                .checked_f64(self.as_f64_checked()?, other_float)?
243                .into_py_any(py)
244        } else if let Ok(other_price) = other.extract::<Self>() {
245            ArithmeticOperation::Div
246                .checked_decimal(self.as_decimal(), other_price.as_decimal())?
247                .into_py_any(py)
248        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
249            ArithmeticOperation::Div
250                .checked_decimal(self.as_decimal(), other_dec)?
251                .into_py_any(py)
252        } else {
253            let pytype_name = get_pytype_name(other)?;
254            Err(to_pytype_err(format!(
255                "Unsupported type for __truediv__, was `{pytype_name}`"
256            )))
257        }
258    }
259
260    fn __rtruediv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
261        if other.is_instance_of::<PyFloat>() {
262            let other_float: f64 = other.extract()?;
263            ArithmeticOperation::Div
264                .checked_f64(other_float, self.as_f64_checked()?)?
265                .into_py_any(py)
266        } else if let Ok(other_price) = other.extract::<Self>() {
267            ArithmeticOperation::Div
268                .checked_decimal(other_price.as_decimal(), self.as_decimal())?
269                .into_py_any(py)
270        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
271            ArithmeticOperation::Div
272                .checked_decimal(other_dec, self.as_decimal())?
273                .into_py_any(py)
274        } else {
275            let pytype_name = get_pytype_name(other)?;
276            Err(to_pytype_err(format!(
277                "Unsupported type for __rtruediv__, was `{pytype_name}`"
278            )))
279        }
280    }
281
282    fn __floordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
283        if other.is_instance_of::<PyFloat>() {
284            let other_float: f64 = other.extract()?;
285            ArithmeticOperation::Div
286                .checked_f64(self.as_f64_checked()?, other_float)?
287                .floor()
288                .into_py_any(py)
289        } else if let Ok(other_price) = other.extract::<Self>() {
290            ArithmeticOperation::Div
291                .checked_decimal(self.as_decimal(), other_price.as_decimal())?
292                .floor()
293                .into_py_any(py)
294        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
295            ArithmeticOperation::Div
296                .checked_decimal(self.as_decimal(), other_dec)?
297                .floor()
298                .into_py_any(py)
299        } else {
300            let pytype_name = get_pytype_name(other)?;
301            Err(to_pytype_err(format!(
302                "Unsupported type for __floordiv__, was `{pytype_name}`"
303            )))
304        }
305    }
306
307    fn __rfloordiv__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
308        if other.is_instance_of::<PyFloat>() {
309            let other_float: f64 = other.extract()?;
310            ArithmeticOperation::Div
311                .checked_f64(other_float, self.as_f64_checked()?)?
312                .floor()
313                .into_py_any(py)
314        } else if let Ok(other_price) = other.extract::<Self>() {
315            ArithmeticOperation::Div
316                .checked_decimal(other_price.as_decimal(), self.as_decimal())?
317                .floor()
318                .into_py_any(py)
319        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
320            ArithmeticOperation::Div
321                .checked_decimal(other_dec, self.as_decimal())?
322                .floor()
323                .into_py_any(py)
324        } else {
325            let pytype_name = get_pytype_name(other)?;
326            Err(to_pytype_err(format!(
327                "Unsupported type for __rfloordiv__, was `{pytype_name}`"
328            )))
329        }
330    }
331
332    fn __mod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
333        if other.is_instance_of::<PyFloat>() {
334            let other_float: f64 = other.extract()?;
335            ArithmeticOperation::Rem
336                .checked_f64(self.as_f64_checked()?, other_float)?
337                .into_py_any(py)
338        } else if let Ok(other_price) = other.extract::<Self>() {
339            ArithmeticOperation::Rem
340                .checked_decimal(self.as_decimal(), other_price.as_decimal())?
341                .into_py_any(py)
342        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
343            ArithmeticOperation::Rem
344                .checked_decimal(self.as_decimal(), other_dec)?
345                .into_py_any(py)
346        } else {
347            let pytype_name = get_pytype_name(other)?;
348            Err(to_pytype_err(format!(
349                "Unsupported type for __mod__, was `{pytype_name}`"
350            )))
351        }
352    }
353
354    fn __rmod__(&self, other: &Bound<'_, PyAny>, py: Python) -> PyResult<Py<PyAny>> {
355        if other.is_instance_of::<PyFloat>() {
356            let other_float: f64 = other.extract()?;
357            ArithmeticOperation::Rem
358                .checked_f64(other_float, self.as_f64_checked()?)?
359                .into_py_any(py)
360        } else if let Ok(other_price) = other.extract::<Self>() {
361            ArithmeticOperation::Rem
362                .checked_decimal(other_price.as_decimal(), self.as_decimal())?
363                .into_py_any(py)
364        } else if let Some(other_dec) = extract_arithmetic_decimal(other) {
365            ArithmeticOperation::Rem
366                .checked_decimal(other_dec, self.as_decimal())?
367                .into_py_any(py)
368        } else {
369            let pytype_name = get_pytype_name(other)?;
370            Err(to_pytype_err(format!(
371                "Unsupported type for __rmod__, was `{pytype_name}`"
372            )))
373        }
374    }
375
376    fn __neg__(&self) -> Self {
377        -*self
378    }
379
380    fn __pos__(&self) -> Self {
381        *self
382    }
383
384    fn __abs__(&self) -> Self {
385        if self.is_negative() { -*self } else { *self }
386    }
387
388    fn __int__(&self) -> PriceRaw {
389        let scale = PriceRaw::try_from(raw_scale(self.precision))
390            .expect("effective raw scale should fit in PriceRaw");
391        self.raw() / scale
392    }
393
394    fn __float__(&self) -> PyResult<f64> {
395        self.as_f64_checked()
396    }
397
398    #[pyo3(signature = (ndigits=None))]
399    fn __round__(&self, ndigits: Option<u32>) -> Decimal {
400        self.as_decimal()
401            .round_dp_with_strategy(ndigits.unwrap_or(0), RoundingStrategy::MidpointNearestEven)
402    }
403
404    fn __repr__(&self) -> String {
405        format!("{self:?}")
406    }
407
408    fn __str__(&self) -> String {
409        self.to_string()
410    }
411
412    /// Returns the stored fixed-point integer without rescaling.
413    ///
414    /// Use this for serialization and explicit fixed-point conversions. Prefer domain
415    /// operations for calculations; the storage scale can differ from display precision.
416    ///
417    /// Direct field access is restricted to this crate:
418    ///
419    /// ```compile_fail
420    /// use nautilus_model::types::Price;
421    /// let value = Price::from("1");
422    /// let raw = value.raw;
423    /// ```
424    #[getter(raw)]
425    fn py_raw(&self) -> PriceRaw {
426        self.raw()
427    }
428
429    #[getter]
430    fn precision(&self) -> u8 {
431        self.precision
432    }
433
434    /// Creates a new `Price` instance from the given `raw` fixed-point value and `precision`.
435    #[staticmethod]
436    #[pyo3(name = "from_raw")]
437    fn py_from_raw(raw: PriceRaw, precision: u8) -> PyResult<Self> {
438        Self::from_raw_checked(raw, precision).map_err(correctness_error_to_pyvalue_err)
439    }
440
441    /// Creates a new `Price` instance with a value of zero with the given `precision`.
442    #[staticmethod]
443    #[pyo3(name = "zero")]
444    #[pyo3(signature = (precision = 0))]
445    fn py_zero(precision: u8) -> PyResult<Self> {
446        Self::new_checked(0.0, precision).map_err(to_pyvalue_err)
447    }
448
449    #[staticmethod]
450    #[pyo3(name = "from_int")]
451    fn py_from_int(value: u64) -> PyResult<Self> {
452        Self::new_checked(value as f64, 0).map_err(to_pyvalue_err)
453    }
454
455    #[staticmethod]
456    #[pyo3(name = "from_str")]
457    fn py_from_str(value: &str) -> PyResult<Self> {
458        Self::from_str(value).map_err(to_pyvalue_err)
459    }
460
461    /// Creates a new `Price` from a `Decimal` value with precision inferred from the decimal's scale.
462    ///
463    /// The precision is determined by the scale of the decimal (number of decimal places).
464    /// The value is rounded to the inferred precision using banker's rounding (round half to even).
465    ///
466    /// # Errors
467    ///
468    /// Returns an error if:
469    /// - The inferred precision exceeds `FIXED_PRECISION`.
470    /// - The decimal value cannot be converted to the raw representation.
471    /// - Overflow occurs during scaling.
472    #[staticmethod]
473    #[pyo3(name = "from_decimal")]
474    fn py_from_decimal(decimal: Decimal) -> PyResult<Self> {
475        Self::from_decimal(decimal).map_err(to_pyvalue_err)
476    }
477
478    /// Creates a new `Price` from a `Decimal` value with specified precision.
479    ///
480    /// Uses pure integer arithmetic on the Decimal's mantissa and scale for fast conversion.
481    /// The value is rounded to the specified precision using banker's rounding (round half to even).
482    ///
483    /// # Errors
484    ///
485    /// Returns an error if:
486    /// - `precision` exceeds `FIXED_PRECISION`.
487    /// - The decimal value cannot be converted to the raw representation.
488    /// - Overflow occurs during scaling.
489    #[staticmethod]
490    #[pyo3(name = "from_decimal_dp")]
491    fn py_from_decimal_dp(decimal: Decimal, precision: u8) -> PyResult<Self> {
492        Self::from_decimal_dp(decimal, precision).map_err(to_pyvalue_err)
493    }
494
495    /// Creates a new `Price` from a mantissa/exponent pair using pure integer arithmetic.
496    ///
497    /// The value is `mantissa * 10^exponent`. This avoids all floating-point and Decimal
498    /// operations, making it ideal for exchange data that arrives as mantissa/exponent pairs.
499    #[staticmethod]
500    #[pyo3(name = "from_mantissa_exponent")]
501    fn py_from_mantissa_exponent(mantissa: i64, exponent: i8, precision: u8) -> PyResult<Self> {
502        Self::from_mantissa_exponent_checked(mantissa, exponent, precision)
503            .map_err(correctness_error_to_pyvalue_err)
504    }
505
506    /// Returns `true` if the value of this instance is zero.
507    #[pyo3(name = "is_zero")]
508    fn py_is_zero(&self) -> bool {
509        self.is_zero()
510    }
511
512    /// Returns `true` if the value of this instance is position (> 0).
513    #[pyo3(name = "is_positive")]
514    fn py_is_positive(&self) -> bool {
515        self.is_positive()
516    }
517
518    /// Returns the value of this instance as a `Decimal`.
519    #[pyo3(name = "as_decimal")]
520    fn py_as_decimal(&self) -> Decimal {
521        self.as_decimal()
522    }
523
524    #[pyo3(name = "to_formatted_str")]
525    fn py_to_formatted_str(&self) -> String {
526        self.to_formatted_string()
527    }
528
529    /// Performs a checked addition, returning `None` on raw integer overflow, when the
530    /// result falls outside `[PRICE_RAW_MIN, PRICE_RAW_MAX]`, when either operand is a
531    /// sentinel (`PRICE_UNDEF`, `PRICE_ERROR`, or `ERROR_PRICE`), or when the operands
532    /// have mixed raw scales (one at `FIXED_PRECISION` scale, the other at a defi
533    /// `WEI_PRECISION` scale).
534    ///
535    /// Precision follows the `Add` implementation: uses the maximum precision of both operands.
536    #[pyo3(name = "checked_add")]
537    fn py_checked_add(&self, other: Self) -> Option<Self> {
538        self.checked_add(other)
539    }
540
541    /// Performs a checked subtraction, returning `None` on raw integer underflow, when
542    /// the result falls outside `[PRICE_RAW_MIN, PRICE_RAW_MAX]`, when either operand
543    /// is a sentinel (`PRICE_UNDEF`, `PRICE_ERROR`, or `ERROR_PRICE`), or when the
544    /// operands have mixed raw scales (one at `FIXED_PRECISION` scale, the other at a
545    /// defi `WEI_PRECISION` scale).
546    ///
547    /// Precision follows the `Sub` implementation: uses the maximum precision of both operands.
548    #[pyo3(name = "checked_sub")]
549    fn py_checked_sub(&self, other: Self) -> Option<Self> {
550        self.checked_sub(other)
551    }
552
553    #[pyo3(name = "as_double")]
554    fn py_as_double(&self) -> PyResult<f64> {
555        self.as_f64_checked()
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use pyo3::Python;
562    use rstest::rstest;
563
564    use super::*;
565    use crate::types::{
566        fixed::FIXED_PRECISION,
567        price::{PRICE_RAW_MAX, PRICE_RAW_MIN},
568    };
569
570    #[rstest]
571    #[case("0", 0)]
572    #[case("0.000000001", 0)]
573    #[case("-0.000000001", 0)]
574    #[case("1.999999999", 1)]
575    #[case("-1.999999999", -1)]
576    #[case("50.25", 50)]
577    #[case("9007199253.999999999", 9_007_199_253)]
578    fn test_int_uses_exact_raw_value(#[case] value: &str, #[case] expected: i64) {
579        let price = Price::from_str(value).unwrap();
580
581        assert_eq!(price.__int__(), PriceRaw::from(expected));
582    }
583
584    #[rstest]
585    fn test_int_preserves_domain_boundaries() {
586        let expected_max: PriceRaw = if cfg!(feature = "high-precision") {
587            17_014_118_346_046
588        } else {
589            9_223_372_036
590        };
591        let expected_min = -expected_max;
592
593        assert_eq!(
594            Price::from_raw(PRICE_RAW_MAX, FIXED_PRECISION).__int__(),
595            expected_max
596        );
597        assert_eq!(
598            Price::from_raw(PRICE_RAW_MIN, FIXED_PRECISION).__int__(),
599            expected_min
600        );
601    }
602
603    #[rstest]
604    #[cfg(feature = "defi")]
605    fn test_int_uses_defi_raw_scale() {
606        let price = Price::from_raw(1_999_999_999_999_999_999, crate::defi::WEI_PRECISION);
607
608        assert_eq!(price.__int__(), 1);
609    }
610
611    #[rstest]
612    fn test_py_from_raw_rejects_out_of_range_raw_value() {
613        Python::initialize();
614        Python::attach(|_| {
615            let raw = PRICE_RAW_MAX.saturating_add(1);
616            let error = Price::py_from_raw(raw, 0).unwrap_err();
617
618            assert_eq!(
619                error.to_string(),
620                format!(
621                    "ValueError: raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
622                )
623            );
624        });
625    }
626
627    #[rstest]
628    fn test_py_from_mantissa_exponent_handles_precision_and_overflow() {
629        Python::initialize();
630        Python::attach(|_| {
631            #[cfg(feature = "defi")]
632            let max_precision = crate::defi::WEI_PRECISION;
633            #[cfg(not(feature = "defi"))]
634            let max_precision = FIXED_PRECISION;
635
636            let exponent = -i8::try_from(max_precision).unwrap();
637            let price = Price::py_from_mantissa_exponent(1, exponent, max_precision).unwrap();
638            let invalid_precision = max_precision + 1;
639            let precision_error =
640                Price::py_from_mantissa_exponent(1, 0, invalid_precision).unwrap_err();
641            let overflow_error = Price::py_from_mantissa_exponent(i64::MAX, 100, 0).unwrap_err();
642            let precision_name = if cfg!(feature = "defi") {
643                "WEI_PRECISION"
644            } else {
645                "FIXED_PRECISION"
646            };
647
648            assert_eq!(price.raw(), 1);
649            assert_eq!(price.precision, max_precision);
650            assert_eq!(
651                precision_error.to_string(),
652                format!(
653                    "ValueError: `precision` exceeded maximum `{precision_name}` ({max_precision}), was {invalid_precision}"
654                )
655            );
656            assert_eq!(
657                overflow_error.to_string(),
658                "ValueError: Overflow in Price::from_mantissa_exponent \
659                 (mantissa=9223372036854775807, exponent=100, precision=0)"
660            );
661        });
662    }
663}