Skip to main content

nautilus_model/types/
quantity.rs

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