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::{FAILED, check_in_range_inclusive_f64, check_predicate_true},
54    formatting::Separable,
55};
56use rust_decimal::Decimal;
57use serde::{Deserialize, Deserializer, Serialize};
58
59use super::fixed::{
60    FIXED_PRECISION, FIXED_SCALAR, MAX_FLOAT_PRECISION, check_fixed_precision,
61    mantissa_exponent_to_fixed_i128,
62};
63#[cfg(not(feature = "high-precision"))]
64use super::fixed::{f64_to_fixed_u64, fixed_u64_to_f64};
65#[cfg(feature = "high-precision")]
66use super::fixed::{f64_to_fixed_u128, fixed_u128_to_f64};
67
68// -----------------------------------------------------------------------------
69// QuantityRaw
70// -----------------------------------------------------------------------------
71
72#[cfg(feature = "high-precision")]
73pub type QuantityRaw = u128;
74
75#[cfg(not(feature = "high-precision"))]
76pub type QuantityRaw = u64;
77
78// -----------------------------------------------------------------------------
79
80/// The maximum raw quantity integer value.
81#[unsafe(no_mangle)]
82#[allow(unsafe_code)]
83pub static QUANTITY_RAW_MAX: QuantityRaw = (QUANTITY_MAX * FIXED_SCALAR) as QuantityRaw;
84
85/// The sentinel value for an unset or null quantity.
86pub const QUANTITY_UNDEF: QuantityRaw = QuantityRaw::MAX;
87
88// -----------------------------------------------------------------------------
89// QUANTITY_MAX
90// -----------------------------------------------------------------------------
91
92#[cfg(feature = "high-precision")]
93/// The maximum valid quantity value that can be represented.
94pub const QUANTITY_MAX: f64 = 34_028_236_692_093.0;
95
96#[cfg(not(feature = "high-precision"))]
97/// The maximum valid quantity value that can be represented.
98pub const QUANTITY_MAX: f64 = 18_446_744_073.0;
99
100// -----------------------------------------------------------------------------
101
102/// The minimum valid quantity value that can be represented.
103pub const QUANTITY_MIN: f64 = 0.0;
104
105/// Represents a quantity with a non-negative value and specified precision.
106///
107/// Capable of storing either a whole number (no decimal places) of 'contracts'
108/// or 'shares' (instruments denominated in whole units) or a decimal value
109/// containing decimal places for instruments denominated in fractional units.
110///
111/// Handles up to [`FIXED_PRECISION`] decimals of precision.
112///
113/// - [`QUANTITY_MAX`] - Maximum representable quantity value.
114/// - [`QUANTITY_MIN`] - 0 (non-negative values only).
115#[repr(C)]
116#[derive(Clone, Copy, Default, Eq)]
117#[cfg_attr(
118    feature = "python",
119    pyo3::pyclass(
120        module = "nautilus_trader.core.nautilus_pyo3.model",
121        frozen,
122        from_py_object
123    )
124)]
125#[cfg_attr(
126    feature = "python",
127    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
128)]
129pub struct Quantity {
130    /// Represents the raw fixed-point value, with `precision` defining the number of decimal places.
131    pub raw: QuantityRaw,
132    /// The number of decimal places, with a maximum of [`FIXED_PRECISION`].
133    pub precision: u8,
134}
135
136impl Quantity {
137    /// Creates a new [`Quantity`] instance with correctness checking.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if:
142    /// - `value` is invalid outside the representable range [0, `QUANTITY_MAX`].
143    /// - `precision` is invalid outside the representable range [0, `FIXED_PRECISION`].
144    ///
145    /// # Notes
146    ///
147    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
148    pub fn new_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
149        check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?;
150
151        #[cfg(feature = "defi")]
152        if precision > MAX_FLOAT_PRECISION {
153            // Floats are only reliable up to ~16 decimal digits of precision regardless of feature flags
154            anyhow::bail!(
155                "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Quantity::from_wei()` for wei values instead"
156            );
157        }
158
159        check_fixed_precision(precision)?;
160
161        #[cfg(feature = "high-precision")]
162        let raw = f64_to_fixed_u128(value, precision);
163        #[cfg(not(feature = "high-precision"))]
164        let raw = f64_to_fixed_u64(value, precision);
165
166        Ok(Self { raw, precision })
167    }
168
169    /// Creates a new [`Quantity`] instance with a guaranteed non zero value.
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if:
174    /// - `value` is zero.
175    /// - `value` becomes zero after rounding to `precision`.
176    /// - `value` is invalid outside the representable range [0, `QUANTITY_MAX`].
177    /// - `precision` is invalid outside the representable range [0, `FIXED_PRECISION`].
178    ///
179    /// # Notes
180    ///
181    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
182    pub fn non_zero_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
183        check_predicate_true(value != 0.0, "value was zero")?;
184        check_fixed_precision(precision)?;
185        let rounded_value =
186            (value * 10.0_f64.powi(precision as i32)).round() / 10.0_f64.powi(precision as i32);
187        check_predicate_true(
188            rounded_value != 0.0,
189            &format!("value {value} was zero after rounding to precision {precision}"),
190        )?;
191
192        Self::new_checked(value, precision)
193    }
194
195    /// Creates a new [`Quantity`] instance.
196    ///
197    /// # Panics
198    ///
199    /// Panics if a correctness check fails. See [`Quantity::new_checked`] for more details.
200    pub fn new(value: f64, precision: u8) -> Self {
201        Self::new_checked(value, precision).expect(FAILED)
202    }
203
204    /// Creates a new [`Quantity`] instance with a guaranteed non zero value.
205    ///
206    /// # Panics
207    ///
208    /// Panics if a correctness check fails. See [`Quantity::non_zero_checked`] for more details.
209    pub fn non_zero(value: f64, precision: u8) -> Self {
210        Self::non_zero_checked(value, precision).expect(FAILED)
211    }
212
213    /// Creates a new [`Quantity`] instance from the given `raw` fixed-point value and `precision`.
214    ///
215    /// # Panics
216    ///
217    /// Panics if `raw` exceeds [`QUANTITY_RAW_MAX`] and is not a sentinel value.
218    /// Panics if `precision` exceeds [`FIXED_PRECISION`].
219    pub fn from_raw(raw: QuantityRaw, precision: u8) -> Self {
220        assert!(
221            raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
222            "`raw` value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
223        );
224
225        if raw == QUANTITY_UNDEF {
226            assert!(
227                precision == 0,
228                "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
229            );
230        }
231        check_fixed_precision(precision).expect(FAILED);
232
233        // TODO: Enforce spurious bits validation in v2
234        // if raw != QUANTITY_UNDEF && raw > 0 {
235        //     #[cfg(feature = "high-precision")]
236        //     super::fixed::check_fixed_raw_u128(raw, precision).expect(FAILED);
237        //     #[cfg(not(feature = "high-precision"))]
238        //     super::fixed::check_fixed_raw_u64(raw, precision).expect(FAILED);
239        // }
240
241        Self { raw, precision }
242    }
243
244    /// Creates a new [`Quantity`] instance from the given `raw` fixed-point value and `precision`
245    /// with correctness checking.
246    ///
247    /// # Errors
248    ///
249    /// Returns an error if:
250    /// - `precision` exceeds the maximum fixed precision.
251    /// - `precision` is not 0 when `raw` is `QUANTITY_UNDEF`.
252    /// - `raw` exceeds `QUANTITY_RAW_MAX` and is not a sentinel value.
253    pub fn from_raw_checked(raw: QuantityRaw, precision: u8) -> anyhow::Result<Self> {
254        if raw == QUANTITY_UNDEF {
255            anyhow::ensure!(
256                precision == 0,
257                "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
258            );
259        }
260        anyhow::ensure!(
261            raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
262            "raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}"
263        );
264        check_fixed_precision(precision)?;
265
266        Ok(Self { raw, precision })
267    }
268
269    /// Computes a saturating subtraction between two quantities, logging when clamped.
270    ///
271    /// When `rhs` is greater than `self`, the result is clamped to zero and a warning is logged.
272    /// Precision follows the `Sub` implementation: uses the maximum precision of both operands.
273    #[must_use]
274    pub fn saturating_sub(self, rhs: Self) -> Self {
275        let precision = self.precision.max(rhs.precision);
276        let raw = self.raw.saturating_sub(rhs.raw);
277        if raw == 0 && self.raw < rhs.raw {
278            log::warn!(
279                "Saturating Quantity subtraction: {self} - {rhs} < 0, clamped to 0 (precision={precision})"
280            );
281        }
282
283        Self { raw, precision }
284    }
285
286    /// Creates a new [`Quantity`] instance with a value of zero with the given `precision`.
287    ///
288    /// # Panics
289    ///
290    /// Panics if a correctness check fails. See [`Quantity::new_checked`] for more details.
291    #[must_use]
292    pub fn zero(precision: u8) -> Self {
293        check_fixed_precision(precision).expect(FAILED);
294        Self::new(0.0, precision)
295    }
296
297    /// Returns `true` if the value of this instance is undefined.
298    #[must_use]
299    pub fn is_undefined(&self) -> bool {
300        self.raw == QUANTITY_UNDEF
301    }
302
303    /// Returns `true` if the value of this instance is zero.
304    #[must_use]
305    pub fn is_zero(&self) -> bool {
306        self.raw == 0
307    }
308
309    /// Returns `true` if the value of this instance is position (> 0).
310    #[must_use]
311    pub fn is_positive(&self) -> bool {
312        self.raw != QUANTITY_UNDEF && self.raw > 0
313    }
314
315    #[cfg(feature = "high-precision")]
316    /// Returns the value of this instance as an `f64`.
317    ///
318    /// # Panics
319    ///
320    /// Panics if precision is beyond `MAX_FLOAT_PRECISION` (16).
321    #[must_use]
322    pub fn as_f64(&self) -> f64 {
323        #[cfg(feature = "defi")]
324        assert!(
325            self.precision <= MAX_FLOAT_PRECISION,
326            "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
327        );
328
329        fixed_u128_to_f64(self.raw)
330    }
331
332    #[cfg(not(feature = "high-precision"))]
333    /// Returns the value of this instance as an `f64`.
334    ///
335    /// # Panics
336    ///
337    /// Panics if precision is beyond `MAX_FLOAT_PRECISION` (16).
338    #[must_use]
339    pub fn as_f64(&self) -> f64 {
340        #[cfg(feature = "defi")]
341        if self.precision > MAX_FLOAT_PRECISION {
342            panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
343        }
344
345        fixed_u64_to_f64(self.raw)
346    }
347
348    /// Returns the value of this instance as a `Decimal`.
349    #[must_use]
350    pub fn as_decimal(&self) -> Decimal {
351        // Scale down the raw value to match the precision
352        let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
353        let rescaled_raw = self.raw / QuantityRaw::pow(10, u32::from(precision_diff));
354
355        // The raw value is guaranteed to be within i128 range after scaling
356        // because our quantity constraints ensure the maximum raw value times the scaling
357        // factor cannot exceed i128::MAX (high-precision) or i64::MAX (standard-precision).
358        #[allow(clippy::useless_conversion)]
359        Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
360    }
361
362    /// Returns a formatted string representation of this instance.
363    #[must_use]
364    pub fn to_formatted_string(&self) -> String {
365        format!("{self}").separate_with_underscores()
366    }
367
368    /// Creates a new [`Quantity`] from a `Decimal` value with specified precision.
369    ///
370    /// Uses pure integer arithmetic on the Decimal's mantissa and scale for fast conversion.
371    /// The value is rounded to the specified precision using banker's rounding (round half to even).
372    ///
373    /// # Errors
374    ///
375    /// Returns an error if:
376    /// - `precision` exceeds [`FIXED_PRECISION`].
377    /// - The decimal value is negative.
378    /// - The decimal value cannot be converted to the raw representation.
379    /// - Overflow occurs during scaling.
380    pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> anyhow::Result<Self> {
381        anyhow::ensure!(
382            decimal.mantissa() >= 0,
383            "Decimal value '{decimal}' is negative, Quantity must be non-negative"
384        );
385
386        let exponent = -(decimal.scale() as i8);
387        let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
388
389        let raw: QuantityRaw = raw_i128.try_into().map_err(|_| {
390            anyhow::anyhow!("Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]")
391        })?;
392        anyhow::ensure!(
393            raw <= QUANTITY_RAW_MAX,
394            "Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
395        );
396
397        Ok(Self { raw, precision })
398    }
399
400    /// Creates a new [`Quantity`] from a [`Decimal`] value with precision inferred from the decimal's scale.
401    ///
402    /// The precision is determined by the scale of the decimal (number of decimal places).
403    /// The value is rounded to the inferred precision using banker's rounding (round half to even).
404    ///
405    /// # Errors
406    ///
407    /// Returns an error if:
408    /// - The inferred precision exceeds [`FIXED_PRECISION`].
409    /// - The decimal value cannot be converted to the raw representation.
410    /// - Overflow occurs during scaling.
411    pub fn from_decimal(decimal: Decimal) -> anyhow::Result<Self> {
412        let precision = decimal.scale() as u8;
413        Self::from_decimal_dp(decimal, precision)
414    }
415
416    /// Creates a new [`Quantity`] from a mantissa/exponent pair using pure integer arithmetic.
417    ///
418    /// The value is `mantissa * 10^exponent`. This avoids all floating-point and Decimal
419    /// operations, making it ideal for exchange data that arrives as mantissa/exponent pairs.
420    ///
421    /// # Panics
422    ///
423    /// Panics if the resulting raw value exceeds [`QUANTITY_RAW_MAX`].
424    #[must_use]
425    pub fn from_mantissa_exponent(mantissa: u64, exponent: i8, precision: u8) -> Self {
426        check_fixed_precision(precision).expect(FAILED);
427
428        if mantissa == 0 {
429            return Self { raw: 0, precision };
430        }
431
432        let raw_i128 = mantissa_exponent_to_fixed_i128(mantissa as i128, exponent, precision)
433            .expect("Overflow in Quantity::from_mantissa_exponent");
434
435        let raw: QuantityRaw = raw_i128
436            .try_into()
437            .expect("Raw value exceeds QuantityRaw range in Quantity::from_mantissa_exponent");
438        assert!(
439            raw <= QUANTITY_RAW_MAX,
440            "`raw` value {raw} exceeded QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
441        );
442
443        Self { raw, precision }
444    }
445
446    /// Creates a new [`Quantity`] from a U256 amount with specified precision.
447    ///
448    /// # Errors
449    ///
450    /// Returns an error if:
451    /// - Overflow occurs during scaling when precision is less than [`FIXED_PRECISION`].
452    /// - The scaled U256 amount exceeds the `QuantityRaw` range.
453    #[cfg(feature = "defi")]
454    pub fn from_u256(amount: U256, precision: u8) -> anyhow::Result<Self> {
455        // Quantity expects raw values scaled to at least FIXED_PRECISION or higher(WEI)
456        let scaled_amount = if precision < FIXED_PRECISION {
457            amount
458                .checked_mul(U256::from(10u128.pow((FIXED_PRECISION - precision) as u32)))
459                .ok_or_else(|| {
460                    anyhow::anyhow!(
461                        "Amount overflow during scaling to fixed precision: {} * 10^{}",
462                        amount,
463                        FIXED_PRECISION - precision
464                    )
465                })?
466        } else {
467            amount
468        };
469
470        let raw = QuantityRaw::try_from(scaled_amount).map_err(|_| {
471            anyhow::anyhow!("U256 scaled amount {scaled_amount} exceeds QuantityRaw range")
472        })?;
473
474        Ok(Self::from_raw(raw, precision))
475    }
476}
477
478impl From<Quantity> for f64 {
479    fn from(qty: Quantity) -> Self {
480        qty.as_f64()
481    }
482}
483
484impl From<&Quantity> for f64 {
485    fn from(qty: &Quantity) -> Self {
486        qty.as_f64()
487    }
488}
489
490impl From<i32> for Quantity {
491    /// Creates a `Quantity` from an `i32` value.
492    ///
493    /// # Panics
494    ///
495    /// Panics if `value` is negative. Use `u32` for guaranteed non-negative values.
496    fn from(value: i32) -> Self {
497        assert!(
498            value >= 0,
499            "Cannot create Quantity from negative i32: {value}. Use u32 or check value is non-negative."
500        );
501        Self::new(value as f64, 0)
502    }
503}
504
505impl From<i64> for Quantity {
506    /// Creates a `Quantity` from an `i64` value.
507    ///
508    /// # Panics
509    ///
510    /// Panics if `value` is negative. Use `u64` for guaranteed non-negative values.
511    fn from(value: i64) -> Self {
512        assert!(
513            value >= 0,
514            "Cannot create Quantity from negative i64: {value}. Use u64 or check value is non-negative."
515        );
516        Self::new(value as f64, 0)
517    }
518}
519
520impl From<u32> for Quantity {
521    fn from(value: u32) -> Self {
522        Self::new(value as f64, 0)
523    }
524}
525
526impl From<u64> for Quantity {
527    fn from(value: u64) -> Self {
528        Self::new(value as f64, 0)
529    }
530}
531
532impl Hash for Quantity {
533    fn hash<H: Hasher>(&self, state: &mut H) {
534        self.raw.hash(state);
535    }
536}
537
538impl PartialEq for Quantity {
539    fn eq(&self, other: &Self) -> bool {
540        self.raw == other.raw
541    }
542}
543
544impl PartialOrd for Quantity {
545    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
546        Some(self.cmp(other))
547    }
548
549    fn lt(&self, other: &Self) -> bool {
550        self.raw.lt(&other.raw)
551    }
552
553    fn le(&self, other: &Self) -> bool {
554        self.raw.le(&other.raw)
555    }
556
557    fn gt(&self, other: &Self) -> bool {
558        self.raw.gt(&other.raw)
559    }
560
561    fn ge(&self, other: &Self) -> bool {
562        self.raw.ge(&other.raw)
563    }
564}
565
566impl Ord for Quantity {
567    fn cmp(&self, other: &Self) -> Ordering {
568        self.raw.cmp(&other.raw)
569    }
570}
571
572impl Deref for Quantity {
573    type Target = QuantityRaw;
574
575    fn deref(&self) -> &Self::Target {
576        &self.raw
577    }
578}
579
580impl Add for Quantity {
581    type Output = Self;
582    fn add(self, rhs: Self) -> Self::Output {
583        Self {
584            raw: self
585                .raw
586                .checked_add(rhs.raw)
587                .expect("Overflow occurred when adding `Quantity`"),
588            precision: self.precision.max(rhs.precision),
589        }
590    }
591}
592
593impl Sub for Quantity {
594    type Output = Self;
595    fn sub(self, rhs: Self) -> Self::Output {
596        Self {
597            raw: self
598                .raw
599                .checked_sub(rhs.raw)
600                .expect("Underflow occurred when subtracting `Quantity`"),
601            precision: self.precision.max(rhs.precision),
602        }
603    }
604}
605
606#[allow(
607    clippy::suspicious_arithmetic_impl,
608    reason = "Can use division to scale back"
609)]
610impl Mul for Quantity {
611    type Output = Self;
612    fn mul(self, rhs: Self) -> Self::Output {
613        let result_raw = self
614            .raw
615            .checked_mul(rhs.raw)
616            .expect("Overflow occurred when multiplying `Quantity`");
617
618        Self {
619            raw: result_raw / (FIXED_SCALAR as QuantityRaw),
620            precision: self.precision.max(rhs.precision),
621        }
622    }
623}
624
625impl Add<Decimal> for Quantity {
626    type Output = Decimal;
627    fn add(self, rhs: Decimal) -> Self::Output {
628        self.as_decimal() + rhs
629    }
630}
631
632impl Sub<Decimal> for Quantity {
633    type Output = Decimal;
634    fn sub(self, rhs: Decimal) -> Self::Output {
635        self.as_decimal() - rhs
636    }
637}
638
639impl Mul<Decimal> for Quantity {
640    type Output = Decimal;
641    fn mul(self, rhs: Decimal) -> Self::Output {
642        self.as_decimal() * rhs
643    }
644}
645
646impl Div<Decimal> for Quantity {
647    type Output = Decimal;
648    fn div(self, rhs: Decimal) -> Self::Output {
649        self.as_decimal() / rhs
650    }
651}
652
653impl Add<f64> for Quantity {
654    type Output = f64;
655    fn add(self, rhs: f64) -> Self::Output {
656        self.as_f64() + rhs
657    }
658}
659
660impl Sub<f64> for Quantity {
661    type Output = f64;
662    fn sub(self, rhs: f64) -> Self::Output {
663        self.as_f64() - rhs
664    }
665}
666
667impl Mul<f64> for Quantity {
668    type Output = f64;
669    fn mul(self, rhs: f64) -> Self::Output {
670        self.as_f64() * rhs
671    }
672}
673
674impl Div<f64> for Quantity {
675    type Output = f64;
676    fn div(self, rhs: f64) -> Self::Output {
677        self.as_f64() / rhs
678    }
679}
680
681impl From<Quantity> for QuantityRaw {
682    fn from(value: Quantity) -> Self {
683        value.raw
684    }
685}
686
687impl From<&Quantity> for QuantityRaw {
688    fn from(value: &Quantity) -> Self {
689        value.raw
690    }
691}
692
693impl From<Quantity> for Decimal {
694    fn from(value: Quantity) -> Self {
695        value.as_decimal()
696    }
697}
698
699impl From<&Quantity> for Decimal {
700    fn from(value: &Quantity) -> Self {
701        value.as_decimal()
702    }
703}
704
705impl FromStr for Quantity {
706    type Err = String;
707
708    fn from_str(value: &str) -> Result<Self, Self::Err> {
709        let clean_value = value.replace('_', "");
710
711        let decimal = if clean_value.contains('e') || clean_value.contains('E') {
712            Decimal::from_scientific(&clean_value)
713                .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
714        } else {
715            Decimal::from_str(&clean_value)
716                .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
717        };
718
719        // Use decimal scale to preserve caller-specified precision (including trailing zeros)
720        let precision = decimal.scale() as u8;
721
722        Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string())
723    }
724}
725
726impl From<&str> for Quantity {
727    fn from(value: &str) -> Self {
728        Self::from_str(value).expect(FAILED)
729    }
730}
731
732impl From<String> for Quantity {
733    fn from(value: String) -> Self {
734        Self::from_str(&value).expect(FAILED)
735    }
736}
737
738impl From<&String> for Quantity {
739    fn from(value: &String) -> Self {
740        Self::from_str(value).expect(FAILED)
741    }
742}
743
744impl Debug for Quantity {
745    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
746        if self.precision > MAX_FLOAT_PRECISION {
747            write!(f, "{}({})", stringify!(Quantity), self.raw)
748        } else {
749            write!(f, "{}({})", stringify!(Quantity), self.as_decimal())
750        }
751    }
752}
753
754impl Display for Quantity {
755    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
756        if self.precision > MAX_FLOAT_PRECISION {
757            write!(f, "{}", self.raw)
758        } else {
759            write!(f, "{}", self.as_decimal())
760        }
761    }
762}
763
764impl Serialize for Quantity {
765    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
766    where
767        S: serde::Serializer,
768    {
769        serializer.serialize_str(&self.to_string())
770    }
771}
772
773impl<'de> Deserialize<'de> for Quantity {
774    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
775    where
776        D: Deserializer<'de>,
777    {
778        let qty_str: &str = Deserialize::deserialize(_deserializer)?;
779        let qty: Self = qty_str.into();
780        Ok(qty)
781    }
782}
783
784/// Checks if the quantity `value` is positive.
785///
786/// # Errors
787///
788/// Returns an error if `value` is not positive.
789pub fn check_positive_quantity(value: Quantity, param: &str) -> anyhow::Result<()> {
790    if !value.is_positive() {
791        anyhow::bail!("invalid `Quantity` for '{param}' not positive, was {value}")
792    }
793    Ok(())
794}
795
796#[cfg(test)]
797mod tests {
798    use std::str::FromStr;
799
800    use nautilus_core::approx_eq;
801    use rstest::rstest;
802    use rust_decimal_macros::dec;
803
804    use super::*;
805
806    #[rstest]
807    #[should_panic(expected = "invalid `Quantity` for 'qty' not positive, was 0")]
808    fn test_check_quantity_positive() {
809        let qty = Quantity::new(0.0, 0);
810        check_positive_quantity(qty, "qty").unwrap();
811    }
812
813    #[rstest]
814    #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
815    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 17")]
816    fn test_invalid_precision_new() {
817        // Precision 17 should fail due to DeFi validation
818        let _ = Quantity::new(1.0, 17);
819    }
820
821    #[rstest]
822    #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
823    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
824    fn test_invalid_precision_new() {
825        // Precision 17 should fail due to DeFi validation
826        let _ = Quantity::new(1.0, 17);
827    }
828
829    #[rstest]
830    #[cfg(not(feature = "defi"))]
831    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
832    fn test_invalid_precision_from_raw() {
833        // Precision out of range for fixed
834        let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
835    }
836
837    #[rstest]
838    #[cfg(not(feature = "defi"))]
839    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
840    fn test_invalid_precision_zero() {
841        // Precision out of range for fixed
842        let _ = Quantity::zero(FIXED_PRECISION + 1);
843    }
844
845    #[rstest]
846    fn test_mixed_precision_add() {
847        let q1 = Quantity::new(1.0, 1);
848        let q2 = Quantity::new(1.0, 2);
849        let result = q1 + q2;
850        assert_eq!(result.precision, 2);
851        assert_eq!(result.as_f64(), 2.0);
852    }
853
854    #[rstest]
855    fn test_mixed_precision_sub() {
856        let q1 = Quantity::new(2.0, 1);
857        let q2 = Quantity::new(1.0, 2);
858        let result = q1 - q2;
859        assert_eq!(result.precision, 2);
860        assert_eq!(result.as_f64(), 1.0);
861    }
862
863    #[rstest]
864    fn test_mixed_precision_mul() {
865        let q1 = Quantity::new(2.0, 1);
866        let q2 = Quantity::new(3.0, 2);
867        let result = q1 * q2;
868        assert_eq!(result.precision, 2);
869        assert_eq!(result.as_f64(), 6.0);
870    }
871
872    #[rstest]
873    fn test_new_non_zero_ok() {
874        let qty = Quantity::non_zero_checked(123.456, 3).unwrap();
875        assert_eq!(qty.raw, Quantity::new(123.456, 3).raw);
876        assert!(qty.is_positive());
877    }
878
879    #[rstest]
880    fn test_new_non_zero_zero_input() {
881        assert!(Quantity::non_zero_checked(0.0, 0).is_err());
882    }
883
884    #[rstest]
885    fn test_new_non_zero_rounds_to_zero() {
886        // 0.0004 rounded to 3 dp ⇒ 0.000
887        assert!(Quantity::non_zero_checked(0.0004, 3).is_err());
888    }
889
890    #[rstest]
891    fn test_new_non_zero_negative() {
892        assert!(Quantity::non_zero_checked(-1.0, 0).is_err());
893    }
894
895    #[rstest]
896    fn test_new_non_zero_exceeds_max() {
897        assert!(Quantity::non_zero_checked(QUANTITY_MAX * 10.0, 0).is_err());
898    }
899
900    #[rstest]
901    fn test_new_non_zero_invalid_precision() {
902        assert!(Quantity::non_zero_checked(1.0, FIXED_PRECISION + 1).is_err());
903    }
904
905    #[rstest]
906    fn test_new() {
907        let value = 0.00812;
908        let qty = Quantity::new(value, 8);
909        assert_eq!(qty, qty);
910        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
911        assert_eq!(qty.precision, 8);
912        assert_eq!(qty, Quantity::from("0.00812000"));
913        assert_eq!(qty.as_decimal(), dec!(0.00812000));
914        assert_eq!(qty.to_string(), "0.00812000");
915        assert!(!qty.is_zero());
916        assert!(qty.is_positive());
917        assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
918    }
919
920    #[rstest]
921    fn test_check_quantity_positive_ok() {
922        let qty = Quantity::new(10.0, 0);
923        check_positive_quantity(qty, "qty").unwrap();
924    }
925
926    #[rstest]
927    fn test_negative_quantity_validation() {
928        assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
929    }
930
931    #[rstest]
932    fn test_undefined() {
933        let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
934        assert_eq!(qty.raw, QUANTITY_UNDEF);
935        assert!(qty.is_undefined());
936    }
937
938    #[rstest]
939    fn test_zero() {
940        let qty = Quantity::zero(8);
941        assert_eq!(qty.raw, 0);
942        assert_eq!(qty.precision, 8);
943        assert!(qty.is_zero());
944        assert!(!qty.is_positive());
945    }
946
947    #[rstest]
948    fn test_from_i32() {
949        let value = 100_000i32;
950        let qty = Quantity::from(value);
951        assert_eq!(qty, qty);
952        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
953        assert_eq!(qty.precision, 0);
954    }
955
956    #[rstest]
957    fn test_from_u32() {
958        let value: u32 = 5000;
959        let qty = Quantity::from(value);
960        assert_eq!(qty.raw, Quantity::from(format!("{value}")).raw);
961        assert_eq!(qty.precision, 0);
962    }
963
964    #[rstest]
965    fn test_from_i64() {
966        let value = 100_000i64;
967        let qty = Quantity::from(value);
968        assert_eq!(qty, qty);
969        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
970        assert_eq!(qty.precision, 0);
971    }
972
973    #[rstest]
974    fn test_from_u64() {
975        let value = 100_000u64;
976        let qty = Quantity::from(value);
977        assert_eq!(qty, qty);
978        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
979        assert_eq!(qty.precision, 0);
980    }
981
982    #[rstest] // Test does not panic rather than exact value
983    fn test_with_maximum_value() {
984        let qty = Quantity::new_checked(QUANTITY_MAX, 0);
985        assert!(qty.is_ok());
986    }
987
988    #[rstest]
989    fn test_with_minimum_positive_value() {
990        let value = 0.000_000_001;
991        let qty = Quantity::new(value, 9);
992        assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
993        assert_eq!(qty.as_decimal(), dec!(0.000000001));
994        assert_eq!(qty.to_string(), "0.000000001");
995    }
996
997    #[rstest]
998    fn test_with_minimum_value() {
999        let qty = Quantity::new(QUANTITY_MIN, 9);
1000        assert_eq!(qty.raw, 0);
1001        assert_eq!(qty.as_decimal(), dec!(0));
1002        assert_eq!(qty.to_string(), "0.000000000");
1003    }
1004
1005    #[rstest]
1006    fn test_is_zero() {
1007        let qty = Quantity::zero(8);
1008        assert_eq!(qty, qty);
1009        assert_eq!(qty.raw, 0);
1010        assert_eq!(qty.precision, 8);
1011        assert_eq!(qty, Quantity::from("0.00000000"));
1012        assert_eq!(qty.as_decimal(), dec!(0));
1013        assert_eq!(qty.to_string(), "0.00000000");
1014        assert!(qty.is_zero());
1015    }
1016
1017    #[rstest]
1018    fn test_precision() {
1019        let value = 1.001;
1020        let qty = Quantity::new(value, 2);
1021        assert_eq!(qty.to_string(), "1.00");
1022    }
1023
1024    #[rstest]
1025    fn test_new_from_str() {
1026        let qty = Quantity::new(0.00812000, 8);
1027        assert_eq!(qty, qty);
1028        assert_eq!(qty.precision, 8);
1029        assert_eq!(qty, Quantity::from("0.00812000"));
1030        assert_eq!(qty.to_string(), "0.00812000");
1031    }
1032
1033    #[rstest]
1034    #[case("0", 0)]
1035    #[case("1.1", 1)]
1036    #[case("1.123456789", 9)]
1037    fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
1038        let qty = Quantity::from(input);
1039        assert_eq!(qty.precision, expected_prec);
1040        assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
1041    }
1042
1043    #[rstest]
1044    #[should_panic]
1045    fn test_from_str_invalid_input() {
1046        let input = "invalid";
1047        Quantity::new(f64::from_str(input).unwrap(), 8);
1048    }
1049
1050    #[rstest]
1051    fn test_from_str_errors() {
1052        assert!(Quantity::from_str("invalid").is_err());
1053        assert!(Quantity::from_str("12.34.56").is_err());
1054        assert!(Quantity::from_str("").is_err());
1055        assert!(Quantity::from_str("-1").is_err()); // Negative values not allowed
1056        assert!(Quantity::from_str("-0.001").is_err());
1057    }
1058
1059    #[rstest]
1060    #[case("1e7", 0, 10_000_000.0)]
1061    #[case("2.5e3", 0, 2_500.0)]
1062    #[case("1.234e-2", 5, 0.01234)]
1063    #[case("5E-3", 3, 0.005)]
1064    #[case("1.0e6", 0, 1_000_000.0)]
1065    fn test_from_str_scientific_notation(
1066        #[case] input: &str,
1067        #[case] expected_precision: u8,
1068        #[case] expected_value: f64,
1069    ) {
1070        let qty = Quantity::from_str(input).unwrap();
1071        assert_eq!(qty.precision, expected_precision);
1072        assert!(approx_eq!(
1073            f64,
1074            qty.as_f64(),
1075            expected_value,
1076            epsilon = 1e-10
1077        ));
1078    }
1079
1080    #[rstest]
1081    #[case("1_234.56", 2, 1234.56)]
1082    #[case("1_000_000", 0, 1_000_000.0)]
1083    #[case("99_999.999_99", 5, 99_999.999_99)]
1084    fn test_from_str_with_underscores(
1085        #[case] input: &str,
1086        #[case] expected_precision: u8,
1087        #[case] expected_value: f64,
1088    ) {
1089        let qty = Quantity::from_str(input).unwrap();
1090        assert_eq!(qty.precision, expected_precision);
1091        assert!(approx_eq!(
1092            f64,
1093            qty.as_f64(),
1094            expected_value,
1095            epsilon = 1e-10
1096        ));
1097    }
1098
1099    #[rstest]
1100    fn test_from_decimal_dp_preservation() {
1101        // Test that decimal conversion preserves exact values
1102        let decimal = dec!(123.456789);
1103        let qty = Quantity::from_decimal_dp(decimal, 6).unwrap();
1104        assert_eq!(qty.precision, 6);
1105        assert!(approx_eq!(f64, qty.as_f64(), 123.456789, epsilon = 1e-10));
1106
1107        // Verify raw value is exact
1108        let expected_raw = 123456789_u64 * 10_u64.pow((FIXED_PRECISION - 6) as u32);
1109        assert_eq!(qty.raw, expected_raw as QuantityRaw);
1110    }
1111
1112    #[rstest]
1113    fn test_from_decimal_dp_rounding() {
1114        // Test banker's rounding (round half to even)
1115        let decimal = dec!(1.005);
1116        let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1117        assert_eq!(qty.as_f64(), 1.0); // 1.005 rounds to 1.00 (even)
1118
1119        let decimal = dec!(1.015);
1120        let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1121        assert_eq!(qty.as_f64(), 1.02); // 1.015 rounds to 1.02 (even)
1122    }
1123
1124    #[rstest]
1125    fn test_from_decimal_infers_precision() {
1126        // Test that precision is inferred from decimal's scale
1127        let decimal = dec!(123.456);
1128        let qty = Quantity::from_decimal(decimal).unwrap();
1129        assert_eq!(qty.precision, 3);
1130        assert!(approx_eq!(f64, qty.as_f64(), 123.456, epsilon = 1e-10));
1131
1132        // Test with integer (precision 0)
1133        let decimal = dec!(100);
1134        let qty = Quantity::from_decimal(decimal).unwrap();
1135        assert_eq!(qty.precision, 0);
1136        assert_eq!(qty.as_f64(), 100.0);
1137
1138        // Test with high precision
1139        let decimal = dec!(1.23456789);
1140        let qty = Quantity::from_decimal(decimal).unwrap();
1141        assert_eq!(qty.precision, 8);
1142        assert!(approx_eq!(f64, qty.as_f64(), 1.23456789, epsilon = 1e-10));
1143    }
1144
1145    #[rstest]
1146    fn test_from_decimal_trailing_zeros() {
1147        // Decimal preserves trailing zeros in scale
1148        let decimal = dec!(5.670);
1149        assert_eq!(decimal.scale(), 3); // Has 3 decimal places
1150
1151        // from_decimal infers precision from scale (includes trailing zeros)
1152        let qty = Quantity::from_decimal(decimal).unwrap();
1153        assert_eq!(qty.precision, 3);
1154        assert!(approx_eq!(f64, qty.as_f64(), 5.67, epsilon = 1e-10));
1155
1156        // Normalized removes trailing zeros
1157        let normalized = decimal.normalize();
1158        assert_eq!(normalized.scale(), 2);
1159        let qty_normalized = Quantity::from_decimal(normalized).unwrap();
1160        assert_eq!(qty_normalized.precision, 2);
1161    }
1162
1163    #[rstest]
1164    #[case("1.00", 2)]
1165    #[case("1.0", 1)]
1166    #[case("1.000", 3)]
1167    #[case("100.00", 2)]
1168    #[case("0.10", 2)]
1169    #[case("0.100", 3)]
1170    fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1171        let qty = Quantity::from_str(input).unwrap();
1172        assert_eq!(qty.precision, expected_precision);
1173    }
1174
1175    #[rstest]
1176    fn test_from_decimal_excessive_precision_inference() {
1177        // Create a decimal with more precision than FIXED_PRECISION
1178        // Decimal supports up to 28 decimal places
1179        let decimal = dec!(1.1234567890123456789012345678);
1180
1181        // If scale exceeds FIXED_PRECISION, from_decimal should error
1182        if decimal.scale() > FIXED_PRECISION as u32 {
1183            assert!(Quantity::from_decimal(decimal).is_err());
1184        }
1185    }
1186
1187    #[rstest]
1188    fn test_from_decimal_negative_quantity_errors() {
1189        // Negative quantities should error (Quantity must be non-negative)
1190        let decimal = dec!(-123.45);
1191        let result = Quantity::from_decimal(decimal);
1192        assert!(result.is_err());
1193
1194        // Also test with explicit precision
1195        let result = Quantity::from_decimal_dp(decimal, 2);
1196        assert!(result.is_err());
1197    }
1198
1199    #[rstest]
1200    fn test_add() {
1201        let a = 1.0;
1202        let b = 2.0;
1203        let quantity1 = Quantity::new(1.0, 0);
1204        let quantity2 = Quantity::new(2.0, 0);
1205        let quantity3 = quantity1 + quantity2;
1206        assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
1207    }
1208
1209    #[rstest]
1210    fn test_sub() {
1211        let a = 3.0;
1212        let b = 2.0;
1213        let quantity1 = Quantity::new(a, 0);
1214        let quantity2 = Quantity::new(b, 0);
1215        let quantity3 = quantity1 - quantity2;
1216        assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
1217    }
1218
1219    #[rstest]
1220    fn test_mul() {
1221        let value = 2.0;
1222        let quantity1 = Quantity::new(value, 1);
1223        let quantity2 = Quantity::new(value, 1);
1224        let quantity3 = quantity1 * quantity2;
1225        assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
1226    }
1227
1228    #[rstest]
1229    fn test_comparisons() {
1230        assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
1231        assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
1232        assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
1233        assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
1234        assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
1235        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
1236        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1237        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1238        assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
1239        assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
1240        assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
1241    }
1242
1243    #[rstest]
1244    fn test_debug() {
1245        let quantity = Quantity::from_str("44.12").unwrap();
1246        let result = format!("{quantity:?}");
1247        assert_eq!(result, "Quantity(44.12)");
1248    }
1249
1250    #[rstest]
1251    fn test_display() {
1252        let quantity = Quantity::from_str("44.12").unwrap();
1253        let result = format!("{quantity}");
1254        assert_eq!(result, "44.12");
1255    }
1256
1257    #[rstest]
1258    #[case(44.12, 2, "Quantity(44.12)", "44.12")] // Normal precision
1259    #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] // At max normal precision
1260    #[cfg_attr(
1261        feature = "defi",
1262        case(
1263            1_000_000_000_000_000_000.0,
1264            18,
1265            "Quantity(1000000000000000000)",
1266            "1000000000000000000"
1267        )
1268    )] // High precision
1269    fn test_debug_display_precision_handling(
1270        #[case] value: f64,
1271        #[case] precision: u8,
1272        #[case] expected_debug: &str,
1273        #[case] expected_display: &str,
1274    ) {
1275        let quantity = if precision > MAX_FLOAT_PRECISION {
1276            // For high precision, use from_raw to avoid f64 conversion issues
1277            Quantity::from_raw(value as QuantityRaw, precision)
1278        } else {
1279            Quantity::new(value, precision)
1280        };
1281
1282        assert_eq!(format!("{quantity:?}"), expected_debug);
1283        assert_eq!(format!("{quantity}"), expected_display);
1284    }
1285
1286    #[rstest]
1287    fn test_to_formatted_string() {
1288        let qty = Quantity::new(1234.5678, 4);
1289        let formatted = qty.to_formatted_string();
1290        assert_eq!(formatted, "1_234.5678");
1291        assert_eq!(qty.to_string(), "1234.5678");
1292    }
1293
1294    #[rstest]
1295    fn test_saturating_sub() {
1296        let q1 = Quantity::new(100.0, 2);
1297        let q2 = Quantity::new(50.0, 2);
1298        let q3 = Quantity::new(150.0, 2);
1299
1300        let result = q1.saturating_sub(q2);
1301        assert_eq!(result, Quantity::new(50.0, 2));
1302
1303        let result = q1.saturating_sub(q3);
1304        assert_eq!(result, Quantity::zero(2));
1305        assert_eq!(result.raw, 0);
1306    }
1307
1308    #[rstest]
1309    fn test_saturating_sub_overflow_bug() {
1310        // Reproduces original bug: subtracting a larger quantity from a smaller one
1311        // Raw values must be multiples of 10^(FIXED_PRECISION - precision)
1312        use crate::types::fixed::FIXED_PRECISION;
1313        let precision = 3;
1314        let scale = 10u64.pow(u32::from(FIXED_PRECISION - precision)) as QuantityRaw;
1315
1316        // 79 * scale represents 0.079, 80 * scale represents 0.080
1317        let peak_qty = Quantity::from_raw(79 * scale, precision);
1318        let order_qty = Quantity::from_raw(80 * scale, precision);
1319
1320        // This would have caused panic before fix due to underflow
1321        let result = peak_qty.saturating_sub(order_qty);
1322        assert_eq!(result.raw, 0);
1323        assert_eq!(result, Quantity::zero(precision));
1324    }
1325
1326    #[rstest]
1327    fn test_hash() {
1328        use std::{
1329            collections::hash_map::DefaultHasher,
1330            hash::{Hash, Hasher},
1331        };
1332
1333        let q1 = Quantity::new(100.0, 1);
1334        let q2 = Quantity::new(100.0, 1);
1335        let q3 = Quantity::new(200.0, 1);
1336
1337        let mut s1 = DefaultHasher::new();
1338        let mut s2 = DefaultHasher::new();
1339        let mut s3 = DefaultHasher::new();
1340
1341        q1.hash(&mut s1);
1342        q2.hash(&mut s2);
1343        q3.hash(&mut s3);
1344
1345        assert_eq!(
1346            s1.finish(),
1347            s2.finish(),
1348            "Equal quantities must hash equally"
1349        );
1350        assert_ne!(
1351            s1.finish(),
1352            s3.finish(),
1353            "Different quantities must hash differently"
1354        );
1355    }
1356
1357    #[rstest]
1358    fn test_quantity_serde_json_round_trip() {
1359        let original = Quantity::new(123.456, 3);
1360        let json_str = serde_json::to_string(&original).unwrap();
1361        assert_eq!(json_str, "\"123.456\"");
1362
1363        let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
1364        assert_eq!(deserialized, original);
1365        assert_eq!(deserialized.precision, 3);
1366    }
1367
1368    #[rstest]
1369    fn test_from_mantissa_exponent_exact_precision() {
1370        let qty = Quantity::from_mantissa_exponent(12345, -2, 2);
1371        assert_eq!(qty.as_f64(), 123.45);
1372    }
1373
1374    #[rstest]
1375    fn test_from_mantissa_exponent_excess_rounds_down() {
1376        // 12.344 -> 12.34 (no rounding needed, truncation)
1377        // 12.345 rounds to 12.34 (4 is even, banker's rounding)
1378        let qty = Quantity::from_mantissa_exponent(12345, -3, 2);
1379        assert_eq!(qty.as_f64(), 12.34);
1380    }
1381
1382    #[rstest]
1383    fn test_from_mantissa_exponent_excess_rounds_up() {
1384        // 12.355 rounds to 12.36 (5 is odd, banker's rounding)
1385        let qty = Quantity::from_mantissa_exponent(12355, -3, 2);
1386        assert_eq!(qty.as_f64(), 12.36);
1387    }
1388
1389    #[rstest]
1390    fn test_from_mantissa_exponent_positive_exponent() {
1391        let qty = Quantity::from_mantissa_exponent(5, 2, 0);
1392        assert_eq!(qty.as_f64(), 500.0);
1393    }
1394
1395    #[rstest]
1396    fn test_from_mantissa_exponent_zero() {
1397        let qty = Quantity::from_mantissa_exponent(0, 2, 2);
1398        assert_eq!(qty.as_f64(), 0.0);
1399    }
1400
1401    #[rstest]
1402    #[should_panic]
1403    fn test_from_mantissa_exponent_overflow_panics() {
1404        let _ = Quantity::from_mantissa_exponent(u64::MAX, 9, 0);
1405    }
1406
1407    #[rstest]
1408    #[should_panic(expected = "exceeds i128 range")]
1409    fn test_from_mantissa_exponent_large_exponent_panics() {
1410        let _ = Quantity::from_mantissa_exponent(1, 119, 0);
1411    }
1412
1413    #[rstest]
1414    fn test_from_mantissa_exponent_zero_with_large_exponent() {
1415        let qty = Quantity::from_mantissa_exponent(0, 119, 0);
1416        assert_eq!(qty.as_f64(), 0.0);
1417    }
1418
1419    #[rstest]
1420    fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1421        let qty = Quantity::from_mantissa_exponent(12345, -120, 2);
1422        assert_eq!(qty.as_f64(), 0.0);
1423    }
1424
1425    #[rstest]
1426    fn test_f64_operations() {
1427        let q = Quantity::new(10.5, 2);
1428        assert_eq!(q + 1.0, 11.5);
1429        assert_eq!(q - 1.0, 9.5);
1430        assert_eq!(q * 2.0, 21.0);
1431        assert_eq!(q / 2.0, 5.25);
1432    }
1433
1434    #[rstest]
1435    fn test_decimal_arithmetic_operations() {
1436        let qty = Quantity::new(100.0, 2);
1437        assert_eq!(qty + dec!(50.25), dec!(150.25));
1438        assert_eq!(qty - dec!(30.50), dec!(69.50));
1439        assert_eq!(qty * dec!(1.5), dec!(150.00));
1440        assert_eq!(qty / dec!(4), dec!(25.00));
1441    }
1442
1443    /// Tests `Quantity::from_u256` using real swap event data from Arbitrum transactions, result values sourced from DexScreener.
1444    /// Data sourced from:
1445    /// - Sell tx: https://arbiscan.io/tx/0xb417009ce3bd9b9f2dde7d52277ffc9f1b1733ecedfcc7f8e3dedd5d87160325
1446    #[rstest]
1447    #[cfg(feature = "defi")]
1448    #[case::sell_tx_rain_amount(
1449        U256::from_str_radix("42193532365637161405123", 10).unwrap(),
1450        18,
1451        "42193.532365637161405123"
1452    )]
1453    #[case::sell_tx_weth_amount(
1454        U256::from_str_radix("112633187203033110", 10).unwrap(),
1455        18,
1456        "0.112633187203033110"
1457    )]
1458    fn test_from_u256_real_swap_data(
1459        #[case] amount: U256,
1460        #[case] precision: u8,
1461        #[case] expected_str: &str,
1462    ) {
1463        let qty = Quantity::from_u256(amount, precision).unwrap();
1464        assert_eq!(qty.precision, precision);
1465        assert_eq!(qty.as_decimal().to_string(), expected_str);
1466    }
1467}
1468
1469#[cfg(test)]
1470mod property_tests {
1471    use proptest::prelude::*;
1472    use rstest::rstest;
1473
1474    use super::*;
1475
1476    /// Strategy to generate valid quantity values (non-negative).
1477    fn quantity_value_strategy() -> impl Strategy<Value = f64> {
1478        // Use a reasonable range for quantities - must be non-negative
1479        prop_oneof![
1480            // Small positive values
1481            0.00001..1.0,
1482            // Normal trading range
1483            1.0..100_000.0,
1484            // Large values (but safe)
1485            100_000.0..1_000_000.0,
1486            // Include zero
1487            Just(0.0),
1488            // Boundary cases
1489            Just(QUANTITY_MAX / 2.0),
1490        ]
1491    }
1492
1493    /// Strategy to generate valid precision values.
1494    fn precision_strategy() -> impl Strategy<Value = u8> {
1495        let upper = FIXED_PRECISION.min(MAX_FLOAT_PRECISION);
1496        prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1497    }
1498
1499    fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1500        let upper = FIXED_PRECISION.clamp(1, MAX_FLOAT_PRECISION);
1501        prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1502    }
1503
1504    fn raw_for_precision_strategy() -> impl Strategy<Value = (QuantityRaw, u8)> {
1505        precision_strategy().prop_flat_map(|precision| {
1506            let step_u128 = 10u128.pow(u32::from(FIXED_PRECISION.saturating_sub(precision)));
1507            #[cfg(feature = "high-precision")]
1508            let max_steps_u128 = QUANTITY_RAW_MAX / step_u128;
1509            #[cfg(not(feature = "high-precision"))]
1510            let max_steps_u128 = (QUANTITY_RAW_MAX as u128) / step_u128;
1511
1512            (0u128..=max_steps_u128).prop_map(move |steps_u128| {
1513                let raw_u128 = steps_u128 * step_u128;
1514                #[cfg(feature = "high-precision")]
1515                let raw = raw_u128;
1516                #[cfg(not(feature = "high-precision"))]
1517                let raw = raw_u128
1518                    .try_into()
1519                    .expect("raw value should fit in QuantityRaw");
1520                (raw, precision)
1521            })
1522        })
1523    }
1524
1525    const DECIMAL_MAX_MANTISSA: u128 = 79_228_162_514_264_337_593_543_950_335;
1526
1527    fn decimal_compatible(raw: QuantityRaw, precision: u8) -> bool {
1528        if precision > MAX_FLOAT_PRECISION {
1529            return false;
1530        }
1531        let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
1532        let divisor = 10u128.pow(precision_diff);
1533        #[cfg(feature = "high-precision")]
1534        let rescaled_raw = raw / divisor;
1535        #[cfg(not(feature = "high-precision"))]
1536        let rescaled_raw = (raw as u128) / divisor;
1537        // rust_decimal stores the coefficient in 96 bits; this guard mirrors that bound so
1538        // proptests skip cases the runtime representation cannot encode.
1539        rescaled_raw <= DECIMAL_MAX_MANTISSA
1540    }
1541
1542    proptest! {
1543        /// Property: Quantity string serialization round-trip should preserve value and precision
1544        #[rstest]
1545        fn prop_quantity_serde_round_trip(
1546            (raw, precision) in raw_for_precision_strategy()
1547        ) {
1548            // Only run string-based round-trip checks where decimal formatting is supported.
1549            prop_assume!(decimal_compatible(raw, precision));
1550
1551            let original = Quantity::from_raw(raw, precision);
1552
1553            // String round-trip (this should be exact and is the most important)
1554            let string_repr = original.to_string();
1555            let from_string: Quantity = string_repr.parse().unwrap();
1556            prop_assert_eq!(from_string.raw, original.raw);
1557            prop_assert_eq!(from_string.precision, original.precision);
1558
1559            // JSON round-trip basic validation (just ensure it doesn't crash and preserves precision)
1560            let json = serde_json::to_string(&original).unwrap();
1561            let from_json: Quantity = serde_json::from_str(&json).unwrap();
1562            prop_assert_eq!(from_json.precision, original.precision);
1563            prop_assert_eq!(from_json.raw, original.raw);
1564        }
1565
1566        /// Property: Quantity arithmetic should be associative for same precision
1567        #[rstest]
1568        fn prop_quantity_arithmetic_associative(
1569            a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1570            b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1571            c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1572            precision in precision_strategy()
1573        ) {
1574            let q_a = Quantity::new(a, precision);
1575            let q_b = Quantity::new(b, precision);
1576            let q_c = Quantity::new(c, precision);
1577
1578            // Check if we can perform the operations without overflow using raw arithmetic
1579            let ab_raw = q_a.raw.checked_add(q_b.raw);
1580            let bc_raw = q_b.raw.checked_add(q_c.raw);
1581
1582            if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1583                let ab_c_raw = ab_raw.checked_add(q_c.raw);
1584                let a_bc_raw = q_a.raw.checked_add(bc_raw);
1585
1586                if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1587                    // (a + b) + c == a + (b + c) using raw arithmetic (exact)
1588                    prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1589                }
1590            }
1591        }
1592
1593        /// Property: Quantity addition/subtraction should be inverse operations (when valid)
1594        #[rstest]
1595        fn prop_quantity_addition_subtraction_inverse(
1596            base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1597            delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1598            precision in precision_strategy()
1599        ) {
1600            let q_base = Quantity::new(base, precision);
1601            let q_delta = Quantity::new(delta, precision);
1602
1603            // Use raw arithmetic to avoid floating-point precision issues
1604            if let Some(added_raw) = q_base.raw.checked_add(q_delta.raw)
1605                && let Some(result_raw) = added_raw.checked_sub(q_delta.raw) {
1606                    // (base + delta) - delta should equal base exactly using raw arithmetic
1607                    prop_assert_eq!(result_raw, q_base.raw, "Inverse operation failed in raw arithmetic");
1608                }
1609        }
1610
1611        /// Property: Quantity ordering should be transitive
1612        #[rstest]
1613        fn prop_quantity_ordering_transitive(
1614            a in quantity_value_strategy(),
1615            b in quantity_value_strategy(),
1616            c in quantity_value_strategy(),
1617            precision in precision_strategy()
1618        ) {
1619            let q_a = Quantity::new(a, precision);
1620            let q_b = Quantity::new(b, precision);
1621            let q_c = Quantity::new(c, precision);
1622
1623            // If a <= b and b <= c, then a <= c
1624            if q_a <= q_b && q_b <= q_c {
1625                prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
1626                    q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
1627            }
1628        }
1629
1630        /// Property: String parsing should be consistent with precision inference
1631        #[rstest]
1632        fn prop_quantity_string_parsing_precision(
1633            integral in 0u32..1000000,
1634            fractional in 0u32..1000000,
1635            precision in precision_strategy_non_zero()
1636        ) {
1637            // Create a decimal string with exactly 'precision' decimal places
1638            let pow = 10u128.pow(u32::from(precision));
1639            let fractional_mod = (fractional as u128) % pow;
1640            let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
1641            let quantity_str = format!("{integral}.{fractional_str}");
1642
1643            let parsed: Quantity = quantity_str.parse().unwrap();
1644            prop_assert_eq!(parsed.precision, precision);
1645
1646            // Round-trip should preserve the original string (after normalization)
1647            let round_trip = parsed.to_string();
1648            let expected_value = format!("{integral}.{fractional_str}");
1649            prop_assert_eq!(round_trip, expected_value);
1650        }
1651
1652        /// Property: Quantity with higher precision should contain more or equal information
1653        #[rstest]
1654        fn prop_quantity_precision_information_preservation(
1655            value in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1656            precision1 in precision_strategy_non_zero(),
1657            precision2 in precision_strategy_non_zero()
1658        ) {
1659            // Skip cases where precisions are equal (trivial case)
1660            prop_assume!(precision1 != precision2);
1661
1662            let _q1 = Quantity::new(value, precision1);
1663            let _q2 = Quantity::new(value, precision2);
1664
1665            // When both quantities are created from the same value with different precisions,
1666            // converting both to the lower precision should yield the same result
1667            let min_precision = precision1.min(precision2);
1668
1669            // Round the original value to the minimum precision first
1670            let scale = 10.0_f64.powi(min_precision as i32);
1671            let rounded_value = (value * scale).round() / scale;
1672
1673            let q1_reduced = Quantity::new(rounded_value, min_precision);
1674            let q2_reduced = Quantity::new(rounded_value, min_precision);
1675
1676            // They should be exactly equal when created from the same rounded value
1677            prop_assert_eq!(q1_reduced.raw, q2_reduced.raw, "Precision reduction inconsistent");
1678        }
1679
1680        /// Property: Quantity arithmetic should never produce invalid values
1681        #[rstest]
1682        fn prop_quantity_arithmetic_bounds(
1683            a in quantity_value_strategy(),
1684            b in quantity_value_strategy(),
1685            precision in precision_strategy()
1686        ) {
1687            let q_a = Quantity::new(a, precision);
1688            let q_b = Quantity::new(b, precision);
1689
1690            // Addition should either succeed or fail predictably
1691            let sum_f64 = q_a.as_f64() + q_b.as_f64();
1692            if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
1693                let sum = q_a + q_b;
1694                prop_assert!(sum.as_f64().is_finite());
1695                prop_assert!(!sum.is_undefined());
1696            }
1697
1698            // Subtraction should either succeed or fail predictably
1699            let diff_f64 = q_a.as_f64() - q_b.as_f64();
1700            if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
1701                let diff = q_a - q_b;
1702                prop_assert!(diff.as_f64().is_finite());
1703                prop_assert!(!diff.is_undefined());
1704            }
1705        }
1706
1707        /// Property: Multiplication should preserve non-negativity
1708        #[rstest]
1709        fn prop_quantity_multiplication_non_negative(
1710            a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
1711            b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
1712            precision in precision_strategy()
1713        ) {
1714            let q_a = Quantity::new(a, precision);
1715            let q_b = Quantity::new(b, precision);
1716
1717            // Check if multiplication would overflow at the raw level before performing it
1718            let raw_product_check = q_a.raw.checked_mul(q_b.raw);
1719
1720            if let Some(raw_product) = raw_product_check {
1721                // Additional check to ensure the scaled result won't overflow
1722                let scaled_raw = raw_product / (FIXED_SCALAR as QuantityRaw);
1723                if scaled_raw <= QUANTITY_RAW_MAX {
1724                    // Multiplying two quantities should always result in a non-negative value
1725                    let product = q_a * q_b;
1726                    prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
1727                }
1728            }
1729        }
1730
1731        /// Property: Zero quantity should be identity for addition
1732        #[rstest]
1733        fn prop_quantity_zero_addition_identity(
1734            value in quantity_value_strategy(),
1735            precision in precision_strategy()
1736        ) {
1737            let q = Quantity::new(value, precision);
1738            let zero = Quantity::zero(precision);
1739
1740            // q + 0 = q and 0 + q = q
1741            prop_assert_eq!(q + zero, q);
1742            prop_assert_eq!(zero + q, q);
1743        }
1744    }
1745
1746    proptest! {
1747        /// Property: as_decimal scale always matches precision
1748        #[rstest]
1749        fn prop_quantity_as_decimal_preserves_precision(
1750            (raw, precision) in raw_for_precision_strategy()
1751        ) {
1752            prop_assume!(decimal_compatible(raw, precision));
1753            let quantity = Quantity::from_raw(raw, precision);
1754            let decimal = quantity.as_decimal();
1755            prop_assert_eq!(decimal.scale(), u32::from(precision));
1756        }
1757
1758        /// Property: as_decimal and Display produce the same string
1759        #[rstest]
1760        fn prop_quantity_as_decimal_matches_display(
1761            (raw, precision) in raw_for_precision_strategy()
1762        ) {
1763            prop_assume!(decimal_compatible(raw, precision));
1764            let quantity = Quantity::from_raw(raw, precision);
1765            let display_str = format!("{quantity}");
1766            let decimal_str = quantity.as_decimal().to_string();
1767            prop_assert_eq!(display_str, decimal_str);
1768        }
1769
1770        /// Property: from_decimal roundtrip preserves exact value
1771        #[rstest]
1772        fn prop_quantity_from_decimal_roundtrip(
1773            (raw, precision) in raw_for_precision_strategy()
1774        ) {
1775            prop_assume!(decimal_compatible(raw, precision));
1776            let original = Quantity::from_raw(raw, precision);
1777            let decimal = original.as_decimal();
1778            let reconstructed = Quantity::from_decimal(decimal).unwrap();
1779            prop_assert_eq!(original.raw, reconstructed.raw);
1780            prop_assert_eq!(original.precision, reconstructed.precision);
1781        }
1782
1783        /// Property: constructing from raw within bounds preserves raw/precision
1784        #[rstest]
1785        fn prop_quantity_from_raw_round_trip(
1786            (raw, precision) in raw_for_precision_strategy()
1787        ) {
1788            let quantity = Quantity::from_raw(raw, precision);
1789            prop_assert_eq!(quantity.raw, raw);
1790            prop_assert_eq!(quantity.precision, precision);
1791        }
1792    }
1793}