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