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