Skip to main content

nautilus_model/types/
quantity.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 quantity with a non-negative value and specified precision.
17//!
18//! [`Quantity`] is an immutable value type for representing trade sizes, order quantities,
19//! and position amounts. It enforces non-negative values and provides fixed-point arithmetic
20//! for deterministic calculations.
21//!
22//! # Arithmetic behavior
23//!
24//! Adding or subtracting two `Quantity` 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//! | `Quantity + Quantity`   | `Quantity` | Precision is max of both operands.  |
32//! | `Quantity - Quantity`   | `Quantity` | Panics if result would be negative. |
33//! | `Quantity * Quantity`   | `Quantity` | Precision is max of both operands.  |
34//! | `Quantity + Decimal`    | `Decimal`  |                                     |
35//! | `Quantity - Decimal`    | `Decimal`  |                                     |
36//! | `Quantity * Decimal`    | `Decimal`  |                                     |
37//! | `Quantity / Decimal`    | `Decimal`  |                                     |
38//! | `Quantity + f64`        | `f64`      |                                     |
39//! | `Quantity - f64`        | `f64`      |                                     |
40//! | `Quantity * f64`        | `f64`      |                                     |
41//! | `Quantity / f64`        | `f64`      |                                     |
42//!
43//! Multiplication accepts mixed scales and truncates the result toward zero at the result scale.
44//!
45//! # Immutability
46//!
47//! `Quantity` is immutable. All arithmetic operations return new instances.
48
49use std::{
50    cmp::Ordering,
51    fmt::{Debug, Display},
52    hash::{Hash, Hasher},
53    iter::Sum,
54    ops::{Add, Deref, Div, Mul, Sub},
55    str::FromStr,
56};
57
58#[cfg(feature = "defi")]
59use alloy_primitives::U256;
60use nautilus_core::{
61    correctness::{
62        CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
63        check_in_range_inclusive_f64,
64    },
65    string::formatting::Separable,
66};
67use rust_decimal::Decimal;
68use serde::{Deserialize, Deserializer, Serialize};
69
70#[cfg(not(feature = "defi"))]
71use super::fixed::FIXED_SCALAR_RAW;
72#[cfg(feature = "defi")]
73use super::fixed::compare_raw;
74use super::fixed::{
75    FIXED_PRECISION, FIXED_SCALAR, canonical_raw, check_fixed_precision, checked_mul_div_fixed,
76    checked_mul_div_raw, format_scaled_u128, mantissa_exponent_to_fixed_i128,
77    mantissa_exponent_to_raw_checked, parse_decimal_mantissa, raw_scale, raw_scales_match,
78    remove_underscores, scaled_raw_to_decimal,
79};
80#[cfg(not(feature = "high-precision"))]
81use super::fixed::{f64_to_fixed_u64, fixed_u64_to_f64};
82#[cfg(feature = "high-precision")]
83use super::fixed::{f64_to_fixed_u128, fixed_u128_to_f64};
84use crate::deserialization::StringInput;
85#[cfg(feature = "defi")]
86use crate::types::fixed::MAX_FLOAT_PRECISION;
87
88// -----------------------------------------------------------------------------
89// QuantityRaw
90// -----------------------------------------------------------------------------
91
92#[cfg(feature = "high-precision")]
93pub type QuantityRaw = u128;
94
95#[cfg(not(feature = "high-precision"))]
96pub type QuantityRaw = u64;
97
98// -----------------------------------------------------------------------------
99
100/// The maximum raw quantity integer value.
101///
102/// `QUANTITY_MAX` and `FIXED_SCALAR` are cast to `QuantityRaw` before multiplying, so the
103/// scaling uses exact integer arithmetic rather than a lossy `f64` product. The result
104/// fits within `QuantityRaw`'s range in both high-precision (u128) and standard-precision
105/// (u64) modes, so the multiplication cannot overflow.
106#[unsafe(no_mangle)]
107#[allow(unsafe_code)]
108pub static QUANTITY_RAW_MAX: QuantityRaw =
109    (QUANTITY_MAX as QuantityRaw) * (FIXED_SCALAR as QuantityRaw);
110
111/// The sentinel value for an unset or null quantity.
112pub const QUANTITY_UNDEF: QuantityRaw = QuantityRaw::MAX;
113
114// -----------------------------------------------------------------------------
115// QUANTITY_MAX
116// -----------------------------------------------------------------------------
117
118#[cfg(feature = "high-precision")]
119/// The maximum valid quantity value that can be represented.
120pub const QUANTITY_MAX: f64 = 34_028_236_692_093.0;
121
122#[cfg(not(feature = "high-precision"))]
123/// The maximum valid quantity value that can be represented.
124pub const QUANTITY_MAX: f64 = 18_446_744_073.0;
125
126// -----------------------------------------------------------------------------
127
128/// The minimum valid quantity value that can be represented.
129pub const QUANTITY_MIN: f64 = 0.0;
130
131/// Represents a quantity with a non-negative value and specified precision.
132///
133/// Capable of storing either a whole number (no decimal places) of 'contracts'
134/// or 'shares' (instruments denominated in whole units) or a decimal value
135/// containing decimal places for instruments denominated in fractional units.
136///
137/// Handles up to [`FIXED_PRECISION`] decimals of precision.
138///
139/// - [`QUANTITY_MAX`] - Maximum representable quantity value.
140/// - [`QUANTITY_MIN`] - 0 (non-negative values only).
141#[repr(C)]
142#[derive(Clone, Copy, Default, Eq)]
143#[cfg_attr(
144    feature = "python",
145    pyo3::pyclass(module = "nautilus_trader.model", frozen, from_py_object)
146)]
147#[cfg_attr(
148    feature = "python",
149    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
150)]
151pub struct Quantity {
152    pub(crate) raw: QuantityRaw,
153    /// The number of decimal places, with a maximum of [`FIXED_PRECISION`].
154    pub precision: u8,
155}
156
157impl Quantity {
158    /// Creates a new [`Quantity`] instance with correctness checking.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if:
163    /// - `value` is invalid outside the representable range [0, `QUANTITY_MAX`].
164    /// - `precision` is invalid outside the representable range [0, `FIXED_PRECISION`].
165    ///
166    /// # Notes
167    ///
168    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
169    pub fn new_checked(value: f64, precision: u8) -> CorrectnessResult<Self> {
170        check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?;
171
172        #[cfg(feature = "defi")]
173        if precision > MAX_FLOAT_PRECISION {
174            // Floats are only reliable up to ~16 decimal digits of precision regardless of feature flags
175            return Err(CorrectnessError::PredicateViolation {
176                message: format!(
177                    "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Quantity::from_wei()` for wei values instead"
178                ),
179            });
180        }
181
182        check_fixed_precision(precision)?;
183
184        #[cfg(feature = "high-precision")]
185        let raw = f64_to_fixed_u128(value, precision);
186        #[cfg(not(feature = "high-precision"))]
187        let raw = f64_to_fixed_u64(value, precision);
188
189        Ok(Self { raw, precision })
190    }
191
192    /// Creates a new [`Quantity`] instance.
193    ///
194    /// # Panics
195    ///
196    /// Panics if a correctness check fails. See [`Quantity::new_checked`] for more details.
197    #[must_use]
198    pub fn new(value: f64, precision: u8) -> Self {
199        Self::new_checked(value, precision).expect_display(FAILED)
200    }
201
202    /// Creates a new [`Quantity`] instance from the given `raw` fixed-point value and `precision`.
203    ///
204    /// # Panics
205    ///
206    /// Panics if `raw` exceeds [`QUANTITY_RAW_MAX`] and is not a sentinel value.
207    /// Panics if `precision` exceeds [`FIXED_PRECISION`].
208    #[must_use]
209    pub fn from_raw(raw: QuantityRaw, precision: u8) -> Self {
210        assert!(
211            raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
212            "`raw` value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
213        );
214
215        if raw == QUANTITY_UNDEF {
216            assert!(
217                precision == 0,
218                "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
219            );
220        }
221
222        check_fixed_precision(precision).expect_display(FAILED);
223
224        // TODO: Enforce spurious bits validation in v2
225        // if raw != QUANTITY_UNDEF && raw > 0 {
226        //     #[cfg(feature = "high-precision")]
227        //     super::fixed::check_fixed_raw_u128(raw, precision).expect(FAILED);
228        //     #[cfg(not(feature = "high-precision"))]
229        //     super::fixed::check_fixed_raw_u64(raw, precision).expect(FAILED);
230        // }
231
232        Self { raw, precision }
233    }
234
235    /// Creates a new [`Quantity`] instance from the given `raw` fixed-point value and `precision`
236    /// with correctness checking.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if:
241    /// - `precision` exceeds the maximum fixed precision.
242    /// - `precision` is not 0 when `raw` is `QUANTITY_UNDEF`.
243    /// - `raw` exceeds `QUANTITY_RAW_MAX` and is not a sentinel value.
244    pub fn from_raw_checked(raw: QuantityRaw, precision: u8) -> CorrectnessResult<Self> {
245        if raw == QUANTITY_UNDEF && precision != 0 {
246            return Err(CorrectnessError::PredicateViolation {
247                message: "`precision` must be 0 when `raw` is QUANTITY_UNDEF".to_string(),
248            });
249        }
250
251        if raw != QUANTITY_UNDEF && raw > QUANTITY_RAW_MAX {
252            return Err(CorrectnessError::PredicateViolation {
253                message: format!("raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}"),
254            });
255        }
256
257        check_fixed_precision(precision)?;
258
259        Ok(Self { raw, precision })
260    }
261
262    /// Aligns the stored raw scale to an equal or higher precision without rounding.
263    ///
264    /// Returns `None` for an undefined quantity, unsupported or lower precision, or overflow.
265    #[must_use]
266    pub fn checked_rescale(self, precision: u8) -> Option<Self> {
267        if self.raw > QUANTITY_RAW_MAX
268            || precision < self.precision
269            || check_fixed_precision(self.precision).is_err()
270            || check_fixed_precision(precision).is_err()
271        {
272            return None;
273        }
274
275        let source_scale = FIXED_PRECISION.max(self.precision);
276        let target_scale = FIXED_PRECISION.max(precision);
277        let factor = QuantityRaw::pow(10, u32::from(target_scale - source_scale));
278        let raw = self.raw.checked_mul(factor)?;
279        Self::from_raw_checked(raw, precision).ok()
280    }
281
282    /// Rounds down to a multiple of `increment`, using its precision for the result.
283    ///
284    /// Returns zero for quantities below one increment. Returns `None` for a zero increment,
285    /// invalid operands, or overflow while aligning raw scales.
286    #[must_use]
287    pub fn checked_floor_to_increment(self, increment: Self) -> Option<Self> {
288        if increment.is_zero() {
289            return None;
290        }
291
292        let precision = self.precision.max(increment.precision);
293        let value = self.checked_rescale(precision)?;
294        let step = increment.checked_rescale(precision)?;
295        let raw = value.raw - value.raw % step.raw;
296        let scale = QuantityRaw::pow(
297            10,
298            u32::from(precision.max(FIXED_PRECISION) - increment.precision.max(FIXED_PRECISION)),
299        );
300
301        Self::from_raw_checked(raw / scale, increment.precision).ok()
302    }
303
304    /// Performs a checked addition, returning `None` on raw integer overflow, when the
305    /// result exceeds `QUANTITY_RAW_MAX`, when either operand is `QUANTITY_UNDEF`, or
306    /// when the operands have mixed raw scales (one at `FIXED_PRECISION` scale, the
307    /// other at a defi `WEI_PRECISION` scale).
308    ///
309    /// Precision follows the `Add` implementation: uses the maximum precision of both operands.
310    #[must_use]
311    pub fn checked_add(self, rhs: Self) -> Option<Self> {
312        if self.raw == QUANTITY_UNDEF || rhs.raw == QUANTITY_UNDEF {
313            return None;
314        }
315
316        if !raw_scales_match(self.precision, rhs.precision) {
317            return None;
318        }
319
320        let raw = self.raw.checked_add(rhs.raw)?;
321        if raw > QUANTITY_RAW_MAX {
322            return None;
323        }
324
325        Some(Self {
326            raw,
327            precision: self.precision.max(rhs.precision),
328        })
329    }
330
331    /// Performs a checked subtraction, returning `None` if `rhs` is greater than `self`,
332    /// when either operand is `QUANTITY_UNDEF`, or when the operands have mixed raw
333    /// scales (one at `FIXED_PRECISION` scale, the other at a defi `WEI_PRECISION` scale).
334    ///
335    /// Precision follows the `Sub` implementation: uses the maximum precision of both operands.
336    #[must_use]
337    pub fn checked_sub(self, rhs: Self) -> Option<Self> {
338        if self.raw == QUANTITY_UNDEF || rhs.raw == QUANTITY_UNDEF {
339            return None;
340        }
341
342        if !raw_scales_match(self.precision, rhs.precision) {
343            return None;
344        }
345
346        let raw = self.raw.checked_sub(rhs.raw)?;
347
348        Some(Self {
349            raw,
350            precision: self.precision.max(rhs.precision),
351        })
352    }
353
354    /// Adds two quantities, clamping the result to [`QUANTITY_RAW_MAX`].
355    ///
356    /// Precision follows `Add`: the result uses the maximum operand precision.
357    ///
358    /// # Panics
359    ///
360    /// Panics if the operands have mismatched effective fixed-point scales.
361    #[must_use]
362    pub fn saturating_add(self, rhs: Self) -> Self {
363        assert!(
364            raw_scales_match(self.precision, rhs.precision),
365            "Cannot add `Quantity` values with mismatched decimal scales"
366        );
367
368        Self {
369            raw: self.raw.saturating_add(rhs.raw).min(QUANTITY_RAW_MAX),
370            precision: self.precision.max(rhs.precision),
371        }
372    }
373
374    /// Computes a saturating subtraction between two quantities, logging when clamped.
375    ///
376    /// Operands must use the same effective fixed-point scale. The Python binding raises
377    /// `ValueError` for mismatched scales.
378    ///
379    /// When `rhs` is greater than `self`, the result is clamped to zero and a warning is logged.
380    /// Precision follows the `Sub` implementation: uses the maximum precision of both operands.
381    ///
382    /// # Panics
383    ///
384    /// Panics if the operands have mismatched effective fixed-point scales.
385    #[must_use]
386    pub fn saturating_sub(self, rhs: Self) -> Self {
387        assert!(
388            raw_scales_match(self.precision, rhs.precision),
389            "Cannot subtract `Quantity` values with mismatched decimal scales"
390        );
391        let precision = self.precision.max(rhs.precision);
392        let raw = self.raw.saturating_sub(rhs.raw);
393        if raw == 0 && self.raw < rhs.raw {
394            log::warn!(
395                "Saturating Quantity subtraction: {self} - {rhs} < 0, clamped to 0 (precision={precision})"
396            );
397        }
398
399        Self { raw, precision }
400    }
401
402    /// Creates a new [`Quantity`] instance with a value of zero with the given `precision`.
403    ///
404    /// # Panics
405    ///
406    /// Panics if `precision` exceeds the maximum allowed by [`check_fixed_precision`].
407    #[must_use]
408    pub fn zero(precision: u8) -> Self {
409        check_fixed_precision(precision).expect_display(FAILED);
410        Self { raw: 0, precision }
411    }
412
413    /// Returns `true` if the value of this instance is undefined.
414    #[must_use]
415    pub fn is_undefined(&self) -> bool {
416        self.raw == QUANTITY_UNDEF
417    }
418
419    /// Returns the stored fixed-point integer without rescaling.
420    ///
421    /// Use this for serialization and explicit fixed-point conversions. Prefer domain
422    /// operations for calculations; the storage scale can differ from display precision.
423    ///
424    /// Direct field access is restricted to this crate:
425    ///
426    /// ```compile_fail
427    /// use nautilus_model::types::Quantity;
428    /// let value = Quantity::from("1");
429    /// let raw = value.raw;
430    /// ```
431    #[must_use]
432    #[inline]
433    pub const fn raw(&self) -> QuantityRaw {
434        self.raw
435    }
436
437    /// Returns `true` if the value of this instance is zero.
438    #[must_use]
439    #[inline]
440    pub fn is_zero(&self) -> bool {
441        self.raw == 0
442    }
443
444    /// Returns `true` if the stored value of this instance is nonzero.
445    #[must_use]
446    #[inline]
447    pub fn non_zero(&self) -> bool {
448        self.raw != 0
449    }
450
451    /// Returns `true` if the value of this instance is position (> 0).
452    #[must_use]
453    #[inline]
454    pub fn is_positive(&self) -> bool {
455        self.raw != QUANTITY_UNDEF && self.raw > 0
456    }
457
458    #[cfg(feature = "high-precision")]
459    /// Returns the value of this instance as an `f64`.
460    ///
461    /// # Panics
462    ///
463    /// Panics if the `defi` feature is enabled and precision exceeds `MAX_FLOAT_PRECISION` (16).
464    #[must_use]
465    pub fn as_f64(&self) -> f64 {
466        #[cfg(feature = "defi")]
467        assert!(
468            self.precision <= MAX_FLOAT_PRECISION,
469            "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
470        );
471
472        fixed_u128_to_f64(self.raw)
473    }
474
475    #[cfg(not(feature = "high-precision"))]
476    /// Returns the value of this instance as an `f64`.
477    #[must_use]
478    pub fn as_f64(&self) -> f64 {
479        fixed_u64_to_f64(self.raw)
480    }
481
482    /// Returns the value of this instance as a `Decimal`.
483    #[must_use]
484    pub fn as_decimal(&self) -> Decimal {
485        // Scale down the raw value to match the precision
486        let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
487        let rescaled_raw = self.raw / QuantityRaw::pow(10, u32::from(precision_diff));
488
489        // The raw value is guaranteed to be within i128 range after scaling
490        // because our quantity constraints ensure the maximum raw value times the scaling
491        // factor cannot exceed i128::MAX (high-precision) or i64::MAX (standard-precision).
492        #[allow(
493            clippy::unnecessary_cast,
494            clippy::cast_lossless,
495            reason = "cast is real when QuantityRaw is u64, no-op when u128"
496        )]
497        scaled_raw_to_decimal(rescaled_raw as i128, self.precision)
498    }
499
500    /// Returns a raw fixed-point quantity as a `Decimal`.
501    #[must_use]
502    #[allow(
503        clippy::unnecessary_fallible_conversions,
504        reason = "try_from is infallible when QuantityRaw is u64, fallible when u128"
505    )]
506    #[cfg(not(feature = "defi"))]
507    pub(crate) fn raw_as_decimal(raw: QuantityRaw) -> Decimal {
508        let whole =
509            i128::try_from(raw / FIXED_SCALAR_RAW).expect("Whole raw quantity must fit in Decimal");
510        let fractional = i128::try_from(raw % FIXED_SCALAR_RAW)
511            .expect("Fractional raw quantity must fit in Decimal");
512
513        Decimal::from(whole) + Decimal::from_i128_with_scale(fractional, u32::from(FIXED_PRECISION))
514    }
515
516    /// Returns a formatted string representation of this instance.
517    #[must_use]
518    pub fn to_formatted_string(&self) -> String {
519        format!("{self}").separate_with_underscores()
520    }
521
522    fn raw_at_precision(&self) -> QuantityRaw {
523        let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
524        self.raw / QuantityRaw::pow(10, u32::from(precision_diff))
525    }
526
527    fn raw_as_u128(raw: QuantityRaw) -> u128 {
528        #[allow(
529            clippy::useless_conversion,
530            reason = "u128::from is a widening conversion when QuantityRaw is u64"
531        )]
532        u128::from(raw)
533    }
534
535    /// Creates a new [`Quantity`] from a `Decimal` value with specified precision.
536    ///
537    /// Uses pure integer arithmetic on the Decimal's mantissa and scale for fast conversion.
538    /// The value is rounded to the specified precision using banker's rounding (round half to even).
539    ///
540    /// # Errors
541    ///
542    /// Returns an error if:
543    /// - `precision` exceeds [`FIXED_PRECISION`].
544    /// - The decimal value is negative.
545    /// - The decimal value cannot be converted to the raw representation.
546    /// - Overflow occurs during scaling.
547    pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> CorrectnessResult<Self> {
548        if decimal.mantissa() < 0 {
549            return Err(CorrectnessError::PredicateViolation {
550                message: format!(
551                    "Decimal value '{decimal}' is negative, Quantity must be non-negative"
552                ),
553            });
554        }
555
556        let exponent = -(decimal.scale() as i8);
557        let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
558
559        let raw: QuantityRaw =
560            raw_i128
561                .try_into()
562                .map_err(|_| CorrectnessError::PredicateViolation {
563                    message: format!(
564                        "Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]"
565                    ),
566                })?;
567
568        if raw > QUANTITY_RAW_MAX {
569            return Err(CorrectnessError::PredicateViolation {
570                message: format!(
571                    "Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
572                ),
573            });
574        }
575
576        Ok(Self { raw, precision })
577    }
578
579    /// Creates a new [`Quantity`] from a [`Decimal`] value with precision inferred from the decimal's scale.
580    ///
581    /// The precision is determined by the scale of the decimal (number of decimal places).
582    /// The value is rounded to the inferred precision using banker's rounding (round half to even).
583    ///
584    /// # Errors
585    ///
586    /// Returns an error if:
587    /// - The inferred precision exceeds [`FIXED_PRECISION`].
588    /// - The decimal value cannot be converted to the raw representation.
589    /// - Overflow occurs during scaling.
590    pub fn from_decimal(decimal: Decimal) -> CorrectnessResult<Self> {
591        let precision = decimal.scale() as u8;
592        Self::from_decimal_dp(decimal, precision)
593    }
594
595    /// Creates a new [`Quantity`] from a mantissa/exponent pair using pure integer arithmetic.
596    ///
597    /// The value is `mantissa * 10^exponent`. This avoids all floating-point and Decimal
598    /// operations, making it ideal for exchange data that arrives as mantissa/exponent pairs.
599    ///
600    /// # Panics
601    ///
602    /// Panics if the resulting raw value exceeds [`QUANTITY_RAW_MAX`].
603    #[must_use]
604    pub fn from_mantissa_exponent(mantissa: u64, exponent: i8, precision: u8) -> Self {
605        check_fixed_precision(precision).expect_display(FAILED);
606
607        if mantissa == 0 {
608            return Self { raw: 0, precision };
609        }
610
611        let raw_i128 = mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, precision)
612            .expect("Overflow in Quantity::from_mantissa_exponent");
613
614        let raw: QuantityRaw = raw_i128
615            .try_into()
616            .expect("Raw value exceeds QuantityRaw range in Quantity::from_mantissa_exponent");
617        assert!(
618            raw <= QUANTITY_RAW_MAX,
619            "`raw` value {raw} exceeded QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
620        );
621
622        Self { raw, precision }
623    }
624
625    /// Checked variant of [`Quantity::from_mantissa_exponent`].
626    ///
627    /// # Errors
628    ///
629    /// Returns an error if the precision is invalid or the resulting raw value
630    /// exceeds [`QUANTITY_RAW_MAX`].
631    pub fn from_mantissa_exponent_checked(
632        mantissa: u64,
633        exponent: i8,
634        precision: u8,
635    ) -> CorrectnessResult<Self> {
636        let raw = mantissa_exponent_to_raw_checked::<QuantityRaw>(
637            i128::from(mantissa),
638            exponent,
639            precision,
640            "Quantity::from_mantissa_exponent",
641            "QuantityRaw",
642            "Quantity",
643        )?;
644
645        Self::from_raw_checked(raw, precision)
646    }
647
648    /// Creates a new [`Quantity`] from a U256 amount with specified precision.
649    ///
650    /// # Errors
651    ///
652    /// Returns an error if:
653    /// - Overflow occurs during scaling when precision is less than [`FIXED_PRECISION`].
654    /// - The scaled U256 amount exceeds the `QuantityRaw` range.
655    #[cfg(feature = "defi")]
656    pub fn from_u256(amount: U256, precision: u8) -> CorrectnessResult<Self> {
657        // Quantity expects raw values scaled to at least FIXED_PRECISION or higher(WEI)
658        let scaled_amount = if precision < FIXED_PRECISION {
659            amount
660                .checked_mul(U256::from(
661                    10u128.pow(u32::from(FIXED_PRECISION - precision)),
662                ))
663                .ok_or_else(|| CorrectnessError::PredicateViolation {
664                    message: format!(
665                        "Amount overflow during scaling to fixed precision: {} * 10^{}",
666                        amount,
667                        FIXED_PRECISION - precision
668                    ),
669                })?
670        } else {
671            amount
672        };
673
674        let raw = QuantityRaw::try_from(scaled_amount).map_err(|_| {
675            CorrectnessError::PredicateViolation {
676                message: format!("U256 scaled amount {scaled_amount} exceeds QuantityRaw range"),
677            }
678        })?;
679
680        Self::from_raw_checked(raw, precision)
681    }
682}
683
684impl From<Quantity> for f64 {
685    fn from(qty: Quantity) -> Self {
686        qty.as_f64()
687    }
688}
689
690impl From<&Quantity> for f64 {
691    fn from(qty: &Quantity) -> Self {
692        qty.as_f64()
693    }
694}
695
696impl From<i32> for Quantity {
697    /// Creates a `Quantity` from an `i32` value.
698    ///
699    /// # Panics
700    ///
701    /// Panics if `value` is negative. Use `u32` for guaranteed non-negative values.
702    fn from(value: i32) -> Self {
703        assert!(
704            value >= 0,
705            "Cannot create Quantity from negative i32: {value}. Use u32 or check value is non-negative."
706        );
707        Self::from_mantissa_exponent(u64::from(value.cast_unsigned()), 0, 0)
708    }
709}
710
711impl From<i64> for Quantity {
712    /// Creates a `Quantity` from an `i64` value.
713    ///
714    /// # Panics
715    ///
716    /// Panics if `value` is negative. Use `u64` for guaranteed non-negative values.
717    fn from(value: i64) -> Self {
718        assert!(
719            value >= 0,
720            "Cannot create Quantity from negative i64: {value}. Use u64 or check value is non-negative."
721        );
722        Self::from_mantissa_exponent(value.cast_unsigned(), 0, 0)
723    }
724}
725
726impl From<u32> for Quantity {
727    fn from(value: u32) -> Self {
728        Self::from_mantissa_exponent(u64::from(value), 0, 0)
729    }
730}
731
732impl From<u64> for Quantity {
733    fn from(value: u64) -> Self {
734        Self::from_mantissa_exponent(value, 0, 0)
735    }
736}
737
738impl Hash for Quantity {
739    fn hash<H: Hasher>(&self, state: &mut H) {
740        canonical_raw(self.raw, self.precision).hash(state);
741    }
742}
743
744impl PartialEq for Quantity {
745    #[inline]
746    fn eq(&self, other: &Self) -> bool {
747        self.cmp(other) == Ordering::Equal
748    }
749}
750
751impl PartialOrd for Quantity {
752    #[inline]
753    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
754        Some(self.cmp(other))
755    }
756}
757
758impl Ord for Quantity {
759    #[inline]
760    fn cmp(&self, other: &Self) -> Ordering {
761        #[cfg(feature = "defi")]
762        {
763            compare_raw(self.raw, self.precision, other.raw, other.precision)
764        }
765
766        #[cfg(not(feature = "defi"))]
767        {
768            self.raw.cmp(&other.raw)
769        }
770    }
771}
772
773impl Deref for Quantity {
774    type Target = QuantityRaw;
775
776    fn deref(&self) -> &Self::Target {
777        &self.raw
778    }
779}
780
781impl Add for Quantity {
782    type Output = Self;
783    #[inline]
784    fn add(self, rhs: Self) -> Self::Output {
785        #[cfg(feature = "defi")]
786        assert!(
787            raw_scales_match(self.precision, rhs.precision),
788            "Cannot add `Quantity` values with mismatched decimal scales"
789        );
790        Self {
791            raw: self
792                .raw
793                .checked_add(rhs.raw)
794                .expect("Overflow occurred when adding `Quantity`"),
795            precision: self.precision.max(rhs.precision),
796        }
797    }
798}
799
800impl Sum for Quantity {
801    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
802        iter.reduce(|acc, x| acc + x)
803            .unwrap_or_else(|| Self::zero(0))
804    }
805}
806
807impl<'a> Sum<&'a Self> for Quantity {
808    fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
809        iter.copied().sum()
810    }
811}
812
813impl Sub for Quantity {
814    type Output = Self;
815    #[inline]
816    fn sub(self, rhs: Self) -> Self::Output {
817        #[cfg(feature = "defi")]
818        assert!(
819            raw_scales_match(self.precision, rhs.precision),
820            "Cannot subtract `Quantity` values with mismatched decimal scales"
821        );
822        Self {
823            raw: self
824                .raw
825                .checked_sub(rhs.raw)
826                .expect("Underflow occurred when subtracting `Quantity`"),
827            precision: self.precision.max(rhs.precision),
828        }
829    }
830}
831
832impl Mul for Quantity {
833    type Output = Self;
834    fn mul(self, rhs: Self) -> Self::Output {
835        let result_raw = if self.raw != QUANTITY_UNDEF
836            && rhs.raw != QUANTITY_UNDEF
837            && self.precision <= FIXED_PRECISION
838            && rhs.precision <= FIXED_PRECISION
839        {
840            checked_mul_div_fixed(self.raw, rhs.raw)
841        } else {
842            let scalar = QuantityRaw::try_from(raw_scale(self.precision.min(rhs.precision)))
843                .expect("Fixed-point scale fits QuantityRaw");
844            checked_mul_div_raw(self.raw, rhs.raw, scalar)
845        }
846        .filter(|raw| *raw <= QUANTITY_RAW_MAX)
847        .expect("Overflow occurred when multiplying `Quantity`");
848
849        Self {
850            raw: result_raw,
851            precision: self.precision.max(rhs.precision),
852        }
853    }
854}
855
856impl Add<Decimal> for Quantity {
857    type Output = Decimal;
858    fn add(self, rhs: Decimal) -> Self::Output {
859        self.as_decimal() + rhs
860    }
861}
862
863impl Sub<Decimal> for Quantity {
864    type Output = Decimal;
865    fn sub(self, rhs: Decimal) -> Self::Output {
866        self.as_decimal() - rhs
867    }
868}
869
870impl Mul<Decimal> for Quantity {
871    type Output = Decimal;
872    fn mul(self, rhs: Decimal) -> Self::Output {
873        self.as_decimal() * rhs
874    }
875}
876
877impl Div<Decimal> for Quantity {
878    type Output = Decimal;
879    fn div(self, rhs: Decimal) -> Self::Output {
880        self.as_decimal() / rhs
881    }
882}
883
884impl Add<f64> for Quantity {
885    type Output = f64;
886    fn add(self, rhs: f64) -> Self::Output {
887        self.as_f64() + rhs
888    }
889}
890
891impl Sub<f64> for Quantity {
892    type Output = f64;
893    fn sub(self, rhs: f64) -> Self::Output {
894        self.as_f64() - rhs
895    }
896}
897
898impl Mul<f64> for Quantity {
899    type Output = f64;
900    fn mul(self, rhs: f64) -> Self::Output {
901        self.as_f64() * rhs
902    }
903}
904
905impl Div<f64> for Quantity {
906    type Output = f64;
907    fn div(self, rhs: f64) -> Self::Output {
908        self.as_f64() / rhs
909    }
910}
911
912impl From<Quantity> for QuantityRaw {
913    fn from(value: Quantity) -> Self {
914        value.raw
915    }
916}
917
918impl From<&Quantity> for QuantityRaw {
919    fn from(value: &Quantity) -> Self {
920        value.raw
921    }
922}
923
924impl From<Quantity> for Decimal {
925    fn from(value: Quantity) -> Self {
926        value.as_decimal()
927    }
928}
929
930impl From<&Quantity> for Decimal {
931    fn from(value: &Quantity) -> Self {
932        value.as_decimal()
933    }
934}
935
936impl FromStr for Quantity {
937    type Err = String;
938
939    fn from_str(value: &str) -> Result<Self, Self::Err> {
940        let clean_value = remove_underscores(value);
941        if clean_value.contains('e') || clean_value.contains('E') {
942            let decimal = Decimal::from_scientific(&clean_value)
943                .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?;
944            let precision = decimal.scale() as u8;
945            return Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string());
946        }
947
948        let (mantissa, precision) = parse_decimal_mantissa(&clean_value)
949            .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?;
950        if mantissa < 0 {
951            return Err(format!(
952                "Decimal value '{clean_value}' is negative, Quantity must be non-negative"
953            ));
954        }
955        let exponent = -i8::try_from(precision).map_err(|e| e.to_string())?;
956        let raw = mantissa_exponent_to_raw_checked::<QuantityRaw>(
957            mantissa,
958            exponent,
959            precision,
960            "Quantity::from_str",
961            "QuantityRaw",
962            "Quantity",
963        )
964        .map_err(|e| e.to_string())?;
965        Self::from_raw_checked(raw, precision).map_err(|e| e.to_string())
966    }
967}
968
969impl From<&str> for Quantity {
970    fn from(value: &str) -> Self {
971        Self::from_str(value).expect(FAILED)
972    }
973}
974
975impl From<String> for Quantity {
976    fn from(value: String) -> Self {
977        Self::from_str(&value).expect(FAILED)
978    }
979}
980
981impl From<&String> for Quantity {
982    fn from(value: &String) -> Self {
983        Self::from_str(value).expect(FAILED)
984    }
985}
986
987impl Debug for Quantity {
988    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
989        write!(
990            f,
991            "{}({})",
992            stringify!(Quantity),
993            format_scaled_u128(Self::raw_as_u128(self.raw_at_precision()), self.precision),
994        )
995    }
996}
997
998impl Display for Quantity {
999    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1000        write!(
1001            f,
1002            "{}",
1003            format_scaled_u128(Self::raw_as_u128(self.raw_at_precision()), self.precision),
1004        )
1005    }
1006}
1007
1008impl Serialize for Quantity {
1009    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1010    where
1011        S: serde::Serializer,
1012    {
1013        serializer.serialize_str(&self.to_string())
1014    }
1015}
1016
1017impl<'de> Deserialize<'de> for Quantity {
1018    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1019    where
1020        D: Deserializer<'de>,
1021    {
1022        let StringInput(qty_str) = Deserialize::deserialize(deserializer)?;
1023        Self::from_str(qty_str.as_ref()).map_err(serde::de::Error::custom)
1024    }
1025}
1026
1027/// Checks if the quantity `value` is positive.
1028///
1029/// # Errors
1030///
1031/// Returns an error if `value` is not positive.
1032pub fn check_positive_quantity(value: Quantity, param: &str) -> CorrectnessResult<()> {
1033    if !value.is_positive() {
1034        return Err(CorrectnessError::NotPositive {
1035            param: param.to_string(),
1036            value: value.to_string(),
1037            type_name: "`Quantity`",
1038        });
1039    }
1040    Ok(())
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045    use std::str::FromStr;
1046
1047    use nautilus_core::{approx_eq, correctness::CorrectnessError};
1048    use rstest::rstest;
1049    use rust_decimal_macros::dec;
1050
1051    use super::*;
1052    use crate::types::fixed::FIXED_SCALAR_RAW;
1053    #[cfg(not(feature = "defi"))]
1054    use crate::types::fixed::MAX_FLOAT_PRECISION;
1055
1056    #[cfg(feature = "high-precision")]
1057    #[rstest]
1058    #[case(QUANTITY_RAW_MAX, dec!(34028236692093))]
1059    #[case(80_000_000_000_000_000_000_000_000_000, dec!(8000000000000))]
1060    fn test_as_decimal_above_decimal_mantissa(#[case] raw: QuantityRaw, #[case] expected: Decimal) {
1061        // Regression: a precision-16 quantity above roughly 7.92e12 rescales to a raw value
1062        // beyond `Decimal`'s 96-bit mantissa, which used to panic during conversion.
1063        let qty = Quantity::from_raw(raw, 16);
1064
1065        assert_eq!(qty.as_decimal(), expected);
1066    }
1067
1068    #[rstest]
1069    fn test_max_quantity_round_trips_through_raw() {
1070        // Regression: a lossy `f64` scalar previously left `QUANTITY_RAW_MAX` below the raw
1071        // produced by `new` at the maximum, causing spurious panics and overflow errors.
1072        let qty = Quantity::new(QUANTITY_MAX, 0);
1073
1074        assert_eq!(qty.raw, QUANTITY_RAW_MAX);
1075        assert!(Quantity::from_raw_checked(qty.raw, 0).is_ok());
1076        assert!(qty.checked_add(Quantity::zero(0)).is_some());
1077    }
1078
1079    #[rstest]
1080    fn test_check_quantity_positive() {
1081        let qty = Quantity::new(0.0, 0);
1082        let error = check_positive_quantity(qty, "qty").unwrap_err();
1083
1084        assert_eq!(
1085            error,
1086            CorrectnessError::NotPositive {
1087                param: "qty".to_string(),
1088                value: "0".to_string(),
1089                type_name: "`Quantity`",
1090            }
1091        );
1092        assert_eq!(
1093            error.to_string(),
1094            "invalid `Quantity` for 'qty' not positive, was 0"
1095        );
1096    }
1097
1098    #[rstest]
1099    #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
1100    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 17")]
1101    fn test_invalid_precision_new() {
1102        // Precision 17 should fail due to DeFi validation
1103        let _ = Quantity::new(1.0, 17);
1104    }
1105
1106    #[rstest]
1107    #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
1108    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
1109    fn test_invalid_precision_new() {
1110        // Precision 17 should fail due to DeFi validation
1111        let _ = Quantity::new(1.0, 17);
1112    }
1113
1114    #[rstest]
1115    #[cfg(not(feature = "defi"))]
1116    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
1117    fn test_invalid_precision_from_raw() {
1118        // Precision out of range for fixed
1119        let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
1120    }
1121
1122    #[rstest]
1123    #[cfg(not(feature = "defi"))]
1124    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
1125    fn test_invalid_precision_zero() {
1126        // Precision out of range for fixed
1127        let _ = Quantity::zero(FIXED_PRECISION + 1);
1128    }
1129
1130    #[rstest]
1131    fn test_mixed_precision_add() {
1132        let q1 = Quantity::new(1.0, 1);
1133        let q2 = Quantity::new(1.0, 2);
1134        let result = q1 + q2;
1135        assert_eq!(result.precision, 2);
1136        assert_eq!(result.as_f64(), 2.0);
1137    }
1138
1139    #[rstest]
1140    fn test_sum_owned_quantities() {
1141        let quantities = [Quantity::new(1.25, 2), Quantity::new(2.75, 2)];
1142        let result: Quantity = quantities.into_iter().sum();
1143
1144        assert_eq!(result.as_decimal(), dec!(4.00));
1145        assert_eq!(result.precision, 2);
1146    }
1147
1148    #[rstest]
1149    fn test_sum_borrowed_quantities() {
1150        let quantities = [Quantity::new(0.125, 3), Quantity::new(0.375, 3)];
1151        let result: Quantity = quantities.iter().sum();
1152
1153        assert_eq!(result.as_decimal(), dec!(0.500));
1154        assert_eq!(result.precision, 3);
1155    }
1156
1157    #[rstest]
1158    fn test_sum_mixed_precision_quantities() {
1159        let quantities = [
1160            Quantity::new(1.2, 1),
1161            Quantity::new(3.45, 2),
1162            Quantity::new(0.006, 3),
1163        ];
1164        let result: Quantity = quantities.into_iter().sum();
1165
1166        assert_eq!(result.as_decimal(), dec!(4.656));
1167        assert_eq!(result.precision, 3);
1168    }
1169
1170    #[rstest]
1171    fn test_sum_empty_quantity_iterator() {
1172        let result: Quantity = std::iter::empty::<Quantity>().sum();
1173
1174        assert_eq!(result.as_decimal(), dec!(0));
1175        assert_eq!(result.precision, 0);
1176    }
1177
1178    #[rstest]
1179    fn test_mixed_precision_sub() {
1180        let q1 = Quantity::new(2.0, 1);
1181        let q2 = Quantity::new(1.0, 2);
1182        let result = q1 - q2;
1183        assert_eq!(result.precision, 2);
1184        assert_eq!(result.as_f64(), 1.0);
1185    }
1186
1187    #[rstest]
1188    fn test_mixed_precision_mul() {
1189        let q1 = Quantity::new(2.0, 1);
1190        let q2 = Quantity::new(3.0, 2);
1191        let result = q1 * q2;
1192        assert_eq!(result.precision, 2);
1193        assert_eq!(result.as_f64(), 6.0);
1194    }
1195
1196    #[rstest]
1197    #[case(0, false)]
1198    #[case(1, true)]
1199    #[case(QUANTITY_UNDEF, true)]
1200    fn test_non_zero(#[case] raw: QuantityRaw, #[case] expected: bool) {
1201        let qty = Quantity::from_raw(raw, 0);
1202        assert_eq!(qty.non_zero(), expected);
1203    }
1204
1205    #[rstest]
1206    fn test_new() {
1207        let value = 0.00812;
1208        let qty = Quantity::new(value, 8);
1209        assert_eq!(qty, qty);
1210        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1211        assert_eq!(qty.precision, 8);
1212        assert_eq!(qty, Quantity::from("0.00812000"));
1213        assert_eq!(qty.as_decimal(), dec!(0.00812000));
1214        assert_eq!(qty.to_string(), "0.00812000");
1215        assert!(qty.non_zero());
1216        assert!(qty.is_positive());
1217        assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
1218    }
1219
1220    #[rstest]
1221    fn test_check_quantity_positive_ok() {
1222        let qty = Quantity::new(10.0, 0);
1223        check_positive_quantity(qty, "qty").unwrap();
1224    }
1225
1226    #[rstest]
1227    fn test_negative_quantity_validation() {
1228        assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
1229    }
1230
1231    #[rstest]
1232    fn test_new_checked_returns_typed_error_with_stable_display() {
1233        let error = Quantity::new_checked(QUANTITY_MAX + 1.0, FIXED_PRECISION).unwrap_err();
1234
1235        assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
1236        assert_eq!(
1237            error.to_string(),
1238            format!(
1239                "invalid f64 for 'value' not in range [{QUANTITY_MIN}, {QUANTITY_MAX}], was {}",
1240                QUANTITY_MAX + 1.0
1241            )
1242        );
1243    }
1244
1245    #[rstest]
1246    fn test_from_raw_checked_returns_typed_error_with_stable_display() {
1247        let error = Quantity::from_raw_checked(QUANTITY_UNDEF, 3).unwrap_err();
1248
1249        assert_eq!(
1250            error,
1251            CorrectnessError::PredicateViolation {
1252                message: "`precision` must be 0 when `raw` is QUANTITY_UNDEF".to_string(),
1253            }
1254        );
1255        assert_eq!(
1256            error.to_string(),
1257            "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
1258        );
1259    }
1260
1261    #[rstest]
1262    fn test_undefined() {
1263        let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
1264        assert_eq!(qty.raw, QUANTITY_UNDEF);
1265        assert!(qty.is_undefined());
1266    }
1267
1268    #[rstest]
1269    fn test_zero() {
1270        let qty = Quantity::zero(8);
1271        assert_eq!(qty.raw, 0);
1272        assert_eq!(qty.precision, 8);
1273        assert!(qty.is_zero());
1274        assert!(!qty.is_positive());
1275    }
1276
1277    #[rstest]
1278    fn test_from_i32_exact() {
1279        let values = [0, 1, i32::MAX];
1280        let quantities = values.map(Quantity::from);
1281        let expected =
1282            values.map(|value| (QuantityRaw::try_from(value).unwrap() * FIXED_SCALAR_RAW, 0));
1283
1284        assert_eq!(
1285            quantities.map(|quantity| (quantity.raw, quantity.precision)),
1286            expected
1287        );
1288    }
1289
1290    #[rstest]
1291    fn test_from_i64_exact() {
1292        let max = quantity_max_i64();
1293        let values = [0, 1, max];
1294        let quantities = values.map(Quantity::from);
1295        let expected =
1296            values.map(|value| (QuantityRaw::try_from(value).unwrap() * FIXED_SCALAR_RAW, 0));
1297
1298        assert_eq!(
1299            quantities.map(|quantity| (quantity.raw, quantity.precision)),
1300            expected
1301        );
1302    }
1303
1304    #[rstest]
1305    fn test_from_u32_exact() {
1306        let values = [0, 1, u32::MAX];
1307        let quantities = values.map(Quantity::from);
1308        let expected = values.map(|value| (QuantityRaw::from(value) * FIXED_SCALAR_RAW, 0));
1309
1310        assert_eq!(
1311            quantities.map(|quantity| (quantity.raw, quantity.precision)),
1312            expected
1313        );
1314    }
1315
1316    #[rstest]
1317    fn test_from_u64_exact() {
1318        let max = quantity_max_u64();
1319        let values = [0, 1, max];
1320        let quantities = values.map(Quantity::from);
1321        let expected = values.map(|value| (QuantityRaw::from(value) * FIXED_SCALAR_RAW, 0));
1322
1323        assert_eq!(
1324            quantities.map(|quantity| (quantity.raw, quantity.precision)),
1325            expected
1326        );
1327    }
1328
1329    #[rstest]
1330    #[should_panic(
1331        expected = "Cannot create Quantity from negative i32: -1. Use u32 or check value is non-negative."
1332    )]
1333    fn test_from_i32_negative_panics() {
1334        let _ = Quantity::from(-1_i32);
1335    }
1336
1337    #[rstest]
1338    #[should_panic(
1339        expected = "Cannot create Quantity from negative i64: -1. Use u64 or check value is non-negative."
1340    )]
1341    fn test_from_i64_negative_panics() {
1342        let _ = Quantity::from(-1_i64);
1343    }
1344
1345    #[rstest]
1346    #[cfg_attr(
1347        feature = "high-precision",
1348        should_panic(expected = "exceeded QUANTITY_RAW_MAX")
1349    )]
1350    #[cfg_attr(
1351        not(feature = "high-precision"),
1352        should_panic(expected = "Raw value exceeds QuantityRaw range")
1353    )]
1354    fn test_from_i64_overflow_panics() {
1355        let max = quantity_max_i64();
1356
1357        let _ = Quantity::from(max + 1);
1358    }
1359
1360    #[rstest]
1361    #[cfg_attr(
1362        feature = "high-precision",
1363        should_panic(expected = "exceeded QUANTITY_RAW_MAX")
1364    )]
1365    #[cfg_attr(
1366        not(feature = "high-precision"),
1367        should_panic(expected = "Raw value exceeds QuantityRaw range")
1368    )]
1369    fn test_from_u64_overflow_panics() {
1370        let max = quantity_max_u64();
1371
1372        let _ = Quantity::from(max + 1);
1373    }
1374
1375    fn quantity_max_i64() -> i64 {
1376        i64::try_from(QUANTITY_RAW_MAX / FIXED_SCALAR_RAW).unwrap()
1377    }
1378
1379    #[allow(
1380        clippy::useless_conversion,
1381        reason = "try_from is a no-op when QuantityRaw is u64, and narrows when u128 (high-precision)"
1382    )]
1383    fn quantity_max_u64() -> u64 {
1384        u64::try_from(QUANTITY_RAW_MAX / FIXED_SCALAR_RAW).unwrap()
1385    }
1386
1387    #[rstest] // Test does not panic rather than exact value
1388    fn test_with_maximum_value() {
1389        let qty = Quantity::new_checked(QUANTITY_MAX, 0);
1390        assert!(qty.is_ok());
1391    }
1392
1393    #[rstest]
1394    fn test_with_minimum_positive_value() {
1395        let value = 0.000_000_001;
1396        let qty = Quantity::new(value, 9);
1397        assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
1398        assert_eq!(qty.as_decimal(), dec!(0.000000001));
1399        assert_eq!(qty.to_string(), "0.000000001");
1400    }
1401
1402    #[rstest]
1403    fn test_with_minimum_value() {
1404        let qty = Quantity::new(QUANTITY_MIN, 9);
1405        assert_eq!(qty.raw, 0);
1406        assert_eq!(qty.as_decimal(), dec!(0));
1407        assert_eq!(qty.to_string(), "0.000000000");
1408    }
1409
1410    #[rstest]
1411    fn test_is_zero() {
1412        let qty = Quantity::zero(8);
1413        assert_eq!(qty, qty);
1414        assert_eq!(qty.raw, 0);
1415        assert_eq!(qty.precision, 8);
1416        assert_eq!(qty, Quantity::from("0.00000000"));
1417        assert_eq!(qty.as_decimal(), dec!(0));
1418        assert_eq!(qty.to_string(), "0.00000000");
1419        assert!(qty.is_zero());
1420    }
1421
1422    #[rstest]
1423    fn test_precision() {
1424        let value = 1.001;
1425        let qty = Quantity::new(value, 2);
1426        assert_eq!(qty.to_string(), "1.00");
1427    }
1428
1429    #[rstest]
1430    fn test_new_from_str() {
1431        let qty = Quantity::new(0.008_120_00, 8);
1432        assert_eq!(qty, qty);
1433        assert_eq!(qty.precision, 8);
1434        assert_eq!(qty, Quantity::from("0.00812000"));
1435        assert_eq!(qty.to_string(), "0.00812000");
1436    }
1437
1438    #[rstest]
1439    #[case("0", 0)]
1440    #[case("1.1", 1)]
1441    #[case("1.123456789", 9)]
1442    fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
1443        let qty = Quantity::from(input);
1444        assert_eq!(qty.precision, expected_prec);
1445        assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
1446    }
1447
1448    #[rstest]
1449    #[should_panic(expected = "ParseFloatError")]
1450    fn test_from_str_invalid_input() {
1451        let input = "invalid";
1452        let _ = Quantity::new(f64::from_str(input).unwrap(), 8);
1453    }
1454
1455    #[rstest]
1456    fn test_from_str_errors() {
1457        assert!(Quantity::from_str("invalid").is_err());
1458        assert!(Quantity::from_str("12.34.56").is_err());
1459        assert!(Quantity::from_str("").is_err());
1460        assert!(Quantity::from_str("-1").is_err()); // Negative values not allowed
1461        assert!(Quantity::from_str("-0.001").is_err());
1462    }
1463
1464    #[rstest]
1465    #[case("1e7", 0, 10_000_000.0)]
1466    #[case("2.5e3", 0, 2_500.0)]
1467    #[case("1.234e-2", 5, 0.01234)]
1468    #[case("5E-3", 3, 0.005)]
1469    #[case("1.0e6", 0, 1_000_000.0)]
1470    fn test_from_str_scientific_notation(
1471        #[case] input: &str,
1472        #[case] expected_precision: u8,
1473        #[case] expected_value: f64,
1474    ) {
1475        let qty = Quantity::from_str(input).unwrap();
1476        assert_eq!(qty.precision, expected_precision);
1477        assert!(approx_eq!(
1478            f64,
1479            qty.as_f64(),
1480            expected_value,
1481            epsilon = 1e-10
1482        ));
1483    }
1484
1485    #[rstest]
1486    #[case("1_234.56", 2, 1234.56)]
1487    #[case("1000000", 0, 1_000_000.0)]
1488    #[case("99_999.999_99", 5, 99_999.999_99)]
1489    fn test_from_str_with_underscores(
1490        #[case] input: &str,
1491        #[case] expected_precision: u8,
1492        #[case] expected_value: f64,
1493    ) {
1494        let qty = Quantity::from_str(input).unwrap();
1495        assert_eq!(qty.precision, expected_precision);
1496        assert!(approx_eq!(
1497            f64,
1498            qty.as_f64(),
1499            expected_value,
1500            epsilon = 1e-10
1501        ));
1502    }
1503
1504    #[rstest]
1505    fn test_from_decimal_dp_preservation() {
1506        // Test that decimal conversion preserves exact values
1507        let decimal = dec!(123.456789);
1508        let qty = Quantity::from_decimal_dp(decimal, 6).unwrap();
1509        assert_eq!(qty.precision, 6);
1510        assert!(approx_eq!(f64, qty.as_f64(), 123.456_789, epsilon = 1e-10));
1511
1512        // Verify raw value is exact
1513        let expected_raw = 123_456_789_u64 * 10_u64.pow(u32::from(FIXED_PRECISION - 6));
1514        assert_eq!(qty.raw, QuantityRaw::from(expected_raw));
1515    }
1516
1517    #[rstest]
1518    fn test_from_decimal_dp_rounding() {
1519        // Test banker's rounding (round half to even)
1520        let decimal = dec!(1.005);
1521        let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1522        assert_eq!(qty.as_f64(), 1.0); // 1.005 rounds to 1.00 (even)
1523
1524        let decimal = dec!(1.015);
1525        let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1526        assert_eq!(qty.as_f64(), 1.02); // 1.015 rounds to 1.02 (even)
1527    }
1528
1529    #[rstest]
1530    fn test_from_decimal_infers_precision() {
1531        // Test that precision is inferred from decimal's scale
1532        let decimal = dec!(123.456);
1533        let qty = Quantity::from_decimal(decimal).unwrap();
1534        assert_eq!(qty.precision, 3);
1535        assert!(approx_eq!(f64, qty.as_f64(), 123.456, epsilon = 1e-10));
1536
1537        // Test with integer (precision 0)
1538        let decimal = dec!(100);
1539        let qty = Quantity::from_decimal(decimal).unwrap();
1540        assert_eq!(qty.precision, 0);
1541        assert_eq!(qty.as_f64(), 100.0);
1542
1543        // Test with high precision
1544        let decimal = dec!(1.23456789);
1545        let qty = Quantity::from_decimal(decimal).unwrap();
1546        assert_eq!(qty.precision, 8);
1547        assert!(approx_eq!(f64, qty.as_f64(), 1.234_567_89, epsilon = 1e-10));
1548    }
1549
1550    #[rstest]
1551    fn test_from_decimal_trailing_zeros() {
1552        // Decimal preserves trailing zeros in scale
1553        let decimal = dec!(5.670);
1554        assert_eq!(decimal.scale(), 3); // Has 3 decimal places
1555
1556        // from_decimal infers precision from scale (includes trailing zeros)
1557        let qty = Quantity::from_decimal(decimal).unwrap();
1558        assert_eq!(qty.precision, 3);
1559        assert!(approx_eq!(f64, qty.as_f64(), 5.67, epsilon = 1e-10));
1560
1561        // Normalized removes trailing zeros
1562        let normalized = decimal.normalize();
1563        assert_eq!(normalized.scale(), 2);
1564        let qty_normalized = Quantity::from_decimal(normalized).unwrap();
1565        assert_eq!(qty_normalized.precision, 2);
1566    }
1567
1568    #[rstest]
1569    #[case("1.00", 2)]
1570    #[case("1.0", 1)]
1571    #[case("1.000", 3)]
1572    #[case("100.00", 2)]
1573    #[case("0.10", 2)]
1574    #[case("0.100", 3)]
1575    fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1576        let qty = Quantity::from_str(input).unwrap();
1577        assert_eq!(qty.precision, expected_precision);
1578    }
1579
1580    #[rstest]
1581    fn test_from_decimal_excessive_precision_inference() {
1582        // Create a decimal with more precision than FIXED_PRECISION
1583        // Decimal supports up to 28 decimal places
1584        let decimal = dec!(1.1234567890123456789012345678);
1585
1586        // If scale exceeds FIXED_PRECISION, from_decimal should error
1587        if decimal.scale() > u32::from(FIXED_PRECISION) {
1588            assert!(Quantity::from_decimal(decimal).is_err());
1589        }
1590    }
1591
1592    #[rstest]
1593    fn test_from_decimal_negative_quantity_errors() {
1594        // Negative quantities should error (Quantity must be non-negative)
1595        let decimal = dec!(-123.45);
1596        let result = Quantity::from_decimal(decimal);
1597        assert!(result.is_err());
1598
1599        // Also test with explicit precision
1600        let result = Quantity::from_decimal_dp(decimal, 2);
1601        assert!(result.is_err());
1602    }
1603
1604    #[rstest]
1605    fn test_from_decimal_dp_negative_returns_typed_error_with_stable_display() {
1606        let error = Quantity::from_decimal_dp(dec!(-1.5), 2).unwrap_err();
1607        assert_eq!(
1608            error,
1609            CorrectnessError::PredicateViolation {
1610                message: "Decimal value '-1.5' is negative, Quantity must be non-negative"
1611                    .to_string(),
1612            }
1613        );
1614        assert_eq!(
1615            error.to_string(),
1616            "Decimal value '-1.5' is negative, Quantity must be non-negative",
1617        );
1618    }
1619
1620    #[cfg(not(feature = "high-precision"))]
1621    #[rstest]
1622    fn test_from_decimal_dp_rejects_quantity_raw_overflow() {
1623        let error = Quantity::from_decimal_dp(dec!(20_000_000_000), 0).unwrap_err();
1624
1625        assert_eq!(
1626            error,
1627            CorrectnessError::PredicateViolation {
1628                message: format!("Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]"),
1629            }
1630        );
1631    }
1632
1633    #[cfg(feature = "high-precision")]
1634    #[rstest]
1635    fn test_from_decimal_dp_rejects_value_above_quantity_max() {
1636        let value = 34_028_236_692_094_u64;
1637        let raw = u128::from(value) * FIXED_SCALAR_RAW;
1638        let error = Quantity::from_decimal_dp(Decimal::from(value), 0).unwrap_err();
1639
1640        assert_eq!(
1641            error,
1642            CorrectnessError::PredicateViolation {
1643                message: format!(
1644                    "Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
1645                ),
1646            }
1647        );
1648    }
1649
1650    #[rstest]
1651    fn test_add() {
1652        let a = 1.0;
1653        let b = 2.0;
1654        let quantity1 = Quantity::new(1.0, 0);
1655        let quantity2 = Quantity::new(2.0, 0);
1656        let quantity3 = quantity1 + quantity2;
1657        assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
1658    }
1659
1660    #[rstest]
1661    fn test_sub() {
1662        let a = 3.0;
1663        let b = 2.0;
1664        let quantity1 = Quantity::new(a, 0);
1665        let quantity2 = Quantity::new(b, 0);
1666        let quantity3 = quantity1 - quantity2;
1667        assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
1668    }
1669
1670    #[rstest]
1671    fn test_quantity_checked_add_within_bounds() {
1672        let a = Quantity::new(10.0, 2);
1673        let b = Quantity::new(5.0, 2);
1674        assert_eq!(a.checked_add(b), Some(Quantity::new(15.0, 2)));
1675    }
1676
1677    #[rstest]
1678    fn test_quantity_checked_rescale_native_precision() {
1679        let quantity = Quantity::from("1.25");
1680        let rescaled = quantity.checked_rescale(4).unwrap();
1681
1682        assert_eq!(rescaled.raw, quantity.raw);
1683        assert_eq!(rescaled.precision, 4);
1684        assert_eq!(rescaled.as_decimal(), dec!(1.25));
1685        assert_eq!(quantity.checked_rescale(0), None);
1686        assert_eq!(
1687            Quantity::from_raw(QUANTITY_UNDEF, 0).checked_rescale(0),
1688            None
1689        );
1690        assert_eq!(quantity.checked_rescale(u8::MAX), None);
1691        let mut invalid = quantity;
1692        invalid.precision = u8::MAX;
1693        assert_eq!(invalid.checked_rescale(0), None);
1694    }
1695
1696    #[cfg(feature = "defi")]
1697    #[rstest]
1698    #[case("1.25", 18, Some("1.250000000000000000"))]
1699    #[case("1.250000000000000000", 2, None)]
1700    #[case("1.250000000000000001", 2, None)]
1701    #[case("1.25000000000000001", 18, Some("1.250000000000000010"))]
1702    #[case("1.250000000000000001", 17, None)]
1703    fn test_quantity_checked_rescale_mixed_scales(
1704        #[case] value: &str,
1705        #[case] precision: u8,
1706        #[case] expected: Option<&str>,
1707    ) {
1708        let actual = Quantity::from(value).checked_rescale(precision);
1709
1710        assert_eq!(actual, expected.map(Quantity::from));
1711        assert_eq!(actual.map(|qty| qty.precision), expected.map(|_| precision));
1712    }
1713
1714    #[cfg(feature = "defi")]
1715    #[rstest]
1716    fn test_quantity_checked_rescale_rejects_overflow() {
1717        let quantity = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
1718
1719        assert_eq!(quantity.checked_rescale(18), None);
1720        assert_eq!(
1721            quantity.checked_floor_to_increment(Quantity::from("0.000000000000000001")),
1722            None
1723        );
1724    }
1725
1726    #[rstest]
1727    #[case("0.749", "0.50", "0.50")]
1728    #[case("0.49", "0.50", "0.00")]
1729    #[case("1.23456789", "0.000001", "1.234567")]
1730    #[case("0", "0.5", "0.0")]
1731    fn test_quantity_checked_floor_to_increment(
1732        #[case] value: &str,
1733        #[case] increment: &str,
1734        #[case] expected: &str,
1735    ) {
1736        let increment = Quantity::from(increment);
1737        let actual = Quantity::from(value)
1738            .checked_floor_to_increment(increment)
1739            .unwrap();
1740
1741        assert_eq!(actual, Quantity::from(expected));
1742        assert_eq!(actual.precision, increment.precision);
1743    }
1744
1745    #[cfg(feature = "defi")]
1746    #[rstest]
1747    #[case("0.59999999999999999", "0.005000", "0.595000")]
1748    #[case("0.599999999999999999", "0.005000", "0.595000")]
1749    #[case("0.600000", "0.00000000000000001", "0.60000000000000000")]
1750    #[case("0.59999999999999999", "0.000000000000000001", "0.599999999999999990")]
1751    fn test_quantity_checked_floor_aligns_raw_scales(
1752        #[case] value: &str,
1753        #[case] increment: &str,
1754        #[case] expected: &str,
1755    ) {
1756        let increment = Quantity::from(increment);
1757        let actual = Quantity::from(value)
1758            .checked_floor_to_increment(increment)
1759            .unwrap();
1760
1761        assert_eq!(actual, Quantity::from(expected));
1762        assert_eq!(actual.precision, increment.precision);
1763    }
1764
1765    #[rstest]
1766    fn test_quantity_checked_floor_rejects_invalid_operands() {
1767        let quantity = Quantity::from(1);
1768        let invalid = Quantity::from_raw(QUANTITY_UNDEF, 0);
1769
1770        assert_eq!(quantity.checked_floor_to_increment(Quantity::from(0)), None);
1771        assert_eq!(quantity.checked_floor_to_increment(invalid), None);
1772        assert_eq!(invalid.checked_floor_to_increment(quantity), None);
1773    }
1774
1775    #[rstest]
1776    fn test_quantity_checked_add_above_max_returns_none() {
1777        let near_max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
1778        let one = Quantity::new(1.0, 0);
1779        assert_eq!(near_max.checked_add(one), None);
1780    }
1781
1782    #[rstest]
1783    fn test_quantity_checked_sub_within_bounds() {
1784        let a = Quantity::new(10.0, 2);
1785        let b = Quantity::new(3.0, 2);
1786        assert_eq!(a.checked_sub(b), Some(Quantity::new(7.0, 2)));
1787    }
1788
1789    #[rstest]
1790    fn test_quantity_checked_sub_underflow_returns_none() {
1791        let a = Quantity::new(3.0, 2);
1792        let b = Quantity::new(10.0, 2);
1793        assert_eq!(a.checked_sub(b), None);
1794    }
1795
1796    #[rstest]
1797    fn test_quantity_checked_sub_to_zero() {
1798        let a = Quantity::new(5.0, 2);
1799        assert_eq!(a.checked_sub(a), Some(Quantity::zero(2)));
1800    }
1801
1802    #[rstest]
1803    fn test_quantity_from_raw_checked_allows_undef_and_rejects_above_max() {
1804        assert_eq!(
1805            Quantity::from_raw_checked(QUANTITY_UNDEF, 0).unwrap().raw,
1806            QUANTITY_UNDEF
1807        );
1808        assert_eq!(
1809            Quantity::from_raw_checked(QUANTITY_RAW_MAX + 1, 0)
1810                .unwrap_err()
1811                .to_string(),
1812            format!(
1813                "raw value {} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}",
1814                QUANTITY_RAW_MAX + 1
1815            )
1816        );
1817    }
1818
1819    #[rstest]
1820    fn test_quantity_as_f64() {
1821        assert_eq!(Quantity::new(2.5, 1).as_f64(), 2.5);
1822        assert_eq!(Quantity::new(0.0, 1).as_f64(), 0.0);
1823    }
1824
1825    #[rstest]
1826    fn test_quantity_deref_yields_raw() {
1827        assert_eq!(*Quantity::from_raw(2_500, 1), 2_500);
1828    }
1829
1830    #[rstest]
1831    fn test_quantity_checked_arith_rejects_undef() {
1832        let undef = Quantity::from_raw(QUANTITY_UNDEF, 0);
1833        let one = Quantity::new(1.0, 0);
1834        assert_eq!(undef.checked_add(one), None);
1835        assert_eq!(one.checked_add(undef), None);
1836        assert_eq!(undef.checked_sub(one), None);
1837        assert_eq!(one.checked_sub(undef), None);
1838    }
1839
1840    #[rstest]
1841    fn test_quantity_checked_add_at_exact_max_returns_some() {
1842        let near_max = Quantity::from_raw(QUANTITY_RAW_MAX - 1, 0);
1843        let one_unit = Quantity::from_raw(1, 0);
1844        assert_eq!(
1845            near_max.checked_add(one_unit),
1846            Some(Quantity::from_raw(QUANTITY_RAW_MAX, 0)),
1847        );
1848    }
1849
1850    #[rstest]
1851    fn test_quantity_checked_arith_uses_max_precision() {
1852        let a = Quantity::new(10.5, 1);
1853        let b = Quantity::new(2.25, 2);
1854        let sum = a.checked_add(b).unwrap();
1855        assert_eq!(sum.precision, 2);
1856        assert_eq!(sum.as_f64(), 12.75);
1857
1858        let diff = a.checked_sub(b).unwrap();
1859        assert_eq!(diff.precision, 2);
1860        assert_eq!(diff.as_f64(), 8.25);
1861    }
1862
1863    #[rstest]
1864    fn test_mul() {
1865        let value = 2.0;
1866        let quantity1 = Quantity::new(value, 1);
1867        let quantity2 = Quantity::new(value, 1);
1868        let quantity3 = quantity1 * quantity2;
1869        assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
1870    }
1871
1872    #[rstest]
1873    fn test_mul_avoids_intermediate_raw_overflow() {
1874        let scalar = FIXED_SCALAR_RAW;
1875        #[cfg(feature = "high-precision")]
1876        let (lhs_raw, rhs_raw, expected_raw) =
1877            (100_000 * scalar, 100 * scalar, 10_000_000 * scalar);
1878        #[cfg(not(feature = "high-precision"))]
1879        let (lhs_raw, rhs_raw, expected_raw) = (
1880            9_000_000_000 * scalar,
1881            2 * scalar + 1,
1882            18_000_000_009 * scalar,
1883        );
1884        let lhs = Quantity::from_raw(lhs_raw, FIXED_PRECISION);
1885        let rhs = Quantity::from_raw(rhs_raw, FIXED_PRECISION);
1886        let result = lhs * rhs;
1887
1888        assert_eq!(lhs_raw.checked_mul(rhs_raw), None);
1889        assert_eq!(result.raw, expected_raw);
1890        assert_eq!(result.precision, FIXED_PRECISION);
1891    }
1892
1893    #[rstest]
1894    #[should_panic(expected = "Overflow occurred when multiplying `Quantity`")]
1895    fn test_mul_panics_when_scaled_result_exceeds_quantity_max() {
1896        let lhs = Quantity::from_raw(QUANTITY_RAW_MAX, FIXED_PRECISION);
1897        let rhs = Quantity::from(2);
1898
1899        let _ = lhs * rhs;
1900    }
1901
1902    #[rstest]
1903    fn test_comparisons() {
1904        assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
1905        assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
1906        assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
1907        assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
1908        assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
1909        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
1910        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1911        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1912        assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
1913        assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
1914        assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
1915    }
1916
1917    #[rstest]
1918    fn test_debug() {
1919        let quantity = Quantity::from_str("44.12").unwrap();
1920        let result = format!("{quantity:?}");
1921        assert_eq!(result, "Quantity(44.12)");
1922    }
1923
1924    #[rstest]
1925    fn test_display() {
1926        let quantity = Quantity::from_str("44.12").unwrap();
1927        let result = format!("{quantity}");
1928        assert_eq!(result, "44.12");
1929    }
1930
1931    #[rstest]
1932    #[case(44.12, 2, "Quantity(44.12)", "44.12")] // Normal precision
1933    #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] // At max normal precision
1934    #[cfg_attr(
1935        feature = "defi",
1936        case(
1937            1_000_000_000_000_000_000.0,
1938            18,
1939            "Quantity(1.000000000000000000)",
1940            "1.000000000000000000"
1941        )
1942    )] // High precision
1943    fn test_debug_display_precision_handling(
1944        #[case] value: f64,
1945        #[case] precision: u8,
1946        #[case] expected_debug: &str,
1947        #[case] expected_display: &str,
1948    ) {
1949        let quantity = if precision > MAX_FLOAT_PRECISION {
1950            // For high precision, use from_raw to avoid f64 conversion issues
1951            Quantity::from_raw(value as QuantityRaw, precision)
1952        } else {
1953            Quantity::new(value, precision)
1954        };
1955
1956        assert_eq!(format!("{quantity:?}"), expected_debug);
1957        assert_eq!(format!("{quantity}"), expected_display);
1958    }
1959
1960    #[rstest]
1961    fn test_to_formatted_string() {
1962        let qty = Quantity::new(1234.5678, 4);
1963        let formatted = qty.to_formatted_string();
1964        assert_eq!(formatted, "1_234.5678");
1965        assert_eq!(qty.to_string(), "1234.5678");
1966    }
1967
1968    #[rstest]
1969    fn test_saturating_sub() {
1970        let q1 = Quantity::new(100.0, 2);
1971        let q2 = Quantity::new(50.0, 2);
1972        let q3 = Quantity::new(150.0, 2);
1973
1974        let result = q1.saturating_sub(q2);
1975        assert_eq!(result, Quantity::new(50.0, 2));
1976
1977        let result = q1.saturating_sub(q3);
1978        assert_eq!(result, Quantity::zero(2));
1979        assert_eq!(result.raw, 0);
1980    }
1981
1982    #[rstest]
1983    fn test_saturating_sub_overflow_bug() {
1984        // Reproduces original bug: subtracting a larger quantity from a smaller one
1985        // Raw values must be multiples of 10^(FIXED_PRECISION - precision)
1986        use crate::types::fixed::FIXED_PRECISION;
1987        let precision = 3;
1988        let scale = QuantityRaw::from(10u64.pow(u32::from(FIXED_PRECISION - precision)));
1989
1990        // 79 * scale represents 0.079, 80 * scale represents 0.080
1991        let peak_qty = Quantity::from_raw(79 * scale, precision);
1992        let order_qty = Quantity::from_raw(80 * scale, precision);
1993
1994        // This would have caused panic before fix due to underflow
1995        let result = peak_qty.saturating_sub(order_qty);
1996        assert_eq!(result.raw, 0);
1997        assert_eq!(result, Quantity::zero(precision));
1998    }
1999
2000    #[rstest]
2001    fn test_hash() {
2002        use std::{
2003            collections::hash_map::DefaultHasher,
2004            hash::{Hash, Hasher},
2005        };
2006
2007        let q1 = Quantity::new(100.0, 1);
2008        let q2 = Quantity::new(100.0, 1);
2009        let q3 = Quantity::new(200.0, 1);
2010
2011        let mut s1 = DefaultHasher::new();
2012        let mut s2 = DefaultHasher::new();
2013        let mut s3 = DefaultHasher::new();
2014
2015        q1.hash(&mut s1);
2016        q2.hash(&mut s2);
2017        q3.hash(&mut s3);
2018
2019        assert_eq!(
2020            s1.finish(),
2021            s2.finish(),
2022            "Equal quantities must hash equally"
2023        );
2024        assert_ne!(
2025            s1.finish(),
2026            s3.finish(),
2027            "Different quantities must hash differently"
2028        );
2029    }
2030
2031    #[rstest]
2032    fn test_quantity_serde_json_round_trip() {
2033        let original = Quantity::new(123.456, 3);
2034        let json_str = serde_json::to_string(&original).unwrap();
2035        assert_eq!(json_str, "\"123.456\"");
2036
2037        let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
2038        assert_eq!(deserialized, original);
2039        assert_eq!(deserialized.precision, 3);
2040    }
2041
2042    #[rstest]
2043    fn test_quantity_serde_json_from_value_round_trip() {
2044        let original = Quantity::new(123.456, 3);
2045        let value = serde_json::to_value(original).unwrap();
2046        assert_eq!(value, serde_json::json!("123.456"));
2047
2048        let deserialized: Quantity = serde_json::from_value(value).unwrap();
2049        assert_eq!(deserialized, original);
2050        assert_eq!(deserialized.precision, 3);
2051    }
2052
2053    #[rstest]
2054    fn test_quantity_deserialize_invalid_string_returns_error() {
2055        let result = serde_json::from_str::<Quantity>("\"not-a-quantity\"");
2056        let error = result.unwrap_err();
2057        assert!(
2058            error.to_string().contains("Error parsing"),
2059            "unexpected message: {error}"
2060        );
2061    }
2062
2063    #[rstest]
2064    fn test_quantity_deserialize_negative_returns_error() {
2065        let result = serde_json::from_str::<Quantity>("\"-1.5\"");
2066        let error = result.unwrap_err();
2067        assert!(
2068            error.to_string().contains("negative"),
2069            "unexpected message: {error}"
2070        );
2071    }
2072
2073    #[rstest]
2074    fn test_from_mantissa_exponent_exact_precision() {
2075        let qty = Quantity::from_mantissa_exponent(12345, -2, 2);
2076        assert_eq!(qty.as_f64(), 123.45);
2077    }
2078
2079    #[rstest]
2080    fn test_from_mantissa_exponent_excess_rounds_down() {
2081        // 12.344 -> 12.34 (no rounding needed, truncation)
2082        // 12.345 rounds to 12.34 (4 is even, banker's rounding)
2083        let qty = Quantity::from_mantissa_exponent(12345, -3, 2);
2084        assert_eq!(qty.as_f64(), 12.34);
2085    }
2086
2087    #[rstest]
2088    fn test_from_mantissa_exponent_excess_rounds_up() {
2089        // 12.355 rounds to 12.36 (5 is odd, banker's rounding)
2090        let qty = Quantity::from_mantissa_exponent(12355, -3, 2);
2091        assert_eq!(qty.as_f64(), 12.36);
2092    }
2093
2094    #[rstest]
2095    fn test_from_mantissa_exponent_positive_exponent() {
2096        let qty = Quantity::from_mantissa_exponent(5, 2, 0);
2097        assert_eq!(qty.as_f64(), 500.0);
2098    }
2099
2100    #[rstest]
2101    fn test_from_mantissa_exponent_zero() {
2102        let qty = Quantity::from_mantissa_exponent(0, 2, 2);
2103        assert_eq!(qty.as_f64(), 0.0);
2104    }
2105
2106    #[cfg(feature = "high-precision")]
2107    #[rstest]
2108    fn test_max_raw_precision_16_string_round_trip() {
2109        let quantity = Quantity::from_raw(QUANTITY_RAW_MAX, 16);
2110
2111        let decoded = quantity.to_string().parse::<Quantity>().unwrap();
2112
2113        assert_eq!(decoded, quantity);
2114        assert_eq!(decoded.raw, QUANTITY_RAW_MAX);
2115        assert_eq!(decoded.precision, 16);
2116    }
2117
2118    #[cfg(all(feature = "defi", feature = "high-precision"))]
2119    #[rstest]
2120    fn test_wei_above_decimal_mantissa_formats_exactly() {
2121        let raw = 80_000_000_000_000_000_250_000_000_000_u128;
2122        let qty = Quantity::from_raw(raw, 18);
2123
2124        assert_eq!(qty.to_string(), "80000000000.000000250000000000");
2125    }
2126
2127    #[rstest]
2128    fn test_from_mantissa_exponent_checked_exact_precision() {
2129        let qty = Quantity::from_mantissa_exponent_checked(12345, -2, 2).unwrap();
2130        assert_eq!(qty.as_decimal(), dec!(123.45));
2131    }
2132
2133    #[rstest]
2134    fn test_from_mantissa_exponent_checked_zero_with_large_exponent() {
2135        let qty = Quantity::from_mantissa_exponent_checked(0, 119, 2).unwrap();
2136        assert_eq!(qty.as_decimal(), dec!(0.00));
2137    }
2138
2139    #[rstest]
2140    fn test_from_mantissa_exponent_checked_invalid_precision() {
2141        #[cfg(feature = "defi")]
2142        let invalid_precision = crate::defi::WEI_PRECISION + 1;
2143        #[cfg(not(feature = "defi"))]
2144        let invalid_precision = FIXED_PRECISION + 1;
2145
2146        let error = Quantity::from_mantissa_exponent_checked(1, 0, invalid_precision).unwrap_err();
2147        assert!(error.to_string().contains("`precision` exceeded maximum"));
2148    }
2149
2150    #[rstest]
2151    fn test_from_mantissa_exponent_checked_overflow_returns_error() {
2152        let error = Quantity::from_mantissa_exponent_checked(u64::MAX, 100, 0).unwrap_err();
2153        assert!(
2154            error
2155                .to_string()
2156                .contains("Overflow in Quantity::from_mantissa_exponent")
2157        );
2158    }
2159
2160    #[rstest]
2161    #[should_panic(expected = "Quantity::from_mantissa_exponent")]
2162    fn test_from_mantissa_exponent_overflow_panics() {
2163        let _ = Quantity::from_mantissa_exponent(u64::MAX, 9, 0);
2164    }
2165
2166    #[rstest]
2167    #[should_panic(expected = "exceeds i128 range")]
2168    fn test_from_mantissa_exponent_large_exponent_panics() {
2169        let _ = Quantity::from_mantissa_exponent(1, 119, 0);
2170    }
2171
2172    #[rstest]
2173    fn test_from_mantissa_exponent_zero_with_large_exponent() {
2174        let qty = Quantity::from_mantissa_exponent(0, 119, 0);
2175        assert_eq!(qty.as_f64(), 0.0);
2176    }
2177
2178    #[rstest]
2179    fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
2180        let qty = Quantity::from_mantissa_exponent(12345, -120, 2);
2181        assert_eq!(qty.as_f64(), 0.0);
2182    }
2183
2184    #[rstest]
2185    fn test_f64_operations() {
2186        let q = Quantity::new(10.5, 2);
2187        assert_eq!(q + 1.0, 11.5);
2188        assert_eq!(q - 1.0, 9.5);
2189        assert_eq!(q * 2.0, 21.0);
2190        assert_eq!(q / 2.0, 5.25);
2191    }
2192
2193    #[rstest]
2194    fn test_decimal_arithmetic_operations() {
2195        let qty = Quantity::new(100.0, 2);
2196        assert_eq!(qty + dec!(50.25), dec!(150.25));
2197        assert_eq!(qty - dec!(30.50), dec!(69.50));
2198        assert_eq!(qty * dec!(1.5), dec!(150.00));
2199        assert_eq!(qty / dec!(4), dec!(25.00));
2200    }
2201
2202    /// Tests `Quantity::from_u256` using real swap event data from Arbitrum transactions, result values sourced from `DexScreener`.
2203    /// Data sourced from:
2204    /// - Sell tx: <https://arbiscan.io/tx/0xb417009ce3bd9b9f2dde7d52277ffc9f1b1733ecedfcc7f8e3dedd5d87160325>
2205    #[rstest]
2206    #[cfg(feature = "defi")]
2207    #[case::sell_tx_rain_amount(
2208        U256::from_str_radix("42193532365637161405123", 10).unwrap(),
2209        18,
2210        "42193.532365637161405123"
2211    )]
2212    #[case::sell_tx_weth_amount(
2213        U256::from_str_radix("112633187203033110", 10).unwrap(),
2214        18,
2215        "0.112633187203033110"
2216    )]
2217    fn test_from_u256_real_swap_data(
2218        #[case] amount: U256,
2219        #[case] precision: u8,
2220        #[case] expected_str: &str,
2221    ) {
2222        let qty = Quantity::from_u256(amount, precision).unwrap();
2223        assert_eq!(qty.precision, precision);
2224        assert_eq!(qty.as_decimal().to_string(), expected_str);
2225    }
2226
2227    #[rstest]
2228    #[cfg(feature = "defi")]
2229    fn test_from_u256_overflow_returns_typed_error_with_stable_display() {
2230        let error = Quantity::from_u256(U256::MAX, 0).unwrap_err();
2231        match error {
2232            CorrectnessError::PredicateViolation { ref message } => {
2233                assert!(
2234                    message.contains("Amount overflow during scaling to fixed precision"),
2235                    "unexpected message: {message:?}",
2236                );
2237            }
2238            _ => panic!("expected PredicateViolation, was {error:?}"),
2239        }
2240    }
2241
2242    #[rstest]
2243    #[cfg(feature = "defi")]
2244    fn test_from_u256_rejects_amount_above_quantity_raw_range() {
2245        let amount = U256::from(u128::MAX) + U256::from(1_u8);
2246        let error = Quantity::from_u256(amount, 18).unwrap_err();
2247
2248        assert_eq!(
2249            error,
2250            CorrectnessError::PredicateViolation {
2251                message: format!("U256 scaled amount {amount} exceeds QuantityRaw range"),
2252            }
2253        );
2254    }
2255
2256    #[rstest]
2257    #[cfg(feature = "defi")]
2258    fn test_from_u256_invalid_precision_returns_typed_error() {
2259        let error = Quantity::from_u256(U256::from(1u8), 19).unwrap_err();
2260        match error {
2261            CorrectnessError::PredicateViolation { ref message } => {
2262                assert!(
2263                    message.contains("WEI_PRECISION"),
2264                    "unexpected message: {message:?}",
2265                );
2266            }
2267            _ => panic!("expected PredicateViolation, was {error:?}"),
2268        }
2269    }
2270
2271    #[rstest]
2272    #[cfg(feature = "defi")]
2273    fn test_from_u256_raw_above_max_returns_typed_error() {
2274        // Pick a U256 value whose scaled raw lies between QUANTITY_RAW_MAX and
2275        // QuantityRaw::MAX so try_from succeeds but from_raw_checked rejects it.
2276        let raw = QUANTITY_RAW_MAX + 1;
2277        let error = Quantity::from_u256(U256::from(raw), FIXED_PRECISION).unwrap_err();
2278        match error {
2279            CorrectnessError::PredicateViolation { ref message } => {
2280                assert!(
2281                    message.contains("QUANTITY_RAW_MAX"),
2282                    "unexpected message: {message:?}",
2283                );
2284            }
2285            _ => panic!("expected PredicateViolation, was {error:?}"),
2286        }
2287    }
2288}
2289
2290#[cfg(test)]
2291mod property_tests {
2292    use proptest::prelude::*;
2293    use rstest::rstest;
2294
2295    use super::*;
2296    use crate::types::fixed::FIXED_SCALAR_RAW;
2297    #[cfg(not(feature = "defi"))]
2298    use crate::types::fixed::MAX_FLOAT_PRECISION;
2299
2300    /// Strategy to generate valid quantity values (non-negative).
2301    fn quantity_value_strategy() -> impl Strategy<Value = f64> {
2302        // Use a reasonable range for quantities - must be non-negative
2303        prop_oneof![
2304            // Small positive values
2305            0.00001..1.0,
2306            // Normal trading range
2307            1.0..100_000.0,
2308            // Large values (but safe)
2309            100_000.0..1_000_000.0,
2310            // Include zero
2311            Just(0.0),
2312            // Boundary cases
2313            Just(QUANTITY_MAX / 2.0),
2314        ]
2315    }
2316
2317    /// Strategy to generate valid precision values.
2318    fn precision_strategy() -> impl Strategy<Value = u8> {
2319        let upper = FIXED_PRECISION.min(MAX_FLOAT_PRECISION);
2320        prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
2321    }
2322
2323    fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
2324        let upper = FIXED_PRECISION.clamp(1, MAX_FLOAT_PRECISION);
2325        prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
2326    }
2327
2328    fn raw_for_precision_strategy() -> impl Strategy<Value = (QuantityRaw, u8)> {
2329        precision_strategy().prop_flat_map(|precision| {
2330            let step_u128 = 10u128.pow(u32::from(FIXED_PRECISION.saturating_sub(precision)));
2331            #[cfg(feature = "high-precision")]
2332            let max_steps_u128 = QUANTITY_RAW_MAX / step_u128;
2333            #[cfg(not(feature = "high-precision"))]
2334            let max_steps_u128 = u128::from(QUANTITY_RAW_MAX) / step_u128;
2335
2336            (0u128..=max_steps_u128).prop_map(move |steps_u128| {
2337                let raw_u128 = steps_u128 * step_u128;
2338                #[cfg(feature = "high-precision")]
2339                let raw = raw_u128;
2340                #[cfg(not(feature = "high-precision"))]
2341                let raw = raw_u128
2342                    .try_into()
2343                    .expect("raw value should fit in QuantityRaw");
2344                (raw, precision)
2345            })
2346        })
2347    }
2348
2349    const DECIMAL_MAX_MANTISSA: u128 = 79_228_162_514_264_337_593_543_950_335;
2350
2351    fn decimal_compatible(raw: QuantityRaw, precision: u8) -> bool {
2352        if precision > MAX_FLOAT_PRECISION {
2353            return false;
2354        }
2355        let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
2356        let divisor = 10u128.pow(precision_diff);
2357        #[cfg(feature = "high-precision")]
2358        let rescaled_raw = raw / divisor;
2359        #[cfg(not(feature = "high-precision"))]
2360        let rescaled_raw = u128::from(raw) / divisor;
2361        // rust_decimal stores the coefficient in 96 bits; this guard mirrors that bound so
2362        // proptests skip cases the runtime representation cannot encode.
2363        rescaled_raw <= DECIMAL_MAX_MANTISSA
2364    }
2365
2366    proptest! {
2367        /// Property: Quantity string serialization round-trip should preserve value and precision
2368        #[rstest]
2369        fn prop_quantity_serde_round_trip(
2370            (raw, precision) in raw_for_precision_strategy()
2371        ) {
2372            let original = Quantity::from_raw(raw, precision);
2373
2374            // String round-trip (this should be exact and is the most important)
2375            let string_repr = original.to_string();
2376            let from_string: Quantity = string_repr.parse().unwrap();
2377            prop_assert_eq!(from_string.raw, original.raw);
2378            prop_assert_eq!(from_string.precision, original.precision);
2379
2380            // JSON round-trip basic validation (just ensure it doesn't crash and preserves precision)
2381            let json = serde_json::to_string(&original).unwrap();
2382            let from_json: Quantity = serde_json::from_str(&json).unwrap();
2383            prop_assert_eq!(from_json.precision, original.precision);
2384            prop_assert_eq!(from_json.raw, original.raw);
2385        }
2386
2387        /// Property: Quantity arithmetic should be associative for same precision
2388        #[rstest]
2389        fn prop_quantity_arithmetic_associative(
2390            a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2391            b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2392            c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2393            precision in precision_strategy()
2394        ) {
2395            let q_a = Quantity::new(a, precision);
2396            let q_b = Quantity::new(b, precision);
2397            let q_c = Quantity::new(c, precision);
2398
2399            let expected = q_a
2400                .raw
2401                .checked_add(q_b.raw)
2402                .and_then(|sum| sum.checked_add(q_c.raw))
2403                .filter(|sum| *sum <= QUANTITY_RAW_MAX);
2404
2405            if let Some(expected) = expected {
2406                let left = (q_a + q_b) + q_c;
2407                let right = q_a + (q_b + q_c);
2408                prop_assert_eq!(left.raw, expected);
2409                prop_assert_eq!(right.raw, expected);
2410            }
2411        }
2412
2413        /// Property: Quantity addition/subtraction should be inverse operations (when valid)
2414        #[rstest]
2415        fn prop_quantity_addition_subtraction_inverse(
2416            base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
2417            delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2418            precision in precision_strategy()
2419        ) {
2420            let q_base = Quantity::new(base, precision);
2421            let q_delta = Quantity::new(delta, precision);
2422
2423            let expected = q_base
2424                .raw
2425                .checked_add(q_delta.raw)
2426                .filter(|sum| *sum <= QUANTITY_RAW_MAX);
2427
2428            if expected.is_some() {
2429                prop_assert_eq!((q_base + q_delta) - q_delta, q_base);
2430            }
2431        }
2432
2433        /// Property: checked_add agrees with raw checked_add when result is in bounds and
2434        /// no operand is QUANTITY_UNDEF; returns None otherwise.
2435        #[rstest]
2436        fn prop_quantity_checked_add_matches_spec(
2437            a in quantity_value_strategy(),
2438            b in quantity_value_strategy(),
2439            precision in precision_strategy()
2440        ) {
2441            let q_a = Quantity::new(a, precision);
2442            let q_b = Quantity::new(b, precision);
2443            let expected = q_a.raw
2444                .checked_add(q_b.raw)
2445                .filter(|r| *r <= QUANTITY_RAW_MAX)
2446                .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
2447                .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
2448            prop_assert_eq!(q_a.checked_add(q_b), expected);
2449        }
2450
2451        /// Property: checked_sub agrees with raw checked_sub when no operand is
2452        /// QUANTITY_UNDEF; returns None otherwise.
2453        #[rstest]
2454        fn prop_quantity_checked_sub_matches_spec(
2455            a in quantity_value_strategy(),
2456            b in quantity_value_strategy(),
2457            precision in precision_strategy()
2458        ) {
2459            let q_a = Quantity::new(a, precision);
2460            let q_b = Quantity::new(b, precision);
2461            let expected = q_a.raw
2462                .checked_sub(q_b.raw)
2463                .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
2464                .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
2465            prop_assert_eq!(q_a.checked_sub(q_b), expected);
2466        }
2467
2468        /// Property: Quantity ordering should be transitive
2469        #[rstest]
2470        fn prop_quantity_ordering_transitive(
2471            a in quantity_value_strategy(),
2472            b in quantity_value_strategy(),
2473            c in quantity_value_strategy(),
2474            precision in precision_strategy()
2475        ) {
2476            let q_a = Quantity::new(a, precision);
2477            let q_b = Quantity::new(b, precision);
2478            let q_c = Quantity::new(c, precision);
2479
2480            // If a <= b and b <= c, then a <= c
2481            if q_a <= q_b && q_b <= q_c {
2482                prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
2483                    q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
2484            }
2485        }
2486
2487        /// Property: String parsing should be consistent with precision inference
2488        #[rstest]
2489        fn prop_quantity_string_parsing_precision(
2490            integral in 0u32..1_000_000,
2491            fractional in 0u32..1_000_000,
2492            precision in precision_strategy_non_zero()
2493        ) {
2494            // Create a decimal string with exactly 'precision' decimal places
2495            let pow = 10u128.pow(u32::from(precision));
2496            let fractional_mod = u128::from(fractional) % pow;
2497            let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
2498            let quantity_str = format!("{integral}.{fractional_str}");
2499
2500            let parsed: Quantity = quantity_str.parse().unwrap();
2501            prop_assert_eq!(parsed.precision, precision);
2502
2503            // Round-trip should preserve the original string (after normalization)
2504            let round_trip = parsed.to_string();
2505            let expected_value = format!("{integral}.{fractional_str}");
2506            prop_assert_eq!(round_trip, expected_value);
2507        }
2508
2509        /// Property: Quantity arithmetic should never produce invalid values
2510        #[rstest]
2511        fn prop_quantity_arithmetic_bounds(
2512            a in quantity_value_strategy(),
2513            b in quantity_value_strategy(),
2514            precision in precision_strategy()
2515        ) {
2516            let q_a = Quantity::new(a, precision);
2517            let q_b = Quantity::new(b, precision);
2518
2519            // Addition should either succeed or fail predictably
2520            let sum_f64 = q_a.as_f64() + q_b.as_f64();
2521            if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
2522                let sum = q_a + q_b;
2523                prop_assert!(sum.as_f64().is_finite());
2524                prop_assert!(!sum.is_undefined());
2525            }
2526
2527            // Subtraction should either succeed or fail predictably
2528            let diff_f64 = q_a.as_f64() - q_b.as_f64();
2529            if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
2530                let diff = q_a - q_b;
2531                prop_assert!(diff.as_f64().is_finite());
2532                prop_assert!(!diff.is_undefined());
2533            }
2534        }
2535
2536        /// Property: Multiplication should preserve non-negativity
2537        #[rstest]
2538        fn prop_quantity_multiplication_non_negative(
2539            a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2540            b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2541            precision in precision_strategy()
2542        ) {
2543            let q_a = Quantity::new(a, precision);
2544            let q_b = Quantity::new(b, precision);
2545
2546            // Check if multiplication would overflow at the raw level before performing it
2547            let raw_product_check = q_a.raw.checked_mul(q_b.raw);
2548
2549            if let Some(raw_product) = raw_product_check {
2550                // Additional check to ensure the scaled result won't overflow
2551                let scaled_raw = raw_product / FIXED_SCALAR_RAW;
2552                if scaled_raw <= QUANTITY_RAW_MAX {
2553                    // Multiplying two quantities should always result in a non-negative value
2554                    let product = q_a * q_b;
2555                    prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
2556                }
2557            }
2558        }
2559
2560        /// Property: Zero quantity should be identity for addition
2561        #[rstest]
2562        fn prop_quantity_zero_addition_identity(
2563            value in quantity_value_strategy(),
2564            precision in precision_strategy()
2565        ) {
2566            let q = Quantity::new(value, precision);
2567            let zero = Quantity::zero(precision);
2568
2569            // q + 0 = q and 0 + q = q
2570            prop_assert_eq!(q + zero, q);
2571            prop_assert_eq!(zero + q, q);
2572        }
2573    }
2574
2575    proptest! {
2576        /// Property: as_decimal scale always matches precision
2577        #[rstest]
2578        fn prop_quantity_as_decimal_preserves_precision(
2579            (raw, precision) in raw_for_precision_strategy()
2580        ) {
2581            prop_assume!(decimal_compatible(raw, precision));
2582            let quantity = Quantity::from_raw(raw, precision);
2583            let decimal = quantity.as_decimal();
2584            prop_assert_eq!(decimal.scale(), u32::from(precision));
2585        }
2586
2587        /// Property: as_decimal and Display produce the same string
2588        #[rstest]
2589        fn prop_quantity_as_decimal_matches_display(
2590            (raw, precision) in raw_for_precision_strategy()
2591        ) {
2592            prop_assume!(decimal_compatible(raw, precision));
2593            let quantity = Quantity::from_raw(raw, precision);
2594            let display_str = format!("{quantity}");
2595            let decimal_str = quantity.as_decimal().to_string();
2596            prop_assert_eq!(display_str, decimal_str);
2597        }
2598
2599        /// Property: from_decimal roundtrip preserves exact value
2600        #[rstest]
2601        fn prop_quantity_from_decimal_roundtrip(
2602            (raw, precision) in raw_for_precision_strategy()
2603        ) {
2604            prop_assume!(decimal_compatible(raw, precision));
2605            let original = Quantity::from_raw(raw, precision);
2606            let decimal = original.as_decimal();
2607            let reconstructed = Quantity::from_decimal(decimal).unwrap();
2608            prop_assert_eq!(original.raw, reconstructed.raw);
2609            prop_assert_eq!(original.precision, reconstructed.precision);
2610        }
2611
2612        /// Property: constructing from raw within bounds preserves raw/precision
2613        #[rstest]
2614        fn prop_quantity_from_raw_round_trip(
2615            (raw, precision) in raw_for_precision_strategy()
2616        ) {
2617            let quantity = Quantity::from_raw(raw, precision);
2618            prop_assert_eq!(quantity.raw, raw);
2619            prop_assert_eq!(quantity.precision, precision);
2620        }
2621    }
2622}