Skip to main content

nautilus_model/types/
quantity.rs

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