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, 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};
76#[cfg(feature = "defi")]
77use crate::types::fixed::MAX_FLOAT_PRECISION;
78
79// -----------------------------------------------------------------------------
80// PriceRaw
81// -----------------------------------------------------------------------------
82
83// Use 128-bit integers when either `high-precision` or `defi` features are enabled. This is
84// required for the extended 18-decimal wei precision used in DeFi contexts.
85
86#[cfg(feature = "high-precision")]
87pub type PriceRaw = i128;
88
89#[cfg(not(feature = "high-precision"))]
90pub type PriceRaw = i64;
91
92// -----------------------------------------------------------------------------
93
94/// The maximum raw price integer value.
95///
96/// # Safety
97///
98/// `PRICE_MAX` and `FIXED_SCALAR` are cast to `PriceRaw` before multiplying, so the
99/// scaling uses exact integer arithmetic rather than a lossy `f64` product. The result
100/// fits within `PriceRaw`'s range in both high-precision (i128) and standard-precision
101/// (i64) modes, so the multiplication cannot overflow.
102#[unsafe(no_mangle)]
103#[allow(unsafe_code)]
104pub static PRICE_RAW_MAX: PriceRaw = (PRICE_MAX as PriceRaw) * (FIXED_SCALAR as PriceRaw);
105
106/// The minimum raw price integer value.
107///
108/// # Safety
109///
110/// `PRICE_MIN` and `FIXED_SCALAR` are cast to `PriceRaw` before multiplying, so the
111/// scaling uses exact integer arithmetic rather than a lossy `f64` product. The result
112/// fits within `PriceRaw`'s range in both high-precision (i128) and standard-precision
113/// (i64) modes, so the multiplication cannot overflow.
114#[unsafe(no_mangle)]
115#[allow(unsafe_code)]
116pub static PRICE_RAW_MIN: PriceRaw = (PRICE_MIN as PriceRaw) * (FIXED_SCALAR as PriceRaw);
117
118/// The sentinel value for an unset or null price.
119pub const PRICE_UNDEF: PriceRaw = PriceRaw::MAX;
120
121/// The sentinel value for an error or invalid price.
122pub const PRICE_ERROR: PriceRaw = PriceRaw::MIN;
123
124// -----------------------------------------------------------------------------
125// PRICE_MAX
126// -----------------------------------------------------------------------------
127
128/// The maximum valid price value that can be represented.
129#[cfg(feature = "high-precision")]
130pub const PRICE_MAX: f64 = 17_014_118_346_046.0;
131
132#[cfg(not(feature = "high-precision"))]
133/// The maximum valid price value that can be represented.
134pub const PRICE_MAX: f64 = 9_223_372_036.0;
135
136// -----------------------------------------------------------------------------
137// PRICE_MIN
138// -----------------------------------------------------------------------------
139
140#[cfg(feature = "high-precision")]
141/// The minimum valid price value that can be represented.
142pub const PRICE_MIN: f64 = -17_014_118_346_046.0;
143
144#[cfg(not(feature = "high-precision"))]
145/// The minimum valid price value that can be represented.
146pub const PRICE_MIN: f64 = -9_223_372_036.0;
147
148// -----------------------------------------------------------------------------
149
150/// The sentinel `Price` representing an error, returned by C FFI functions that
151/// cannot signal errors through `Option` or `Result`.
152pub const ERROR_PRICE: Price = Price {
153    raw: 0,
154    precision: 255,
155};
156
157/// Represents a price in a market with a specified precision.
158///
159/// The number of decimal places may vary. For certain asset classes, prices may
160/// have negative values. For example, prices for options instruments can be
161/// negative under certain conditions.
162///
163/// Handles up to [`FIXED_PRECISION`] decimals of precision.
164///
165/// - [`PRICE_MAX`] - Maximum representable price value.
166/// - [`PRICE_MIN`] - Minimum representable price value.
167#[repr(C)]
168#[derive(Clone, Copy, Default, Eq)]
169#[cfg_attr(
170    feature = "python",
171    pyo3::pyclass(module = "nautilus_trader.model", frozen, from_py_object)
172)]
173#[cfg_attr(
174    feature = "python",
175    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
176)]
177pub struct Price {
178    pub(crate) raw: PriceRaw,
179    /// The number of decimal places, with a maximum of [`FIXED_PRECISION`].
180    pub precision: u8,
181}
182
183impl Price {
184    /// Creates a new [`Price`] instance with correctness checking.
185    ///
186    /// # Errors
187    ///
188    /// Returns an error if:
189    /// - `value` is invalid outside the representable range [`PRICE_MIN`, `PRICE_MAX`].
190    /// - `precision` is invalid outside the representable range [0, `FIXED_PRECISION`].
191    ///
192    /// # Notes
193    ///
194    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
195    pub fn new_checked(value: f64, precision: u8) -> CorrectnessResult<Self> {
196        check_in_range_inclusive_f64(value, PRICE_MIN, PRICE_MAX, "value")?;
197
198        #[cfg(feature = "defi")]
199        if precision > MAX_FLOAT_PRECISION {
200            // Floats are only reliable up to ~16 decimal digits of precision regardless of feature flags
201            return Err(CorrectnessError::PredicateViolation {
202                message: format!(
203                    "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Price::from_wei()` for wei values instead"
204                ),
205            });
206        }
207
208        check_fixed_precision(precision)?;
209
210        #[cfg(feature = "high-precision")]
211        let raw = f64_to_fixed_i128(value, precision);
212
213        #[cfg(not(feature = "high-precision"))]
214        let raw = f64_to_fixed_i64(value, precision);
215
216        Ok(Self { raw, precision })
217    }
218
219    /// Creates a new [`Price`] instance.
220    ///
221    /// # Panics
222    ///
223    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
224    #[must_use]
225    pub fn new(value: f64, precision: u8) -> Self {
226        Self::new_checked(value, precision).expect_display(FAILED)
227    }
228
229    /// Creates a new [`Price`] instance from the given `raw` fixed-point value and `precision`.
230    ///
231    /// # Panics
232    ///
233    /// Panics if `raw` is outside the valid range and is not a sentinel value.
234    /// Panics if `precision` exceeds [`FIXED_PRECISION`].
235    #[must_use]
236    pub fn from_raw(raw: PriceRaw, precision: u8) -> Self {
237        assert!(
238            raw == PRICE_ERROR
239                || raw == PRICE_UNDEF
240                || (raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX),
241            "`raw` value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
242        );
243
244        if raw == PRICE_UNDEF {
245            assert!(
246                precision == 0,
247                "`precision` must be 0 when `raw` is PRICE_UNDEF"
248            );
249        }
250        check_fixed_precision(precision).expect_display(FAILED);
251
252        // TODO: Enforce spurious bits validation in v2
253        // if !matches!(raw, PRICE_UNDEF | PRICE_ERROR) && raw != 0 {
254        //     #[cfg(feature = "high-precision")]
255        //     super::fixed::check_fixed_raw_i128(raw, precision).expect(FAILED);
256        //     #[cfg(not(feature = "high-precision"))]
257        //     super::fixed::check_fixed_raw_i64(raw, precision).expect(FAILED);
258        // }
259
260        Self { raw, precision }
261    }
262
263    /// Creates a new [`Price`] instance from the given `raw` fixed-point value and `precision`
264    /// with correctness checking.
265    ///
266    /// # Errors
267    ///
268    /// Returns an error if:
269    /// - `precision` exceeds the maximum fixed precision.
270    /// - `precision` is not 0 when `raw` is `PRICE_UNDEF`.
271    /// - `raw` is outside the valid range `[PRICE_RAW_MIN, PRICE_RAW_MAX]`
272    ///   and is not a sentinel value.
273    pub fn from_raw_checked(raw: PriceRaw, precision: u8) -> CorrectnessResult<Self> {
274        if raw == PRICE_UNDEF && precision != 0 {
275            return Err(CorrectnessError::PredicateViolation {
276                message: "`precision` must be 0 when `raw` is PRICE_UNDEF".to_string(),
277            });
278        }
279
280        if raw != PRICE_ERROR && raw != PRICE_UNDEF && (raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX)
281        {
282            return Err(CorrectnessError::PredicateViolation {
283                message: format!(
284                    "raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
285                ),
286            });
287        }
288
289        check_fixed_precision(precision)?;
290
291        Ok(Self { raw, precision })
292    }
293
294    /// Creates a new [`Price`] instance with a value of zero with the given `precision`.
295    ///
296    /// # Panics
297    ///
298    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
299    #[must_use]
300    pub fn zero(precision: u8) -> Self {
301        check_fixed_precision(precision).expect_display(FAILED);
302        Self { raw: 0, precision }
303    }
304
305    /// Creates a new [`Price`] instance with the maximum representable value with the given `precision`.
306    ///
307    /// # Panics
308    ///
309    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
310    #[must_use]
311    pub fn max(precision: u8) -> Self {
312        check_fixed_precision(precision).expect_display(FAILED);
313        Self {
314            raw: PRICE_RAW_MAX,
315            precision,
316        }
317    }
318
319    /// Creates a new [`Price`] instance with the minimum representable value with the given `precision`.
320    ///
321    /// # Panics
322    ///
323    /// Panics if a correctness check fails. See [`Price::new_checked`] for more details.
324    #[must_use]
325    pub fn min(precision: u8) -> Self {
326        check_fixed_precision(precision).expect_display(FAILED);
327        Self {
328            raw: PRICE_RAW_MIN,
329            precision,
330        }
331    }
332
333    /// Performs a checked addition, returning `None` on raw integer overflow, when the
334    /// result falls outside `[PRICE_RAW_MIN, PRICE_RAW_MAX]`, when either operand is a
335    /// sentinel (`PRICE_UNDEF`, `PRICE_ERROR`, or `ERROR_PRICE`), or when the operands
336    /// have mixed raw scales (one at `FIXED_PRECISION` scale, the other at a defi
337    /// `WEI_PRECISION` scale).
338    ///
339    /// Precision follows the `Add` implementation: uses the maximum precision of both operands.
340    #[must_use]
341    pub fn checked_add(self, rhs: Self) -> Option<Self> {
342        if self.is_sentinel() || rhs.is_sentinel() {
343            return None;
344        }
345
346        if !raw_scales_match(self.precision, rhs.precision) {
347            return None;
348        }
349
350        let raw = self.raw.checked_add(rhs.raw)?;
351        if raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX {
352            return None;
353        }
354
355        Some(Self {
356            raw,
357            precision: self.precision.max(rhs.precision),
358        })
359    }
360
361    /// Performs a checked subtraction, returning `None` on raw integer underflow, when
362    /// the result falls outside `[PRICE_RAW_MIN, PRICE_RAW_MAX]`, when either operand
363    /// is a sentinel (`PRICE_UNDEF`, `PRICE_ERROR`, or `ERROR_PRICE`), or when the
364    /// operands have mixed raw scales (one at `FIXED_PRECISION` scale, the other at a
365    /// defi `WEI_PRECISION` scale).
366    ///
367    /// Precision follows the `Sub` implementation: uses the maximum precision of both operands.
368    #[must_use]
369    pub fn checked_sub(self, rhs: Self) -> Option<Self> {
370        if self.is_sentinel() || rhs.is_sentinel() {
371            return None;
372        }
373
374        if !raw_scales_match(self.precision, rhs.precision) {
375            return None;
376        }
377
378        let raw = self.raw.checked_sub(rhs.raw)?;
379        if raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX {
380            return None;
381        }
382
383        Some(Self {
384            raw,
385            precision: self.precision.max(rhs.precision),
386        })
387    }
388
389    #[inline]
390    fn is_sentinel(self) -> bool {
391        // ERROR_PRICE uses precision == u8::MAX as its sentinel marker, distinct from
392        // valid high-precision values (e.g. defi `from_wei` uses precision 18 which is
393        // > FIXED_PRECISION but is not a sentinel).
394        self.raw == PRICE_UNDEF || self.raw == PRICE_ERROR || self.precision == u8::MAX
395    }
396
397    /// Returns `true` if the value of this instance is undefined.
398    #[must_use]
399    pub fn is_undefined(&self) -> bool {
400        self.raw == PRICE_UNDEF
401    }
402
403    /// Returns `true` if the value of this instance is the error sentinel.
404    #[must_use]
405    #[inline]
406    pub fn is_error(&self) -> bool {
407        self.raw == PRICE_ERROR
408    }
409
410    /// Returns the stored fixed-point integer without rescaling.
411    ///
412    /// Use this for serialization and explicit fixed-point conversions. Prefer domain
413    /// operations for calculations; the storage scale can differ from display precision.
414    ///
415    /// Direct field access is restricted to this crate:
416    ///
417    /// ```compile_fail
418    /// use nautilus_model::types::Price;
419    /// let value = Price::from("1");
420    /// let raw = value.raw;
421    /// ```
422    #[must_use]
423    #[inline]
424    pub const fn raw(&self) -> PriceRaw {
425        self.raw
426    }
427
428    /// Returns `true` if the value of this instance is zero.
429    #[must_use]
430    #[inline]
431    pub fn is_zero(&self) -> bool {
432        self.raw == 0
433    }
434
435    /// Returns `true` if the value of this instance is position (> 0).
436    #[must_use]
437    #[inline]
438    pub fn is_positive(&self) -> bool {
439        self.raw != PRICE_UNDEF && self.raw > 0
440    }
441
442    /// Returns `true` if the value of this instance is negative (< 0).
443    #[must_use]
444    #[inline]
445    pub fn is_negative(&self) -> bool {
446        self.raw != PRICE_UNDEF && self.raw < 0
447    }
448
449    #[cfg(feature = "high-precision")]
450    /// Returns the value of this instance as an `f64`.
451    ///
452    /// # Panics
453    ///
454    /// With the `defi` feature, panics if precision exceeds `MAX_FLOAT_PRECISION` (16).
455    #[must_use]
456    pub fn as_f64(&self) -> f64 {
457        #[cfg(feature = "defi")]
458        assert!(
459            self.precision <= MAX_FLOAT_PRECISION,
460            "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
461        );
462
463        fixed_i128_to_f64(self.raw)
464    }
465
466    #[cfg(not(feature = "high-precision"))]
467    /// Returns the value of this instance as an `f64`.
468    #[must_use]
469    pub fn as_f64(&self) -> f64 {
470        fixed_i64_to_f64(self.raw)
471    }
472
473    /// Returns the value of this instance as a `Decimal`.
474    #[must_use]
475    pub fn as_decimal(&self) -> Decimal {
476        // Scale down the raw value to match the precision
477        let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
478        let rescaled_raw = self.raw / PriceRaw::pow(10, u32::from(precision_diff));
479        #[allow(
480            clippy::unnecessary_cast,
481            clippy::cast_lossless,
482            reason = "cast is real when PriceRaw is i64, no-op when i128"
483        )]
484        scaled_raw_to_decimal(rescaled_raw as i128, self.precision)
485    }
486
487    /// Returns a formatted string representation of this instance.
488    #[must_use]
489    pub fn to_formatted_string(&self) -> String {
490        format!("{self}").separate_with_underscores()
491    }
492
493    fn raw_at_precision(&self) -> i128 {
494        let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
495        let rescaled_raw = self.raw / PriceRaw::pow(10, u32::from(precision_diff));
496        Self::raw_as_i128(rescaled_raw)
497    }
498
499    fn raw_as_i128(raw: PriceRaw) -> i128 {
500        #[allow(
501            clippy::useless_conversion,
502            reason = "i128::from is a widening conversion when PriceRaw is i64"
503        )]
504        i128::from(raw)
505    }
506
507    /// Creates a new [`Price`] from a `Decimal` value with specified precision.
508    ///
509    /// Uses pure integer arithmetic on the Decimal's mantissa and scale for fast conversion.
510    /// The value is rounded to the specified precision using banker's rounding (round half to even).
511    ///
512    /// # Errors
513    ///
514    /// Returns an error if:
515    /// - `precision` exceeds [`FIXED_PRECISION`].
516    /// - The decimal value cannot be converted to the raw representation.
517    /// - Overflow occurs during scaling.
518    pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> CorrectnessResult<Self> {
519        let exponent = -(decimal.scale() as i8);
520        let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
521
522        #[allow(
523            clippy::useless_conversion,
524            reason = "i128 to PriceRaw is real when not high-precision"
525        )]
526        let raw: PriceRaw =
527            raw_i128
528                .try_into()
529                .map_err(|_| CorrectnessError::PredicateViolation {
530                    message: format!(
531                        "Decimal value exceeds PriceRaw range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
532                    ),
533                })?;
534
535        if !(raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX) {
536            return Err(CorrectnessError::PredicateViolation {
537                message: format!(
538                    "Raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
539                ),
540            });
541        }
542
543        Ok(Self { raw, precision })
544    }
545
546    /// Creates a new [`Price`] from a [`Decimal`] value with precision inferred from the decimal's scale.
547    ///
548    /// The precision is determined by the scale of the decimal (number of decimal places).
549    /// The value is rounded to the inferred precision using banker's rounding (round half to even).
550    ///
551    /// # Errors
552    ///
553    /// Returns an error if:
554    /// - The inferred precision exceeds [`FIXED_PRECISION`].
555    /// - The decimal value cannot be converted to the raw representation.
556    /// - Overflow occurs during scaling.
557    pub fn from_decimal(decimal: Decimal) -> CorrectnessResult<Self> {
558        let precision = decimal.scale() as u8;
559        Self::from_decimal_dp(decimal, precision)
560    }
561
562    /// Creates a new [`Price`] from a mantissa/exponent pair using pure integer arithmetic.
563    ///
564    /// The value is `mantissa * 10^exponent`. This avoids all floating-point and Decimal
565    /// operations, making it ideal for exchange data that arrives as mantissa/exponent pairs.
566    ///
567    /// # Panics
568    ///
569    /// Panics if the resulting raw value exceeds [`PRICE_RAW_MAX`] or [`PRICE_RAW_MIN`].
570    #[must_use]
571    pub fn from_mantissa_exponent(mantissa: i64, exponent: i8, precision: u8) -> Self {
572        check_fixed_precision(precision).expect_display(FAILED);
573
574        if mantissa == 0 {
575            return Self { raw: 0, precision };
576        }
577
578        let raw_i128 = mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, precision)
579            .expect("Overflow in Price::from_mantissa_exponent");
580
581        #[allow(
582            clippy::useless_conversion,
583            reason = "i128 to PriceRaw is real when not high-precision"
584        )]
585        let raw: PriceRaw = raw_i128
586            .try_into()
587            .expect("Raw value exceeds PriceRaw range in Price::from_mantissa_exponent");
588        assert!(
589            raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX,
590            "`raw` value {raw} exceeded bounds [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
591        );
592
593        Self { raw, precision }
594    }
595
596    /// Checked variant of [`Price::from_mantissa_exponent`].
597    ///
598    /// # Errors
599    ///
600    /// Returns an error if the precision is invalid or the resulting raw value
601    /// exceeds the `PriceRaw` bounds.
602    pub fn from_mantissa_exponent_checked(
603        mantissa: i64,
604        exponent: i8,
605        precision: u8,
606    ) -> CorrectnessResult<Self> {
607        let raw = mantissa_exponent_to_raw_checked::<PriceRaw>(
608            i128::from(mantissa),
609            exponent,
610            precision,
611            "Price::from_mantissa_exponent",
612            "PriceRaw",
613            "Price",
614        )?;
615
616        Self::from_raw_checked(raw, precision)
617    }
618}
619
620impl FromStr for Price {
621    type Err = String;
622
623    fn from_str(value: &str) -> Result<Self, Self::Err> {
624        let clean_value = value.replace('_', "");
625
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 price_str: std::borrow::Cow<'de, 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}