nautilus_model/types/
quantity.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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
26use nautilus_core::{
27    correctness::{FAILED, check_in_range_inclusive_f64, check_predicate_true},
28    parsing::precision_from_str,
29};
30use rust_decimal::Decimal;
31use serde::{Deserialize, Deserializer, Serialize};
32use thousands::Separable;
33
34use super::fixed::{FIXED_PRECISION, FIXED_SCALAR, check_fixed_precision};
35#[cfg(not(feature = "high-precision"))]
36use super::fixed::{f64_to_fixed_u64, fixed_u64_to_f64};
37#[cfg(feature = "high-precision")]
38use super::fixed::{f64_to_fixed_u128, fixed_u128_to_f64};
39#[cfg(feature = "defi")]
40use crate::types::fixed::MAX_FLOAT_PRECISION;
41
42// -----------------------------------------------------------------------------
43// QuantityRaw
44// -----------------------------------------------------------------------------
45
46#[cfg(feature = "high-precision")]
47pub type QuantityRaw = u128;
48
49#[cfg(not(feature = "high-precision"))]
50pub type QuantityRaw = u64;
51
52// -----------------------------------------------------------------------------
53
54/// The maximum raw quantity integer value.
55#[unsafe(no_mangle)]
56#[allow(unsafe_code)]
57pub static QUANTITY_RAW_MAX: QuantityRaw = (QUANTITY_MAX * FIXED_SCALAR) as QuantityRaw;
58
59/// The sentinel value for an unset or null quantity.
60pub const QUANTITY_UNDEF: QuantityRaw = QuantityRaw::MAX;
61
62// -----------------------------------------------------------------------------
63// QUANTITY_MAX
64// -----------------------------------------------------------------------------
65
66#[cfg(feature = "high-precision")]
67/// The maximum valid quantity value that can be represented.
68pub const QUANTITY_MAX: f64 = 34_028_236_692_093.0;
69
70#[cfg(not(feature = "high-precision"))]
71/// The maximum valid quantity value that can be represented.
72pub const QUANTITY_MAX: f64 = 18_446_744_073.0;
73
74// -----------------------------------------------------------------------------
75
76/// The minimum valid quantity value that can be represented.
77pub const QUANTITY_MIN: f64 = 0.0;
78
79/// Represents a quantity with a non-negative value and specified precision.
80///
81/// Capable of storing either a whole number (no decimal places) of 'contracts'
82/// or 'shares' (instruments denominated in whole units) or a decimal value
83/// containing decimal places for instruments denominated in fractional units.
84///
85/// Handles up to [`FIXED_PRECISION`] decimals of precision.
86///
87/// - [`QUANTITY_MAX`] - Maximum representable quantity value.
88/// - [`QUANTITY_MIN`] - 0 (non-negative values only).
89#[repr(C)]
90#[derive(Clone, Copy, Default, Eq)]
91#[cfg_attr(
92    feature = "python",
93    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
94)]
95pub struct Quantity {
96    /// Represents the raw fixed-point value, with `precision` defining the number of decimal places.
97    pub raw: QuantityRaw,
98    /// The number of decimal places, with a maximum of [`FIXED_PRECISION`].
99    pub precision: u8,
100}
101
102impl Quantity {
103    /// Creates a new [`Quantity`] instance with correctness checking.
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if:
108    /// - `value` is invalid outside the representable range [0, `QUANTITY_MAX`].
109    /// - `precision` is invalid outside the representable range [0, `FIXED_PRECISION`].
110    ///
111    /// # Notes
112    ///
113    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
114    pub fn new_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
115        check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?;
116
117        #[cfg(feature = "defi")]
118        if precision > MAX_FLOAT_PRECISION {
119            // Floats are only reliable up to ~16 decimal digits of precision regardless of feature flags
120            anyhow::bail!(
121                "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Quantity::from_wei()` for WEI values instead"
122            );
123        }
124
125        check_fixed_precision(precision)?;
126
127        #[cfg(feature = "high-precision")]
128        let raw = f64_to_fixed_u128(value, precision);
129        #[cfg(not(feature = "high-precision"))]
130        let raw = f64_to_fixed_u64(value, precision);
131
132        Ok(Self { raw, precision })
133    }
134
135    /// Creates a new [`Quantity`] instance with a guaranteed non zero value.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if:
140    /// - `value` is zero.
141    /// - `value` becomes zero after rounding to `precision`.
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 non_zero_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
149        check_predicate_true(value != 0.0, "value was zero")?;
150        check_fixed_precision(precision)?;
151        let rounded_value =
152            (value * 10.0_f64.powi(precision as i32)).round() / 10.0_f64.powi(precision as i32);
153        check_predicate_true(
154            rounded_value != 0.0,
155            &format!("value {value} was zero after rounding to precision {precision}"),
156        )?;
157
158        Self::new_checked(value, precision)
159    }
160
161    /// Creates a new [`Quantity`] instance.
162    ///
163    /// # Panics
164    ///
165    /// Panics if a correctness check fails. See [`Quantity::new_checked`] for more details.
166    pub fn new(value: f64, precision: u8) -> Self {
167        Self::new_checked(value, precision).expect(FAILED)
168    }
169
170    /// Creates a new [`Quantity`] instance with a guaranteed non zero value.
171    ///
172    /// # Panics
173    ///
174    /// Panics if a correctness check fails. See [`Quantity::non_zero_checked`] for more details.
175    pub fn non_zero(value: f64, precision: u8) -> Self {
176        Self::non_zero_checked(value, precision).expect(FAILED)
177    }
178
179    /// Creates a new [`Quantity`] instance from the given `raw` fixed-point value and `precision`.
180    ///
181    /// # Panics
182    ///
183    /// Panics if a correctness check fails. See [`Quantity::new_checked`] for more details.
184    pub fn from_raw(raw: QuantityRaw, precision: u8) -> Self {
185        if raw == QUANTITY_UNDEF {
186            check_predicate_true(
187                precision == 0,
188                "`precision` must be 0 when `raw` is QUANTITY_UNDEF",
189            )
190            .expect(FAILED);
191        }
192        check_predicate_true(
193            raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
194            &format!("raw outside valid range, was {raw}"),
195        )
196        .expect(FAILED);
197        check_fixed_precision(precision).expect(FAILED);
198        Self { raw, precision }
199    }
200
201    /// Creates a new [`Quantity`] instance with a value of zero with the given `precision`.
202    ///
203    /// # Panics
204    ///
205    /// Panics if a correctness check fails. See [`Quantity::new_checked`] for more details.
206    #[must_use]
207    pub fn zero(precision: u8) -> Self {
208        check_fixed_precision(precision).expect(FAILED);
209        Self::new(0.0, precision)
210    }
211
212    /// Returns `true` if the value of this instance is undefined.
213    #[must_use]
214    pub fn is_undefined(&self) -> bool {
215        self.raw == QUANTITY_UNDEF
216    }
217
218    /// Returns `true` if the value of this instance is zero.
219    #[must_use]
220    pub fn is_zero(&self) -> bool {
221        self.raw == 0
222    }
223
224    /// Returns `true` if the value of this instance is position (> 0).
225    #[must_use]
226    pub fn is_positive(&self) -> bool {
227        self.raw != QUANTITY_UNDEF && self.raw > 0
228    }
229
230    #[cfg(feature = "high-precision")]
231    /// Returns the value of this instance as an `f64`.
232    ///
233    /// # Panics
234    ///
235    /// Panics if precision is beyond [`MAX_FLOAT_PRECISION`] (16).
236    #[must_use]
237    pub fn as_f64(&self) -> f64 {
238        #[cfg(feature = "defi")]
239        if self.precision > MAX_FLOAT_PRECISION {
240            panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
241        }
242
243        fixed_u128_to_f64(self.raw)
244    }
245
246    #[cfg(not(feature = "high-precision"))]
247    /// Returns the value of this instance as an `f64`.
248    ///
249    /// # Panics
250    ///
251    /// Panics if precision is beyond [`MAX_FLOAT_PRECISION`] (16).
252    #[must_use]
253    pub fn as_f64(&self) -> f64 {
254        #[cfg(feature = "defi")]
255        if self.precision > MAX_FLOAT_PRECISION {
256            panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
257        }
258
259        fixed_u64_to_f64(self.raw)
260    }
261
262    /// Returns the value of this instance as a `Decimal`.
263    #[must_use]
264    pub fn as_decimal(&self) -> Decimal {
265        // Scale down the raw value to match the precision
266        let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
267        let rescaled_raw = self.raw / QuantityRaw::pow(10, u32::from(precision_diff));
268
269        // SAFETY: The raw value is guaranteed to be within i128 range after scaling
270        // because our quantity constraints ensure the maximum raw value times the scaling
271        // factor cannot exceed i128::MAX (high-precision) or i64::MAX (standard-precision).
272        #[allow(clippy::useless_conversion)] // Required for precision modes
273        Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
274    }
275
276    /// Returns a formatted string representation of this instance.
277    #[must_use]
278    pub fn to_formatted_string(&self) -> String {
279        format!("{self}").separate_with_underscores()
280    }
281}
282
283impl From<Quantity> for f64 {
284    fn from(qty: Quantity) -> Self {
285        qty.as_f64()
286    }
287}
288
289impl From<&Quantity> for f64 {
290    fn from(qty: &Quantity) -> Self {
291        qty.as_f64()
292    }
293}
294
295impl From<i32> for Quantity {
296    fn from(value: i32) -> Self {
297        Self::new(value as f64, 0)
298    }
299}
300
301impl From<i64> for Quantity {
302    fn from(value: i64) -> Self {
303        Self::new(value as f64, 0)
304    }
305}
306
307impl From<u32> for Quantity {
308    fn from(value: u32) -> Self {
309        Self::new(value as f64, 0)
310    }
311}
312
313impl From<u64> for Quantity {
314    fn from(value: u64) -> Self {
315        Self::new(value as f64, 0)
316    }
317}
318
319impl Hash for Quantity {
320    fn hash<H: Hasher>(&self, state: &mut H) {
321        self.raw.hash(state);
322    }
323}
324
325impl PartialEq for Quantity {
326    fn eq(&self, other: &Self) -> bool {
327        self.raw == other.raw
328    }
329}
330
331impl PartialOrd for Quantity {
332    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
333        Some(self.cmp(other))
334    }
335
336    fn lt(&self, other: &Self) -> bool {
337        self.raw.lt(&other.raw)
338    }
339
340    fn le(&self, other: &Self) -> bool {
341        self.raw.le(&other.raw)
342    }
343
344    fn gt(&self, other: &Self) -> bool {
345        self.raw.gt(&other.raw)
346    }
347
348    fn ge(&self, other: &Self) -> bool {
349        self.raw.ge(&other.raw)
350    }
351}
352
353impl Ord for Quantity {
354    fn cmp(&self, other: &Self) -> Ordering {
355        self.raw.cmp(&other.raw)
356    }
357}
358
359impl Deref for Quantity {
360    type Target = QuantityRaw;
361
362    fn deref(&self) -> &Self::Target {
363        &self.raw
364    }
365}
366
367impl Add for Quantity {
368    type Output = Self;
369    fn add(self, rhs: Self) -> Self::Output {
370        let precision = match self.precision {
371            0 => rhs.precision,
372            _ => self.precision,
373        };
374        assert!(
375            self.precision >= rhs.precision,
376            "Precision mismatch: cannot add precision {} to precision {} (precision loss)",
377            rhs.precision,
378            self.precision,
379        );
380        Self {
381            raw: self
382                .raw
383                .checked_add(rhs.raw)
384                .expect("Overflow occurred when adding `Quantity`"),
385            precision,
386        }
387    }
388}
389
390impl Sub for Quantity {
391    type Output = Self;
392    fn sub(self, rhs: Self) -> Self::Output {
393        let precision = match self.precision {
394            0 => rhs.precision,
395            _ => self.precision,
396        };
397        assert!(
398            self.precision >= rhs.precision,
399            "Precision mismatch: cannot subtract precision {} from precision {} (precision loss)",
400            rhs.precision,
401            self.precision,
402        );
403        Self {
404            raw: self
405                .raw
406                .checked_sub(rhs.raw)
407                .expect("Underflow occurred when subtracting `Quantity`"),
408            precision,
409        }
410    }
411}
412
413#[allow(clippy::suspicious_arithmetic_impl)] // Can use division to scale back
414impl Mul for Quantity {
415    type Output = Self;
416    fn mul(self, rhs: Self) -> Self::Output {
417        let precision = match self.precision {
418            0 => rhs.precision,
419            _ => self.precision,
420        };
421        assert!(
422            self.precision >= rhs.precision,
423            "Precision mismatch: cannot multiply precision {} with precision {} (precision loss)",
424            rhs.precision,
425            self.precision,
426        );
427
428        let result_raw = self
429            .raw
430            .checked_mul(rhs.raw)
431            .expect("Overflow occurred when multiplying `Quantity`");
432
433        Self {
434            raw: result_raw / (FIXED_SCALAR as QuantityRaw),
435            precision,
436        }
437    }
438}
439
440impl Mul<f64> for Quantity {
441    type Output = f64;
442    fn mul(self, rhs: f64) -> Self::Output {
443        self.as_f64() * rhs
444    }
445}
446
447impl From<Quantity> for QuantityRaw {
448    fn from(value: Quantity) -> Self {
449        value.raw
450    }
451}
452
453impl From<&Quantity> for QuantityRaw {
454    fn from(value: &Quantity) -> Self {
455        value.raw
456    }
457}
458
459impl FromStr for Quantity {
460    type Err = String;
461
462    fn from_str(value: &str) -> Result<Self, Self::Err> {
463        let float_from_input = value
464            .replace('_', "")
465            .parse::<f64>()
466            .map_err(|e| format!("Error parsing `input` string '{value}' as f64: {e}"))?;
467
468        Self::new_checked(float_from_input, precision_from_str(value)).map_err(|e| e.to_string())
469    }
470}
471
472// Note: we can't implement `AsRef<str>` due overlapping traits (maybe there is a way)
473impl From<&str> for Quantity {
474    fn from(value: &str) -> Self {
475        Self::from_str(value).expect("Valid string input for `Quantity`")
476    }
477}
478
479impl From<String> for Quantity {
480    fn from(value: String) -> Self {
481        Self::from_str(&value).expect("Valid string input for `Quantity`")
482    }
483}
484
485impl From<&String> for Quantity {
486    fn from(value: &String) -> Self {
487        Self::from_str(value).expect("Valid string input for `Quantity`")
488    }
489}
490
491impl<T: Into<QuantityRaw>> AddAssign<T> for Quantity {
492    fn add_assign(&mut self, other: T) {
493        self.raw = self
494            .raw
495            .checked_add(other.into())
496            .expect("Overflow occurred when adding `Quantity`");
497    }
498}
499
500impl<T: Into<QuantityRaw>> SubAssign<T> for Quantity {
501    fn sub_assign(&mut self, other: T) {
502        self.raw = self
503            .raw
504            .checked_sub(other.into())
505            .expect("Underflow occurred when subtracting `Quantity`");
506    }
507}
508
509impl<T: Into<QuantityRaw>> MulAssign<T> for Quantity {
510    fn mul_assign(&mut self, other: T) {
511        self.raw = self
512            .raw
513            .checked_mul(other.into())
514            .expect("Overflow occurred when multiplying `Quantity`");
515    }
516}
517
518impl Debug for Quantity {
519    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520        if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
521            write!(f, "{}({})", stringify!(Quantity), self.raw)
522        } else {
523            write!(
524                f,
525                "{}({:.*})",
526                stringify!(Quantity),
527                self.precision as usize,
528                self.as_f64(),
529            )
530        }
531    }
532}
533
534impl Display for Quantity {
535    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
536        if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
537            write!(f, "{}", self.raw)
538        } else {
539            write!(f, "{:.*}", self.precision as usize, self.as_f64(),)
540        }
541    }
542}
543
544impl Serialize for Quantity {
545    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
546    where
547        S: serde::Serializer,
548    {
549        serializer.serialize_str(&self.to_string())
550    }
551}
552
553impl<'de> Deserialize<'de> for Quantity {
554    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
555    where
556        D: Deserializer<'de>,
557    {
558        let qty_str: &str = Deserialize::deserialize(_deserializer)?;
559        let qty: Self = qty_str.into();
560        Ok(qty)
561    }
562}
563
564/// Checks if the quantity `value` is positive.
565///
566/// # Errors
567///
568/// Returns an error if `value` is not positive.
569pub fn check_positive_quantity(value: Quantity, param: &str) -> anyhow::Result<()> {
570    if !value.is_positive() {
571        anyhow::bail!("invalid `Quantity` for '{param}' not positive, was {value}")
572    }
573    Ok(())
574}
575
576////////////////////////////////////////////////////////////////////////////////
577// Tests
578////////////////////////////////////////////////////////////////////////////////
579#[cfg(test)]
580mod tests {
581    use std::str::FromStr;
582
583    use float_cmp::approx_eq;
584    use rstest::rstest;
585    use rust_decimal_macros::dec;
586
587    use super::*;
588
589    #[rstest]
590    #[should_panic(expected = "invalid `Quantity` for 'qty' not positive, was 0")]
591    fn test_check_quantity_positive() {
592        let qty = Quantity::new(0.0, 0);
593        check_positive_quantity(qty, "qty").unwrap();
594    }
595
596    #[rstest]
597    #[cfg(not(feature = "defi"))]
598    #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
599    fn test_invalid_precision_new() {
600        // Precision 17 should fail due to DeFi validation
601        let _ = Quantity::new(1.0, 17);
602    }
603
604    #[rstest]
605    #[cfg(not(feature = "defi"))]
606    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
607    fn test_invalid_precision_from_raw() {
608        // Precision out of range for fixed
609        let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
610    }
611
612    #[rstest]
613    #[cfg(not(feature = "defi"))]
614    #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
615    fn test_invalid_precision_zero() {
616        // Precision out of range for fixed
617        let _ = Quantity::zero(FIXED_PRECISION + 1);
618    }
619
620    #[rstest]
621    #[should_panic(
622        expected = "Precision mismatch: cannot add precision 2 to precision 1 (precision loss)"
623    )]
624    fn test_precision_mismatch_add() {
625        let q1 = Quantity::new(1.0, 1);
626        let q2 = Quantity::new(1.0, 2);
627        let _ = q1 + q2;
628    }
629
630    #[rstest]
631    #[should_panic(
632        expected = "Precision mismatch: cannot subtract precision 2 from precision 1 (precision loss)"
633    )]
634    fn test_precision_mismatch_sub() {
635        let q1 = Quantity::new(1.0, 1);
636        let q2 = Quantity::new(1.0, 2);
637        let _ = q1 - q2;
638    }
639
640    #[rstest]
641    #[should_panic(
642        expected = "Precision mismatch: cannot multiply precision 2 with precision 1 (precision loss)"
643    )]
644    fn test_precision_mismatch_mul() {
645        let q1 = Quantity::new(2.0, 1);
646        let q2 = Quantity::new(3.0, 2);
647        let _ = q1 * q2;
648    }
649
650    #[rstest]
651    fn test_new_non_zero_ok() {
652        let qty = Quantity::non_zero_checked(123.456, 3).unwrap();
653        assert_eq!(qty.raw, Quantity::new(123.456, 3).raw);
654        assert!(qty.is_positive());
655    }
656
657    #[rstest]
658    fn test_new_non_zero_zero_input() {
659        assert!(Quantity::non_zero_checked(0.0, 0).is_err());
660    }
661
662    #[rstest]
663    fn test_new_non_zero_rounds_to_zero() {
664        // 0.0004 rounded to 3 dp ⇒ 0.000
665        assert!(Quantity::non_zero_checked(0.0004, 3).is_err());
666    }
667
668    #[rstest]
669    fn test_new_non_zero_negative() {
670        assert!(Quantity::non_zero_checked(-1.0, 0).is_err());
671    }
672
673    #[rstest]
674    fn test_new_non_zero_exceeds_max() {
675        assert!(Quantity::non_zero_checked(QUANTITY_MAX * 10.0, 0).is_err());
676    }
677
678    #[rstest]
679    fn test_new_non_zero_invalid_precision() {
680        assert!(Quantity::non_zero_checked(1.0, FIXED_PRECISION + 1).is_err());
681    }
682
683    #[rstest]
684    fn test_new() {
685        let value = 0.00812;
686        let qty = Quantity::new(value, 8);
687        assert_eq!(qty, qty);
688        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
689        assert_eq!(qty.precision, 8);
690        assert_eq!(qty, Quantity::from("0.00812000"));
691        assert_eq!(qty.as_decimal(), dec!(0.00812000));
692        assert_eq!(qty.to_string(), "0.00812000");
693        assert!(!qty.is_zero());
694        assert!(qty.is_positive());
695        assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
696    }
697
698    #[rstest]
699    fn test_check_quantity_positive_ok() {
700        let qty = Quantity::new(10.0, 0);
701        check_positive_quantity(qty, "qty").unwrap();
702    }
703
704    #[rstest]
705    fn test_negative_quantity_validation() {
706        assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
707    }
708
709    #[rstest]
710    fn test_undefined() {
711        let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
712        assert_eq!(qty.raw, QUANTITY_UNDEF);
713        assert!(qty.is_undefined());
714    }
715
716    #[rstest]
717    fn test_zero() {
718        let qty = Quantity::zero(8);
719        assert_eq!(qty.raw, 0);
720        assert_eq!(qty.precision, 8);
721        assert!(qty.is_zero());
722        assert!(!qty.is_positive());
723    }
724
725    #[rstest]
726    fn test_from_i32() {
727        let value = 100_000i32;
728        let qty = Quantity::from(value);
729        assert_eq!(qty, qty);
730        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
731        assert_eq!(qty.precision, 0);
732    }
733
734    #[rstest]
735    fn test_from_u32() {
736        let value: u32 = 5000;
737        let qty = Quantity::from(value);
738        assert_eq!(qty.raw, Quantity::from(format!("{value}")).raw);
739        assert_eq!(qty.precision, 0);
740    }
741
742    #[rstest]
743    fn test_from_i64() {
744        let value = 100_000i64;
745        let qty = Quantity::from(value);
746        assert_eq!(qty, qty);
747        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
748        assert_eq!(qty.precision, 0);
749    }
750
751    #[rstest]
752    fn test_from_u64() {
753        let value = 100_000u64;
754        let qty = Quantity::from(value);
755        assert_eq!(qty, qty);
756        assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
757        assert_eq!(qty.precision, 0);
758    }
759
760    #[rstest] // Test does not panic rather than exact value
761    fn test_with_maximum_value() {
762        let qty = Quantity::new_checked(QUANTITY_MAX, 0);
763        assert!(qty.is_ok());
764    }
765
766    #[rstest]
767    fn test_with_minimum_positive_value() {
768        let value = 0.000_000_001;
769        let qty = Quantity::new(value, 9);
770        assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
771        assert_eq!(qty.as_decimal(), dec!(0.000000001));
772        assert_eq!(qty.to_string(), "0.000000001");
773    }
774
775    #[rstest]
776    fn test_with_minimum_value() {
777        let qty = Quantity::new(QUANTITY_MIN, 9);
778        assert_eq!(qty.raw, 0);
779        assert_eq!(qty.as_decimal(), dec!(0));
780        assert_eq!(qty.to_string(), "0.000000000");
781    }
782
783    #[rstest]
784    fn test_is_zero() {
785        let qty = Quantity::zero(8);
786        assert_eq!(qty, qty);
787        assert_eq!(qty.raw, 0);
788        assert_eq!(qty.precision, 8);
789        assert_eq!(qty, Quantity::from("0.00000000"));
790        assert_eq!(qty.as_decimal(), dec!(0));
791        assert_eq!(qty.to_string(), "0.00000000");
792        assert!(qty.is_zero());
793    }
794
795    #[rstest]
796    fn test_precision() {
797        let value = 1.001;
798        let qty = Quantity::new(value, 2);
799        assert_eq!(qty.to_string(), "1.00");
800    }
801
802    #[rstest]
803    fn test_new_from_str() {
804        let qty = Quantity::new(0.00812000, 8);
805        assert_eq!(qty, qty);
806        assert_eq!(qty.precision, 8);
807        assert_eq!(qty, Quantity::from("0.00812000"));
808        assert_eq!(qty.to_string(), "0.00812000");
809    }
810
811    #[rstest]
812    #[case("0", 0)]
813    #[case("1.1", 1)]
814    #[case("1.123456789", 9)]
815    fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
816        let qty = Quantity::from(input);
817        assert_eq!(qty.precision, expected_prec);
818        assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
819    }
820
821    #[rstest]
822    #[should_panic]
823    fn test_from_str_invalid_input() {
824        let input = "invalid";
825        Quantity::new(f64::from_str(input).unwrap(), 8);
826    }
827
828    #[rstest]
829    fn test_add() {
830        let a = 1.0;
831        let b = 2.0;
832        let quantity1 = Quantity::new(1.0, 0);
833        let quantity2 = Quantity::new(2.0, 0);
834        let quantity3 = quantity1 + quantity2;
835        assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
836    }
837
838    #[rstest]
839    fn test_sub() {
840        let a = 3.0;
841        let b = 2.0;
842        let quantity1 = Quantity::new(a, 0);
843        let quantity2 = Quantity::new(b, 0);
844        let quantity3 = quantity1 - quantity2;
845        assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
846    }
847
848    #[rstest]
849    fn test_add_assign() {
850        let a = 1.0;
851        let b = 2.0;
852        let mut quantity1 = Quantity::new(a, 0);
853        let quantity2 = Quantity::new(b, 0);
854        quantity1 += quantity2;
855        assert_eq!(quantity1.raw, Quantity::new(a + b, 0).raw);
856    }
857
858    #[rstest]
859    fn test_sub_assign() {
860        let a = 3.0;
861        let b = 2.0;
862        let mut quantity1 = Quantity::new(a, 0);
863        let quantity2 = Quantity::new(b, 0);
864        quantity1 -= quantity2;
865        assert_eq!(quantity1.raw, Quantity::new(a - b, 0).raw);
866    }
867
868    #[rstest]
869    fn test_mul() {
870        let value = 2.0;
871        let quantity1 = Quantity::new(value, 1);
872        let quantity2 = Quantity::new(value, 1);
873        let quantity3 = quantity1 * quantity2;
874        assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
875    }
876
877    #[rstest]
878    fn test_mul_assign() {
879        let mut quantity = Quantity::new(2.0, 0);
880        quantity *= 3u64; // calls MulAssign<T: Into<QuantityRaw>>
881        assert_eq!(quantity.raw, Quantity::new(6.0, 0).raw);
882
883        let mut fraction = Quantity::new(1.5, 2);
884        fraction *= 2u64; // => 1.5 * 2 = 3.0 => raw=300, precision=2
885        assert_eq!(fraction.raw, Quantity::new(3.0, 2).raw);
886    }
887
888    #[rstest]
889    fn test_comparisons() {
890        assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
891        assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
892        assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
893        assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
894        assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
895        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
896        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
897        assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
898        assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
899        assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
900        assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
901    }
902
903    #[rstest]
904    fn test_debug() {
905        let quantity = Quantity::from_str("44.12").unwrap();
906        let result = format!("{quantity:?}");
907        assert_eq!(result, "Quantity(44.12)");
908    }
909
910    #[rstest]
911    fn test_display() {
912        let quantity = Quantity::from_str("44.12").unwrap();
913        let result = format!("{quantity}");
914        assert_eq!(result, "44.12");
915    }
916
917    #[rstest]
918    #[case(44.12, 2, "Quantity(44.12)", "44.12")] // Normal precision
919    #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] // At max normal precision
920    #[cfg_attr(
921        feature = "defi",
922        case(
923            1_000_000_000_000_000_000.0,
924            18,
925            "Quantity(1000000000000000000)",
926            "1000000000000000000"
927        )
928    )] // High precision
929    fn test_debug_display_precision_handling(
930        #[case] value: f64,
931        #[case] precision: u8,
932        #[case] expected_debug: &str,
933        #[case] expected_display: &str,
934    ) {
935        let quantity = if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
936            // For high precision, use from_raw to avoid f64 conversion issues
937            Quantity::from_raw(value as QuantityRaw, precision)
938        } else {
939            Quantity::new(value, precision)
940        };
941
942        assert_eq!(format!("{quantity:?}"), expected_debug);
943        assert_eq!(format!("{quantity}"), expected_display);
944    }
945
946    #[rstest]
947    fn test_to_formatted_string() {
948        let qty = Quantity::new(1234.5678, 4);
949        let formatted = qty.to_formatted_string();
950        assert_eq!(formatted, "1_234.5678");
951        assert_eq!(qty.to_string(), "1234.5678");
952    }
953
954    #[rstest]
955    fn test_hash() {
956        use std::{
957            collections::hash_map::DefaultHasher,
958            hash::{Hash, Hasher},
959        };
960
961        let q1 = Quantity::new(100.0, 1);
962        let q2 = Quantity::new(100.0, 1);
963        let q3 = Quantity::new(200.0, 1);
964
965        let mut s1 = DefaultHasher::new();
966        let mut s2 = DefaultHasher::new();
967        let mut s3 = DefaultHasher::new();
968
969        q1.hash(&mut s1);
970        q2.hash(&mut s2);
971        q3.hash(&mut s3);
972
973        assert_eq!(
974            s1.finish(),
975            s2.finish(),
976            "Equal quantities must hash equally"
977        );
978        assert_ne!(
979            s1.finish(),
980            s3.finish(),
981            "Different quantities must hash differently"
982        );
983    }
984
985    #[rstest]
986    fn test_quantity_serde_json_round_trip() {
987        let original = Quantity::new(123.456, 3);
988        let json_str = serde_json::to_string(&original).unwrap();
989        assert_eq!(json_str, "\"123.456\"");
990
991        let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
992        assert_eq!(deserialized, original);
993        assert_eq!(deserialized.precision, 3);
994    }
995}
996
997////////////////////////////////////////////////////////////////////////////////
998// Property-based tests
999////////////////////////////////////////////////////////////////////////////////
1000#[cfg(test)]
1001mod property_tests {
1002    use proptest::prelude::*;
1003
1004    use super::*;
1005
1006    /// Strategy to generate valid quantity values (non-negative).
1007    fn quantity_value_strategy() -> impl Strategy<Value = f64> {
1008        // Use a reasonable range for quantities - must be non-negative
1009        prop_oneof![
1010            // Small positive values
1011            0.00001..1.0,
1012            // Normal trading range
1013            1.0..100_000.0,
1014            // Large values (but safe)
1015            100_000.0..1_000_000.0,
1016            // Include zero
1017            Just(0.0),
1018        ]
1019    }
1020
1021    /// Strategy to generate valid precision values.
1022    fn precision_strategy() -> impl Strategy<Value = u8> {
1023        #[cfg(feature = "defi")]
1024        {
1025            // In DeFi mode, exclude precision 17 (invalid) and 18 (f64 constructor incompatible)
1026            0..=16u8
1027        }
1028        #[cfg(not(feature = "defi"))]
1029        {
1030            0..=FIXED_PRECISION
1031        }
1032    }
1033
1034    proptest! {
1035        /// Property: Quantity string serialization round-trip should preserve value and precision
1036        #[test]
1037        fn prop_quantity_serde_round_trip(
1038            value in quantity_value_strategy(),
1039            precision in 0u8..=6u8  // Limit precision to avoid extreme floating-point cases
1040        ) {
1041            let original = Quantity::new(value, precision);
1042
1043            // String round-trip (this should be exact and is the most important)
1044            let string_repr = original.to_string();
1045            let from_string: Quantity = string_repr.parse().unwrap();
1046            prop_assert_eq!(from_string.raw, original.raw);
1047            prop_assert_eq!(from_string.precision, original.precision);
1048
1049            // JSON round-trip basic validation (just ensure it doesn't crash and preserves precision)
1050            let json = serde_json::to_string(&original).unwrap();
1051            let from_json: Quantity = serde_json::from_str(&json).unwrap();
1052            prop_assert_eq!(from_json.precision, original.precision);
1053            // Note: JSON may have minor floating-point precision differences due to f64 limitations
1054        }
1055
1056        /// Property: Quantity arithmetic should be associative for same precision
1057        #[test]
1058        fn prop_quantity_arithmetic_associative(
1059            a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1060            b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1061            c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1062            precision in 0u8..=6u8  // Limit precision to avoid extreme cases
1063        ) {
1064            let q_a = Quantity::new(a, precision);
1065            let q_b = Quantity::new(b, precision);
1066            let q_c = Quantity::new(c, precision);
1067
1068            // Check if we can perform the operations without overflow using raw arithmetic
1069            let ab_raw = q_a.raw.checked_add(q_b.raw);
1070            let bc_raw = q_b.raw.checked_add(q_c.raw);
1071
1072            if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1073                let ab_c_raw = ab_raw.checked_add(q_c.raw);
1074                let a_bc_raw = q_a.raw.checked_add(bc_raw);
1075
1076                if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1077                    // (a + b) + c == a + (b + c) using raw arithmetic (exact)
1078                    prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1079                }
1080            }
1081        }
1082
1083        /// Property: Quantity addition/subtraction should be inverse operations (when valid)
1084        #[test]
1085        fn prop_quantity_addition_subtraction_inverse(
1086            base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1087            delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
1088            precision in 0u8..=6u8  // Limit precision to avoid extreme cases
1089        ) {
1090            let q_base = Quantity::new(base, precision);
1091            let q_delta = Quantity::new(delta, precision);
1092
1093            // Use raw arithmetic to avoid floating-point precision issues
1094            if let Some(added_raw) = q_base.raw.checked_add(q_delta.raw) {
1095                if let Some(result_raw) = added_raw.checked_sub(q_delta.raw) {
1096                    // (base + delta) - delta should equal base exactly using raw arithmetic
1097                    prop_assert_eq!(result_raw, q_base.raw, "Inverse operation failed in raw arithmetic");
1098                }
1099            }
1100        }
1101
1102        /// Property: Quantity ordering should be transitive
1103        #[test]
1104        fn prop_quantity_ordering_transitive(
1105            a in quantity_value_strategy(),
1106            b in quantity_value_strategy(),
1107            c in quantity_value_strategy(),
1108            precision in precision_strategy()
1109        ) {
1110            let q_a = Quantity::new(a, precision);
1111            let q_b = Quantity::new(b, precision);
1112            let q_c = Quantity::new(c, precision);
1113
1114            // If a <= b and b <= c, then a <= c
1115            if q_a <= q_b && q_b <= q_c {
1116                prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
1117                    q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
1118            }
1119        }
1120
1121        /// Property: String parsing should be consistent with precision inference
1122        #[test]
1123        fn prop_quantity_string_parsing_precision(
1124            integral in 0u32..1000000,
1125            fractional in 0u32..1000000,
1126            precision in 1u8..=6
1127        ) {
1128            // Create a decimal string with exactly 'precision' decimal places
1129            let fractional_str = format!("{:0width$}", fractional % 10_u32.pow(precision as u32), width = precision as usize);
1130            let quantity_str = format!("{integral}.{fractional_str}");
1131
1132            let parsed: Quantity = quantity_str.parse().unwrap();
1133            prop_assert_eq!(parsed.precision, precision);
1134
1135            // Round-trip should preserve the original string (after normalization)
1136            let round_trip = parsed.to_string();
1137            let expected_value = format!("{integral}.{fractional_str}");
1138            prop_assert_eq!(round_trip, expected_value);
1139        }
1140
1141        /// Property: Quantity with higher precision should contain more or equal information
1142        #[test]
1143        fn prop_quantity_precision_information_preservation(
1144            value in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
1145            precision1 in 1u8..=6u8,  // Limit precision range for more predictable behavior
1146            precision2 in 1u8..=6u8
1147        ) {
1148            // Skip cases where precisions are equal (trivial case)
1149            prop_assume!(precision1 != precision2);
1150
1151            let _q1 = Quantity::new(value, precision1);
1152            let _q2 = Quantity::new(value, precision2);
1153
1154            // When both quantities are created from the same value with different precisions,
1155            // converting both to the lower precision should yield the same result
1156            let min_precision = precision1.min(precision2);
1157
1158            // Round the original value to the minimum precision first
1159            let scale = 10.0_f64.powi(min_precision as i32);
1160            let rounded_value = (value * scale).round() / scale;
1161
1162            let q1_reduced = Quantity::new(rounded_value, min_precision);
1163            let q2_reduced = Quantity::new(rounded_value, min_precision);
1164
1165            // They should be exactly equal when created from the same rounded value
1166            prop_assert_eq!(q1_reduced.raw, q2_reduced.raw, "Precision reduction inconsistent");
1167        }
1168
1169        /// Property: Quantity arithmetic should never produce invalid values
1170        #[test]
1171        fn prop_quantity_arithmetic_bounds(
1172            a in quantity_value_strategy(),
1173            b in quantity_value_strategy(),
1174            precision in precision_strategy()
1175        ) {
1176            let q_a = Quantity::new(a, precision);
1177            let q_b = Quantity::new(b, precision);
1178
1179            // Addition should either succeed or fail predictably
1180            let sum_f64 = q_a.as_f64() + q_b.as_f64();
1181            if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
1182                let sum = q_a + q_b;
1183                prop_assert!(sum.as_f64().is_finite());
1184                prop_assert!(!sum.is_undefined());
1185            }
1186
1187            // Subtraction should either succeed or fail predictably
1188            let diff_f64 = q_a.as_f64() - q_b.as_f64();
1189            if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
1190                let diff = q_a - q_b;
1191                prop_assert!(diff.as_f64().is_finite());
1192                prop_assert!(!diff.is_undefined());
1193            }
1194        }
1195
1196        /// Property: Multiplication should preserve non-negativity
1197        #[test]
1198        fn prop_quantity_multiplication_non_negative(
1199            a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 100.0),
1200            b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 100.0),
1201            precision in precision_strategy()
1202        ) {
1203            let q_a = Quantity::new(a, precision);
1204            let q_b = Quantity::new(b, precision);
1205
1206            // Check if multiplication would overflow before performing it
1207            let product_f64 = q_a.as_f64() * q_b.as_f64();
1208            // More conservative overflow check for high-precision modes
1209            let safe_limit = if cfg!(any(feature = "defi", feature = "high-precision")) {
1210                QUANTITY_MAX / 10000.0  // Much more conservative in high-precision mode
1211            } else {
1212                QUANTITY_MAX
1213            };
1214
1215            if product_f64.is_finite() && product_f64 <= safe_limit {
1216                // Multiplying two quantities should always result in a non-negative value
1217                let product = q_a * q_b;
1218                prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
1219            }
1220        }
1221
1222        /// Property: Zero quantity should be identity for addition
1223        #[test]
1224        fn prop_quantity_zero_addition_identity(
1225            value in quantity_value_strategy(),
1226            precision in precision_strategy()
1227        ) {
1228            let q = Quantity::new(value, precision);
1229            let zero = Quantity::zero(precision);
1230
1231            // q + 0 = q and 0 + q = q
1232            prop_assert_eq!(q + zero, q);
1233            prop_assert_eq!(zero + q, q);
1234        }
1235    }
1236}