Skip to main content

nautilus_model/types/
quantity.rs

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