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 `precision` exceeds the maximum allowed by [`check_fixed_precision`].
353    #[must_use]
354    pub fn zero(precision: u8) -> Self {
355        check_fixed_precision(precision).expect_display(FAILED);
356        Self { raw: 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        Self::from_str(qty_str.as_ref()).map_err(serde::de::Error::custom)
885    }
886}
887
888/// Checks if the quantity `value` is positive.
889///
890/// # Errors
891///
892/// Returns an error if `value` is not positive.
893pub fn check_positive_quantity(value: Quantity, param: &str) -> CorrectnessResult<()> {
894    if !value.is_positive() {
895        return Err(CorrectnessError::NotPositive {
896            param: param.to_string(),
897            value: value.to_string(),
898            type_name: "`Quantity`",
899        });
900    }
901    Ok(())
902}
903
904#[cfg(test)]
905mod tests {
906    use std::str::FromStr;
907
908    use nautilus_core::{approx_eq, correctness::CorrectnessError};
909    use rstest::rstest;
910    use rust_decimal_macros::dec;
911
912    use super::*;
913
914    #[rstest]
915    fn test_max_quantity_round_trips_through_raw() {
916        // Regression: a lossy `f64` scalar previously left `QUANTITY_RAW_MAX` below the raw
917        // produced by `new` at the maximum, causing spurious panics and overflow errors.
918        let qty = Quantity::new(QUANTITY_MAX, 0);
919
920        assert_eq!(qty.raw, QUANTITY_RAW_MAX);
921        assert!(Quantity::from_raw_checked(qty.raw, 0).is_ok());
922        assert!(qty.checked_add(Quantity::zero(0)).is_some());
923    }
924
925    #[rstest]
926    fn test_check_quantity_positive() {
927        let qty = Quantity::new(0.0, 0);
928        let error = check_positive_quantity(qty, "qty").unwrap_err();
929
930        assert_eq!(
931            error,
932            CorrectnessError::NotPositive {
933                param: "qty".to_string(),
934                value: "0".to_string(),
935                type_name: "`Quantity`",
936            }
937        );
938        assert_eq!(
939            error.to_string(),
940            "invalid `Quantity` for 'qty' not positive, was 0"
941        );
942    }
943
944    #[rstest]
945    #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
946    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 17")]
947    fn test_invalid_precision_new() {
948        // Precision 17 should fail due to DeFi validation
949        let _ = Quantity::new(1.0, 17);
950    }
951
952    #[rstest]
953    #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
954    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
955    fn test_invalid_precision_new() {
956        // Precision 17 should fail due to DeFi validation
957        let _ = Quantity::new(1.0, 17);
958    }
959
960    #[rstest]
961    #[cfg(not(feature = "defi"))]
962    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
963    fn test_invalid_precision_from_raw() {
964        // Precision out of range for fixed
965        let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
966    }
967
968    #[rstest]
969    #[cfg(not(feature = "defi"))]
970    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
971    fn test_invalid_precision_zero() {
972        // Precision out of range for fixed
973        let _ = Quantity::zero(FIXED_PRECISION + 1);
974    }
975
976    #[rstest]
977    fn test_mixed_precision_add() {
978        let q1 = Quantity::new(1.0, 1);
979        let q2 = Quantity::new(1.0, 2);
980        let result = q1 + q2;
981        assert_eq!(result.precision, 2);
982        assert_eq!(result.as_f64(), 2.0);
983    }
984
985    #[rstest]
986    fn test_mixed_precision_sub() {
987        let q1 = Quantity::new(2.0, 1);
988        let q2 = Quantity::new(1.0, 2);
989        let result = q1 - q2;
990        assert_eq!(result.precision, 2);
991        assert_eq!(result.as_f64(), 1.0);
992    }
993
994    #[rstest]
995    fn test_mixed_precision_mul() {
996        let q1 = Quantity::new(2.0, 1);
997        let q2 = Quantity::new(3.0, 2);
998        let result = q1 * q2;
999        assert_eq!(result.precision, 2);
1000        assert_eq!(result.as_f64(), 6.0);
1001    }
1002
1003    #[rstest]
1004    fn test_new_non_zero_ok() {
1005        let qty = Quantity::non_zero_checked(123.456, 3).unwrap();
1006        assert_eq!(qty.raw, Quantity::new(123.456, 3).raw);
1007        assert!(qty.is_positive());
1008    }
1009
1010    #[rstest]
1011    fn test_new_non_zero_zero_input() {
1012        assert!(Quantity::non_zero_checked(0.0, 0).is_err());
1013    }
1014
1015    #[rstest]
1016    fn test_new_non_zero_rounds_to_zero() {
1017        // 0.0004 rounded to 3 dp ⇒ 0.000
1018        assert!(Quantity::non_zero_checked(0.0004, 3).is_err());
1019    }
1020
1021    #[rstest]
1022    fn test_new_non_zero_negative() {
1023        assert!(Quantity::non_zero_checked(-1.0, 0).is_err());
1024    }
1025
1026    #[rstest]
1027    fn test_new_non_zero_exceeds_max() {
1028        assert!(Quantity::non_zero_checked(QUANTITY_MAX * 10.0, 0).is_err());
1029    }
1030
1031    #[rstest]
1032    fn test_new_non_zero_invalid_precision() {
1033        assert!(Quantity::non_zero_checked(1.0, FIXED_PRECISION + 1).is_err());
1034    }
1035
1036    #[rstest]
1037    fn test_new() {
1038        let value = 0.00812;
1039        let qty = Quantity::new(value, 8);
1040        assert_eq!(qty, qty);
1041        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1042        assert_eq!(qty.precision, 8);
1043        assert_eq!(qty, Quantity::from("0.00812000"));
1044        assert_eq!(qty.as_decimal(), dec!(0.00812000));
1045        assert_eq!(qty.to_string(), "0.00812000");
1046        assert!(!qty.is_zero());
1047        assert!(qty.is_positive());
1048        assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
1049    }
1050
1051    #[rstest]
1052    fn test_check_quantity_positive_ok() {
1053        let qty = Quantity::new(10.0, 0);
1054        check_positive_quantity(qty, "qty").unwrap();
1055    }
1056
1057    #[rstest]
1058    fn test_negative_quantity_validation() {
1059        assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
1060    }
1061
1062    #[rstest]
1063    fn test_new_checked_returns_typed_error_with_stable_display() {
1064        let error = Quantity::new_checked(QUANTITY_MAX + 1.0, FIXED_PRECISION).unwrap_err();
1065
1066        assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
1067        assert_eq!(
1068            error.to_string(),
1069            format!(
1070                "invalid f64 for 'value' not in range [{QUANTITY_MIN}, {QUANTITY_MAX}], was {}",
1071                QUANTITY_MAX + 1.0
1072            )
1073        );
1074    }
1075
1076    #[rstest]
1077    fn test_from_raw_checked_returns_typed_error_with_stable_display() {
1078        let error = Quantity::from_raw_checked(QUANTITY_UNDEF, 3).unwrap_err();
1079
1080        assert_eq!(
1081            error,
1082            CorrectnessError::PredicateViolation {
1083                message: "`precision` must be 0 when `raw` is QUANTITY_UNDEF".to_string(),
1084            }
1085        );
1086        assert_eq!(
1087            error.to_string(),
1088            "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
1089        );
1090    }
1091
1092    #[rstest]
1093    fn test_undefined() {
1094        let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
1095        assert_eq!(qty.raw, QUANTITY_UNDEF);
1096        assert!(qty.is_undefined());
1097    }
1098
1099    #[rstest]
1100    fn test_zero() {
1101        let qty = Quantity::zero(8);
1102        assert_eq!(qty.raw, 0);
1103        assert_eq!(qty.precision, 8);
1104        assert!(qty.is_zero());
1105        assert!(!qty.is_positive());
1106    }
1107
1108    #[rstest]
1109    fn test_from_i32() {
1110        let value = 100_000i32;
1111        let qty = Quantity::from(value);
1112        assert_eq!(qty, qty);
1113        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1114        assert_eq!(qty.precision, 0);
1115    }
1116
1117    #[rstest]
1118    fn test_from_u32() {
1119        let value: u32 = 5000;
1120        let qty = Quantity::from(value);
1121        assert_eq!(qty.raw, Quantity::from(format!("{value}")).raw);
1122        assert_eq!(qty.precision, 0);
1123    }
1124
1125    #[rstest]
1126    fn test_from_i64() {
1127        let value = 100_000i64;
1128        let qty = Quantity::from(value);
1129        assert_eq!(qty, qty);
1130        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1131        assert_eq!(qty.precision, 0);
1132    }
1133
1134    #[rstest]
1135    fn test_from_u64() {
1136        let value = 100_000u64;
1137        let qty = Quantity::from(value);
1138        assert_eq!(qty, qty);
1139        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1140        assert_eq!(qty.precision, 0);
1141    }
1142
1143    #[rstest] // Test does not panic rather than exact value
1144    fn test_with_maximum_value() {
1145        let qty = Quantity::new_checked(QUANTITY_MAX, 0);
1146        assert!(qty.is_ok());
1147    }
1148
1149    #[rstest]
1150    fn test_with_minimum_positive_value() {
1151        let value = 0.000_000_001;
1152        let qty = Quantity::new(value, 9);
1153        assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
1154        assert_eq!(qty.as_decimal(), dec!(0.000000001));
1155        assert_eq!(qty.to_string(), "0.000000001");
1156    }
1157
1158    #[rstest]
1159    fn test_with_minimum_value() {
1160        let qty = Quantity::new(QUANTITY_MIN, 9);
1161        assert_eq!(qty.raw, 0);
1162        assert_eq!(qty.as_decimal(), dec!(0));
1163        assert_eq!(qty.to_string(), "0.000000000");
1164    }
1165
1166    #[rstest]
1167    fn test_is_zero() {
1168        let qty = Quantity::zero(8);
1169        assert_eq!(qty, qty);
1170        assert_eq!(qty.raw, 0);
1171        assert_eq!(qty.precision, 8);
1172        assert_eq!(qty, Quantity::from("0.00000000"));
1173        assert_eq!(qty.as_decimal(), dec!(0));
1174        assert_eq!(qty.to_string(), "0.00000000");
1175        assert!(qty.is_zero());
1176    }
1177
1178    #[rstest]
1179    fn test_precision() {
1180        let value = 1.001;
1181        let qty = Quantity::new(value, 2);
1182        assert_eq!(qty.to_string(), "1.00");
1183    }
1184
1185    #[rstest]
1186    fn test_new_from_str() {
1187        let qty = Quantity::new(0.008_120_00, 8);
1188        assert_eq!(qty, qty);
1189        assert_eq!(qty.precision, 8);
1190        assert_eq!(qty, Quantity::from("0.00812000"));
1191        assert_eq!(qty.to_string(), "0.00812000");
1192    }
1193
1194    #[rstest]
1195    #[case("0", 0)]
1196    #[case("1.1", 1)]
1197    #[case("1.123456789", 9)]
1198    fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
1199        let qty = Quantity::from(input);
1200        assert_eq!(qty.precision, expected_prec);
1201        assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
1202    }
1203
1204    #[rstest]
1205    #[should_panic(expected = "ParseFloatError")]
1206    fn test_from_str_invalid_input() {
1207        let input = "invalid";
1208        let _ = Quantity::new(f64::from_str(input).unwrap(), 8);
1209    }
1210
1211    #[rstest]
1212    fn test_from_str_errors() {
1213        assert!(Quantity::from_str("invalid").is_err());
1214        assert!(Quantity::from_str("12.34.56").is_err());
1215        assert!(Quantity::from_str("").is_err());
1216        assert!(Quantity::from_str("-1").is_err()); // Negative values not allowed
1217        assert!(Quantity::from_str("-0.001").is_err());
1218    }
1219
1220    #[rstest]
1221    #[case("1e7", 0, 10_000_000.0)]
1222    #[case("2.5e3", 0, 2_500.0)]
1223    #[case("1.234e-2", 5, 0.01234)]
1224    #[case("5E-3", 3, 0.005)]
1225    #[case("1.0e6", 0, 1_000_000.0)]
1226    fn test_from_str_scientific_notation(
1227        #[case] input: &str,
1228        #[case] expected_precision: u8,
1229        #[case] expected_value: f64,
1230    ) {
1231        let qty = Quantity::from_str(input).unwrap();
1232        assert_eq!(qty.precision, expected_precision);
1233        assert!(approx_eq!(
1234            f64,
1235            qty.as_f64(),
1236            expected_value,
1237            epsilon = 1e-10
1238        ));
1239    }
1240
1241    #[rstest]
1242    #[case("1_234.56", 2, 1234.56)]
1243    #[case("1000000", 0, 1_000_000.0)]
1244    #[case("99_999.999_99", 5, 99_999.999_99)]
1245    fn test_from_str_with_underscores(
1246        #[case] input: &str,
1247        #[case] expected_precision: u8,
1248        #[case] expected_value: f64,
1249    ) {
1250        let qty = Quantity::from_str(input).unwrap();
1251        assert_eq!(qty.precision, expected_precision);
1252        assert!(approx_eq!(
1253            f64,
1254            qty.as_f64(),
1255            expected_value,
1256            epsilon = 1e-10
1257        ));
1258    }
1259
1260    #[rstest]
1261    fn test_from_decimal_dp_preservation() {
1262        // Test that decimal conversion preserves exact values
1263        let decimal = dec!(123.456789);
1264        let qty = Quantity::from_decimal_dp(decimal, 6).unwrap();
1265        assert_eq!(qty.precision, 6);
1266        assert!(approx_eq!(f64, qty.as_f64(), 123.456_789, epsilon = 1e-10));
1267
1268        // Verify raw value is exact
1269        let expected_raw = 123_456_789_u64 * 10_u64.pow(u32::from(FIXED_PRECISION - 6));
1270        assert_eq!(qty.raw, QuantityRaw::from(expected_raw));
1271    }
1272
1273    #[rstest]
1274    fn test_from_decimal_dp_rounding() {
1275        // Test banker's rounding (round half to even)
1276        let decimal = dec!(1.005);
1277        let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1278        assert_eq!(qty.as_f64(), 1.0); // 1.005 rounds to 1.00 (even)
1279
1280        let decimal = dec!(1.015);
1281        let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1282        assert_eq!(qty.as_f64(), 1.02); // 1.015 rounds to 1.02 (even)
1283    }
1284
1285    #[rstest]
1286    fn test_from_decimal_infers_precision() {
1287        // Test that precision is inferred from decimal's scale
1288        let decimal = dec!(123.456);
1289        let qty = Quantity::from_decimal(decimal).unwrap();
1290        assert_eq!(qty.precision, 3);
1291        assert!(approx_eq!(f64, qty.as_f64(), 123.456, epsilon = 1e-10));
1292
1293        // Test with integer (precision 0)
1294        let decimal = dec!(100);
1295        let qty = Quantity::from_decimal(decimal).unwrap();
1296        assert_eq!(qty.precision, 0);
1297        assert_eq!(qty.as_f64(), 100.0);
1298
1299        // Test with high precision
1300        let decimal = dec!(1.23456789);
1301        let qty = Quantity::from_decimal(decimal).unwrap();
1302        assert_eq!(qty.precision, 8);
1303        assert!(approx_eq!(f64, qty.as_f64(), 1.234_567_89, epsilon = 1e-10));
1304    }
1305
1306    #[rstest]
1307    fn test_from_decimal_trailing_zeros() {
1308        // Decimal preserves trailing zeros in scale
1309        let decimal = dec!(5.670);
1310        assert_eq!(decimal.scale(), 3); // Has 3 decimal places
1311
1312        // from_decimal infers precision from scale (includes trailing zeros)
1313        let qty = Quantity::from_decimal(decimal).unwrap();
1314        assert_eq!(qty.precision, 3);
1315        assert!(approx_eq!(f64, qty.as_f64(), 5.67, epsilon = 1e-10));
1316
1317        // Normalized removes trailing zeros
1318        let normalized = decimal.normalize();
1319        assert_eq!(normalized.scale(), 2);
1320        let qty_normalized = Quantity::from_decimal(normalized).unwrap();
1321        assert_eq!(qty_normalized.precision, 2);
1322    }
1323
1324    #[rstest]
1325    #[case("1.00", 2)]
1326    #[case("1.0", 1)]
1327    #[case("1.000", 3)]
1328    #[case("100.00", 2)]
1329    #[case("0.10", 2)]
1330    #[case("0.100", 3)]
1331    fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1332        let qty = Quantity::from_str(input).unwrap();
1333        assert_eq!(qty.precision, expected_precision);
1334    }
1335
1336    #[rstest]
1337    fn test_from_decimal_excessive_precision_inference() {
1338        // Create a decimal with more precision than FIXED_PRECISION
1339        // Decimal supports up to 28 decimal places
1340        let decimal = dec!(1.1234567890123456789012345678);
1341
1342        // If scale exceeds FIXED_PRECISION, from_decimal should error
1343        if decimal.scale() > u32::from(FIXED_PRECISION) {
1344            assert!(Quantity::from_decimal(decimal).is_err());
1345        }
1346    }
1347
1348    #[rstest]
1349    fn test_from_decimal_negative_quantity_errors() {
1350        // Negative quantities should error (Quantity must be non-negative)
1351        let decimal = dec!(-123.45);
1352        let result = Quantity::from_decimal(decimal);
1353        assert!(result.is_err());
1354
1355        // Also test with explicit precision
1356        let result = Quantity::from_decimal_dp(decimal, 2);
1357        assert!(result.is_err());
1358    }
1359
1360    #[rstest]
1361    fn test_from_decimal_dp_negative_returns_typed_error_with_stable_display() {
1362        let error = Quantity::from_decimal_dp(dec!(-1.5), 2).unwrap_err();
1363        assert_eq!(
1364            error,
1365            CorrectnessError::PredicateViolation {
1366                message: "Decimal value '-1.5' is negative, Quantity must be non-negative"
1367                    .to_string(),
1368            }
1369        );
1370        assert_eq!(
1371            error.to_string(),
1372            "Decimal value '-1.5' is negative, Quantity must be non-negative",
1373        );
1374    }
1375
1376    #[rstest]
1377    fn test_add() {
1378        let a = 1.0;
1379        let b = 2.0;
1380        let quantity1 = Quantity::new(1.0, 0);
1381        let quantity2 = Quantity::new(2.0, 0);
1382        let quantity3 = quantity1 + quantity2;
1383        assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
1384    }
1385
1386    #[rstest]
1387    fn test_sub() {
1388        let a = 3.0;
1389        let b = 2.0;
1390        let quantity1 = Quantity::new(a, 0);
1391        let quantity2 = Quantity::new(b, 0);
1392        let quantity3 = quantity1 - quantity2;
1393        assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
1394    }
1395
1396    #[rstest]
1397    fn test_quantity_checked_add_within_bounds() {
1398        let a = Quantity::new(10.0, 2);
1399        let b = Quantity::new(5.0, 2);
1400        assert_eq!(a.checked_add(b), Some(Quantity::new(15.0, 2)));
1401    }
1402
1403    #[rstest]
1404    fn test_quantity_checked_add_above_max_returns_none() {
1405        let near_max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
1406        let one = Quantity::new(1.0, 0);
1407        assert_eq!(near_max.checked_add(one), None);
1408    }
1409
1410    #[rstest]
1411    fn test_quantity_checked_sub_within_bounds() {
1412        let a = Quantity::new(10.0, 2);
1413        let b = Quantity::new(3.0, 2);
1414        assert_eq!(a.checked_sub(b), Some(Quantity::new(7.0, 2)));
1415    }
1416
1417    #[rstest]
1418    fn test_quantity_checked_sub_underflow_returns_none() {
1419        let a = Quantity::new(3.0, 2);
1420        let b = Quantity::new(10.0, 2);
1421        assert_eq!(a.checked_sub(b), None);
1422    }
1423
1424    #[rstest]
1425    fn test_quantity_checked_sub_to_zero() {
1426        let a = Quantity::new(5.0, 2);
1427        assert_eq!(a.checked_sub(a), Some(Quantity::zero(2)));
1428    }
1429
1430    #[rstest]
1431    fn test_quantity_checked_arith_rejects_undef() {
1432        let undef = Quantity::from_raw(QUANTITY_UNDEF, 0);
1433        let one = Quantity::new(1.0, 0);
1434        assert_eq!(undef.checked_add(one), None);
1435        assert_eq!(one.checked_add(undef), None);
1436        assert_eq!(undef.checked_sub(one), None);
1437        assert_eq!(one.checked_sub(undef), None);
1438    }
1439
1440    #[rstest]
1441    fn test_quantity_checked_add_at_exact_max_returns_some() {
1442        let near_max = Quantity::from_raw(QUANTITY_RAW_MAX - 1, 0);
1443        let one_unit = Quantity::from_raw(1, 0);
1444        assert_eq!(
1445            near_max.checked_add(one_unit),
1446            Some(Quantity::from_raw(QUANTITY_RAW_MAX, 0)),
1447        );
1448    }
1449
1450    #[rstest]
1451    fn test_quantity_checked_arith_uses_max_precision() {
1452        let a = Quantity::new(10.5, 1);
1453        let b = Quantity::new(2.25, 2);
1454        let sum = a.checked_add(b).unwrap();
1455        assert_eq!(sum.precision, 2);
1456        assert_eq!(sum.as_f64(), 12.75);
1457
1458        let diff = a.checked_sub(b).unwrap();
1459        assert_eq!(diff.precision, 2);
1460        assert_eq!(diff.as_f64(), 8.25);
1461    }
1462
1463    #[rstest]
1464    fn test_mul() {
1465        let value = 2.0;
1466        let quantity1 = Quantity::new(value, 1);
1467        let quantity2 = Quantity::new(value, 1);
1468        let quantity3 = quantity1 * quantity2;
1469        assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
1470    }
1471
1472    #[rstest]
1473    fn test_comparisons() {
1474        assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
1475        assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
1476        assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
1477        assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
1478        assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
1479        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
1480        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1481        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1482        assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
1483        assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
1484        assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
1485    }
1486
1487    #[rstest]
1488    fn test_debug() {
1489        let quantity = Quantity::from_str("44.12").unwrap();
1490        let result = format!("{quantity:?}");
1491        assert_eq!(result, "Quantity(44.12)");
1492    }
1493
1494    #[rstest]
1495    fn test_display() {
1496        let quantity = Quantity::from_str("44.12").unwrap();
1497        let result = format!("{quantity}");
1498        assert_eq!(result, "44.12");
1499    }
1500
1501    #[rstest]
1502    #[case(44.12, 2, "Quantity(44.12)", "44.12")] // Normal precision
1503    #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] // At max normal precision
1504    #[cfg_attr(
1505        feature = "defi",
1506        case(
1507            1_000_000_000_000_000_000.0,
1508            18,
1509            "Quantity(1000000000000000000)",
1510            "1000000000000000000"
1511        )
1512    )] // High precision
1513    fn test_debug_display_precision_handling(
1514        #[case] value: f64,
1515        #[case] precision: u8,
1516        #[case] expected_debug: &str,
1517        #[case] expected_display: &str,
1518    ) {
1519        let quantity = if precision > MAX_FLOAT_PRECISION {
1520            // For high precision, use from_raw to avoid f64 conversion issues
1521            Quantity::from_raw(value as QuantityRaw, precision)
1522        } else {
1523            Quantity::new(value, precision)
1524        };
1525
1526        assert_eq!(format!("{quantity:?}"), expected_debug);
1527        assert_eq!(format!("{quantity}"), expected_display);
1528    }
1529
1530    #[rstest]
1531    fn test_to_formatted_string() {
1532        let qty = Quantity::new(1234.5678, 4);
1533        let formatted = qty.to_formatted_string();
1534        assert_eq!(formatted, "1_234.5678");
1535        assert_eq!(qty.to_string(), "1234.5678");
1536    }
1537
1538    #[rstest]
1539    fn test_saturating_sub() {
1540        let q1 = Quantity::new(100.0, 2);
1541        let q2 = Quantity::new(50.0, 2);
1542        let q3 = Quantity::new(150.0, 2);
1543
1544        let result = q1.saturating_sub(q2);
1545        assert_eq!(result, Quantity::new(50.0, 2));
1546
1547        let result = q1.saturating_sub(q3);
1548        assert_eq!(result, Quantity::zero(2));
1549        assert_eq!(result.raw, 0);
1550    }
1551
1552    #[rstest]
1553    fn test_saturating_sub_overflow_bug() {
1554        // Reproduces original bug: subtracting a larger quantity from a smaller one
1555        // Raw values must be multiples of 10^(FIXED_PRECISION - precision)
1556        use crate::types::fixed::FIXED_PRECISION;
1557        let precision = 3;
1558        let scale = QuantityRaw::from(10u64.pow(u32::from(FIXED_PRECISION - precision)));
1559
1560        // 79 * scale represents 0.079, 80 * scale represents 0.080
1561        let peak_qty = Quantity::from_raw(79 * scale, precision);
1562        let order_qty = Quantity::from_raw(80 * scale, precision);
1563
1564        // This would have caused panic before fix due to underflow
1565        let result = peak_qty.saturating_sub(order_qty);
1566        assert_eq!(result.raw, 0);
1567        assert_eq!(result, Quantity::zero(precision));
1568    }
1569
1570    #[rstest]
1571    fn test_hash() {
1572        use std::{
1573            collections::hash_map::DefaultHasher,
1574            hash::{Hash, Hasher},
1575        };
1576
1577        let q1 = Quantity::new(100.0, 1);
1578        let q2 = Quantity::new(100.0, 1);
1579        let q3 = Quantity::new(200.0, 1);
1580
1581        let mut s1 = DefaultHasher::new();
1582        let mut s2 = DefaultHasher::new();
1583        let mut s3 = DefaultHasher::new();
1584
1585        q1.hash(&mut s1);
1586        q2.hash(&mut s2);
1587        q3.hash(&mut s3);
1588
1589        assert_eq!(
1590            s1.finish(),
1591            s2.finish(),
1592            "Equal quantities must hash equally"
1593        );
1594        assert_ne!(
1595            s1.finish(),
1596            s3.finish(),
1597            "Different quantities must hash differently"
1598        );
1599    }
1600
1601    #[rstest]
1602    fn test_quantity_serde_json_round_trip() {
1603        let original = Quantity::new(123.456, 3);
1604        let json_str = serde_json::to_string(&original).unwrap();
1605        assert_eq!(json_str, "\"123.456\"");
1606
1607        let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
1608        assert_eq!(deserialized, original);
1609        assert_eq!(deserialized.precision, 3);
1610    }
1611
1612    #[rstest]
1613    fn test_quantity_serde_json_from_value_round_trip() {
1614        let original = Quantity::new(123.456, 3);
1615        let value = serde_json::to_value(original).unwrap();
1616        assert_eq!(value, serde_json::json!("123.456"));
1617
1618        let deserialized: Quantity = serde_json::from_value(value).unwrap();
1619        assert_eq!(deserialized, original);
1620        assert_eq!(deserialized.precision, 3);
1621    }
1622
1623    #[rstest]
1624    fn test_quantity_deserialize_invalid_string_returns_error() {
1625        let result = serde_json::from_str::<Quantity>("\"not-a-quantity\"");
1626        let error = result.unwrap_err();
1627        assert!(
1628            error.to_string().contains("Error parsing"),
1629            "unexpected message: {error}"
1630        );
1631    }
1632
1633    #[rstest]
1634    fn test_quantity_deserialize_negative_returns_error() {
1635        let result = serde_json::from_str::<Quantity>("\"-1.5\"");
1636        let error = result.unwrap_err();
1637        assert!(
1638            error.to_string().contains("negative"),
1639            "unexpected message: {error}"
1640        );
1641    }
1642
1643    #[rstest]
1644    fn test_from_mantissa_exponent_exact_precision() {
1645        let qty = Quantity::from_mantissa_exponent(12345, -2, 2);
1646        assert_eq!(qty.as_f64(), 123.45);
1647    }
1648
1649    #[rstest]
1650    fn test_from_mantissa_exponent_excess_rounds_down() {
1651        // 12.344 -> 12.34 (no rounding needed, truncation)
1652        // 12.345 rounds to 12.34 (4 is even, banker's rounding)
1653        let qty = Quantity::from_mantissa_exponent(12345, -3, 2);
1654        assert_eq!(qty.as_f64(), 12.34);
1655    }
1656
1657    #[rstest]
1658    fn test_from_mantissa_exponent_excess_rounds_up() {
1659        // 12.355 rounds to 12.36 (5 is odd, banker's rounding)
1660        let qty = Quantity::from_mantissa_exponent(12355, -3, 2);
1661        assert_eq!(qty.as_f64(), 12.36);
1662    }
1663
1664    #[rstest]
1665    fn test_from_mantissa_exponent_positive_exponent() {
1666        let qty = Quantity::from_mantissa_exponent(5, 2, 0);
1667        assert_eq!(qty.as_f64(), 500.0);
1668    }
1669
1670    #[rstest]
1671    fn test_from_mantissa_exponent_zero() {
1672        let qty = Quantity::from_mantissa_exponent(0, 2, 2);
1673        assert_eq!(qty.as_f64(), 0.0);
1674    }
1675
1676    #[rstest]
1677    fn test_from_mantissa_exponent_checked_exact_precision() {
1678        let qty = Quantity::from_mantissa_exponent_checked(12345, -2, 2).unwrap();
1679        assert_eq!(qty.as_decimal(), dec!(123.45));
1680    }
1681
1682    #[rstest]
1683    fn test_from_mantissa_exponent_checked_zero_with_large_exponent() {
1684        let qty = Quantity::from_mantissa_exponent_checked(0, 119, 2).unwrap();
1685        assert_eq!(qty.as_decimal(), dec!(0.00));
1686    }
1687
1688    #[rstest]
1689    fn test_from_mantissa_exponent_checked_invalid_precision() {
1690        #[cfg(feature = "defi")]
1691        let invalid_precision = crate::defi::WEI_PRECISION + 1;
1692        #[cfg(not(feature = "defi"))]
1693        let invalid_precision = FIXED_PRECISION + 1;
1694
1695        let error = Quantity::from_mantissa_exponent_checked(1, 0, invalid_precision).unwrap_err();
1696        assert!(error.to_string().contains("`precision` exceeded maximum"));
1697    }
1698
1699    #[rstest]
1700    fn test_from_mantissa_exponent_checked_overflow_returns_error() {
1701        let error = Quantity::from_mantissa_exponent_checked(u64::MAX, 100, 0).unwrap_err();
1702        assert!(
1703            error
1704                .to_string()
1705                .contains("Overflow in Quantity::from_mantissa_exponent")
1706        );
1707    }
1708
1709    #[rstest]
1710    #[should_panic(expected = "Quantity::from_mantissa_exponent")]
1711    fn test_from_mantissa_exponent_overflow_panics() {
1712        let _ = Quantity::from_mantissa_exponent(u64::MAX, 9, 0);
1713    }
1714
1715    #[rstest]
1716    #[should_panic(expected = "exceeds i128 range")]
1717    fn test_from_mantissa_exponent_large_exponent_panics() {
1718        let _ = Quantity::from_mantissa_exponent(1, 119, 0);
1719    }
1720
1721    #[rstest]
1722    fn test_from_mantissa_exponent_zero_with_large_exponent() {
1723        let qty = Quantity::from_mantissa_exponent(0, 119, 0);
1724        assert_eq!(qty.as_f64(), 0.0);
1725    }
1726
1727    #[rstest]
1728    fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1729        let qty = Quantity::from_mantissa_exponent(12345, -120, 2);
1730        assert_eq!(qty.as_f64(), 0.0);
1731    }
1732
1733    #[rstest]
1734    fn test_f64_operations() {
1735        let q = Quantity::new(10.5, 2);
1736        assert_eq!(q + 1.0, 11.5);
1737        assert_eq!(q - 1.0, 9.5);
1738        assert_eq!(q * 2.0, 21.0);
1739        assert_eq!(q / 2.0, 5.25);
1740    }
1741
1742    #[rstest]
1743    fn test_decimal_arithmetic_operations() {
1744        let qty = Quantity::new(100.0, 2);
1745        assert_eq!(qty + dec!(50.25), dec!(150.25));
1746        assert_eq!(qty - dec!(30.50), dec!(69.50));
1747        assert_eq!(qty * dec!(1.5), dec!(150.00));
1748        assert_eq!(qty / dec!(4), dec!(25.00));
1749    }
1750
1751    /// Tests `Quantity::from_u256` using real swap event data from Arbitrum transactions, result values sourced from `DexScreener`.
1752    /// Data sourced from:
1753    /// - Sell tx: <https://arbiscan.io/tx/0xb417009ce3bd9b9f2dde7d52277ffc9f1b1733ecedfcc7f8e3dedd5d87160325>
1754    #[rstest]
1755    #[cfg(feature = "defi")]
1756    #[case::sell_tx_rain_amount(
1757        U256::from_str_radix("42193532365637161405123", 10).unwrap(),
1758        18,
1759        "42193.532365637161405123"
1760    )]
1761    #[case::sell_tx_weth_amount(
1762        U256::from_str_radix("112633187203033110", 10).unwrap(),
1763        18,
1764        "0.112633187203033110"
1765    )]
1766    fn test_from_u256_real_swap_data(
1767        #[case] amount: U256,
1768        #[case] precision: u8,
1769        #[case] expected_str: &str,
1770    ) {
1771        let qty = Quantity::from_u256(amount, precision).unwrap();
1772        assert_eq!(qty.precision, precision);
1773        assert_eq!(qty.as_decimal().to_string(), expected_str);
1774    }
1775
1776    #[rstest]
1777    #[cfg(feature = "defi")]
1778    fn test_from_u256_overflow_returns_typed_error_with_stable_display() {
1779        let error = Quantity::from_u256(U256::MAX, 0).unwrap_err();
1780        match error {
1781            CorrectnessError::PredicateViolation { ref message } => {
1782                assert!(
1783                    message.contains("Amount overflow during scaling to fixed precision"),
1784                    "unexpected message: {message:?}",
1785                );
1786            }
1787            _ => panic!("expected PredicateViolation, was {error:?}"),
1788        }
1789    }
1790
1791    #[rstest]
1792    #[cfg(feature = "defi")]
1793    fn test_from_u256_invalid_precision_returns_typed_error() {
1794        let error = Quantity::from_u256(U256::from(1u8), 19).unwrap_err();
1795        match error {
1796            CorrectnessError::PredicateViolation { ref message } => {
1797                assert!(
1798                    message.contains("WEI_PRECISION"),
1799                    "unexpected message: {message:?}",
1800                );
1801            }
1802            _ => panic!("expected PredicateViolation, was {error:?}"),
1803        }
1804    }
1805
1806    #[rstest]
1807    #[cfg(feature = "defi")]
1808    fn test_from_u256_raw_above_max_returns_typed_error() {
1809        // Pick a U256 value whose scaled raw lies between QUANTITY_RAW_MAX and
1810        // QuantityRaw::MAX so try_from succeeds but from_raw_checked rejects it.
1811        let raw = QUANTITY_RAW_MAX + 1;
1812        let error = Quantity::from_u256(U256::from(raw), FIXED_PRECISION).unwrap_err();
1813        match error {
1814            CorrectnessError::PredicateViolation { ref message } => {
1815                assert!(
1816                    message.contains("QUANTITY_RAW_MAX"),
1817                    "unexpected message: {message:?}",
1818                );
1819            }
1820            _ => panic!("expected PredicateViolation, was {error:?}"),
1821        }
1822    }
1823}
1824
1825#[cfg(test)]
1826mod property_tests {
1827    use proptest::prelude::*;
1828    use rstest::rstest;
1829
1830    use super::*;
1831
1832    /// Strategy to generate valid quantity values (non-negative).
1833    fn quantity_value_strategy() -> impl Strategy<Value = f64> {
1834        // Use a reasonable range for quantities - must be non-negative
1835        prop_oneof![
1836            // Small positive values
1837            0.00001..1.0,
1838            // Normal trading range
1839            1.0..100_000.0,
1840            // Large values (but safe)
1841            100_000.0..1_000_000.0,
1842            // Include zero
1843            Just(0.0),
1844            // Boundary cases
1845            Just(QUANTITY_MAX / 2.0),
1846        ]
1847    }
1848
1849    /// Strategy to generate valid precision values.
1850    fn precision_strategy() -> impl Strategy<Value = u8> {
1851        let upper = FIXED_PRECISION.min(MAX_FLOAT_PRECISION);
1852        prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1853    }
1854
1855    fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1856        let upper = FIXED_PRECISION.clamp(1, MAX_FLOAT_PRECISION);
1857        prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1858    }
1859
1860    fn raw_for_precision_strategy() -> impl Strategy<Value = (QuantityRaw, u8)> {
1861        precision_strategy().prop_flat_map(|precision| {
1862            let step_u128 = 10u128.pow(u32::from(FIXED_PRECISION.saturating_sub(precision)));
1863            #[cfg(feature = "high-precision")]
1864            let max_steps_u128 = QUANTITY_RAW_MAX / step_u128;
1865            #[cfg(not(feature = "high-precision"))]
1866            let max_steps_u128 = (QUANTITY_RAW_MAX as u128) / step_u128;
1867
1868            (0u128..=max_steps_u128).prop_map(move |steps_u128| {
1869                let raw_u128 = steps_u128 * step_u128;
1870                #[cfg(feature = "high-precision")]
1871                let raw = raw_u128;
1872                #[cfg(not(feature = "high-precision"))]
1873                let raw = raw_u128
1874                    .try_into()
1875                    .expect("raw value should fit in QuantityRaw");
1876                (raw, precision)
1877            })
1878        })
1879    }
1880
1881    const DECIMAL_MAX_MANTISSA: u128 = 79_228_162_514_264_337_593_543_950_335;
1882
1883    fn decimal_compatible(raw: QuantityRaw, precision: u8) -> bool {
1884        if precision > MAX_FLOAT_PRECISION {
1885            return false;
1886        }
1887        let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
1888        let divisor = 10u128.pow(precision_diff);
1889        #[cfg(feature = "high-precision")]
1890        let rescaled_raw = raw / divisor;
1891        #[cfg(not(feature = "high-precision"))]
1892        let rescaled_raw = (raw as u128) / divisor;
1893        // rust_decimal stores the coefficient in 96 bits; this guard mirrors that bound so
1894        // proptests skip cases the runtime representation cannot encode.
1895        rescaled_raw <= DECIMAL_MAX_MANTISSA
1896    }
1897
1898    proptest! {
1899        /// Property: Quantity string serialization round-trip should preserve value and precision
1900        #[rstest]
1901        fn prop_quantity_serde_round_trip(
1902            (raw, precision) in raw_for_precision_strategy()
1903        ) {
1904            // Only run string-based round-trip checks where decimal formatting is supported.
1905            prop_assume!(decimal_compatible(raw, precision));
1906
1907            let original = Quantity::from_raw(raw, precision);
1908
1909            // String round-trip (this should be exact and is the most important)
1910            let string_repr = original.to_string();
1911            let from_string: Quantity = string_repr.parse().unwrap();
1912            prop_assert_eq!(from_string.raw, original.raw);
1913            prop_assert_eq!(from_string.precision, original.precision);
1914
1915            // JSON round-trip basic validation (just ensure it doesn't crash and preserves precision)
1916            let json = serde_json::to_string(&original).unwrap();
1917            let from_json: Quantity = serde_json::from_str(&json).unwrap();
1918            prop_assert_eq!(from_json.precision, original.precision);
1919            prop_assert_eq!(from_json.raw, original.raw);
1920        }
1921
1922        /// Property: Quantity arithmetic should be associative for same precision
1923        #[rstest]
1924        fn prop_quantity_arithmetic_associative(
1925            a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1926            b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1927            c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1928            precision in precision_strategy()
1929        ) {
1930            let q_a = Quantity::new(a, precision);
1931            let q_b = Quantity::new(b, precision);
1932            let q_c = Quantity::new(c, precision);
1933
1934            // Check if we can perform the operations without overflow using raw arithmetic
1935            let ab_raw = q_a.raw.checked_add(q_b.raw);
1936            let bc_raw = q_b.raw.checked_add(q_c.raw);
1937
1938            if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1939                let ab_c_raw = ab_raw.checked_add(q_c.raw);
1940                let a_bc_raw = q_a.raw.checked_add(bc_raw);
1941
1942                if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1943                    // (a + b) + c == a + (b + c) using raw arithmetic (exact)
1944                    prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1945                }
1946            }
1947        }
1948
1949        /// Property: Quantity addition/subtraction should be inverse operations (when valid)
1950        #[rstest]
1951        fn prop_quantity_addition_subtraction_inverse(
1952            base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1953            delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1954            precision in precision_strategy()
1955        ) {
1956            let q_base = Quantity::new(base, precision);
1957            let q_delta = Quantity::new(delta, precision);
1958
1959            // Use raw arithmetic to avoid floating-point precision issues
1960            if let Some(added_raw) = q_base.raw.checked_add(q_delta.raw)
1961                && let Some(result_raw) = added_raw.checked_sub(q_delta.raw) {
1962                    // (base + delta) - delta should equal base exactly using raw arithmetic
1963                    prop_assert_eq!(result_raw, q_base.raw, "Inverse operation failed in raw arithmetic");
1964                }
1965        }
1966
1967        /// Property: checked_add agrees with raw checked_add when result is in bounds and
1968        /// no operand is QUANTITY_UNDEF; returns None otherwise.
1969        #[rstest]
1970        fn prop_quantity_checked_add_matches_spec(
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            let expected = q_a.raw
1978                .checked_add(q_b.raw)
1979                .filter(|r| *r <= QUANTITY_RAW_MAX)
1980                .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
1981                .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
1982            prop_assert_eq!(q_a.checked_add(q_b), expected);
1983        }
1984
1985        /// Property: checked_sub agrees with raw checked_sub when no operand is
1986        /// QUANTITY_UNDEF; returns None otherwise.
1987        #[rstest]
1988        fn prop_quantity_checked_sub_matches_spec(
1989            a in quantity_value_strategy(),
1990            b in quantity_value_strategy(),
1991            precision in precision_strategy()
1992        ) {
1993            let q_a = Quantity::new(a, precision);
1994            let q_b = Quantity::new(b, precision);
1995            let expected = q_a.raw
1996                .checked_sub(q_b.raw)
1997                .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
1998                .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
1999            prop_assert_eq!(q_a.checked_sub(q_b), expected);
2000        }
2001
2002        /// Property: Quantity ordering should be transitive
2003        #[rstest]
2004        fn prop_quantity_ordering_transitive(
2005            a in quantity_value_strategy(),
2006            b in quantity_value_strategy(),
2007            c in quantity_value_strategy(),
2008            precision in precision_strategy()
2009        ) {
2010            let q_a = Quantity::new(a, precision);
2011            let q_b = Quantity::new(b, precision);
2012            let q_c = Quantity::new(c, precision);
2013
2014            // If a <= b and b <= c, then a <= c
2015            if q_a <= q_b && q_b <= q_c {
2016                prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
2017                    q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
2018            }
2019        }
2020
2021        /// Property: String parsing should be consistent with precision inference
2022        #[rstest]
2023        fn prop_quantity_string_parsing_precision(
2024            integral in 0u32..1_000_000,
2025            fractional in 0u32..1_000_000,
2026            precision in precision_strategy_non_zero()
2027        ) {
2028            // Create a decimal string with exactly 'precision' decimal places
2029            let pow = 10u128.pow(u32::from(precision));
2030            let fractional_mod = u128::from(fractional) % pow;
2031            let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
2032            let quantity_str = format!("{integral}.{fractional_str}");
2033
2034            let parsed: Quantity = quantity_str.parse().unwrap();
2035            prop_assert_eq!(parsed.precision, precision);
2036
2037            // Round-trip should preserve the original string (after normalization)
2038            let round_trip = parsed.to_string();
2039            let expected_value = format!("{integral}.{fractional_str}");
2040            prop_assert_eq!(round_trip, expected_value);
2041        }
2042
2043        /// Property: Quantity with higher precision should contain more or equal information
2044        #[rstest]
2045        fn prop_quantity_precision_information_preservation(
2046            value in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
2047            precision1 in precision_strategy_non_zero(),
2048            precision2 in precision_strategy_non_zero()
2049        ) {
2050            // Skip cases where precisions are equal (trivial case)
2051            prop_assume!(precision1 != precision2);
2052
2053            let _q1 = Quantity::new(value, precision1);
2054            let _q2 = Quantity::new(value, precision2);
2055
2056            // When both quantities are created from the same value with different precisions,
2057            // converting both to the lower precision should yield the same result
2058            let min_precision = precision1.min(precision2);
2059
2060            // Round the original value to the minimum precision first
2061            let scale = 10.0_f64.powi(i32::from(min_precision));
2062            let rounded_value = (value * scale).round() / scale;
2063
2064            let q1_reduced = Quantity::new(rounded_value, min_precision);
2065            let q2_reduced = Quantity::new(rounded_value, min_precision);
2066
2067            // They should be exactly equal when created from the same rounded value
2068            prop_assert_eq!(q1_reduced.raw, q2_reduced.raw, "Precision reduction inconsistent");
2069        }
2070
2071        /// Property: Quantity arithmetic should never produce invalid values
2072        #[rstest]
2073        fn prop_quantity_arithmetic_bounds(
2074            a in quantity_value_strategy(),
2075            b in quantity_value_strategy(),
2076            precision in precision_strategy()
2077        ) {
2078            let q_a = Quantity::new(a, precision);
2079            let q_b = Quantity::new(b, precision);
2080
2081            // Addition should either succeed or fail predictably
2082            let sum_f64 = q_a.as_f64() + q_b.as_f64();
2083            if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
2084                let sum = q_a + q_b;
2085                prop_assert!(sum.as_f64().is_finite());
2086                prop_assert!(!sum.is_undefined());
2087            }
2088
2089            // Subtraction should either succeed or fail predictably
2090            let diff_f64 = q_a.as_f64() - q_b.as_f64();
2091            if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
2092                let diff = q_a - q_b;
2093                prop_assert!(diff.as_f64().is_finite());
2094                prop_assert!(!diff.is_undefined());
2095            }
2096        }
2097
2098        /// Property: Multiplication should preserve non-negativity
2099        #[rstest]
2100        fn prop_quantity_multiplication_non_negative(
2101            a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2102            b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2103            precision in precision_strategy()
2104        ) {
2105            let q_a = Quantity::new(a, precision);
2106            let q_b = Quantity::new(b, precision);
2107
2108            // Check if multiplication would overflow at the raw level before performing it
2109            let raw_product_check = q_a.raw.checked_mul(q_b.raw);
2110
2111            if let Some(raw_product) = raw_product_check {
2112                // Additional check to ensure the scaled result won't overflow
2113                let scaled_raw = raw_product / (FIXED_SCALAR as QuantityRaw);
2114                if scaled_raw <= QUANTITY_RAW_MAX {
2115                    // Multiplying two quantities should always result in a non-negative value
2116                    let product = q_a * q_b;
2117                    prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
2118                }
2119            }
2120        }
2121
2122        /// Property: Zero quantity should be identity for addition
2123        #[rstest]
2124        fn prop_quantity_zero_addition_identity(
2125            value in quantity_value_strategy(),
2126            precision in precision_strategy()
2127        ) {
2128            let q = Quantity::new(value, precision);
2129            let zero = Quantity::zero(precision);
2130
2131            // q + 0 = q and 0 + q = q
2132            prop_assert_eq!(q + zero, q);
2133            prop_assert_eq!(zero + q, q);
2134        }
2135    }
2136
2137    proptest! {
2138        /// Property: as_decimal scale always matches precision
2139        #[rstest]
2140        fn prop_quantity_as_decimal_preserves_precision(
2141            (raw, precision) in raw_for_precision_strategy()
2142        ) {
2143            prop_assume!(decimal_compatible(raw, precision));
2144            let quantity = Quantity::from_raw(raw, precision);
2145            let decimal = quantity.as_decimal();
2146            prop_assert_eq!(decimal.scale(), u32::from(precision));
2147        }
2148
2149        /// Property: as_decimal and Display produce the same string
2150        #[rstest]
2151        fn prop_quantity_as_decimal_matches_display(
2152            (raw, precision) in raw_for_precision_strategy()
2153        ) {
2154            prop_assume!(decimal_compatible(raw, precision));
2155            let quantity = Quantity::from_raw(raw, precision);
2156            let display_str = format!("{quantity}");
2157            let decimal_str = quantity.as_decimal().to_string();
2158            prop_assert_eq!(display_str, decimal_str);
2159        }
2160
2161        /// Property: from_decimal roundtrip preserves exact value
2162        #[rstest]
2163        fn prop_quantity_from_decimal_roundtrip(
2164            (raw, precision) in raw_for_precision_strategy()
2165        ) {
2166            prop_assume!(decimal_compatible(raw, precision));
2167            let original = Quantity::from_raw(raw, precision);
2168            let decimal = original.as_decimal();
2169            let reconstructed = Quantity::from_decimal(decimal).unwrap();
2170            prop_assert_eq!(original.raw, reconstructed.raw);
2171            prop_assert_eq!(original.precision, reconstructed.precision);
2172        }
2173
2174        /// Property: constructing from raw within bounds preserves raw/precision
2175        #[rstest]
2176        fn prop_quantity_from_raw_round_trip(
2177            (raw, precision) in raw_for_precision_strategy()
2178        ) {
2179            let quantity = Quantity::from_raw(raw, precision);
2180            prop_assert_eq!(quantity.raw, raw);
2181            prop_assert_eq!(quantity.precision, precision);
2182        }
2183    }
2184}