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