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