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