Skip to main content

nautilus_model/types/
quantity.rs

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