Skip to main content

nautilus_model/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
16//! Represents a price in a market with a specified precision.
17//!
18//! [`Price`] is an immutable value type for representing market prices, bid/ask quotes,
19//! and price levels. Unlike [`Quantity`](super::Quantity), prices can be negative (useful for spreads,
20//! basis trades, or certain derivative instruments).
21//!
22//! # Arithmetic behavior
23//!
24//! Adding or subtracting two `Price` values requires matching effective fixed-point scales.
25//! These operations panic on a scale mismatch.
26//! Comparisons and hashes account for scale differences without rounding.
27//! Without the `defi` feature, constructors restrict values to a single storage scale.
28//!
29//! | Operation         | Result    | Notes                              |
30//! |-------------------|-----------|------------------------------------|
31//! | `Price + Price`   | `Price`   | Precision is max of both operands. |
32//! | `Price - Price`   | `Price`   | Precision is max of both operands. |
33//! | `Price + Decimal` | `Decimal` |                                    |
34//! | `Price - Decimal` | `Decimal` |                                    |
35//! | `Price * Decimal` | `Decimal` |                                    |
36//! | `Price / Decimal` | `Decimal` |                                    |
37//! | `Price + f64`     | `f64`     |                                    |
38//! | `Price - f64`     | `f64`     |                                    |
39//! | `Price * f64`     | `f64`     |                                    |
40//! | `Price / f64`     | `f64`     |                                    |
41//! | `-Price`          | `Price`   |                                    |
42//!
43//! # Immutability
44//!
45//! `Price` is immutable. All arithmetic operations return new instances.
46
47use std::{
48    cmp::Ordering,
49    fmt::{Debug, Display},
50    hash::{Hash, Hasher},
51    ops::{Add, Deref, Div, Mul, Neg, Sub},
52    str::FromStr,
53};
54
55use nautilus_core::{
56    correctness::{
57        CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
58        check_in_range_inclusive_f64,
59    },
60    string::formatting::Separable,
61};
62use rust_decimal::Decimal;
63use serde::{Deserialize, Deserializer, Serialize};
64
65#[cfg(feature = "defi")]
66use super::fixed::compare_raw_signed;
67use super::fixed::{
68    FIXED_PRECISION, FIXED_SCALAR, canonical_raw, check_fixed_precision, format_scaled_i128,
69    mantissa_exponent_to_fixed_i128, mantissa_exponent_to_raw_checked, parse_decimal_mantissa,
70    raw_scales_match, remove_underscores, scaled_raw_to_decimal,
71};
72#[cfg(feature = "high-precision")]
73use super::fixed::{PRECISION_DIFF_SCALAR, f64_to_fixed_i128, fixed_i128_to_f64};
74#[cfg(not(feature = "high-precision"))]
75use super::fixed::{f64_to_fixed_i64, fixed_i64_to_f64};
76use crate::deserialization::StringInput;
77#[cfg(feature = "defi")]
78use crate::types::fixed::MAX_FLOAT_PRECISION;
79
80// -----------------------------------------------------------------------------
81// PriceRaw
82// -----------------------------------------------------------------------------
83
84// Use 128-bit integers when either `high-precision` or `defi` features are enabled. This is
85// required for the extended 18-decimal wei precision used in DeFi contexts.
86
87#[cfg(feature = "high-precision")]
88pub type PriceRaw = i128;
89
90#[cfg(not(feature = "high-precision"))]
91pub type PriceRaw = i64;
92
93// -----------------------------------------------------------------------------
94
95/// The maximum raw price integer value.
96///
97/// # Safety
98///
99/// `PRICE_MAX` and `FIXED_SCALAR` are cast to `PriceRaw` before multiplying, so the
100/// scaling uses exact integer arithmetic rather than a lossy `f64` product. The result
101/// fits within `PriceRaw`'s range in both high-precision (i128) and standard-precision
102/// (i64) modes, so the multiplication cannot overflow.
103#[unsafe(no_mangle)]
104#[allow(unsafe_code)]
105pub static PRICE_RAW_MAX: PriceRaw = (PRICE_MAX as PriceRaw) * (FIXED_SCALAR as PriceRaw);
106
107/// The minimum raw price integer value.
108///
109/// # Safety
110///
111/// `PRICE_MIN` and `FIXED_SCALAR` are cast to `PriceRaw` before multiplying, so the
112/// scaling uses exact integer arithmetic rather than a lossy `f64` product. The result
113/// fits within `PriceRaw`'s range in both high-precision (i128) and standard-precision
114/// (i64) modes, so the multiplication cannot overflow.
115#[unsafe(no_mangle)]
116#[allow(unsafe_code)]
117pub static PRICE_RAW_MIN: PriceRaw = (PRICE_MIN as PriceRaw) * (FIXED_SCALAR as PriceRaw);
118
119/// The sentinel value for an unset or null price.
120pub const PRICE_UNDEF: PriceRaw = PriceRaw::MAX;
121
122/// The sentinel value for an error or invalid price.
123pub const PRICE_ERROR: PriceRaw = PriceRaw::MIN;
124
125// -----------------------------------------------------------------------------
126// PRICE_MAX
127// -----------------------------------------------------------------------------
128
129/// The maximum valid price value that can be represented.
130#[cfg(feature = "high-precision")]
131pub const PRICE_MAX: f64 = 17_014_118_346_046.0;
132
133#[cfg(not(feature = "high-precision"))]
134/// The maximum valid price value that can be represented.
135pub const PRICE_MAX: f64 = 9_223_372_036.0;
136
137// -----------------------------------------------------------------------------
138// PRICE_MIN
139// -----------------------------------------------------------------------------
140
141#[cfg(feature = "high-precision")]
142/// The minimum valid price value that can be represented.
143pub const PRICE_MIN: f64 = -17_014_118_346_046.0;
144
145#[cfg(not(feature = "high-precision"))]
146/// The minimum valid price value that can be represented.
147pub const PRICE_MIN: f64 = -9_223_372_036.0;
148
149// -----------------------------------------------------------------------------
150
151/// The sentinel `Price` representing an error, returned by C FFI functions that
152/// cannot signal errors through `Option` or `Result`.
153pub const ERROR_PRICE: Price = Price {
154    raw: 0,
155    precision: 255,
156};
157
158/// Represents a price in a market with a specified precision.
159///
160/// The number of decimal places may vary. For certain asset classes, prices may
161/// have negative values. For example, prices for options instruments can be
162/// negative under certain conditions.
163///
164/// Handles up to [`FIXED_PRECISION`] decimals of precision.
165///
166/// - [`PRICE_MAX`] - Maximum representable price value.
167/// - [`PRICE_MIN`] - Minimum representable price value.
168#[repr(C)]
169#[derive(Clone, Copy, Default, Eq)]
170#[cfg_attr(
171    feature = "python",
172    pyo3::pyclass(module = "nautilus_trader.model", frozen, from_py_object)
173)]
174#[cfg_attr(
175    feature = "python",
176    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
177)]
178pub struct Price {
179    pub(crate) raw: PriceRaw,
180    /// The number of decimal places, with a maximum of [`FIXED_PRECISION`].
181    pub precision: u8,
182}
183
184impl Price {
185    /// Creates a new [`Price`] instance with correctness checking.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if:
190    /// - `value` is invalid outside the representable range [`PRICE_MIN`, `PRICE_MAX`].
191    /// - `precision` is invalid outside the representable range [0, `FIXED_PRECISION`].
192    ///
193    /// # Notes
194    ///
195    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
196    pub fn new_checked(value: f64, precision: u8) -> CorrectnessResult<Self> {
197        check_in_range_inclusive_f64(value, PRICE_MIN, PRICE_MAX, "value")?;
198
199        #[cfg(feature = "defi")]
200        if precision > MAX_FLOAT_PRECISION {
201            // Floats are only reliable up to ~16 decimal digits of precision regardless of feature flags
202            return Err(CorrectnessError::PredicateViolation {
203                message: format!(
204                    "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Price::from_wei()` for wei values instead"
205                ),
206            });
207        }
208
209        check_fixed_precision(precision)?;
210
211        #[cfg(feature = "high-precision")]
212        let raw = f64_to_fixed_i128(value, precision);
213
214        #[cfg(not(feature = "high-precision"))]
215        let raw = f64_to_fixed_i64(value, precision);
216
217        Ok(Self { raw, precision })
218    }
219
220    /// Creates a new [`Price`] instance.
221    ///
222    /// # Panics
223    ///
224    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
225    #[must_use]
226    pub fn new(value: f64, precision: u8) -> Self {
227        Self::new_checked(value, precision).expect_display(FAILED)
228    }
229
230    /// Creates a new [`Price`] instance from the given `raw` fixed-point value and `precision`.
231    ///
232    /// # Panics
233    ///
234    /// Panics if `raw` is outside the valid range and is not a sentinel value.
235    /// Panics if `precision` exceeds [`FIXED_PRECISION`].
236    #[must_use]
237    pub fn from_raw(raw: PriceRaw, precision: u8) -> Self {
238        assert!(
239            raw == PRICE_ERROR
240                || raw == PRICE_UNDEF
241                || (raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX),
242            "`raw` value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
243        );
244
245        if raw == PRICE_UNDEF {
246            assert!(
247                precision == 0,
248                "`precision` must be 0 when `raw` is PRICE_UNDEF"
249            );
250        }
251        check_fixed_precision(precision).expect_display(FAILED);
252
253        // TODO: Enforce spurious bits validation in v2
254        // if !matches!(raw, PRICE_UNDEF | PRICE_ERROR) && raw != 0 {
255        //     #[cfg(feature = "high-precision")]
256        //     super::fixed::check_fixed_raw_i128(raw, precision).expect(FAILED);
257        //     #[cfg(not(feature = "high-precision"))]
258        //     super::fixed::check_fixed_raw_i64(raw, precision).expect(FAILED);
259        // }
260
261        Self { raw, precision }
262    }
263
264    /// Creates a new [`Price`] instance from the given `raw` fixed-point value and `precision`
265    /// with correctness checking.
266    ///
267    /// # Errors
268    ///
269    /// Returns an error if:
270    /// - `precision` exceeds the maximum fixed precision.
271    /// - `precision` is not 0 when `raw` is `PRICE_UNDEF`.
272    /// - `raw` is outside the valid range `[PRICE_RAW_MIN, PRICE_RAW_MAX]`
273    ///   and is not a sentinel value.
274    pub fn from_raw_checked(raw: PriceRaw, precision: u8) -> CorrectnessResult<Self> {
275        if raw == PRICE_UNDEF && precision != 0 {
276            return Err(CorrectnessError::PredicateViolation {
277                message: "`precision` must be 0 when `raw` is PRICE_UNDEF".to_string(),
278            });
279        }
280
281        if raw != PRICE_ERROR && raw != PRICE_UNDEF && (raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX)
282        {
283            return Err(CorrectnessError::PredicateViolation {
284                message: format!(
285                    "raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
286                ),
287            });
288        }
289
290        check_fixed_precision(precision)?;
291
292        Ok(Self { raw, precision })
293    }
294
295    /// Creates a new [`Price`] instance with a value of zero with the given `precision`.
296    ///
297    /// # Panics
298    ///
299    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
300    #[must_use]
301    pub fn zero(precision: u8) -> Self {
302        check_fixed_precision(precision).expect_display(FAILED);
303        Self { raw: 0, precision }
304    }
305
306    /// Creates a new [`Price`] instance with the maximum representable value with the given `precision`.
307    ///
308    /// # Panics
309    ///
310    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
311    #[must_use]
312    pub fn max(precision: u8) -> Self {
313        check_fixed_precision(precision).expect_display(FAILED);
314        Self {
315            raw: PRICE_RAW_MAX,
316            precision,
317        }
318    }
319
320    /// Creates a new [`Price`] instance with the minimum representable value with the given `precision`.
321    ///
322    /// # Panics
323    ///
324    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
325    #[must_use]
326    pub fn min(precision: u8) -> Self {
327        check_fixed_precision(precision).expect_display(FAILED);
328        Self {
329            raw: PRICE_RAW_MIN,
330            precision,
331        }
332    }
333
334    /// Performs a checked addition, returning `None` on raw integer overflow, when the
335    /// result falls outside `[PRICE_RAW_MIN, PRICE_RAW_MAX]`, when either operand is a
336    /// sentinel (`PRICE_UNDEF`, `PRICE_ERROR`, or `ERROR_PRICE`), or when the operands
337    /// have mixed raw scales (one at `FIXED_PRECISION` scale, the other at a defi
338    /// `WEI_PRECISION` scale).
339    ///
340    /// Precision follows the `Add` implementation: uses the maximum precision of both operands.
341    #[must_use]
342    pub fn checked_add(self, rhs: Self) -> Option<Self> {
343        if self.is_sentinel() || rhs.is_sentinel() {
344            return None;
345        }
346
347        if !raw_scales_match(self.precision, rhs.precision) {
348            return None;
349        }
350
351        let raw = self.raw.checked_add(rhs.raw)?;
352        if raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX {
353            return None;
354        }
355
356        Some(Self {
357            raw,
358            precision: self.precision.max(rhs.precision),
359        })
360    }
361
362    /// Performs a checked subtraction, returning `None` on raw integer underflow, when
363    /// the result falls outside `[PRICE_RAW_MIN, PRICE_RAW_MAX]`, when either operand
364    /// is a sentinel (`PRICE_UNDEF`, `PRICE_ERROR`, or `ERROR_PRICE`), or when the
365    /// operands have mixed raw scales (one at `FIXED_PRECISION` scale, the other at a
366    /// defi `WEI_PRECISION` scale).
367    ///
368    /// Precision follows the `Sub` implementation: uses the maximum precision of both operands.
369    #[must_use]
370    pub fn checked_sub(self, rhs: Self) -> Option<Self> {
371        if self.is_sentinel() || rhs.is_sentinel() {
372            return None;
373        }
374
375        if !raw_scales_match(self.precision, rhs.precision) {
376            return None;
377        }
378
379        let raw = self.raw.checked_sub(rhs.raw)?;
380        if raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX {
381            return None;
382        }
383
384        Some(Self {
385            raw,
386            precision: self.precision.max(rhs.precision),
387        })
388    }
389
390    #[inline]
391    fn is_sentinel(self) -> bool {
392        // ERROR_PRICE uses precision == u8::MAX as its sentinel marker, distinct from
393        // valid high-precision values (e.g. defi `from_wei` uses precision 18 which is
394        // > FIXED_PRECISION but is not a sentinel).
395        self.raw == PRICE_UNDEF || self.raw == PRICE_ERROR || self.precision == u8::MAX
396    }
397
398    /// Returns `true` if the value of this instance is undefined.
399    #[must_use]
400    pub fn is_undefined(&self) -> bool {
401        self.raw == PRICE_UNDEF
402    }
403
404    /// Returns `true` if the value of this instance is the error sentinel.
405    #[must_use]
406    #[inline]
407    pub fn is_error(&self) -> bool {
408        self.raw == PRICE_ERROR
409    }
410
411    /// Returns the stored fixed-point integer without rescaling.
412    ///
413    /// Use this for serialization and explicit fixed-point conversions. Prefer domain
414    /// operations for calculations; the storage scale can differ from display precision.
415    ///
416    /// Direct field access is restricted to this crate:
417    ///
418    /// ```compile_fail
419    /// use nautilus_model::types::Price;
420    /// let value = Price::from("1");
421    /// let raw = value.raw;
422    /// ```
423    #[must_use]
424    #[inline]
425    pub const fn raw(&self) -> PriceRaw {
426        self.raw
427    }
428
429    /// Returns `true` if the value of this instance is zero.
430    #[must_use]
431    #[inline]
432    pub fn is_zero(&self) -> bool {
433        self.raw == 0
434    }
435
436    /// Returns `true` if the value of this instance is position (> 0).
437    #[must_use]
438    #[inline]
439    pub fn is_positive(&self) -> bool {
440        self.raw != PRICE_UNDEF && self.raw > 0
441    }
442
443    /// Returns `true` if the value of this instance is negative (< 0).
444    #[must_use]
445    #[inline]
446    pub fn is_negative(&self) -> bool {
447        self.raw != PRICE_UNDEF && self.raw < 0
448    }
449
450    #[cfg(feature = "high-precision")]
451    /// Returns the value of this instance as an `f64`.
452    ///
453    /// # Panics
454    ///
455    /// Panics if the `defi` feature is enabled and precision exceeds `MAX_FLOAT_PRECISION` (16).
456    #[must_use]
457    pub fn as_f64(&self) -> f64 {
458        #[cfg(feature = "defi")]
459        assert!(
460            self.precision <= MAX_FLOAT_PRECISION,
461            "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
462        );
463
464        fixed_i128_to_f64(self.raw)
465    }
466
467    #[cfg(not(feature = "high-precision"))]
468    /// Returns the value of this instance as an `f64`.
469    #[must_use]
470    pub fn as_f64(&self) -> f64 {
471        fixed_i64_to_f64(self.raw)
472    }
473
474    /// Returns the value of this instance as a `Decimal`.
475    #[must_use]
476    pub fn as_decimal(&self) -> Decimal {
477        // Scale down the raw value to match the precision
478        let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
479        let rescaled_raw = self.raw / PriceRaw::pow(10, u32::from(precision_diff));
480        #[allow(
481            clippy::unnecessary_cast,
482            clippy::cast_lossless,
483            reason = "cast is real when PriceRaw is i64, no-op when i128"
484        )]
485        scaled_raw_to_decimal(rescaled_raw as i128, self.precision)
486    }
487
488    /// Returns a formatted string representation of this instance.
489    #[must_use]
490    pub fn to_formatted_string(&self) -> String {
491        format!("{self}").separate_with_underscores()
492    }
493
494    fn raw_at_precision(&self) -> i128 {
495        let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
496        let rescaled_raw = self.raw / PriceRaw::pow(10, u32::from(precision_diff));
497        Self::raw_as_i128(rescaled_raw)
498    }
499
500    fn raw_as_i128(raw: PriceRaw) -> i128 {
501        #[allow(
502            clippy::useless_conversion,
503            reason = "i128::from is a widening conversion when PriceRaw is i64"
504        )]
505        i128::from(raw)
506    }
507
508    /// Creates a new [`Price`] from a `Decimal` value with specified precision.
509    ///
510    /// Uses pure integer arithmetic on the Decimal's mantissa and scale for fast conversion.
511    /// The value is rounded to the specified precision using banker's rounding (round half to even).
512    ///
513    /// # Errors
514    ///
515    /// Returns an error if:
516    /// - `precision` exceeds [`FIXED_PRECISION`].
517    /// - The decimal value cannot be converted to the raw representation.
518    /// - Overflow occurs during scaling.
519    pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> CorrectnessResult<Self> {
520        let exponent = -(decimal.scale() as i8);
521        let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
522
523        #[allow(
524            clippy::useless_conversion,
525            reason = "i128 to PriceRaw is real when not high-precision"
526        )]
527        let raw: PriceRaw =
528            raw_i128
529                .try_into()
530                .map_err(|_| CorrectnessError::PredicateViolation {
531                    message: format!(
532                        "Decimal value exceeds PriceRaw range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
533                    ),
534                })?;
535
536        if !(raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX) {
537            return Err(CorrectnessError::PredicateViolation {
538                message: format!(
539                    "Raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
540                ),
541            });
542        }
543
544        Ok(Self { raw, precision })
545    }
546
547    /// Creates a new [`Price`] from a [`Decimal`] value with precision inferred from the decimal's scale.
548    ///
549    /// The precision is determined by the scale of the decimal (number of decimal places).
550    /// The value is rounded to the inferred precision using banker's rounding (round half to even).
551    ///
552    /// # Errors
553    ///
554    /// Returns an error if:
555    /// - The inferred precision exceeds [`FIXED_PRECISION`].
556    /// - The decimal value cannot be converted to the raw representation.
557    /// - Overflow occurs during scaling.
558    pub fn from_decimal(decimal: Decimal) -> CorrectnessResult<Self> {
559        let precision = decimal.scale() as u8;
560        Self::from_decimal_dp(decimal, precision)
561    }
562
563    /// Creates a new [`Price`] from a mantissa/exponent pair using pure integer arithmetic.
564    ///
565    /// The value is `mantissa * 10^exponent`. This avoids all floating-point and Decimal
566    /// operations, making it ideal for exchange data that arrives as mantissa/exponent pairs.
567    ///
568    /// # Panics
569    ///
570    /// Panics if the resulting raw value exceeds [`PRICE_RAW_MAX`] or [`PRICE_RAW_MIN`].
571    #[must_use]
572    pub fn from_mantissa_exponent(mantissa: i64, exponent: i8, precision: u8) -> Self {
573        check_fixed_precision(precision).expect_display(FAILED);
574
575        if mantissa == 0 {
576            return Self { raw: 0, precision };
577        }
578
579        let raw_i128 = mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, precision)
580            .expect("Overflow in Price::from_mantissa_exponent");
581
582        #[allow(
583            clippy::useless_conversion,
584            reason = "i128 to PriceRaw is real when not high-precision"
585        )]
586        let raw: PriceRaw = raw_i128
587            .try_into()
588            .expect("Raw value exceeds PriceRaw range in Price::from_mantissa_exponent");
589        assert!(
590            raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX,
591            "`raw` value {raw} exceeded bounds [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
592        );
593
594        Self { raw, precision }
595    }
596
597    /// Checked variant of [`Price::from_mantissa_exponent`].
598    ///
599    /// # Errors
600    ///
601    /// Returns an error if the precision is invalid or the resulting raw value
602    /// exceeds the `PriceRaw` bounds.
603    pub fn from_mantissa_exponent_checked(
604        mantissa: i64,
605        exponent: i8,
606        precision: u8,
607    ) -> CorrectnessResult<Self> {
608        let raw = mantissa_exponent_to_raw_checked::<PriceRaw>(
609            i128::from(mantissa),
610            exponent,
611            precision,
612            "Price::from_mantissa_exponent",
613            "PriceRaw",
614            "Price",
615        )?;
616
617        Self::from_raw_checked(raw, precision)
618    }
619}
620
621impl FromStr for Price {
622    type Err = String;
623
624    fn from_str(value: &str) -> Result<Self, Self::Err> {
625        let clean_value = remove_underscores(value);
626        if clean_value.contains('e') || clean_value.contains('E') {
627            let decimal = Decimal::from_scientific(&clean_value)
628                .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?;
629            let precision = decimal.scale() as u8;
630            return Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string());
631        }
632
633        let (mantissa, precision) = parse_decimal_mantissa(&clean_value)
634            .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?;
635        let exponent = -i8::try_from(precision).map_err(|e| e.to_string())?;
636        let raw = mantissa_exponent_to_raw_checked::<PriceRaw>(
637            mantissa,
638            exponent,
639            precision,
640            "Price::from_str",
641            "PriceRaw",
642            "Price",
643        )
644        .map_err(|e| e.to_string())?;
645        Self::from_raw_checked(raw, precision).map_err(|e| e.to_string())
646    }
647}
648
649impl<T: AsRef<str>> From<T> for Price {
650    fn from(value: T) -> Self {
651        Self::from_str(value.as_ref()).expect(FAILED)
652    }
653}
654
655impl From<Price> for f64 {
656    fn from(price: Price) -> Self {
657        price.as_f64()
658    }
659}
660
661impl From<&Price> for f64 {
662    fn from(price: &Price) -> Self {
663        price.as_f64()
664    }
665}
666
667impl From<Price> for Decimal {
668    fn from(value: Price) -> Self {
669        value.as_decimal()
670    }
671}
672
673impl From<&Price> for Decimal {
674    fn from(value: &Price) -> Self {
675        value.as_decimal()
676    }
677}
678
679impl Hash for Price {
680    fn hash<H: Hasher>(&self, state: &mut H) {
681        self.raw.signum().hash(state);
682        if self.raw == PRICE_ERROR {
683            self.raw.hash(state);
684        } else {
685            canonical_raw(self.raw.unsigned_abs(), self.precision).hash(state);
686        }
687    }
688}
689
690impl PartialEq for Price {
691    #[inline]
692    fn eq(&self, other: &Self) -> bool {
693        self.cmp(other) == Ordering::Equal
694    }
695}
696
697impl PartialOrd for Price {
698    #[inline]
699    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
700        Some(self.cmp(other))
701    }
702}
703
704impl Ord for Price {
705    #[inline]
706    fn cmp(&self, other: &Self) -> Ordering {
707        // PRICE_ERROR is a precision-independent sentinel below every valid price.
708        if self.raw == PRICE_ERROR || other.raw == PRICE_ERROR {
709            return self.raw.cmp(&other.raw);
710        }
711
712        #[cfg(feature = "defi")]
713        {
714            compare_raw_signed(self.raw, self.precision, other.raw, other.precision)
715        }
716
717        #[cfg(not(feature = "defi"))]
718        {
719            self.raw.cmp(&other.raw)
720        }
721    }
722}
723
724impl Deref for Price {
725    type Target = PriceRaw;
726
727    fn deref(&self) -> &Self::Target {
728        &self.raw
729    }
730}
731
732impl Neg for Price {
733    type Output = Self;
734    fn neg(self) -> Self::Output {
735        // Preserve sentinel values (negating PRICE_ERROR would also overflow)
736        if self.raw == PRICE_ERROR || self.raw == PRICE_UNDEF {
737            return self;
738        }
739        Self {
740            raw: -self.raw,
741            precision: self.precision,
742        }
743    }
744}
745
746impl Add for Price {
747    type Output = Self;
748    #[inline]
749    fn add(self, rhs: Self) -> Self::Output {
750        assert!(
751            raw_scales_match(self.precision, rhs.precision),
752            "Cannot add `Price` values with mismatched decimal scales"
753        );
754        Self {
755            raw: self
756                .raw
757                .checked_add(rhs.raw)
758                .expect("Overflow occurred when adding `Price`"),
759            precision: self.precision.max(rhs.precision),
760        }
761    }
762}
763
764impl Sub for Price {
765    type Output = Self;
766    #[inline]
767    fn sub(self, rhs: Self) -> Self::Output {
768        assert!(
769            raw_scales_match(self.precision, rhs.precision),
770            "Cannot subtract `Price` values with mismatched decimal scales"
771        );
772        Self {
773            raw: self
774                .raw
775                .checked_sub(rhs.raw)
776                .expect("Underflow occurred when subtracting `Price`"),
777            precision: self.precision.max(rhs.precision),
778        }
779    }
780}
781
782impl Add<Decimal> for Price {
783    type Output = Decimal;
784    fn add(self, rhs: Decimal) -> Self::Output {
785        self.as_decimal() + rhs
786    }
787}
788
789impl Sub<Decimal> for Price {
790    type Output = Decimal;
791    fn sub(self, rhs: Decimal) -> Self::Output {
792        self.as_decimal() - rhs
793    }
794}
795
796impl Mul<Decimal> for Price {
797    type Output = Decimal;
798    fn mul(self, rhs: Decimal) -> Self::Output {
799        self.as_decimal() * rhs
800    }
801}
802
803impl Div<Decimal> for Price {
804    type Output = Decimal;
805    fn div(self, rhs: Decimal) -> Self::Output {
806        self.as_decimal() / rhs
807    }
808}
809
810impl Add<f64> for Price {
811    type Output = f64;
812    fn add(self, rhs: f64) -> Self::Output {
813        self.as_f64() + rhs
814    }
815}
816
817impl Sub<f64> for Price {
818    type Output = f64;
819    fn sub(self, rhs: f64) -> Self::Output {
820        self.as_f64() - rhs
821    }
822}
823
824impl Mul<f64> for Price {
825    type Output = f64;
826    fn mul(self, rhs: f64) -> Self::Output {
827        self.as_f64() * rhs
828    }
829}
830
831impl Div<f64> for Price {
832    type Output = f64;
833    fn div(self, rhs: f64) -> Self::Output {
834        self.as_f64() / rhs
835    }
836}
837
838impl Debug for Price {
839    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
840        write!(f, "{}({self})", stringify!(Price))
841    }
842}
843
844impl Display for Price {
845    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846        if self.precision == ERROR_PRICE.precision {
847            return write!(f, "{}", self.raw);
848        }
849
850        write!(
851            f,
852            "{}",
853            format_scaled_i128(self.raw_at_precision(), self.precision),
854        )
855    }
856}
857
858impl Serialize for Price {
859    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
860    where
861        S: serde::Serializer,
862    {
863        serializer.serialize_str(&self.to_string())
864    }
865}
866
867impl<'de> Deserialize<'de> for Price {
868    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
869    where
870        D: Deserializer<'de>,
871    {
872        let StringInput(price_str) = Deserialize::deserialize(deserializer)?;
873        Self::from_str(price_str.as_ref()).map_err(serde::de::Error::custom)
874    }
875}
876
877/// Checks the price `value` is positive.
878///
879/// # Errors
880///
881/// Returns an error if `value` is `PRICE_UNDEF` or not positive.
882pub fn check_positive_price(value: Price, param: &str) -> CorrectnessResult<()> {
883    if value.raw == PRICE_UNDEF {
884        return Err(CorrectnessError::InvalidValue {
885            param: param.to_string(),
886            value: "PRICE_UNDEF".to_string(),
887            type_name: "`Price`",
888        });
889    }
890
891    if !value.is_positive() {
892        return Err(CorrectnessError::NotPositive {
893            param: param.to_string(),
894            value: value.to_string(),
895            type_name: "`Price`",
896        });
897    }
898    Ok(())
899}
900
901#[cfg(feature = "high-precision")]
902/// The raw i64 price has already been scaled by 10^9. Further scale it by the difference to
903/// `FIXED_PRECISION` to make it high/defi-precision raw price.
904#[must_use]
905pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
906    PriceRaw::from(value) * PRECISION_DIFF_SCALAR as PriceRaw
907}
908
909#[cfg(not(feature = "high-precision"))]
910#[must_use]
911pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
912    value
913}
914
915#[cfg(test)]
916mod tests {
917    use nautilus_core::{approx_eq, correctness::CorrectnessError};
918    use rstest::rstest;
919    use rust_decimal_macros::dec;
920
921    use super::*;
922
923    #[cfg(feature = "high-precision")]
924    #[rstest]
925    #[case(PRICE_RAW_MAX, dec!(17014118346046))]
926    #[case(PRICE_RAW_MIN, dec!(-17014118346046))]
927    fn test_as_decimal_above_decimal_mantissa(#[case] raw: PriceRaw, #[case] expected: Decimal) {
928        // Regression: a precision-16 price above roughly 7.92e12 rescales to a raw value beyond
929        // `Decimal`'s 96-bit mantissa, which used to panic during conversion.
930        let price = Price::from_raw(raw, 16);
931
932        assert_eq!(price.as_decimal(), expected);
933    }
934
935    #[rstest]
936    fn test_error_sentinel_formatting() {
937        assert_eq!(ERROR_PRICE.to_string(), "0");
938        assert_eq!(format!("{ERROR_PRICE:?}"), "Price(0)");
939        assert_eq!(ERROR_PRICE.to_formatted_string(), "0");
940    }
941
942    #[rstest]
943    fn test_error_sentinel_comparisons_preserve_raw_zero_semantics() {
944        let zero = Price::zero(FIXED_PRECISION);
945        let positive = Price::from_mantissa_exponent(1, 0, FIXED_PRECISION);
946        let negative = -positive;
947
948        assert_eq!(ERROR_PRICE, zero);
949        assert_eq!(ERROR_PRICE.cmp(&zero), Ordering::Equal);
950        assert_eq!(ERROR_PRICE.cmp(&positive), Ordering::Less);
951        assert_eq!(negative.cmp(&ERROR_PRICE), Ordering::Less);
952    }
953
954    #[rstest]
955    fn test_extreme_prices_round_trip_through_raw() {
956        // Regression: a lossy `f64` scalar previously left `PRICE_RAW_MAX`/`PRICE_RAW_MIN`
957        // beyond the raw produced by `new` at the bounds, causing spurious panics and errors.
958        let max = Price::new(PRICE_MAX, 0);
959        let min = Price::new(PRICE_MIN, 0);
960
961        assert_eq!(max.raw, PRICE_RAW_MAX);
962        assert_eq!(min.raw, PRICE_RAW_MIN);
963        assert!(Price::from_raw_checked(max.raw, 0).is_ok());
964        assert!(Price::from_raw_checked(min.raw, 0).is_ok());
965    }
966
967    #[rstest]
968    #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
969    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 50")]
970    fn test_invalid_precision_new() {
971        // Precision exceeds float precision limit
972        let _ = Price::new(1.0, 50);
973    }
974
975    #[rstest]
976    #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
977    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 50")]
978    fn test_invalid_precision_new() {
979        // Precision exceeds float precision limit
980        let _ = Price::new(1.0, 50);
981    }
982
983    #[rstest]
984    #[cfg(not(feature = "defi"))]
985    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
986    fn test_invalid_precision_from_raw() {
987        // Precision out of range for fixed
988        let _ = Price::from_raw(1, FIXED_PRECISION + 1);
989    }
990
991    #[rstest]
992    #[cfg(not(feature = "defi"))]
993    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
994    fn test_invalid_precision_max() {
995        // Precision out of range for fixed
996        let _ = Price::max(FIXED_PRECISION + 1);
997    }
998
999    #[rstest]
1000    #[cfg(not(feature = "defi"))]
1001    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
1002    fn test_invalid_precision_min() {
1003        // Precision out of range for fixed
1004        let _ = Price::min(FIXED_PRECISION + 1);
1005    }
1006
1007    #[rstest]
1008    #[cfg(not(feature = "defi"))]
1009    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
1010    fn test_invalid_precision_zero() {
1011        // Precision out of range for fixed
1012        let _ = Price::zero(FIXED_PRECISION + 1);
1013    }
1014
1015    #[rstest]
1016    #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
1017    fn test_max_value_exceeded() {
1018        let _ = Price::new(PRICE_MAX + 0.1, FIXED_PRECISION);
1019    }
1020
1021    #[rstest]
1022    #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
1023    fn test_min_value_exceeded() {
1024        let _ = Price::new(PRICE_MIN - 0.1, FIXED_PRECISION);
1025    }
1026
1027    #[rstest]
1028    fn test_is_positive_ok() {
1029        // A normal, non-zero price should be positive.
1030        let price = Price::new(42.0, 2);
1031        assert!(price.is_positive());
1032
1033        // `check_positive_price` should accept it without error.
1034        check_positive_price(price, "price").unwrap();
1035    }
1036
1037    #[rstest]
1038    fn test_is_positive_rejects_non_positive() {
1039        // Zero is NOT positive.
1040        let zero = Price::zero(2);
1041        let error = check_positive_price(zero, "price").unwrap_err();
1042
1043        assert_eq!(
1044            error,
1045            CorrectnessError::NotPositive {
1046                param: "price".to_string(),
1047                value: "0.00".to_string(),
1048                type_name: "`Price`",
1049            }
1050        );
1051        assert_eq!(
1052            error.to_string(),
1053            "invalid `Price` for 'price' not positive, was 0.00"
1054        );
1055    }
1056
1057    #[rstest]
1058    fn test_is_positive_rejects_undefined() {
1059        // PRICE_UNDEF must also be rejected.
1060        let undef = Price::from_raw(PRICE_UNDEF, 0);
1061        let error = check_positive_price(undef, "price").unwrap_err();
1062
1063        assert_eq!(
1064            error,
1065            CorrectnessError::InvalidValue {
1066                param: "price".to_string(),
1067                value: "PRICE_UNDEF".to_string(),
1068                type_name: "`Price`",
1069            }
1070        );
1071        assert_eq!(
1072            error.to_string(),
1073            "invalid `Price` for 'price', was PRICE_UNDEF"
1074        );
1075    }
1076
1077    #[rstest]
1078    fn test_construction() {
1079        let price = Price::new_checked(1.23456, 4);
1080        assert!(price.is_ok());
1081        let price = price.unwrap();
1082        assert_eq!(price.precision, 4);
1083        assert!(approx_eq!(f64, price.as_f64(), 1.23456, epsilon = 0.0001));
1084    }
1085
1086    #[rstest]
1087    fn test_negative_price_in_range() {
1088        // Use max fixed precision which varies based on feature flags
1089        let neg_price = Price::new(PRICE_MIN / 2.0, FIXED_PRECISION);
1090        assert!(neg_price.raw < 0);
1091    }
1092
1093    #[rstest]
1094    fn test_new_checked() {
1095        // Use max fixed precision which varies based on feature flags
1096        assert!(Price::new_checked(1.0, FIXED_PRECISION).is_ok());
1097        assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
1098        assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
1099    }
1100
1101    #[rstest]
1102    fn test_new_checked_returns_typed_error_with_stable_display() {
1103        let error = Price::new_checked(PRICE_MAX + 1.0, FIXED_PRECISION).unwrap_err();
1104
1105        assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
1106        assert_eq!(
1107            error.to_string(),
1108            format!(
1109                "invalid f64 for 'value' not in range [{PRICE_MIN}, {PRICE_MAX}], was {}",
1110                PRICE_MAX + 1.0
1111            )
1112        );
1113    }
1114
1115    #[rstest]
1116    fn test_from_raw_checked_returns_typed_error_with_stable_display() {
1117        let error = Price::from_raw_checked(PRICE_UNDEF, 3).unwrap_err();
1118
1119        assert_eq!(
1120            error,
1121            CorrectnessError::PredicateViolation {
1122                message: "`precision` must be 0 when `raw` is PRICE_UNDEF".to_string(),
1123            }
1124        );
1125        assert_eq!(
1126            error.to_string(),
1127            "`precision` must be 0 when `raw` is PRICE_UNDEF"
1128        );
1129    }
1130
1131    #[rstest]
1132    #[case::below_minimum(PRICE_RAW_MIN - 1)]
1133    #[case::above_maximum(PRICE_RAW_MAX + 1)]
1134    fn test_from_raw_checked_rejects_out_of_range_value(#[case] raw: PriceRaw) {
1135        let error = Price::from_raw_checked(raw, 0).unwrap_err();
1136
1137        assert_eq!(
1138            error,
1139            CorrectnessError::PredicateViolation {
1140                message: format!(
1141                    "raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
1142                ),
1143            }
1144        );
1145    }
1146
1147    #[rstest]
1148    #[should_panic(expected = "outside valid range")]
1149    fn test_from_raw_out_of_range_panics() {
1150        let _ = Price::from_raw(PRICE_RAW_MAX + 1, 0);
1151    }
1152
1153    #[rstest]
1154    fn test_from_raw() {
1155        let raw = 100 * FIXED_SCALAR as PriceRaw;
1156        let price = Price::from_raw(raw, 2);
1157        assert_eq!(price.raw, raw);
1158        assert_eq!(price.precision, 2);
1159    }
1160
1161    #[rstest]
1162    fn test_zero_constructor() {
1163        let zero = Price::zero(3);
1164        assert!(zero.is_zero());
1165        assert_eq!(zero.precision, 3);
1166    }
1167
1168    #[rstest]
1169    fn test_max_constructor() {
1170        let max = Price::max(4);
1171        assert_eq!(max.raw, PRICE_RAW_MAX);
1172        assert_eq!(max.precision, 4);
1173    }
1174
1175    #[rstest]
1176    fn test_min_constructor() {
1177        let min = Price::min(4);
1178        assert_eq!(min.raw, PRICE_RAW_MIN);
1179        assert_eq!(min.precision, 4);
1180    }
1181
1182    #[rstest]
1183    fn test_nan_validation() {
1184        assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
1185    }
1186
1187    #[rstest]
1188    fn test_infinity_validation() {
1189        assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
1190        assert!(Price::new_checked(f64::NEG_INFINITY, FIXED_PRECISION).is_err());
1191    }
1192
1193    #[rstest]
1194    fn test_special_values() {
1195        let zero = Price::zero(5);
1196        assert!(zero.is_zero());
1197        assert_eq!(zero.to_string(), "0.00000");
1198
1199        let undef = Price::from_raw(PRICE_UNDEF, 0);
1200        assert!(undef.is_undefined());
1201
1202        let error = ERROR_PRICE;
1203        assert_eq!(error.precision, 255);
1204    }
1205
1206    #[rstest]
1207    fn test_string_parsing() {
1208        let price: Price = "123.456".into();
1209        assert_eq!(price.precision, 3);
1210        assert_eq!(price, Price::from("123.456"));
1211    }
1212
1213    #[rstest]
1214    #[case(PRICE_RAW_MIN)]
1215    #[case(PRICE_RAW_MAX)]
1216    fn test_from_str_raw_limits(#[case] raw: PriceRaw) {
1217        let original = Price::from_raw(raw, FIXED_PRECISION);
1218        let decoded = original.to_string().parse::<Price>().unwrap();
1219
1220        assert_eq!(decoded.raw, raw);
1221        assert_eq!(decoded.precision, FIXED_PRECISION);
1222    }
1223
1224    #[rstest]
1225    fn test_negative_price_from_str() {
1226        let price: Price = "-123.45".parse().unwrap();
1227        assert_eq!(price.precision, 2);
1228        assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-9));
1229    }
1230
1231    #[rstest]
1232    fn test_string_parsing_errors() {
1233        assert!(Price::from_str("invalid").is_err());
1234    }
1235
1236    #[rstest]
1237    #[case("1e7", 0, 10_000_000.0)]
1238    #[case("1.5e3", 0, 1_500.0)]
1239    #[case("1.234e-2", 5, 0.01234)]
1240    #[case("5E-3", 3, 0.005)]
1241    fn test_from_str_scientific_notation(
1242        #[case] input: &str,
1243        #[case] expected_precision: u8,
1244        #[case] expected_value: f64,
1245    ) {
1246        let price = Price::from_str(input).unwrap();
1247        assert_eq!(price.precision, expected_precision);
1248        assert!(approx_eq!(
1249            f64,
1250            price.as_f64(),
1251            expected_value,
1252            epsilon = 1e-10
1253        ));
1254    }
1255
1256    #[rstest]
1257    #[case("1_234.56", 2, 1234.56)]
1258    #[case("1000000", 0, 1_000_000.0)]
1259    #[case("99_999.999_99", 5, 99_999.999_99)]
1260    fn test_from_str_with_underscores(
1261        #[case] input: &str,
1262        #[case] expected_precision: u8,
1263        #[case] expected_value: f64,
1264    ) {
1265        let price = Price::from_str(input).unwrap();
1266        assert_eq!(price.precision, expected_precision);
1267        assert!(approx_eq!(
1268            f64,
1269            price.as_f64(),
1270            expected_value,
1271            epsilon = 1e-10
1272        ));
1273    }
1274
1275    #[rstest]
1276    fn test_from_decimal_dp_preservation() {
1277        // Test that decimal conversion preserves exact values
1278        let decimal = dec!(123.456789);
1279        let price = Price::from_decimal_dp(decimal, 6).unwrap();
1280        assert_eq!(price.precision, 6);
1281        assert!(approx_eq!(
1282            f64,
1283            price.as_f64(),
1284            123.456_789,
1285            epsilon = 1e-10
1286        ));
1287
1288        // Verify raw value is exact
1289        let expected_raw = 123_456_789 * 10_i64.pow(u32::from(FIXED_PRECISION - 6));
1290        assert_eq!(price.raw, PriceRaw::from(expected_raw));
1291    }
1292
1293    #[rstest]
1294    fn test_from_decimal_dp_rounding() {
1295        // Test banker's rounding (round half to even)
1296        let decimal = dec!(1.005);
1297        let price = Price::from_decimal_dp(decimal, 2).unwrap();
1298        assert_eq!(price.as_f64(), 1.0); // 1.005 rounds to 1.00 (even)
1299
1300        let decimal = dec!(1.015);
1301        let price = Price::from_decimal_dp(decimal, 2).unwrap();
1302        assert_eq!(price.as_f64(), 1.02); // 1.015 rounds to 1.02 (even)
1303    }
1304
1305    #[rstest]
1306    fn test_from_decimal_infers_precision() {
1307        // Test that precision is inferred from decimal's scale
1308        let decimal = dec!(123.456);
1309        let price = Price::from_decimal(decimal).unwrap();
1310        assert_eq!(price.precision, 3);
1311        assert!(approx_eq!(f64, price.as_f64(), 123.456, epsilon = 1e-10));
1312
1313        // Test with integer (precision 0)
1314        let decimal = dec!(100);
1315        let price = Price::from_decimal(decimal).unwrap();
1316        assert_eq!(price.precision, 0);
1317        assert_eq!(price.as_f64(), 100.0);
1318
1319        // Test with high precision
1320        let decimal = dec!(1.23456789);
1321        let price = Price::from_decimal(decimal).unwrap();
1322        assert_eq!(price.precision, 8);
1323        assert!(approx_eq!(
1324            f64,
1325            price.as_f64(),
1326            1.234_567_89,
1327            epsilon = 1e-10
1328        ));
1329    }
1330
1331    #[rstest]
1332    fn test_from_decimal_trailing_zeros() {
1333        // Decimal preserves trailing zeros in scale
1334        let decimal = dec!(1.230);
1335        assert_eq!(decimal.scale(), 3); // Has 3 decimal places
1336
1337        // from_decimal infers precision from scale (includes trailing zeros)
1338        let price = Price::from_decimal(decimal).unwrap();
1339        assert_eq!(price.precision, 3);
1340        assert!(approx_eq!(f64, price.as_f64(), 1.23, epsilon = 1e-10));
1341
1342        // Normalized removes trailing zeros
1343        let normalized = decimal.normalize();
1344        assert_eq!(normalized.scale(), 2);
1345        let price_normalized = Price::from_decimal(normalized).unwrap();
1346        assert_eq!(price_normalized.precision, 2);
1347    }
1348
1349    #[rstest]
1350    #[case("1.00", 2)]
1351    #[case("1.0", 1)]
1352    #[case("1.000", 3)]
1353    #[case("100.00", 2)]
1354    #[case("0.10", 2)]
1355    #[case("0.100", 3)]
1356    fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1357        let price = Price::from_str(input).unwrap();
1358        assert_eq!(price.precision, expected_precision);
1359    }
1360
1361    #[rstest]
1362    fn test_from_decimal_excessive_precision_inference() {
1363        // Create a decimal with more precision than FIXED_PRECISION
1364        // Decimal supports up to 28 decimal places
1365        let decimal = dec!(1.1234567890123456789012345678);
1366
1367        // If scale exceeds FIXED_PRECISION, from_decimal should error
1368        if decimal.scale() > u32::from(FIXED_PRECISION) {
1369            assert!(Price::from_decimal(decimal).is_err());
1370        }
1371    }
1372
1373    #[rstest]
1374    fn test_from_decimal_dp_rejects_raw_between_price_and_raw_bounds() {
1375        let at_max = Decimal::try_from(PRICE_MAX).unwrap();
1376        let above_max = at_max + dec!(0.5);
1377        let expected_raw = PRICE_RAW_MAX + 5 * PriceRaw::pow(10, u32::from(FIXED_PRECISION - 1));
1378
1379        let error = Price::from_decimal_dp(above_max, 1).unwrap_err();
1380
1381        assert_eq!(
1382            error.to_string(),
1383            format!(
1384                "Raw value {expected_raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
1385            )
1386        );
1387        assert_eq!(
1388            Price::from_decimal_dp(at_max, 1).unwrap().raw,
1389            PRICE_RAW_MAX
1390        );
1391    }
1392
1393    #[rstest]
1394    fn test_from_decimal_dp_out_of_range_returns_typed_error_with_stable_display() {
1395        let huge = Decimal::from_str("99999999999999999999.99").unwrap();
1396        let error = Price::from_decimal_dp(huge, 2).unwrap_err();
1397        match error {
1398            CorrectnessError::PredicateViolation { ref message } => {
1399                assert!(
1400                    message.contains("PriceRaw range") || message.contains("for Price"),
1401                    "unexpected message: {message:?}",
1402                );
1403            }
1404            _ => panic!("expected PredicateViolation, was {error:?}"),
1405        }
1406    }
1407
1408    #[rstest]
1409    fn test_from_decimal_negative_price() {
1410        // Negative prices are valid for Price
1411        let decimal = dec!(-123.45);
1412        let price = Price::from_decimal(decimal).unwrap();
1413        assert_eq!(price.precision, 2);
1414        assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-10));
1415        assert!(price.raw < 0);
1416    }
1417
1418    #[rstest]
1419    fn test_string_formatting() {
1420        assert_eq!(format!("{}", Price::new(1234.5678, 4)), "1234.5678");
1421        assert_eq!(
1422            format!("{:?}", Price::new(1234.5678, 4)),
1423            "Price(1234.5678)"
1424        );
1425        assert_eq!(Price::new(1234.5678, 4).to_formatted_string(), "1_234.5678");
1426    }
1427
1428    #[rstest]
1429    #[case(1234.5678, 4, "Price(1234.5678)", "1234.5678")] // Normal precision
1430    #[case(123.456_789_012_345, 8, "Price(123.45678901)", "123.45678901")] // At max normal precision
1431    #[cfg_attr(
1432        feature = "defi",
1433        case(
1434            2_000_000_000_000_000_000.0,
1435            18,
1436            "Price(2.000000000000000000)",
1437            "2.000000000000000000"
1438        )
1439    )] // High precision
1440    fn test_string_formatting_precision_handling(
1441        #[case] value: f64,
1442        #[case] precision: u8,
1443        #[case] expected_debug: &str,
1444        #[case] expected_display: &str,
1445    ) {
1446        let price = if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
1447            Price::from_raw(value as PriceRaw, precision)
1448        } else {
1449            Price::new(value, precision)
1450        };
1451
1452        assert_eq!(format!("{price:?}"), expected_debug);
1453        assert_eq!(format!("{price}"), expected_display);
1454        assert_eq!(
1455            price.to_formatted_string().replace('_', ""),
1456            expected_display
1457        );
1458    }
1459
1460    #[rstest]
1461    fn test_decimal_conversions() {
1462        let price = Price::new(123.456, 3);
1463        assert_eq!(price.as_decimal(), dec!(123.456));
1464
1465        let price = Price::new(0.000_001, 6);
1466        assert_eq!(price.as_decimal(), dec!(0.000001));
1467    }
1468
1469    #[rstest]
1470    #[case(PRICE_ERROR, true)]
1471    #[case(PRICE_UNDEF, false)]
1472    #[case(-1, false)]
1473    #[case(0, false)]
1474    #[case(1, false)]
1475    fn test_is_error(#[case] raw: PriceRaw, #[case] expected: bool) {
1476        let price = Price::from_raw(raw, 0);
1477        assert_eq!(price.is_error(), expected);
1478    }
1479
1480    #[rstest]
1481    fn test_basic_arithmetic() {
1482        let p1 = Price::new(10.5, 2);
1483        let p2 = Price::new(5.25, 2);
1484        assert_eq!(p1 + p2, Price::from("15.75"));
1485        assert_eq!(p1 - p2, Price::from("5.25"));
1486        assert_eq!(-p1, Price::from("-10.5"));
1487    }
1488
1489    #[rstest]
1490    #[case::error(PRICE_ERROR)]
1491    #[case::undefined(PRICE_UNDEF)]
1492    fn test_neg_preserves_sentinel(#[case] raw: PriceRaw) {
1493        let price = Price::from_raw(raw, 0);
1494
1495        assert_eq!(-price, price);
1496    }
1497
1498    #[rstest]
1499    fn test_price_checked_add_within_bounds() {
1500        let a = Price::new(10.0, 2);
1501        let b = Price::new(5.0, 2);
1502        assert_eq!(a.checked_add(b), Some(Price::new(15.0, 2)));
1503
1504        let neg = Price::new(-3.0, 2);
1505        assert_eq!(a.checked_add(neg), Some(Price::new(7.0, 2)));
1506    }
1507
1508    #[rstest]
1509    fn test_price_checked_add_above_max_returns_none() {
1510        let near_max = Price::from_raw(PRICE_RAW_MAX, 0);
1511        let one = Price::new(1.0, 0);
1512        assert_eq!(near_max.checked_add(one), None);
1513    }
1514
1515    #[rstest]
1516    fn test_price_checked_sub_within_bounds() {
1517        let a = Price::new(10.0, 2);
1518        let b = Price::new(3.0, 2);
1519        assert_eq!(a.checked_sub(b), Some(Price::new(7.0, 2)));
1520        assert_eq!(b.checked_sub(a), Some(Price::new(-7.0, 2)));
1521    }
1522
1523    #[rstest]
1524    fn test_price_checked_sub_below_min_returns_none() {
1525        let near_min = Price::from_raw(PRICE_RAW_MIN, 0);
1526        let one = Price::new(1.0, 0);
1527        assert_eq!(near_min.checked_sub(one), None);
1528    }
1529
1530    #[rstest]
1531    fn test_price_checked_arith_uses_max_precision() {
1532        let a = Price::new(10.5, 1);
1533        let b = Price::new(5.25, 2);
1534        let sum = a.checked_add(b).unwrap();
1535        assert_eq!(sum.precision, 2);
1536        assert_eq!(sum.as_f64(), 15.75);
1537    }
1538
1539    #[rstest]
1540    fn test_price_checked_add_rejects_sentinel_undef() {
1541        let undef = Price::from_raw(PRICE_UNDEF, 0);
1542        let one = Price::new(1.0, 0);
1543        assert_eq!(undef.checked_add(one), None);
1544        assert_eq!(one.checked_add(undef), None);
1545    }
1546
1547    #[rstest]
1548    fn test_price_checked_sub_rejects_sentinel_undef() {
1549        let undef = Price::from_raw(PRICE_UNDEF, 0);
1550        let neg_one = Price::new(-1.0, 0);
1551        assert_eq!(undef.checked_sub(neg_one), None);
1552    }
1553
1554    #[rstest]
1555    fn test_price_is_zero() {
1556        assert!(!Price::new(1.5, 2).is_zero());
1557        assert!(Price::new(0.0, 2).is_zero());
1558    }
1559
1560    #[rstest]
1561    fn test_price_as_f64() {
1562        assert_eq!(Price::new(1.5, 2).as_f64(), 1.5);
1563        assert_eq!(Price::new(0.0, 2).as_f64(), 0.0);
1564    }
1565
1566    #[rstest]
1567    fn test_price_checked_arith_rejects_out_of_bounds_without_integer_overflow() {
1568        let one_unit = Price::from_raw(1, 0);
1569
1570        assert_eq!(
1571            Price::from_raw(PRICE_RAW_MAX, 0).checked_add(one_unit),
1572            None
1573        );
1574        assert_eq!(
1575            Price::from_raw(PRICE_RAW_MIN, 0).checked_sub(one_unit),
1576            None
1577        );
1578    }
1579
1580    #[rstest]
1581    fn test_price_checked_sub_rejects_sentinel_before_bounds_check() {
1582        let undef = Price::from_raw(PRICE_UNDEF, 0);
1583        let max = Price::from_raw(PRICE_RAW_MAX, 0);
1584
1585        assert_eq!(undef.checked_sub(max), None);
1586    }
1587
1588    #[rstest]
1589    fn test_price_checked_arith_rejects_error_price() {
1590        let one = Price::new(1.0, 0);
1591        assert_eq!(ERROR_PRICE.checked_add(one), None);
1592        assert_eq!(one.checked_sub(ERROR_PRICE), None);
1593    }
1594
1595    #[rstest]
1596    fn test_price_checked_arith_rejects_raw_error() {
1597        let error = Price::from_raw(PRICE_ERROR, 0);
1598        let one = Price::new(1.0, 0);
1599        assert_eq!(error.checked_add(one), None);
1600        assert_eq!(one.checked_add(error), None);
1601        assert_eq!(error.checked_sub(one), None);
1602        assert_eq!(one.checked_sub(error), None);
1603    }
1604
1605    #[rstest]
1606    fn test_price_checked_add_at_exact_max_returns_some() {
1607        let near_max = Price::from_raw(PRICE_RAW_MAX - 1, 0);
1608        let one_unit = Price::from_raw(1, 0);
1609        assert_eq!(
1610            near_max.checked_add(one_unit),
1611            Some(Price::from_raw(PRICE_RAW_MAX, 0)),
1612        );
1613    }
1614
1615    #[rstest]
1616    fn test_price_checked_sub_at_exact_min_returns_some() {
1617        let near_min = Price::from_raw(PRICE_RAW_MIN + 1, 0);
1618        let one_unit = Price::from_raw(1, 0);
1619        assert_eq!(
1620            near_min.checked_sub(one_unit),
1621            Some(Price::from_raw(PRICE_RAW_MIN, 0)),
1622        );
1623    }
1624
1625    #[rstest]
1626    fn test_mixed_precision_add() {
1627        let p1 = Price::new(10.5, 1);
1628        let p2 = Price::new(5.25, 2);
1629        let result = p1 + p2;
1630        assert_eq!(result.precision, 2);
1631        assert_eq!(result.as_f64(), 15.75);
1632    }
1633
1634    #[rstest]
1635    fn test_mixed_precision_sub() {
1636        let p1 = Price::new(10.5, 1);
1637        let p2 = Price::new(5.25, 2);
1638        let result = p1 - p2;
1639        assert_eq!(result.precision, 2);
1640        assert_eq!(result.as_f64(), 5.25);
1641    }
1642
1643    #[rstest]
1644    fn test_f64_operations() {
1645        let p = Price::new(10.5, 2);
1646        assert_eq!(p + 1.0, 11.5);
1647        assert_eq!(p - 1.0, 9.5);
1648        assert_eq!(p * 2.0, 21.0);
1649        assert_eq!(p / 2.0, 5.25);
1650    }
1651
1652    #[rstest]
1653    fn test_equality_and_comparisons() {
1654        let p1 = Price::new(10.0, 1);
1655        let p2 = Price::new(20.0, 1);
1656        let p3 = Price::new(10.0, 1);
1657
1658        assert!(p1 < p2);
1659        assert!(p2 > p1);
1660        assert!(p1 <= p3);
1661        assert!(p1 >= p3);
1662        assert_eq!(p1, p3);
1663        assert_ne!(p1, p2);
1664
1665        assert_eq!(Price::from("1.0"), Price::from("1.0"));
1666        assert_ne!(Price::from("1.1"), Price::from("1.0"));
1667        assert!(Price::from("1.0") <= Price::from("1.0"));
1668        assert!(Price::from("1.1") > Price::from("1.0"));
1669        assert!(Price::from("1.0") >= Price::from("1.0"));
1670        assert!(Price::from("1.0") >= Price::from("1.0"));
1671        assert!(Price::from("1.0") >= Price::from("1.0"));
1672        assert!(Price::from("0.9") < Price::from("1.0"));
1673        assert!(Price::from("0.9") <= Price::from("1.0"));
1674        assert!(Price::from("0.9") <= Price::from("1.0"));
1675    }
1676
1677    #[rstest]
1678    fn test_deref() {
1679        let price = Price::new(10.0, 1);
1680        assert_eq!(*price, price.raw);
1681    }
1682
1683    #[rstest]
1684    fn test_decode_raw_price_i64() {
1685        let raw_scaled_by_1e9 = 42_000_000_000i64; // 42.0 * 10^9
1686        let decoded = decode_raw_price_i64(raw_scaled_by_1e9);
1687        let price = Price::from_raw(decoded, FIXED_PRECISION);
1688        assert!(
1689            approx_eq!(f64, price.as_f64(), 42.0, epsilon = 1e-9),
1690            "Expected 42.0 f64, was {} (precision = {})",
1691            price.as_f64(),
1692            price.precision
1693        );
1694    }
1695
1696    #[rstest]
1697    fn test_hash() {
1698        use std::{
1699            collections::hash_map::DefaultHasher,
1700            hash::{Hash, Hasher},
1701        };
1702
1703        let price1 = Price::new(1.0, 2);
1704        let price2 = Price::new(1.0, 2);
1705        let price3 = Price::new(1.1, 2);
1706
1707        let mut hasher1 = DefaultHasher::new();
1708        let mut hasher2 = DefaultHasher::new();
1709        let mut hasher3 = DefaultHasher::new();
1710
1711        price1.hash(&mut hasher1);
1712        price2.hash(&mut hasher2);
1713        price3.hash(&mut hasher3);
1714
1715        assert_eq!(hasher1.finish(), hasher2.finish());
1716        assert_ne!(hasher1.finish(), hasher3.finish());
1717    }
1718
1719    #[rstest]
1720    fn test_price_serde_json_round_trip() {
1721        let price = Price::new(1.0500, 4);
1722        let json = serde_json::to_string(&price).unwrap();
1723        let deserialized: Price = serde_json::from_str(&json).unwrap();
1724        assert_eq!(deserialized, price);
1725    }
1726
1727    #[rstest]
1728    fn test_price_serde_json_from_value_round_trip() {
1729        let price = Price::new(1.0500, 4);
1730        let value = serde_json::to_value(price).unwrap();
1731
1732        let deserialized: Price = serde_json::from_value(value).unwrap();
1733        assert_eq!(deserialized, price);
1734        assert_eq!(deserialized.precision, 4);
1735    }
1736
1737    #[cfg(feature = "high-precision")]
1738    #[rstest]
1739    fn test_high_precision_16_serde_json_round_trip() {
1740        let price = Price::from_raw(1_234_567_890_123_456_789_i128, 16);
1741        let json = serde_json::to_string(&price).unwrap();
1742        let deserialized: Price = serde_json::from_str(&json).unwrap();
1743
1744        assert_eq!(deserialized, price);
1745        assert_eq!(deserialized.precision, 16);
1746        assert_eq!(deserialized.raw, 1_234_567_890_123_456_789_i128);
1747    }
1748
1749    #[cfg(all(feature = "high-precision", feature = "defi"))]
1750    #[rstest]
1751    #[case(17, 12_345_678_901_234_567_891_i128)]
1752    #[case(18, 123_456_789_012_345_678_901_i128)]
1753    fn test_defi_precision_serde_json_round_trip(#[case] precision: u8, #[case] raw: PriceRaw) {
1754        let price = Price::from_raw(raw, precision);
1755        let json = serde_json::to_string(&price).unwrap();
1756        let deserialized: Price = serde_json::from_str(&json).unwrap();
1757
1758        assert_eq!(deserialized, price);
1759        assert_eq!(deserialized.precision, precision);
1760        assert_eq!(deserialized.raw, raw);
1761    }
1762
1763    #[rstest]
1764    fn test_price_deserialize_invalid_string_returns_error() {
1765        let result = serde_json::from_str::<Price>("\"not-a-price\"");
1766        let error = result.unwrap_err();
1767        assert!(
1768            error.to_string().contains("Error parsing"),
1769            "unexpected message: {error}"
1770        );
1771    }
1772
1773    #[rstest]
1774    fn test_price_deserialize_out_of_range_returns_error() {
1775        let result = serde_json::from_str::<Price>("\"99999999999999999999.99\"");
1776        assert!(result.is_err());
1777    }
1778
1779    #[rstest]
1780    fn test_from_mantissa_exponent_exact_precision() {
1781        let price = Price::from_mantissa_exponent(12345, -2, 2);
1782        assert_eq!(price.as_f64(), 123.45);
1783    }
1784
1785    #[rstest]
1786    fn test_from_mantissa_exponent_excess_rounds_down() {
1787        // 12.345 rounds to 12.34 (4 is even, banker's rounding)
1788        let price = Price::from_mantissa_exponent(12345, -3, 2);
1789        assert_eq!(price.as_f64(), 12.34);
1790    }
1791
1792    #[rstest]
1793    fn test_from_mantissa_exponent_excess_rounds_up() {
1794        // 12.355 rounds to 12.36 (5 is odd, banker's rounding)
1795        let price = Price::from_mantissa_exponent(12355, -3, 2);
1796        assert_eq!(price.as_f64(), 12.36);
1797    }
1798
1799    #[rstest]
1800    fn test_from_mantissa_exponent_positive_exponent() {
1801        let price = Price::from_mantissa_exponent(5, 2, 0);
1802        assert_eq!(price.as_f64(), 500.0);
1803    }
1804
1805    #[rstest]
1806    fn test_from_mantissa_exponent_negative_mantissa() {
1807        let price = Price::from_mantissa_exponent(-12345, -2, 2);
1808        assert_eq!(price.as_f64(), -123.45);
1809    }
1810
1811    #[rstest]
1812    fn test_from_mantissa_exponent_zero() {
1813        let price = Price::from_mantissa_exponent(0, 2, 2);
1814        assert_eq!(price.as_f64(), 0.0);
1815    }
1816
1817    #[cfg(all(feature = "defi", feature = "high-precision"))]
1818    #[rstest]
1819    fn test_wei_above_decimal_mantissa_formats_exactly() {
1820        let raw = 80_000_000_000_000_000_250_000_000_000_i128;
1821        let price = Price::from_raw(raw, 18);
1822
1823        assert_eq!(price.to_string(), "80000000000.000000250000000000");
1824        assert_eq!(Price::from_str(&price.to_string()).unwrap(), price);
1825        assert_eq!(
1826            serde_json::from_str::<Price>(&serde_json::to_string(&price).unwrap()).unwrap(),
1827            price,
1828        );
1829    }
1830
1831    #[rstest]
1832    fn test_from_mantissa_exponent_checked_exact_precision() {
1833        let price = Price::from_mantissa_exponent_checked(12345, -2, 2).unwrap();
1834        assert_eq!(price.as_decimal(), dec!(123.45));
1835    }
1836
1837    #[rstest]
1838    fn test_from_mantissa_exponent_checked_zero_with_large_exponent() {
1839        let price = Price::from_mantissa_exponent_checked(0, 119, 2).unwrap();
1840        assert_eq!(price.as_decimal(), dec!(0.00));
1841    }
1842
1843    #[rstest]
1844    fn test_from_mantissa_exponent_checked_invalid_precision() {
1845        #[cfg(feature = "defi")]
1846        let invalid_precision = crate::defi::WEI_PRECISION + 1;
1847        #[cfg(not(feature = "defi"))]
1848        let invalid_precision = FIXED_PRECISION + 1;
1849
1850        let error = Price::from_mantissa_exponent_checked(1, 0, invalid_precision).unwrap_err();
1851        assert!(error.to_string().contains("`precision` exceeded maximum"));
1852    }
1853
1854    #[rstest]
1855    fn test_from_mantissa_exponent_checked_overflow_returns_error() {
1856        let error = Price::from_mantissa_exponent_checked(i64::MAX, 100, 0).unwrap_err();
1857        assert!(
1858            error
1859                .to_string()
1860                .contains("Overflow in Price::from_mantissa_exponent")
1861        );
1862    }
1863
1864    #[rstest]
1865    #[should_panic(expected = "Price::from_mantissa_exponent")]
1866    fn test_from_mantissa_exponent_overflow_panics() {
1867        let _ = Price::from_mantissa_exponent(i64::MAX, 9, 0);
1868    }
1869
1870    #[rstest]
1871    #[should_panic(expected = "exceeds i128 range")]
1872    fn test_from_mantissa_exponent_large_exponent_panics() {
1873        let _ = Price::from_mantissa_exponent(1, 119, 0);
1874    }
1875
1876    #[rstest]
1877    fn test_from_mantissa_exponent_zero_with_large_exponent() {
1878        let price = Price::from_mantissa_exponent(0, 119, 0);
1879        assert_eq!(price.as_f64(), 0.0);
1880    }
1881
1882    #[rstest]
1883    fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1884        let price = Price::from_mantissa_exponent(12345, -120, 2);
1885        assert_eq!(price.as_f64(), 0.0);
1886    }
1887
1888    #[rstest]
1889    fn test_decimal_arithmetic_operations() {
1890        let price = Price::new(100.0, 2);
1891        assert_eq!(price + dec!(50.25), dec!(150.25));
1892        assert_eq!(price - dec!(30.50), dec!(69.50));
1893        assert_eq!(price * dec!(1.5), dec!(150.00));
1894        assert_eq!(price / dec!(4), dec!(25.00));
1895    }
1896}
1897
1898#[cfg(test)]
1899mod property_tests {
1900    use proptest::prelude::*;
1901    use rstest::rstest;
1902
1903    use super::*;
1904
1905    /// Strategy to generate valid price values within the allowed range.
1906    fn price_value_strategy() -> impl Strategy<Value = f64> {
1907        // Use a reasonable range that's well within PRICE_MIN/PRICE_MAX
1908        // but still tests edge cases with various scales
1909        prop_oneof![
1910            // Small positive values
1911            0.00001..1.0,
1912            // Normal trading range
1913            1.0..100_000.0,
1914            // Large values (but safe)
1915            100_000.0..1_000_000.0,
1916            // Small negative values (for spreads, etc.)
1917            -1_000.0..0.0,
1918            // Boundary values close to the extremes
1919            Just(PRICE_MIN / 2.0),
1920            Just(PRICE_MAX / 2.0),
1921        ]
1922    }
1923
1924    fn float_precision_upper_bound() -> u8 {
1925        FIXED_PRECISION.min(crate::types::fixed::MAX_FLOAT_PRECISION)
1926    }
1927
1928    /// Strategy to exercise both typical and extreme precision values.
1929    fn precision_strategy() -> impl Strategy<Value = u8> {
1930        let upper = float_precision_upper_bound();
1931        prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1932    }
1933
1934    fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1935        let upper = float_precision_upper_bound().max(1);
1936        prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1937    }
1938
1939    /// Strategy to generate a valid (precision, raw) pair where raw is properly scaled.
1940    ///
1941    /// Raw values must be multiples of `10^(FIXED_PRECISION` - precision) to pass validation.
1942    fn valid_precision_raw_strategy() -> impl Strategy<Value = (u8, PriceRaw)> {
1943        precision_strategy().prop_flat_map(|precision| {
1944            let scale: PriceRaw = if precision >= FIXED_PRECISION {
1945                1
1946            } else {
1947                (10 as PriceRaw).pow(u32::from(FIXED_PRECISION - precision))
1948            };
1949            // Generate a base value, then multiply by scale to ensure valid raw
1950            let max_base = PRICE_RAW_MAX / scale;
1951            let min_base = PRICE_RAW_MIN / scale;
1952            (min_base..=max_base).prop_map(move |base| (precision, base * scale))
1953        })
1954    }
1955
1956    /// Strategy to generate valid precision values for float-based constructors.
1957    fn float_precision_strategy() -> impl Strategy<Value = u8> {
1958        precision_strategy()
1959    }
1960
1961    const DECIMAL_MAX_MANTISSA: i128 = 79_228_162_514_264_337_593_543_950_335;
1962
1963    #[allow(
1964        clippy::useless_conversion,
1965        reason = "PriceRaw is i64 or i128 depending on feature; the conversion is only useless in high-precision builds"
1966    )]
1967    fn decimal_compatible(raw: PriceRaw, precision: u8) -> bool {
1968        if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
1969            return false;
1970        }
1971        let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
1972        let divisor = (10 as PriceRaw).pow(precision_diff);
1973        let rescaled_raw = raw / divisor;
1974        i128::from(rescaled_raw.abs()) <= DECIMAL_MAX_MANTISSA
1975    }
1976
1977    proptest! {
1978        /// Property: Price string serialization round-trip should preserve value and precision
1979        #[rstest]
1980        fn prop_price_serde_round_trip(
1981            (precision, raw) in valid_precision_raw_strategy()
1982        ) {
1983            let original = Price::from_raw(raw, precision);
1984
1985            // String round-trip (this should be exact and is the most important)
1986            let string_repr = original.to_string();
1987            let from_string: Price = string_repr.parse().unwrap();
1988            prop_assert_eq!(from_string.raw, original.raw);
1989            prop_assert_eq!(from_string.precision, original.precision);
1990
1991            // JSON uses the same canonical decimal string as Display, so it must be exact.
1992            let json = serde_json::to_string(&original).unwrap();
1993            let from_json: Price = serde_json::from_str(&json).unwrap();
1994            prop_assert_eq!(from_json.precision, original.precision);
1995            prop_assert_eq!(from_json.raw, original.raw);
1996        }
1997
1998        /// Property: Price arithmetic should be associative for same precision
1999        #[rstest]
2000        fn prop_price_arithmetic_associative(
2001            a in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
2002            b in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
2003            c in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
2004            precision in precision_strategy()
2005        ) {
2006            let p_a = Price::new(a, precision);
2007            let p_b = Price::new(b, precision);
2008            let p_c = Price::new(c, precision);
2009
2010            let expected = p_a
2011                .raw
2012                .checked_add(p_b.raw)
2013                .and_then(|sum| sum.checked_add(p_c.raw))
2014                .filter(|sum| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(sum));
2015
2016            if let Some(expected) = expected {
2017                let left = (p_a + p_b) + p_c;
2018                let right = p_a + (p_b + p_c);
2019                prop_assert_eq!(left.raw, expected);
2020                prop_assert_eq!(right.raw, expected);
2021            }
2022        }
2023
2024        /// Property: Price addition/subtraction should be inverse operations
2025        #[rstest]
2026        fn prop_price_addition_subtraction_inverse(
2027            base in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
2028            delta in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
2029            precision in precision_strategy()
2030        ) {
2031            let p_base = Price::new(base, precision);
2032            let p_delta = Price::new(delta, precision);
2033
2034            if p_base
2035                .raw
2036                .checked_add(p_delta.raw)
2037                .is_some_and(|sum| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(&sum))
2038            {
2039                prop_assert_eq!((p_base + p_delta) - p_delta, p_base);
2040            }
2041        }
2042
2043        /// Property: Price ordering should be transitive
2044        #[rstest]
2045        fn prop_price_ordering_transitive(
2046            a in price_value_strategy(),
2047            b in price_value_strategy(),
2048            c in price_value_strategy(),
2049            precision in float_precision_strategy()
2050        ) {
2051            let p_a = Price::new(a, precision);
2052            let p_b = Price::new(b, precision);
2053            let p_c = Price::new(c, precision);
2054
2055            // If a <= b and b <= c, then a <= c
2056            if p_a <= p_b && p_b <= p_c {
2057                prop_assert!(p_a <= p_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
2058                    p_a.as_f64(), p_b.as_f64(), p_c.as_f64(), p_a.as_f64(), p_c.as_f64());
2059            }
2060        }
2061
2062        /// Property: String parsing should be consistent with precision inference
2063        #[rstest]
2064        fn prop_price_string_parsing_precision(
2065            integral in 0u32..1_000_000,
2066            fractional in 0u32..1_000_000,
2067            precision in precision_strategy_non_zero()
2068        ) {
2069            // Create a decimal string with exactly 'precision' decimal places
2070            let pow = 10u128.pow(u32::from(precision));
2071            let fractional_mod = u128::from(fractional) % pow;
2072            let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
2073            let price_str = format!("{integral}.{fractional_str}");
2074
2075            let parsed: Price = price_str.parse().unwrap();
2076            prop_assert_eq!(parsed.precision, precision);
2077
2078            // Round-trip should preserve the original string (after normalization)
2079            let round_trip = parsed.to_string();
2080            let expected_value = format!("{integral}.{fractional_str}");
2081            prop_assert_eq!(round_trip, expected_value);
2082        }
2083
2084        /// Property: Price arithmetic should never produce invalid values
2085        #[rstest]
2086        fn prop_price_arithmetic_bounds(
2087            a in price_value_strategy(),
2088            b in price_value_strategy(),
2089            precision in float_precision_strategy()
2090        ) {
2091            let p_a = Price::new(a, precision);
2092            let p_b = Price::new(b, precision);
2093
2094            // Addition should either succeed or fail predictably
2095            let sum_f64 = p_a.as_f64() + p_b.as_f64();
2096            if sum_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&sum_f64) {
2097                let sum = p_a + p_b;
2098                prop_assert!(sum.as_f64().is_finite());
2099                prop_assert!(!sum.is_undefined());
2100            }
2101
2102            // Subtraction should either succeed or fail predictably
2103            let diff_f64 = p_a.as_f64() - p_b.as_f64();
2104            if diff_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&diff_f64) {
2105                let diff = p_a - p_b;
2106                prop_assert!(diff.as_f64().is_finite());
2107                prop_assert!(!diff.is_undefined());
2108            }
2109        }
2110
2111        /// Property: checked_add agrees with Add when bounds and sentinel guards hold,
2112        /// and returns None otherwise.
2113        #[rstest]
2114        fn prop_price_checked_add_matches_spec(
2115            a in price_value_strategy(),
2116            b in price_value_strategy(),
2117            precision in float_precision_strategy()
2118        ) {
2119            let p_a = Price::new(a, precision);
2120            let p_b = Price::new(b, precision);
2121            let expected = p_a.raw
2122                .checked_add(p_b.raw)
2123                .filter(|r| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(r))
2124                .filter(|_| !p_a.is_sentinel() && !p_b.is_sentinel())
2125                .map(|raw| Price { raw, precision: p_a.precision.max(p_b.precision) });
2126            prop_assert_eq!(p_a.checked_add(p_b), expected);
2127        }
2128
2129        /// Property: checked_sub agrees with Sub when bounds and sentinel guards hold,
2130        /// and returns None otherwise.
2131        #[rstest]
2132        fn prop_price_checked_sub_matches_spec(
2133            a in price_value_strategy(),
2134            b in price_value_strategy(),
2135            precision in float_precision_strategy()
2136        ) {
2137            let p_a = Price::new(a, precision);
2138            let p_b = Price::new(b, precision);
2139            let expected = p_a.raw
2140                .checked_sub(p_b.raw)
2141                .filter(|r| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(r))
2142                .filter(|_| !p_a.is_sentinel() && !p_b.is_sentinel())
2143                .map(|raw| Price { raw, precision: p_a.precision.max(p_b.precision) });
2144            prop_assert_eq!(p_a.checked_sub(p_b), expected);
2145        }
2146    }
2147
2148    proptest! {
2149        /// Property: as_decimal scale always matches precision
2150        #[rstest]
2151        fn prop_price_as_decimal_preserves_precision(
2152            (precision, raw) in valid_precision_raw_strategy()
2153        ) {
2154            prop_assume!(decimal_compatible(raw, precision));
2155            let price = Price::from_raw(raw, precision);
2156            let decimal = price.as_decimal();
2157            prop_assert_eq!(decimal.scale(), u32::from(precision));
2158        }
2159
2160        /// Property: as_decimal and Display produce the same string
2161        #[rstest]
2162        fn prop_price_as_decimal_matches_display(
2163            value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
2164            precision in float_precision_strategy()
2165        ) {
2166            let price = Price::new(value, precision);
2167            prop_assume!(decimal_compatible(price.raw, precision));
2168            let display_str = format!("{price}");
2169            let decimal_str = price.as_decimal().to_string();
2170            prop_assert_eq!(display_str, decimal_str);
2171        }
2172
2173        /// Property: from_decimal roundtrip preserves exact value
2174        #[rstest]
2175        fn prop_price_from_decimal_roundtrip(
2176            (precision, raw) in valid_precision_raw_strategy()
2177        ) {
2178            prop_assume!(decimal_compatible(raw, precision));
2179            let original = Price::from_raw(raw, precision);
2180            let decimal = original.as_decimal();
2181            let reconstructed = Price::from_decimal(decimal).unwrap();
2182            prop_assert_eq!(original.raw, reconstructed.raw);
2183            prop_assert_eq!(original.precision, reconstructed.precision);
2184        }
2185
2186        /// Property: constructing from valid raw values preserves raw/precision fields
2187        #[rstest]
2188        fn prop_price_from_raw_round_trip(
2189            (precision, raw) in valid_precision_raw_strategy()
2190        ) {
2191            let price = Price::from_raw(raw, precision);
2192            prop_assert_eq!(price.raw, raw);
2193            prop_assert_eq!(price.precision, precision);
2194        }
2195    }
2196}