1use std::{
50 cmp::Ordering,
51 fmt::{Debug, Display},
52 hash::{Hash, Hasher},
53 iter::Sum,
54 ops::{Add, Deref, Div, Mul, Sub},
55 str::FromStr,
56};
57
58#[cfg(feature = "defi")]
59use alloy_primitives::U256;
60use nautilus_core::{
61 correctness::{
62 CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
63 check_in_range_inclusive_f64,
64 },
65 string::formatting::Separable,
66};
67use rust_decimal::Decimal;
68use serde::{Deserialize, Deserializer, Serialize};
69
70#[cfg(not(feature = "defi"))]
71use super::fixed::FIXED_SCALAR_RAW;
72#[cfg(feature = "defi")]
73use super::fixed::compare_raw;
74use super::fixed::{
75 FIXED_PRECISION, FIXED_SCALAR, canonical_raw, check_fixed_precision, checked_mul_div_fixed,
76 checked_mul_div_raw, format_scaled_u128, mantissa_exponent_to_fixed_i128,
77 mantissa_exponent_to_raw_checked, parse_decimal_mantissa, raw_scale, raw_scales_match,
78 remove_underscores, scaled_raw_to_decimal,
79};
80#[cfg(not(feature = "high-precision"))]
81use super::fixed::{f64_to_fixed_u64, fixed_u64_to_f64};
82#[cfg(feature = "high-precision")]
83use super::fixed::{f64_to_fixed_u128, fixed_u128_to_f64};
84use crate::deserialization::StringInput;
85#[cfg(feature = "defi")]
86use crate::types::fixed::MAX_FLOAT_PRECISION;
87
88#[cfg(feature = "high-precision")]
93pub type QuantityRaw = u128;
94
95#[cfg(not(feature = "high-precision"))]
96pub type QuantityRaw = u64;
97
98#[unsafe(no_mangle)]
107#[allow(unsafe_code)]
108pub static QUANTITY_RAW_MAX: QuantityRaw =
109 (QUANTITY_MAX as QuantityRaw) * (FIXED_SCALAR as QuantityRaw);
110
111pub const QUANTITY_UNDEF: QuantityRaw = QuantityRaw::MAX;
113
114#[cfg(feature = "high-precision")]
119pub const QUANTITY_MAX: f64 = 34_028_236_692_093.0;
121
122#[cfg(not(feature = "high-precision"))]
123pub const QUANTITY_MAX: f64 = 18_446_744_073.0;
125
126pub const QUANTITY_MIN: f64 = 0.0;
130
131#[repr(C)]
142#[derive(Clone, Copy, Default, Eq)]
143#[cfg_attr(
144 feature = "python",
145 pyo3::pyclass(module = "nautilus_trader.model", frozen, from_py_object)
146)]
147#[cfg_attr(
148 feature = "python",
149 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
150)]
151pub struct Quantity {
152 pub(crate) raw: QuantityRaw,
153 pub precision: u8,
155}
156
157impl Quantity {
158 pub fn new_checked(value: f64, precision: u8) -> CorrectnessResult<Self> {
170 check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?;
171
172 #[cfg(feature = "defi")]
173 if precision > MAX_FLOAT_PRECISION {
174 return Err(CorrectnessError::PredicateViolation {
176 message: format!(
177 "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Quantity::from_wei()` for wei values instead"
178 ),
179 });
180 }
181
182 check_fixed_precision(precision)?;
183
184 #[cfg(feature = "high-precision")]
185 let raw = f64_to_fixed_u128(value, precision);
186 #[cfg(not(feature = "high-precision"))]
187 let raw = f64_to_fixed_u64(value, precision);
188
189 Ok(Self { raw, precision })
190 }
191
192 #[must_use]
198 pub fn new(value: f64, precision: u8) -> Self {
199 Self::new_checked(value, precision).expect_display(FAILED)
200 }
201
202 #[must_use]
209 pub fn from_raw(raw: QuantityRaw, precision: u8) -> Self {
210 assert!(
211 raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
212 "`raw` value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
213 );
214
215 if raw == QUANTITY_UNDEF {
216 assert!(
217 precision == 0,
218 "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
219 );
220 }
221
222 check_fixed_precision(precision).expect_display(FAILED);
223
224 Self { raw, precision }
233 }
234
235 pub fn from_raw_checked(raw: QuantityRaw, precision: u8) -> CorrectnessResult<Self> {
245 if raw == QUANTITY_UNDEF && precision != 0 {
246 return Err(CorrectnessError::PredicateViolation {
247 message: "`precision` must be 0 when `raw` is QUANTITY_UNDEF".to_string(),
248 });
249 }
250
251 if raw != QUANTITY_UNDEF && raw > QUANTITY_RAW_MAX {
252 return Err(CorrectnessError::PredicateViolation {
253 message: format!("raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}"),
254 });
255 }
256
257 check_fixed_precision(precision)?;
258
259 Ok(Self { raw, precision })
260 }
261
262 #[must_use]
266 pub fn checked_rescale(self, precision: u8) -> Option<Self> {
267 if self.raw > QUANTITY_RAW_MAX
268 || precision < self.precision
269 || check_fixed_precision(self.precision).is_err()
270 || check_fixed_precision(precision).is_err()
271 {
272 return None;
273 }
274
275 let source_scale = FIXED_PRECISION.max(self.precision);
276 let target_scale = FIXED_PRECISION.max(precision);
277 let factor = QuantityRaw::pow(10, u32::from(target_scale - source_scale));
278 let raw = self.raw.checked_mul(factor)?;
279 Self::from_raw_checked(raw, precision).ok()
280 }
281
282 #[must_use]
287 pub fn checked_floor_to_increment(self, increment: Self) -> Option<Self> {
288 if increment.is_zero() {
289 return None;
290 }
291
292 let precision = self.precision.max(increment.precision);
293 let value = self.checked_rescale(precision)?;
294 let step = increment.checked_rescale(precision)?;
295 let raw = value.raw - value.raw % step.raw;
296 let scale = QuantityRaw::pow(
297 10,
298 u32::from(precision.max(FIXED_PRECISION) - increment.precision.max(FIXED_PRECISION)),
299 );
300
301 Self::from_raw_checked(raw / scale, increment.precision).ok()
302 }
303
304 #[must_use]
311 pub fn checked_add(self, rhs: Self) -> Option<Self> {
312 if self.raw == QUANTITY_UNDEF || rhs.raw == QUANTITY_UNDEF {
313 return None;
314 }
315
316 if !raw_scales_match(self.precision, rhs.precision) {
317 return None;
318 }
319
320 let raw = self.raw.checked_add(rhs.raw)?;
321 if raw > QUANTITY_RAW_MAX {
322 return None;
323 }
324
325 Some(Self {
326 raw,
327 precision: self.precision.max(rhs.precision),
328 })
329 }
330
331 #[must_use]
337 pub fn checked_sub(self, rhs: Self) -> Option<Self> {
338 if self.raw == QUANTITY_UNDEF || rhs.raw == QUANTITY_UNDEF {
339 return None;
340 }
341
342 if !raw_scales_match(self.precision, rhs.precision) {
343 return None;
344 }
345
346 let raw = self.raw.checked_sub(rhs.raw)?;
347
348 Some(Self {
349 raw,
350 precision: self.precision.max(rhs.precision),
351 })
352 }
353
354 #[must_use]
362 pub fn saturating_add(self, rhs: Self) -> Self {
363 assert!(
364 raw_scales_match(self.precision, rhs.precision),
365 "Cannot add `Quantity` values with mismatched decimal scales"
366 );
367
368 Self {
369 raw: self.raw.saturating_add(rhs.raw).min(QUANTITY_RAW_MAX),
370 precision: self.precision.max(rhs.precision),
371 }
372 }
373
374 #[must_use]
386 pub fn saturating_sub(self, rhs: Self) -> Self {
387 assert!(
388 raw_scales_match(self.precision, rhs.precision),
389 "Cannot subtract `Quantity` values with mismatched decimal scales"
390 );
391 let precision = self.precision.max(rhs.precision);
392 let raw = self.raw.saturating_sub(rhs.raw);
393 if raw == 0 && self.raw < rhs.raw {
394 log::warn!(
395 "Saturating Quantity subtraction: {self} - {rhs} < 0, clamped to 0 (precision={precision})"
396 );
397 }
398
399 Self { raw, precision }
400 }
401
402 #[must_use]
408 pub fn zero(precision: u8) -> Self {
409 check_fixed_precision(precision).expect_display(FAILED);
410 Self { raw: 0, precision }
411 }
412
413 #[must_use]
415 pub fn is_undefined(&self) -> bool {
416 self.raw == QUANTITY_UNDEF
417 }
418
419 #[must_use]
432 #[inline]
433 pub const fn raw(&self) -> QuantityRaw {
434 self.raw
435 }
436
437 #[must_use]
439 #[inline]
440 pub fn is_zero(&self) -> bool {
441 self.raw == 0
442 }
443
444 #[must_use]
446 #[inline]
447 pub fn non_zero(&self) -> bool {
448 self.raw != 0
449 }
450
451 #[must_use]
453 #[inline]
454 pub fn is_positive(&self) -> bool {
455 self.raw != QUANTITY_UNDEF && self.raw > 0
456 }
457
458 #[cfg(feature = "high-precision")]
459 #[must_use]
465 pub fn as_f64(&self) -> f64 {
466 #[cfg(feature = "defi")]
467 assert!(
468 self.precision <= MAX_FLOAT_PRECISION,
469 "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
470 );
471
472 fixed_u128_to_f64(self.raw)
473 }
474
475 #[cfg(not(feature = "high-precision"))]
476 #[must_use]
478 pub fn as_f64(&self) -> f64 {
479 fixed_u64_to_f64(self.raw)
480 }
481
482 #[must_use]
484 pub fn as_decimal(&self) -> Decimal {
485 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
487 let rescaled_raw = self.raw / QuantityRaw::pow(10, u32::from(precision_diff));
488
489 #[allow(
493 clippy::unnecessary_cast,
494 clippy::cast_lossless,
495 reason = "cast is real when QuantityRaw is u64, no-op when u128"
496 )]
497 scaled_raw_to_decimal(rescaled_raw as i128, self.precision)
498 }
499
500 #[must_use]
502 #[allow(
503 clippy::unnecessary_fallible_conversions,
504 reason = "try_from is infallible when QuantityRaw is u64, fallible when u128"
505 )]
506 #[cfg(not(feature = "defi"))]
507 pub(crate) fn raw_as_decimal(raw: QuantityRaw) -> Decimal {
508 let whole =
509 i128::try_from(raw / FIXED_SCALAR_RAW).expect("Whole raw quantity must fit in Decimal");
510 let fractional = i128::try_from(raw % FIXED_SCALAR_RAW)
511 .expect("Fractional raw quantity must fit in Decimal");
512
513 Decimal::from(whole) + Decimal::from_i128_with_scale(fractional, u32::from(FIXED_PRECISION))
514 }
515
516 #[must_use]
518 pub fn to_formatted_string(&self) -> String {
519 format!("{self}").separate_with_underscores()
520 }
521
522 fn raw_at_precision(&self) -> QuantityRaw {
523 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
524 self.raw / QuantityRaw::pow(10, u32::from(precision_diff))
525 }
526
527 fn raw_as_u128(raw: QuantityRaw) -> u128 {
528 #[allow(
529 clippy::useless_conversion,
530 reason = "u128::from is a widening conversion when QuantityRaw is u64"
531 )]
532 u128::from(raw)
533 }
534
535 pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> CorrectnessResult<Self> {
548 if decimal.mantissa() < 0 {
549 return Err(CorrectnessError::PredicateViolation {
550 message: format!(
551 "Decimal value '{decimal}' is negative, Quantity must be non-negative"
552 ),
553 });
554 }
555
556 let exponent = -(decimal.scale() as i8);
557 let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
558
559 let raw: QuantityRaw =
560 raw_i128
561 .try_into()
562 .map_err(|_| CorrectnessError::PredicateViolation {
563 message: format!(
564 "Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]"
565 ),
566 })?;
567
568 if raw > QUANTITY_RAW_MAX {
569 return Err(CorrectnessError::PredicateViolation {
570 message: format!(
571 "Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
572 ),
573 });
574 }
575
576 Ok(Self { raw, precision })
577 }
578
579 pub fn from_decimal(decimal: Decimal) -> CorrectnessResult<Self> {
591 let precision = decimal.scale() as u8;
592 Self::from_decimal_dp(decimal, precision)
593 }
594
595 #[must_use]
604 pub fn from_mantissa_exponent(mantissa: u64, exponent: i8, precision: u8) -> Self {
605 check_fixed_precision(precision).expect_display(FAILED);
606
607 if mantissa == 0 {
608 return Self { raw: 0, precision };
609 }
610
611 let raw_i128 = mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, precision)
612 .expect("Overflow in Quantity::from_mantissa_exponent");
613
614 let raw: QuantityRaw = raw_i128
615 .try_into()
616 .expect("Raw value exceeds QuantityRaw range in Quantity::from_mantissa_exponent");
617 assert!(
618 raw <= QUANTITY_RAW_MAX,
619 "`raw` value {raw} exceeded QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
620 );
621
622 Self { raw, precision }
623 }
624
625 pub fn from_mantissa_exponent_checked(
632 mantissa: u64,
633 exponent: i8,
634 precision: u8,
635 ) -> CorrectnessResult<Self> {
636 let raw = mantissa_exponent_to_raw_checked::<QuantityRaw>(
637 i128::from(mantissa),
638 exponent,
639 precision,
640 "Quantity::from_mantissa_exponent",
641 "QuantityRaw",
642 "Quantity",
643 )?;
644
645 Self::from_raw_checked(raw, precision)
646 }
647
648 #[cfg(feature = "defi")]
656 pub fn from_u256(amount: U256, precision: u8) -> CorrectnessResult<Self> {
657 let scaled_amount = if precision < FIXED_PRECISION {
659 amount
660 .checked_mul(U256::from(
661 10u128.pow(u32::from(FIXED_PRECISION - precision)),
662 ))
663 .ok_or_else(|| CorrectnessError::PredicateViolation {
664 message: format!(
665 "Amount overflow during scaling to fixed precision: {} * 10^{}",
666 amount,
667 FIXED_PRECISION - precision
668 ),
669 })?
670 } else {
671 amount
672 };
673
674 let raw = QuantityRaw::try_from(scaled_amount).map_err(|_| {
675 CorrectnessError::PredicateViolation {
676 message: format!("U256 scaled amount {scaled_amount} exceeds QuantityRaw range"),
677 }
678 })?;
679
680 Self::from_raw_checked(raw, precision)
681 }
682}
683
684impl From<Quantity> for f64 {
685 fn from(qty: Quantity) -> Self {
686 qty.as_f64()
687 }
688}
689
690impl From<&Quantity> for f64 {
691 fn from(qty: &Quantity) -> Self {
692 qty.as_f64()
693 }
694}
695
696impl From<i32> for Quantity {
697 fn from(value: i32) -> Self {
703 assert!(
704 value >= 0,
705 "Cannot create Quantity from negative i32: {value}. Use u32 or check value is non-negative."
706 );
707 Self::from_mantissa_exponent(u64::from(value.cast_unsigned()), 0, 0)
708 }
709}
710
711impl From<i64> for Quantity {
712 fn from(value: i64) -> Self {
718 assert!(
719 value >= 0,
720 "Cannot create Quantity from negative i64: {value}. Use u64 or check value is non-negative."
721 );
722 Self::from_mantissa_exponent(value.cast_unsigned(), 0, 0)
723 }
724}
725
726impl From<u32> for Quantity {
727 fn from(value: u32) -> Self {
728 Self::from_mantissa_exponent(u64::from(value), 0, 0)
729 }
730}
731
732impl From<u64> for Quantity {
733 fn from(value: u64) -> Self {
734 Self::from_mantissa_exponent(value, 0, 0)
735 }
736}
737
738impl Hash for Quantity {
739 fn hash<H: Hasher>(&self, state: &mut H) {
740 canonical_raw(self.raw, self.precision).hash(state);
741 }
742}
743
744impl PartialEq for Quantity {
745 #[inline]
746 fn eq(&self, other: &Self) -> bool {
747 self.cmp(other) == Ordering::Equal
748 }
749}
750
751impl PartialOrd for Quantity {
752 #[inline]
753 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
754 Some(self.cmp(other))
755 }
756}
757
758impl Ord for Quantity {
759 #[inline]
760 fn cmp(&self, other: &Self) -> Ordering {
761 #[cfg(feature = "defi")]
762 {
763 compare_raw(self.raw, self.precision, other.raw, other.precision)
764 }
765
766 #[cfg(not(feature = "defi"))]
767 {
768 self.raw.cmp(&other.raw)
769 }
770 }
771}
772
773impl Deref for Quantity {
774 type Target = QuantityRaw;
775
776 fn deref(&self) -> &Self::Target {
777 &self.raw
778 }
779}
780
781impl Add for Quantity {
782 type Output = Self;
783 #[inline]
784 fn add(self, rhs: Self) -> Self::Output {
785 #[cfg(feature = "defi")]
786 assert!(
787 raw_scales_match(self.precision, rhs.precision),
788 "Cannot add `Quantity` values with mismatched decimal scales"
789 );
790 Self {
791 raw: self
792 .raw
793 .checked_add(rhs.raw)
794 .expect("Overflow occurred when adding `Quantity`"),
795 precision: self.precision.max(rhs.precision),
796 }
797 }
798}
799
800impl Sum for Quantity {
801 fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
802 iter.reduce(|acc, x| acc + x)
803 .unwrap_or_else(|| Self::zero(0))
804 }
805}
806
807impl<'a> Sum<&'a Self> for Quantity {
808 fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
809 iter.copied().sum()
810 }
811}
812
813impl Sub for Quantity {
814 type Output = Self;
815 #[inline]
816 fn sub(self, rhs: Self) -> Self::Output {
817 #[cfg(feature = "defi")]
818 assert!(
819 raw_scales_match(self.precision, rhs.precision),
820 "Cannot subtract `Quantity` values with mismatched decimal scales"
821 );
822 Self {
823 raw: self
824 .raw
825 .checked_sub(rhs.raw)
826 .expect("Underflow occurred when subtracting `Quantity`"),
827 precision: self.precision.max(rhs.precision),
828 }
829 }
830}
831
832impl Mul for Quantity {
833 type Output = Self;
834 fn mul(self, rhs: Self) -> Self::Output {
835 let result_raw = if self.raw != QUANTITY_UNDEF
836 && rhs.raw != QUANTITY_UNDEF
837 && self.precision <= FIXED_PRECISION
838 && rhs.precision <= FIXED_PRECISION
839 {
840 checked_mul_div_fixed(self.raw, rhs.raw)
841 } else {
842 let scalar = QuantityRaw::try_from(raw_scale(self.precision.min(rhs.precision)))
843 .expect("Fixed-point scale fits QuantityRaw");
844 checked_mul_div_raw(self.raw, rhs.raw, scalar)
845 }
846 .filter(|raw| *raw <= QUANTITY_RAW_MAX)
847 .expect("Overflow occurred when multiplying `Quantity`");
848
849 Self {
850 raw: result_raw,
851 precision: self.precision.max(rhs.precision),
852 }
853 }
854}
855
856impl Add<Decimal> for Quantity {
857 type Output = Decimal;
858 fn add(self, rhs: Decimal) -> Self::Output {
859 self.as_decimal() + rhs
860 }
861}
862
863impl Sub<Decimal> for Quantity {
864 type Output = Decimal;
865 fn sub(self, rhs: Decimal) -> Self::Output {
866 self.as_decimal() - rhs
867 }
868}
869
870impl Mul<Decimal> for Quantity {
871 type Output = Decimal;
872 fn mul(self, rhs: Decimal) -> Self::Output {
873 self.as_decimal() * rhs
874 }
875}
876
877impl Div<Decimal> for Quantity {
878 type Output = Decimal;
879 fn div(self, rhs: Decimal) -> Self::Output {
880 self.as_decimal() / rhs
881 }
882}
883
884impl Add<f64> for Quantity {
885 type Output = f64;
886 fn add(self, rhs: f64) -> Self::Output {
887 self.as_f64() + rhs
888 }
889}
890
891impl Sub<f64> for Quantity {
892 type Output = f64;
893 fn sub(self, rhs: f64) -> Self::Output {
894 self.as_f64() - rhs
895 }
896}
897
898impl Mul<f64> for Quantity {
899 type Output = f64;
900 fn mul(self, rhs: f64) -> Self::Output {
901 self.as_f64() * rhs
902 }
903}
904
905impl Div<f64> for Quantity {
906 type Output = f64;
907 fn div(self, rhs: f64) -> Self::Output {
908 self.as_f64() / rhs
909 }
910}
911
912impl From<Quantity> for QuantityRaw {
913 fn from(value: Quantity) -> Self {
914 value.raw
915 }
916}
917
918impl From<&Quantity> for QuantityRaw {
919 fn from(value: &Quantity) -> Self {
920 value.raw
921 }
922}
923
924impl From<Quantity> for Decimal {
925 fn from(value: Quantity) -> Self {
926 value.as_decimal()
927 }
928}
929
930impl From<&Quantity> for Decimal {
931 fn from(value: &Quantity) -> Self {
932 value.as_decimal()
933 }
934}
935
936impl FromStr for Quantity {
937 type Err = String;
938
939 fn from_str(value: &str) -> Result<Self, Self::Err> {
940 let clean_value = remove_underscores(value);
941 if clean_value.contains('e') || clean_value.contains('E') {
942 let decimal = Decimal::from_scientific(&clean_value)
943 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?;
944 let precision = decimal.scale() as u8;
945 return Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string());
946 }
947
948 let (mantissa, precision) = parse_decimal_mantissa(&clean_value)
949 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?;
950 if mantissa < 0 {
951 return Err(format!(
952 "Decimal value '{clean_value}' is negative, Quantity must be non-negative"
953 ));
954 }
955 let exponent = -i8::try_from(precision).map_err(|e| e.to_string())?;
956 let raw = mantissa_exponent_to_raw_checked::<QuantityRaw>(
957 mantissa,
958 exponent,
959 precision,
960 "Quantity::from_str",
961 "QuantityRaw",
962 "Quantity",
963 )
964 .map_err(|e| e.to_string())?;
965 Self::from_raw_checked(raw, precision).map_err(|e| e.to_string())
966 }
967}
968
969impl From<&str> for Quantity {
970 fn from(value: &str) -> Self {
971 Self::from_str(value).expect(FAILED)
972 }
973}
974
975impl From<String> for Quantity {
976 fn from(value: String) -> Self {
977 Self::from_str(&value).expect(FAILED)
978 }
979}
980
981impl From<&String> for Quantity {
982 fn from(value: &String) -> Self {
983 Self::from_str(value).expect(FAILED)
984 }
985}
986
987impl Debug for Quantity {
988 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
989 write!(
990 f,
991 "{}({})",
992 stringify!(Quantity),
993 format_scaled_u128(Self::raw_as_u128(self.raw_at_precision()), self.precision),
994 )
995 }
996}
997
998impl Display for Quantity {
999 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1000 write!(
1001 f,
1002 "{}",
1003 format_scaled_u128(Self::raw_as_u128(self.raw_at_precision()), self.precision),
1004 )
1005 }
1006}
1007
1008impl Serialize for Quantity {
1009 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1010 where
1011 S: serde::Serializer,
1012 {
1013 serializer.serialize_str(&self.to_string())
1014 }
1015}
1016
1017impl<'de> Deserialize<'de> for Quantity {
1018 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1019 where
1020 D: Deserializer<'de>,
1021 {
1022 let StringInput(qty_str) = Deserialize::deserialize(deserializer)?;
1023 Self::from_str(qty_str.as_ref()).map_err(serde::de::Error::custom)
1024 }
1025}
1026
1027pub fn check_positive_quantity(value: Quantity, param: &str) -> CorrectnessResult<()> {
1033 if !value.is_positive() {
1034 return Err(CorrectnessError::NotPositive {
1035 param: param.to_string(),
1036 value: value.to_string(),
1037 type_name: "`Quantity`",
1038 });
1039 }
1040 Ok(())
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use std::str::FromStr;
1046
1047 use nautilus_core::{approx_eq, correctness::CorrectnessError};
1048 use rstest::rstest;
1049 use rust_decimal_macros::dec;
1050
1051 use super::*;
1052 use crate::types::fixed::FIXED_SCALAR_RAW;
1053 #[cfg(not(feature = "defi"))]
1054 use crate::types::fixed::MAX_FLOAT_PRECISION;
1055
1056 #[cfg(feature = "high-precision")]
1057 #[rstest]
1058 #[case(QUANTITY_RAW_MAX, dec!(34028236692093))]
1059 #[case(80_000_000_000_000_000_000_000_000_000, dec!(8000000000000))]
1060 fn test_as_decimal_above_decimal_mantissa(#[case] raw: QuantityRaw, #[case] expected: Decimal) {
1061 let qty = Quantity::from_raw(raw, 16);
1064
1065 assert_eq!(qty.as_decimal(), expected);
1066 }
1067
1068 #[rstest]
1069 fn test_max_quantity_round_trips_through_raw() {
1070 let qty = Quantity::new(QUANTITY_MAX, 0);
1073
1074 assert_eq!(qty.raw, QUANTITY_RAW_MAX);
1075 assert!(Quantity::from_raw_checked(qty.raw, 0).is_ok());
1076 assert!(qty.checked_add(Quantity::zero(0)).is_some());
1077 }
1078
1079 #[rstest]
1080 fn test_check_quantity_positive() {
1081 let qty = Quantity::new(0.0, 0);
1082 let error = check_positive_quantity(qty, "qty").unwrap_err();
1083
1084 assert_eq!(
1085 error,
1086 CorrectnessError::NotPositive {
1087 param: "qty".to_string(),
1088 value: "0".to_string(),
1089 type_name: "`Quantity`",
1090 }
1091 );
1092 assert_eq!(
1093 error.to_string(),
1094 "invalid `Quantity` for 'qty' not positive, was 0"
1095 );
1096 }
1097
1098 #[rstest]
1099 #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
1100 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 17")]
1101 fn test_invalid_precision_new() {
1102 let _ = Quantity::new(1.0, 17);
1104 }
1105
1106 #[rstest]
1107 #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
1108 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
1109 fn test_invalid_precision_new() {
1110 let _ = Quantity::new(1.0, 17);
1112 }
1113
1114 #[rstest]
1115 #[cfg(not(feature = "defi"))]
1116 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
1117 fn test_invalid_precision_from_raw() {
1118 let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
1120 }
1121
1122 #[rstest]
1123 #[cfg(not(feature = "defi"))]
1124 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
1125 fn test_invalid_precision_zero() {
1126 let _ = Quantity::zero(FIXED_PRECISION + 1);
1128 }
1129
1130 #[rstest]
1131 fn test_mixed_precision_add() {
1132 let q1 = Quantity::new(1.0, 1);
1133 let q2 = Quantity::new(1.0, 2);
1134 let result = q1 + q2;
1135 assert_eq!(result.precision, 2);
1136 assert_eq!(result.as_f64(), 2.0);
1137 }
1138
1139 #[rstest]
1140 fn test_sum_owned_quantities() {
1141 let quantities = [Quantity::new(1.25, 2), Quantity::new(2.75, 2)];
1142 let result: Quantity = quantities.into_iter().sum();
1143
1144 assert_eq!(result.as_decimal(), dec!(4.00));
1145 assert_eq!(result.precision, 2);
1146 }
1147
1148 #[rstest]
1149 fn test_sum_borrowed_quantities() {
1150 let quantities = [Quantity::new(0.125, 3), Quantity::new(0.375, 3)];
1151 let result: Quantity = quantities.iter().sum();
1152
1153 assert_eq!(result.as_decimal(), dec!(0.500));
1154 assert_eq!(result.precision, 3);
1155 }
1156
1157 #[rstest]
1158 fn test_sum_mixed_precision_quantities() {
1159 let quantities = [
1160 Quantity::new(1.2, 1),
1161 Quantity::new(3.45, 2),
1162 Quantity::new(0.006, 3),
1163 ];
1164 let result: Quantity = quantities.into_iter().sum();
1165
1166 assert_eq!(result.as_decimal(), dec!(4.656));
1167 assert_eq!(result.precision, 3);
1168 }
1169
1170 #[rstest]
1171 fn test_sum_empty_quantity_iterator() {
1172 let result: Quantity = std::iter::empty::<Quantity>().sum();
1173
1174 assert_eq!(result.as_decimal(), dec!(0));
1175 assert_eq!(result.precision, 0);
1176 }
1177
1178 #[rstest]
1179 fn test_mixed_precision_sub() {
1180 let q1 = Quantity::new(2.0, 1);
1181 let q2 = Quantity::new(1.0, 2);
1182 let result = q1 - q2;
1183 assert_eq!(result.precision, 2);
1184 assert_eq!(result.as_f64(), 1.0);
1185 }
1186
1187 #[rstest]
1188 fn test_mixed_precision_mul() {
1189 let q1 = Quantity::new(2.0, 1);
1190 let q2 = Quantity::new(3.0, 2);
1191 let result = q1 * q2;
1192 assert_eq!(result.precision, 2);
1193 assert_eq!(result.as_f64(), 6.0);
1194 }
1195
1196 #[rstest]
1197 #[case(0, false)]
1198 #[case(1, true)]
1199 #[case(QUANTITY_UNDEF, true)]
1200 fn test_non_zero(#[case] raw: QuantityRaw, #[case] expected: bool) {
1201 let qty = Quantity::from_raw(raw, 0);
1202 assert_eq!(qty.non_zero(), expected);
1203 }
1204
1205 #[rstest]
1206 fn test_new() {
1207 let value = 0.00812;
1208 let qty = Quantity::new(value, 8);
1209 assert_eq!(qty, qty);
1210 assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
1211 assert_eq!(qty.precision, 8);
1212 assert_eq!(qty, Quantity::from("0.00812000"));
1213 assert_eq!(qty.as_decimal(), dec!(0.00812000));
1214 assert_eq!(qty.to_string(), "0.00812000");
1215 assert!(qty.non_zero());
1216 assert!(qty.is_positive());
1217 assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
1218 }
1219
1220 #[rstest]
1221 fn test_check_quantity_positive_ok() {
1222 let qty = Quantity::new(10.0, 0);
1223 check_positive_quantity(qty, "qty").unwrap();
1224 }
1225
1226 #[rstest]
1227 fn test_negative_quantity_validation() {
1228 assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
1229 }
1230
1231 #[rstest]
1232 fn test_new_checked_returns_typed_error_with_stable_display() {
1233 let error = Quantity::new_checked(QUANTITY_MAX + 1.0, FIXED_PRECISION).unwrap_err();
1234
1235 assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
1236 assert_eq!(
1237 error.to_string(),
1238 format!(
1239 "invalid f64 for 'value' not in range [{QUANTITY_MIN}, {QUANTITY_MAX}], was {}",
1240 QUANTITY_MAX + 1.0
1241 )
1242 );
1243 }
1244
1245 #[rstest]
1246 fn test_from_raw_checked_returns_typed_error_with_stable_display() {
1247 let error = Quantity::from_raw_checked(QUANTITY_UNDEF, 3).unwrap_err();
1248
1249 assert_eq!(
1250 error,
1251 CorrectnessError::PredicateViolation {
1252 message: "`precision` must be 0 when `raw` is QUANTITY_UNDEF".to_string(),
1253 }
1254 );
1255 assert_eq!(
1256 error.to_string(),
1257 "`precision` must be 0 when `raw` is QUANTITY_UNDEF"
1258 );
1259 }
1260
1261 #[rstest]
1262 fn test_undefined() {
1263 let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
1264 assert_eq!(qty.raw, QUANTITY_UNDEF);
1265 assert!(qty.is_undefined());
1266 }
1267
1268 #[rstest]
1269 fn test_zero() {
1270 let qty = Quantity::zero(8);
1271 assert_eq!(qty.raw, 0);
1272 assert_eq!(qty.precision, 8);
1273 assert!(qty.is_zero());
1274 assert!(!qty.is_positive());
1275 }
1276
1277 #[rstest]
1278 fn test_from_i32_exact() {
1279 let values = [0, 1, i32::MAX];
1280 let quantities = values.map(Quantity::from);
1281 let expected =
1282 values.map(|value| (QuantityRaw::try_from(value).unwrap() * FIXED_SCALAR_RAW, 0));
1283
1284 assert_eq!(
1285 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1286 expected
1287 );
1288 }
1289
1290 #[rstest]
1291 fn test_from_i64_exact() {
1292 let max = quantity_max_i64();
1293 let values = [0, 1, max];
1294 let quantities = values.map(Quantity::from);
1295 let expected =
1296 values.map(|value| (QuantityRaw::try_from(value).unwrap() * FIXED_SCALAR_RAW, 0));
1297
1298 assert_eq!(
1299 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1300 expected
1301 );
1302 }
1303
1304 #[rstest]
1305 fn test_from_u32_exact() {
1306 let values = [0, 1, u32::MAX];
1307 let quantities = values.map(Quantity::from);
1308 let expected = values.map(|value| (QuantityRaw::from(value) * FIXED_SCALAR_RAW, 0));
1309
1310 assert_eq!(
1311 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1312 expected
1313 );
1314 }
1315
1316 #[rstest]
1317 fn test_from_u64_exact() {
1318 let max = quantity_max_u64();
1319 let values = [0, 1, max];
1320 let quantities = values.map(Quantity::from);
1321 let expected = values.map(|value| (QuantityRaw::from(value) * FIXED_SCALAR_RAW, 0));
1322
1323 assert_eq!(
1324 quantities.map(|quantity| (quantity.raw, quantity.precision)),
1325 expected
1326 );
1327 }
1328
1329 #[rstest]
1330 #[should_panic(
1331 expected = "Cannot create Quantity from negative i32: -1. Use u32 or check value is non-negative."
1332 )]
1333 fn test_from_i32_negative_panics() {
1334 let _ = Quantity::from(-1_i32);
1335 }
1336
1337 #[rstest]
1338 #[should_panic(
1339 expected = "Cannot create Quantity from negative i64: -1. Use u64 or check value is non-negative."
1340 )]
1341 fn test_from_i64_negative_panics() {
1342 let _ = Quantity::from(-1_i64);
1343 }
1344
1345 #[rstest]
1346 #[cfg_attr(
1347 feature = "high-precision",
1348 should_panic(expected = "exceeded QUANTITY_RAW_MAX")
1349 )]
1350 #[cfg_attr(
1351 not(feature = "high-precision"),
1352 should_panic(expected = "Raw value exceeds QuantityRaw range")
1353 )]
1354 fn test_from_i64_overflow_panics() {
1355 let max = quantity_max_i64();
1356
1357 let _ = Quantity::from(max + 1);
1358 }
1359
1360 #[rstest]
1361 #[cfg_attr(
1362 feature = "high-precision",
1363 should_panic(expected = "exceeded QUANTITY_RAW_MAX")
1364 )]
1365 #[cfg_attr(
1366 not(feature = "high-precision"),
1367 should_panic(expected = "Raw value exceeds QuantityRaw range")
1368 )]
1369 fn test_from_u64_overflow_panics() {
1370 let max = quantity_max_u64();
1371
1372 let _ = Quantity::from(max + 1);
1373 }
1374
1375 fn quantity_max_i64() -> i64 {
1376 i64::try_from(QUANTITY_RAW_MAX / FIXED_SCALAR_RAW).unwrap()
1377 }
1378
1379 #[allow(
1380 clippy::useless_conversion,
1381 reason = "try_from is a no-op when QuantityRaw is u64, and narrows when u128 (high-precision)"
1382 )]
1383 fn quantity_max_u64() -> u64 {
1384 u64::try_from(QUANTITY_RAW_MAX / FIXED_SCALAR_RAW).unwrap()
1385 }
1386
1387 #[rstest] fn test_with_maximum_value() {
1389 let qty = Quantity::new_checked(QUANTITY_MAX, 0);
1390 assert!(qty.is_ok());
1391 }
1392
1393 #[rstest]
1394 fn test_with_minimum_positive_value() {
1395 let value = 0.000_000_001;
1396 let qty = Quantity::new(value, 9);
1397 assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
1398 assert_eq!(qty.as_decimal(), dec!(0.000000001));
1399 assert_eq!(qty.to_string(), "0.000000001");
1400 }
1401
1402 #[rstest]
1403 fn test_with_minimum_value() {
1404 let qty = Quantity::new(QUANTITY_MIN, 9);
1405 assert_eq!(qty.raw, 0);
1406 assert_eq!(qty.as_decimal(), dec!(0));
1407 assert_eq!(qty.to_string(), "0.000000000");
1408 }
1409
1410 #[rstest]
1411 fn test_is_zero() {
1412 let qty = Quantity::zero(8);
1413 assert_eq!(qty, qty);
1414 assert_eq!(qty.raw, 0);
1415 assert_eq!(qty.precision, 8);
1416 assert_eq!(qty, Quantity::from("0.00000000"));
1417 assert_eq!(qty.as_decimal(), dec!(0));
1418 assert_eq!(qty.to_string(), "0.00000000");
1419 assert!(qty.is_zero());
1420 }
1421
1422 #[rstest]
1423 fn test_precision() {
1424 let value = 1.001;
1425 let qty = Quantity::new(value, 2);
1426 assert_eq!(qty.to_string(), "1.00");
1427 }
1428
1429 #[rstest]
1430 fn test_new_from_str() {
1431 let qty = Quantity::new(0.008_120_00, 8);
1432 assert_eq!(qty, qty);
1433 assert_eq!(qty.precision, 8);
1434 assert_eq!(qty, Quantity::from("0.00812000"));
1435 assert_eq!(qty.to_string(), "0.00812000");
1436 }
1437
1438 #[rstest]
1439 #[case("0", 0)]
1440 #[case("1.1", 1)]
1441 #[case("1.123456789", 9)]
1442 fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
1443 let qty = Quantity::from(input);
1444 assert_eq!(qty.precision, expected_prec);
1445 assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
1446 }
1447
1448 #[rstest]
1449 #[should_panic(expected = "ParseFloatError")]
1450 fn test_from_str_invalid_input() {
1451 let input = "invalid";
1452 let _ = Quantity::new(f64::from_str(input).unwrap(), 8);
1453 }
1454
1455 #[rstest]
1456 fn test_from_str_errors() {
1457 assert!(Quantity::from_str("invalid").is_err());
1458 assert!(Quantity::from_str("12.34.56").is_err());
1459 assert!(Quantity::from_str("").is_err());
1460 assert!(Quantity::from_str("-1").is_err()); assert!(Quantity::from_str("-0.001").is_err());
1462 }
1463
1464 #[rstest]
1465 #[case("1e7", 0, 10_000_000.0)]
1466 #[case("2.5e3", 0, 2_500.0)]
1467 #[case("1.234e-2", 5, 0.01234)]
1468 #[case("5E-3", 3, 0.005)]
1469 #[case("1.0e6", 0, 1_000_000.0)]
1470 fn test_from_str_scientific_notation(
1471 #[case] input: &str,
1472 #[case] expected_precision: u8,
1473 #[case] expected_value: f64,
1474 ) {
1475 let qty = Quantity::from_str(input).unwrap();
1476 assert_eq!(qty.precision, expected_precision);
1477 assert!(approx_eq!(
1478 f64,
1479 qty.as_f64(),
1480 expected_value,
1481 epsilon = 1e-10
1482 ));
1483 }
1484
1485 #[rstest]
1486 #[case("1_234.56", 2, 1234.56)]
1487 #[case("1000000", 0, 1_000_000.0)]
1488 #[case("99_999.999_99", 5, 99_999.999_99)]
1489 fn test_from_str_with_underscores(
1490 #[case] input: &str,
1491 #[case] expected_precision: u8,
1492 #[case] expected_value: f64,
1493 ) {
1494 let qty = Quantity::from_str(input).unwrap();
1495 assert_eq!(qty.precision, expected_precision);
1496 assert!(approx_eq!(
1497 f64,
1498 qty.as_f64(),
1499 expected_value,
1500 epsilon = 1e-10
1501 ));
1502 }
1503
1504 #[rstest]
1505 fn test_from_decimal_dp_preservation() {
1506 let decimal = dec!(123.456789);
1508 let qty = Quantity::from_decimal_dp(decimal, 6).unwrap();
1509 assert_eq!(qty.precision, 6);
1510 assert!(approx_eq!(f64, qty.as_f64(), 123.456_789, epsilon = 1e-10));
1511
1512 let expected_raw = 123_456_789_u64 * 10_u64.pow(u32::from(FIXED_PRECISION - 6));
1514 assert_eq!(qty.raw, QuantityRaw::from(expected_raw));
1515 }
1516
1517 #[rstest]
1518 fn test_from_decimal_dp_rounding() {
1519 let decimal = dec!(1.005);
1521 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1522 assert_eq!(qty.as_f64(), 1.0); let decimal = dec!(1.015);
1525 let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
1526 assert_eq!(qty.as_f64(), 1.02); }
1528
1529 #[rstest]
1530 fn test_from_decimal_infers_precision() {
1531 let decimal = dec!(123.456);
1533 let qty = Quantity::from_decimal(decimal).unwrap();
1534 assert_eq!(qty.precision, 3);
1535 assert!(approx_eq!(f64, qty.as_f64(), 123.456, epsilon = 1e-10));
1536
1537 let decimal = dec!(100);
1539 let qty = Quantity::from_decimal(decimal).unwrap();
1540 assert_eq!(qty.precision, 0);
1541 assert_eq!(qty.as_f64(), 100.0);
1542
1543 let decimal = dec!(1.23456789);
1545 let qty = Quantity::from_decimal(decimal).unwrap();
1546 assert_eq!(qty.precision, 8);
1547 assert!(approx_eq!(f64, qty.as_f64(), 1.234_567_89, epsilon = 1e-10));
1548 }
1549
1550 #[rstest]
1551 fn test_from_decimal_trailing_zeros() {
1552 let decimal = dec!(5.670);
1554 assert_eq!(decimal.scale(), 3); let qty = Quantity::from_decimal(decimal).unwrap();
1558 assert_eq!(qty.precision, 3);
1559 assert!(approx_eq!(f64, qty.as_f64(), 5.67, epsilon = 1e-10));
1560
1561 let normalized = decimal.normalize();
1563 assert_eq!(normalized.scale(), 2);
1564 let qty_normalized = Quantity::from_decimal(normalized).unwrap();
1565 assert_eq!(qty_normalized.precision, 2);
1566 }
1567
1568 #[rstest]
1569 #[case("1.00", 2)]
1570 #[case("1.0", 1)]
1571 #[case("1.000", 3)]
1572 #[case("100.00", 2)]
1573 #[case("0.10", 2)]
1574 #[case("0.100", 3)]
1575 fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1576 let qty = Quantity::from_str(input).unwrap();
1577 assert_eq!(qty.precision, expected_precision);
1578 }
1579
1580 #[rstest]
1581 fn test_from_decimal_excessive_precision_inference() {
1582 let decimal = dec!(1.1234567890123456789012345678);
1585
1586 if decimal.scale() > u32::from(FIXED_PRECISION) {
1588 assert!(Quantity::from_decimal(decimal).is_err());
1589 }
1590 }
1591
1592 #[rstest]
1593 fn test_from_decimal_negative_quantity_errors() {
1594 let decimal = dec!(-123.45);
1596 let result = Quantity::from_decimal(decimal);
1597 assert!(result.is_err());
1598
1599 let result = Quantity::from_decimal_dp(decimal, 2);
1601 assert!(result.is_err());
1602 }
1603
1604 #[rstest]
1605 fn test_from_decimal_dp_negative_returns_typed_error_with_stable_display() {
1606 let error = Quantity::from_decimal_dp(dec!(-1.5), 2).unwrap_err();
1607 assert_eq!(
1608 error,
1609 CorrectnessError::PredicateViolation {
1610 message: "Decimal value '-1.5' is negative, Quantity must be non-negative"
1611 .to_string(),
1612 }
1613 );
1614 assert_eq!(
1615 error.to_string(),
1616 "Decimal value '-1.5' is negative, Quantity must be non-negative",
1617 );
1618 }
1619
1620 #[cfg(not(feature = "high-precision"))]
1621 #[rstest]
1622 fn test_from_decimal_dp_rejects_quantity_raw_overflow() {
1623 let error = Quantity::from_decimal_dp(dec!(20_000_000_000), 0).unwrap_err();
1624
1625 assert_eq!(
1626 error,
1627 CorrectnessError::PredicateViolation {
1628 message: format!("Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]"),
1629 }
1630 );
1631 }
1632
1633 #[cfg(feature = "high-precision")]
1634 #[rstest]
1635 fn test_from_decimal_dp_rejects_value_above_quantity_max() {
1636 let value = 34_028_236_692_094_u64;
1637 let raw = u128::from(value) * FIXED_SCALAR_RAW;
1638 let error = Quantity::from_decimal_dp(Decimal::from(value), 0).unwrap_err();
1639
1640 assert_eq!(
1641 error,
1642 CorrectnessError::PredicateViolation {
1643 message: format!(
1644 "Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
1645 ),
1646 }
1647 );
1648 }
1649
1650 #[rstest]
1651 fn test_add() {
1652 let a = 1.0;
1653 let b = 2.0;
1654 let quantity1 = Quantity::new(1.0, 0);
1655 let quantity2 = Quantity::new(2.0, 0);
1656 let quantity3 = quantity1 + quantity2;
1657 assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
1658 }
1659
1660 #[rstest]
1661 fn test_sub() {
1662 let a = 3.0;
1663 let b = 2.0;
1664 let quantity1 = Quantity::new(a, 0);
1665 let quantity2 = Quantity::new(b, 0);
1666 let quantity3 = quantity1 - quantity2;
1667 assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
1668 }
1669
1670 #[rstest]
1671 fn test_quantity_checked_add_within_bounds() {
1672 let a = Quantity::new(10.0, 2);
1673 let b = Quantity::new(5.0, 2);
1674 assert_eq!(a.checked_add(b), Some(Quantity::new(15.0, 2)));
1675 }
1676
1677 #[rstest]
1678 fn test_quantity_checked_rescale_native_precision() {
1679 let quantity = Quantity::from("1.25");
1680 let rescaled = quantity.checked_rescale(4).unwrap();
1681
1682 assert_eq!(rescaled.raw, quantity.raw);
1683 assert_eq!(rescaled.precision, 4);
1684 assert_eq!(rescaled.as_decimal(), dec!(1.25));
1685 assert_eq!(quantity.checked_rescale(0), None);
1686 assert_eq!(
1687 Quantity::from_raw(QUANTITY_UNDEF, 0).checked_rescale(0),
1688 None
1689 );
1690 assert_eq!(quantity.checked_rescale(u8::MAX), None);
1691 let mut invalid = quantity;
1692 invalid.precision = u8::MAX;
1693 assert_eq!(invalid.checked_rescale(0), None);
1694 }
1695
1696 #[cfg(feature = "defi")]
1697 #[rstest]
1698 #[case("1.25", 18, Some("1.250000000000000000"))]
1699 #[case("1.250000000000000000", 2, None)]
1700 #[case("1.250000000000000001", 2, None)]
1701 #[case("1.25000000000000001", 18, Some("1.250000000000000010"))]
1702 #[case("1.250000000000000001", 17, None)]
1703 fn test_quantity_checked_rescale_mixed_scales(
1704 #[case] value: &str,
1705 #[case] precision: u8,
1706 #[case] expected: Option<&str>,
1707 ) {
1708 let actual = Quantity::from(value).checked_rescale(precision);
1709
1710 assert_eq!(actual, expected.map(Quantity::from));
1711 assert_eq!(actual.map(|qty| qty.precision), expected.map(|_| precision));
1712 }
1713
1714 #[cfg(feature = "defi")]
1715 #[rstest]
1716 fn test_quantity_checked_rescale_rejects_overflow() {
1717 let quantity = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
1718
1719 assert_eq!(quantity.checked_rescale(18), None);
1720 assert_eq!(
1721 quantity.checked_floor_to_increment(Quantity::from("0.000000000000000001")),
1722 None
1723 );
1724 }
1725
1726 #[rstest]
1727 #[case("0.749", "0.50", "0.50")]
1728 #[case("0.49", "0.50", "0.00")]
1729 #[case("1.23456789", "0.000001", "1.234567")]
1730 #[case("0", "0.5", "0.0")]
1731 fn test_quantity_checked_floor_to_increment(
1732 #[case] value: &str,
1733 #[case] increment: &str,
1734 #[case] expected: &str,
1735 ) {
1736 let increment = Quantity::from(increment);
1737 let actual = Quantity::from(value)
1738 .checked_floor_to_increment(increment)
1739 .unwrap();
1740
1741 assert_eq!(actual, Quantity::from(expected));
1742 assert_eq!(actual.precision, increment.precision);
1743 }
1744
1745 #[cfg(feature = "defi")]
1746 #[rstest]
1747 #[case("0.59999999999999999", "0.005000", "0.595000")]
1748 #[case("0.599999999999999999", "0.005000", "0.595000")]
1749 #[case("0.600000", "0.00000000000000001", "0.60000000000000000")]
1750 #[case("0.59999999999999999", "0.000000000000000001", "0.599999999999999990")]
1751 fn test_quantity_checked_floor_aligns_raw_scales(
1752 #[case] value: &str,
1753 #[case] increment: &str,
1754 #[case] expected: &str,
1755 ) {
1756 let increment = Quantity::from(increment);
1757 let actual = Quantity::from(value)
1758 .checked_floor_to_increment(increment)
1759 .unwrap();
1760
1761 assert_eq!(actual, Quantity::from(expected));
1762 assert_eq!(actual.precision, increment.precision);
1763 }
1764
1765 #[rstest]
1766 fn test_quantity_checked_floor_rejects_invalid_operands() {
1767 let quantity = Quantity::from(1);
1768 let invalid = Quantity::from_raw(QUANTITY_UNDEF, 0);
1769
1770 assert_eq!(quantity.checked_floor_to_increment(Quantity::from(0)), None);
1771 assert_eq!(quantity.checked_floor_to_increment(invalid), None);
1772 assert_eq!(invalid.checked_floor_to_increment(quantity), None);
1773 }
1774
1775 #[rstest]
1776 fn test_quantity_checked_add_above_max_returns_none() {
1777 let near_max = Quantity::from_raw(QUANTITY_RAW_MAX, 0);
1778 let one = Quantity::new(1.0, 0);
1779 assert_eq!(near_max.checked_add(one), None);
1780 }
1781
1782 #[rstest]
1783 fn test_quantity_checked_sub_within_bounds() {
1784 let a = Quantity::new(10.0, 2);
1785 let b = Quantity::new(3.0, 2);
1786 assert_eq!(a.checked_sub(b), Some(Quantity::new(7.0, 2)));
1787 }
1788
1789 #[rstest]
1790 fn test_quantity_checked_sub_underflow_returns_none() {
1791 let a = Quantity::new(3.0, 2);
1792 let b = Quantity::new(10.0, 2);
1793 assert_eq!(a.checked_sub(b), None);
1794 }
1795
1796 #[rstest]
1797 fn test_quantity_checked_sub_to_zero() {
1798 let a = Quantity::new(5.0, 2);
1799 assert_eq!(a.checked_sub(a), Some(Quantity::zero(2)));
1800 }
1801
1802 #[rstest]
1803 fn test_quantity_from_raw_checked_allows_undef_and_rejects_above_max() {
1804 assert_eq!(
1805 Quantity::from_raw_checked(QUANTITY_UNDEF, 0).unwrap().raw,
1806 QUANTITY_UNDEF
1807 );
1808 assert_eq!(
1809 Quantity::from_raw_checked(QUANTITY_RAW_MAX + 1, 0)
1810 .unwrap_err()
1811 .to_string(),
1812 format!(
1813 "raw value {} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}",
1814 QUANTITY_RAW_MAX + 1
1815 )
1816 );
1817 }
1818
1819 #[rstest]
1820 fn test_quantity_as_f64() {
1821 assert_eq!(Quantity::new(2.5, 1).as_f64(), 2.5);
1822 assert_eq!(Quantity::new(0.0, 1).as_f64(), 0.0);
1823 }
1824
1825 #[rstest]
1826 fn test_quantity_deref_yields_raw() {
1827 assert_eq!(*Quantity::from_raw(2_500, 1), 2_500);
1828 }
1829
1830 #[rstest]
1831 fn test_quantity_checked_arith_rejects_undef() {
1832 let undef = Quantity::from_raw(QUANTITY_UNDEF, 0);
1833 let one = Quantity::new(1.0, 0);
1834 assert_eq!(undef.checked_add(one), None);
1835 assert_eq!(one.checked_add(undef), None);
1836 assert_eq!(undef.checked_sub(one), None);
1837 assert_eq!(one.checked_sub(undef), None);
1838 }
1839
1840 #[rstest]
1841 fn test_quantity_checked_add_at_exact_max_returns_some() {
1842 let near_max = Quantity::from_raw(QUANTITY_RAW_MAX - 1, 0);
1843 let one_unit = Quantity::from_raw(1, 0);
1844 assert_eq!(
1845 near_max.checked_add(one_unit),
1846 Some(Quantity::from_raw(QUANTITY_RAW_MAX, 0)),
1847 );
1848 }
1849
1850 #[rstest]
1851 fn test_quantity_checked_arith_uses_max_precision() {
1852 let a = Quantity::new(10.5, 1);
1853 let b = Quantity::new(2.25, 2);
1854 let sum = a.checked_add(b).unwrap();
1855 assert_eq!(sum.precision, 2);
1856 assert_eq!(sum.as_f64(), 12.75);
1857
1858 let diff = a.checked_sub(b).unwrap();
1859 assert_eq!(diff.precision, 2);
1860 assert_eq!(diff.as_f64(), 8.25);
1861 }
1862
1863 #[rstest]
1864 fn test_mul() {
1865 let value = 2.0;
1866 let quantity1 = Quantity::new(value, 1);
1867 let quantity2 = Quantity::new(value, 1);
1868 let quantity3 = quantity1 * quantity2;
1869 assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
1870 }
1871
1872 #[rstest]
1873 fn test_mul_avoids_intermediate_raw_overflow() {
1874 let scalar = FIXED_SCALAR_RAW;
1875 #[cfg(feature = "high-precision")]
1876 let (lhs_raw, rhs_raw, expected_raw) =
1877 (100_000 * scalar, 100 * scalar, 10_000_000 * scalar);
1878 #[cfg(not(feature = "high-precision"))]
1879 let (lhs_raw, rhs_raw, expected_raw) = (
1880 9_000_000_000 * scalar,
1881 2 * scalar + 1,
1882 18_000_000_009 * scalar,
1883 );
1884 let lhs = Quantity::from_raw(lhs_raw, FIXED_PRECISION);
1885 let rhs = Quantity::from_raw(rhs_raw, FIXED_PRECISION);
1886 let result = lhs * rhs;
1887
1888 assert_eq!(lhs_raw.checked_mul(rhs_raw), None);
1889 assert_eq!(result.raw, expected_raw);
1890 assert_eq!(result.precision, FIXED_PRECISION);
1891 }
1892
1893 #[rstest]
1894 #[should_panic(expected = "Overflow occurred when multiplying `Quantity`")]
1895 fn test_mul_panics_when_scaled_result_exceeds_quantity_max() {
1896 let lhs = Quantity::from_raw(QUANTITY_RAW_MAX, FIXED_PRECISION);
1897 let rhs = Quantity::from(2);
1898
1899 let _ = lhs * rhs;
1900 }
1901
1902 #[rstest]
1903 fn test_comparisons() {
1904 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
1905 assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
1906 assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
1907 assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
1908 assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
1909 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
1910 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1911 assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
1912 assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
1913 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
1914 assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
1915 }
1916
1917 #[rstest]
1918 fn test_debug() {
1919 let quantity = Quantity::from_str("44.12").unwrap();
1920 let result = format!("{quantity:?}");
1921 assert_eq!(result, "Quantity(44.12)");
1922 }
1923
1924 #[rstest]
1925 fn test_display() {
1926 let quantity = Quantity::from_str("44.12").unwrap();
1927 let result = format!("{quantity}");
1928 assert_eq!(result, "44.12");
1929 }
1930
1931 #[rstest]
1932 #[case(44.12, 2, "Quantity(44.12)", "44.12")] #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] #[cfg_attr(
1935 feature = "defi",
1936 case(
1937 1_000_000_000_000_000_000.0,
1938 18,
1939 "Quantity(1.000000000000000000)",
1940 "1.000000000000000000"
1941 )
1942 )] fn test_debug_display_precision_handling(
1944 #[case] value: f64,
1945 #[case] precision: u8,
1946 #[case] expected_debug: &str,
1947 #[case] expected_display: &str,
1948 ) {
1949 let quantity = if precision > MAX_FLOAT_PRECISION {
1950 Quantity::from_raw(value as QuantityRaw, precision)
1952 } else {
1953 Quantity::new(value, precision)
1954 };
1955
1956 assert_eq!(format!("{quantity:?}"), expected_debug);
1957 assert_eq!(format!("{quantity}"), expected_display);
1958 }
1959
1960 #[rstest]
1961 fn test_to_formatted_string() {
1962 let qty = Quantity::new(1234.5678, 4);
1963 let formatted = qty.to_formatted_string();
1964 assert_eq!(formatted, "1_234.5678");
1965 assert_eq!(qty.to_string(), "1234.5678");
1966 }
1967
1968 #[rstest]
1969 fn test_saturating_sub() {
1970 let q1 = Quantity::new(100.0, 2);
1971 let q2 = Quantity::new(50.0, 2);
1972 let q3 = Quantity::new(150.0, 2);
1973
1974 let result = q1.saturating_sub(q2);
1975 assert_eq!(result, Quantity::new(50.0, 2));
1976
1977 let result = q1.saturating_sub(q3);
1978 assert_eq!(result, Quantity::zero(2));
1979 assert_eq!(result.raw, 0);
1980 }
1981
1982 #[rstest]
1983 fn test_saturating_sub_overflow_bug() {
1984 use crate::types::fixed::FIXED_PRECISION;
1987 let precision = 3;
1988 let scale = QuantityRaw::from(10u64.pow(u32::from(FIXED_PRECISION - precision)));
1989
1990 let peak_qty = Quantity::from_raw(79 * scale, precision);
1992 let order_qty = Quantity::from_raw(80 * scale, precision);
1993
1994 let result = peak_qty.saturating_sub(order_qty);
1996 assert_eq!(result.raw, 0);
1997 assert_eq!(result, Quantity::zero(precision));
1998 }
1999
2000 #[rstest]
2001 fn test_hash() {
2002 use std::{
2003 collections::hash_map::DefaultHasher,
2004 hash::{Hash, Hasher},
2005 };
2006
2007 let q1 = Quantity::new(100.0, 1);
2008 let q2 = Quantity::new(100.0, 1);
2009 let q3 = Quantity::new(200.0, 1);
2010
2011 let mut s1 = DefaultHasher::new();
2012 let mut s2 = DefaultHasher::new();
2013 let mut s3 = DefaultHasher::new();
2014
2015 q1.hash(&mut s1);
2016 q2.hash(&mut s2);
2017 q3.hash(&mut s3);
2018
2019 assert_eq!(
2020 s1.finish(),
2021 s2.finish(),
2022 "Equal quantities must hash equally"
2023 );
2024 assert_ne!(
2025 s1.finish(),
2026 s3.finish(),
2027 "Different quantities must hash differently"
2028 );
2029 }
2030
2031 #[rstest]
2032 fn test_quantity_serde_json_round_trip() {
2033 let original = Quantity::new(123.456, 3);
2034 let json_str = serde_json::to_string(&original).unwrap();
2035 assert_eq!(json_str, "\"123.456\"");
2036
2037 let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
2038 assert_eq!(deserialized, original);
2039 assert_eq!(deserialized.precision, 3);
2040 }
2041
2042 #[rstest]
2043 fn test_quantity_serde_json_from_value_round_trip() {
2044 let original = Quantity::new(123.456, 3);
2045 let value = serde_json::to_value(original).unwrap();
2046 assert_eq!(value, serde_json::json!("123.456"));
2047
2048 let deserialized: Quantity = serde_json::from_value(value).unwrap();
2049 assert_eq!(deserialized, original);
2050 assert_eq!(deserialized.precision, 3);
2051 }
2052
2053 #[rstest]
2054 fn test_quantity_deserialize_invalid_string_returns_error() {
2055 let result = serde_json::from_str::<Quantity>("\"not-a-quantity\"");
2056 let error = result.unwrap_err();
2057 assert!(
2058 error.to_string().contains("Error parsing"),
2059 "unexpected message: {error}"
2060 );
2061 }
2062
2063 #[rstest]
2064 fn test_quantity_deserialize_negative_returns_error() {
2065 let result = serde_json::from_str::<Quantity>("\"-1.5\"");
2066 let error = result.unwrap_err();
2067 assert!(
2068 error.to_string().contains("negative"),
2069 "unexpected message: {error}"
2070 );
2071 }
2072
2073 #[rstest]
2074 fn test_from_mantissa_exponent_exact_precision() {
2075 let qty = Quantity::from_mantissa_exponent(12345, -2, 2);
2076 assert_eq!(qty.as_f64(), 123.45);
2077 }
2078
2079 #[rstest]
2080 fn test_from_mantissa_exponent_excess_rounds_down() {
2081 let qty = Quantity::from_mantissa_exponent(12345, -3, 2);
2084 assert_eq!(qty.as_f64(), 12.34);
2085 }
2086
2087 #[rstest]
2088 fn test_from_mantissa_exponent_excess_rounds_up() {
2089 let qty = Quantity::from_mantissa_exponent(12355, -3, 2);
2091 assert_eq!(qty.as_f64(), 12.36);
2092 }
2093
2094 #[rstest]
2095 fn test_from_mantissa_exponent_positive_exponent() {
2096 let qty = Quantity::from_mantissa_exponent(5, 2, 0);
2097 assert_eq!(qty.as_f64(), 500.0);
2098 }
2099
2100 #[rstest]
2101 fn test_from_mantissa_exponent_zero() {
2102 let qty = Quantity::from_mantissa_exponent(0, 2, 2);
2103 assert_eq!(qty.as_f64(), 0.0);
2104 }
2105
2106 #[cfg(feature = "high-precision")]
2107 #[rstest]
2108 fn test_max_raw_precision_16_string_round_trip() {
2109 let quantity = Quantity::from_raw(QUANTITY_RAW_MAX, 16);
2110
2111 let decoded = quantity.to_string().parse::<Quantity>().unwrap();
2112
2113 assert_eq!(decoded, quantity);
2114 assert_eq!(decoded.raw, QUANTITY_RAW_MAX);
2115 assert_eq!(decoded.precision, 16);
2116 }
2117
2118 #[cfg(all(feature = "defi", feature = "high-precision"))]
2119 #[rstest]
2120 fn test_wei_above_decimal_mantissa_formats_exactly() {
2121 let raw = 80_000_000_000_000_000_250_000_000_000_u128;
2122 let qty = Quantity::from_raw(raw, 18);
2123
2124 assert_eq!(qty.to_string(), "80000000000.000000250000000000");
2125 }
2126
2127 #[rstest]
2128 fn test_from_mantissa_exponent_checked_exact_precision() {
2129 let qty = Quantity::from_mantissa_exponent_checked(12345, -2, 2).unwrap();
2130 assert_eq!(qty.as_decimal(), dec!(123.45));
2131 }
2132
2133 #[rstest]
2134 fn test_from_mantissa_exponent_checked_zero_with_large_exponent() {
2135 let qty = Quantity::from_mantissa_exponent_checked(0, 119, 2).unwrap();
2136 assert_eq!(qty.as_decimal(), dec!(0.00));
2137 }
2138
2139 #[rstest]
2140 fn test_from_mantissa_exponent_checked_invalid_precision() {
2141 #[cfg(feature = "defi")]
2142 let invalid_precision = crate::defi::WEI_PRECISION + 1;
2143 #[cfg(not(feature = "defi"))]
2144 let invalid_precision = FIXED_PRECISION + 1;
2145
2146 let error = Quantity::from_mantissa_exponent_checked(1, 0, invalid_precision).unwrap_err();
2147 assert!(error.to_string().contains("`precision` exceeded maximum"));
2148 }
2149
2150 #[rstest]
2151 fn test_from_mantissa_exponent_checked_overflow_returns_error() {
2152 let error = Quantity::from_mantissa_exponent_checked(u64::MAX, 100, 0).unwrap_err();
2153 assert!(
2154 error
2155 .to_string()
2156 .contains("Overflow in Quantity::from_mantissa_exponent")
2157 );
2158 }
2159
2160 #[rstest]
2161 #[should_panic(expected = "Quantity::from_mantissa_exponent")]
2162 fn test_from_mantissa_exponent_overflow_panics() {
2163 let _ = Quantity::from_mantissa_exponent(u64::MAX, 9, 0);
2164 }
2165
2166 #[rstest]
2167 #[should_panic(expected = "exceeds i128 range")]
2168 fn test_from_mantissa_exponent_large_exponent_panics() {
2169 let _ = Quantity::from_mantissa_exponent(1, 119, 0);
2170 }
2171
2172 #[rstest]
2173 fn test_from_mantissa_exponent_zero_with_large_exponent() {
2174 let qty = Quantity::from_mantissa_exponent(0, 119, 0);
2175 assert_eq!(qty.as_f64(), 0.0);
2176 }
2177
2178 #[rstest]
2179 fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
2180 let qty = Quantity::from_mantissa_exponent(12345, -120, 2);
2181 assert_eq!(qty.as_f64(), 0.0);
2182 }
2183
2184 #[rstest]
2185 fn test_f64_operations() {
2186 let q = Quantity::new(10.5, 2);
2187 assert_eq!(q + 1.0, 11.5);
2188 assert_eq!(q - 1.0, 9.5);
2189 assert_eq!(q * 2.0, 21.0);
2190 assert_eq!(q / 2.0, 5.25);
2191 }
2192
2193 #[rstest]
2194 fn test_decimal_arithmetic_operations() {
2195 let qty = Quantity::new(100.0, 2);
2196 assert_eq!(qty + dec!(50.25), dec!(150.25));
2197 assert_eq!(qty - dec!(30.50), dec!(69.50));
2198 assert_eq!(qty * dec!(1.5), dec!(150.00));
2199 assert_eq!(qty / dec!(4), dec!(25.00));
2200 }
2201
2202 #[rstest]
2206 #[cfg(feature = "defi")]
2207 #[case::sell_tx_rain_amount(
2208 U256::from_str_radix("42193532365637161405123", 10).unwrap(),
2209 18,
2210 "42193.532365637161405123"
2211 )]
2212 #[case::sell_tx_weth_amount(
2213 U256::from_str_radix("112633187203033110", 10).unwrap(),
2214 18,
2215 "0.112633187203033110"
2216 )]
2217 fn test_from_u256_real_swap_data(
2218 #[case] amount: U256,
2219 #[case] precision: u8,
2220 #[case] expected_str: &str,
2221 ) {
2222 let qty = Quantity::from_u256(amount, precision).unwrap();
2223 assert_eq!(qty.precision, precision);
2224 assert_eq!(qty.as_decimal().to_string(), expected_str);
2225 }
2226
2227 #[rstest]
2228 #[cfg(feature = "defi")]
2229 fn test_from_u256_overflow_returns_typed_error_with_stable_display() {
2230 let error = Quantity::from_u256(U256::MAX, 0).unwrap_err();
2231 match error {
2232 CorrectnessError::PredicateViolation { ref message } => {
2233 assert!(
2234 message.contains("Amount overflow during scaling to fixed precision"),
2235 "unexpected message: {message:?}",
2236 );
2237 }
2238 _ => panic!("expected PredicateViolation, was {error:?}"),
2239 }
2240 }
2241
2242 #[rstest]
2243 #[cfg(feature = "defi")]
2244 fn test_from_u256_rejects_amount_above_quantity_raw_range() {
2245 let amount = U256::from(u128::MAX) + U256::from(1_u8);
2246 let error = Quantity::from_u256(amount, 18).unwrap_err();
2247
2248 assert_eq!(
2249 error,
2250 CorrectnessError::PredicateViolation {
2251 message: format!("U256 scaled amount {amount} exceeds QuantityRaw range"),
2252 }
2253 );
2254 }
2255
2256 #[rstest]
2257 #[cfg(feature = "defi")]
2258 fn test_from_u256_invalid_precision_returns_typed_error() {
2259 let error = Quantity::from_u256(U256::from(1u8), 19).unwrap_err();
2260 match error {
2261 CorrectnessError::PredicateViolation { ref message } => {
2262 assert!(
2263 message.contains("WEI_PRECISION"),
2264 "unexpected message: {message:?}",
2265 );
2266 }
2267 _ => panic!("expected PredicateViolation, was {error:?}"),
2268 }
2269 }
2270
2271 #[rstest]
2272 #[cfg(feature = "defi")]
2273 fn test_from_u256_raw_above_max_returns_typed_error() {
2274 let raw = QUANTITY_RAW_MAX + 1;
2277 let error = Quantity::from_u256(U256::from(raw), FIXED_PRECISION).unwrap_err();
2278 match error {
2279 CorrectnessError::PredicateViolation { ref message } => {
2280 assert!(
2281 message.contains("QUANTITY_RAW_MAX"),
2282 "unexpected message: {message:?}",
2283 );
2284 }
2285 _ => panic!("expected PredicateViolation, was {error:?}"),
2286 }
2287 }
2288}
2289
2290#[cfg(test)]
2291mod property_tests {
2292 use proptest::prelude::*;
2293 use rstest::rstest;
2294
2295 use super::*;
2296 use crate::types::fixed::FIXED_SCALAR_RAW;
2297 #[cfg(not(feature = "defi"))]
2298 use crate::types::fixed::MAX_FLOAT_PRECISION;
2299
2300 fn quantity_value_strategy() -> impl Strategy<Value = f64> {
2302 prop_oneof![
2304 0.00001..1.0,
2306 1.0..100_000.0,
2308 100_000.0..1_000_000.0,
2310 Just(0.0),
2312 Just(QUANTITY_MAX / 2.0),
2314 ]
2315 }
2316
2317 fn precision_strategy() -> impl Strategy<Value = u8> {
2319 let upper = FIXED_PRECISION.min(MAX_FLOAT_PRECISION);
2320 prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
2321 }
2322
2323 fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
2324 let upper = FIXED_PRECISION.clamp(1, MAX_FLOAT_PRECISION);
2325 prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
2326 }
2327
2328 fn raw_for_precision_strategy() -> impl Strategy<Value = (QuantityRaw, u8)> {
2329 precision_strategy().prop_flat_map(|precision| {
2330 let step_u128 = 10u128.pow(u32::from(FIXED_PRECISION.saturating_sub(precision)));
2331 #[cfg(feature = "high-precision")]
2332 let max_steps_u128 = QUANTITY_RAW_MAX / step_u128;
2333 #[cfg(not(feature = "high-precision"))]
2334 let max_steps_u128 = u128::from(QUANTITY_RAW_MAX) / step_u128;
2335
2336 (0u128..=max_steps_u128).prop_map(move |steps_u128| {
2337 let raw_u128 = steps_u128 * step_u128;
2338 #[cfg(feature = "high-precision")]
2339 let raw = raw_u128;
2340 #[cfg(not(feature = "high-precision"))]
2341 let raw = raw_u128
2342 .try_into()
2343 .expect("raw value should fit in QuantityRaw");
2344 (raw, precision)
2345 })
2346 })
2347 }
2348
2349 const DECIMAL_MAX_MANTISSA: u128 = 79_228_162_514_264_337_593_543_950_335;
2350
2351 fn decimal_compatible(raw: QuantityRaw, precision: u8) -> bool {
2352 if precision > MAX_FLOAT_PRECISION {
2353 return false;
2354 }
2355 let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
2356 let divisor = 10u128.pow(precision_diff);
2357 #[cfg(feature = "high-precision")]
2358 let rescaled_raw = raw / divisor;
2359 #[cfg(not(feature = "high-precision"))]
2360 let rescaled_raw = u128::from(raw) / divisor;
2361 rescaled_raw <= DECIMAL_MAX_MANTISSA
2364 }
2365
2366 proptest! {
2367 #[rstest]
2369 fn prop_quantity_serde_round_trip(
2370 (raw, precision) in raw_for_precision_strategy()
2371 ) {
2372 let original = Quantity::from_raw(raw, precision);
2373
2374 let string_repr = original.to_string();
2376 let from_string: Quantity = string_repr.parse().unwrap();
2377 prop_assert_eq!(from_string.raw, original.raw);
2378 prop_assert_eq!(from_string.precision, original.precision);
2379
2380 let json = serde_json::to_string(&original).unwrap();
2382 let from_json: Quantity = serde_json::from_str(&json).unwrap();
2383 prop_assert_eq!(from_json.precision, original.precision);
2384 prop_assert_eq!(from_json.raw, original.raw);
2385 }
2386
2387 #[rstest]
2389 fn prop_quantity_arithmetic_associative(
2390 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2391 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2392 c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2393 precision in precision_strategy()
2394 ) {
2395 let q_a = Quantity::new(a, precision);
2396 let q_b = Quantity::new(b, precision);
2397 let q_c = Quantity::new(c, precision);
2398
2399 let expected = q_a
2400 .raw
2401 .checked_add(q_b.raw)
2402 .and_then(|sum| sum.checked_add(q_c.raw))
2403 .filter(|sum| *sum <= QUANTITY_RAW_MAX);
2404
2405 if let Some(expected) = expected {
2406 let left = (q_a + q_b) + q_c;
2407 let right = q_a + (q_b + q_c);
2408 prop_assert_eq!(left.raw, expected);
2409 prop_assert_eq!(right.raw, expected);
2410 }
2411 }
2412
2413 #[rstest]
2415 fn prop_quantity_addition_subtraction_inverse(
2416 base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
2417 delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
2418 precision in precision_strategy()
2419 ) {
2420 let q_base = Quantity::new(base, precision);
2421 let q_delta = Quantity::new(delta, precision);
2422
2423 let expected = q_base
2424 .raw
2425 .checked_add(q_delta.raw)
2426 .filter(|sum| *sum <= QUANTITY_RAW_MAX);
2427
2428 if expected.is_some() {
2429 prop_assert_eq!((q_base + q_delta) - q_delta, q_base);
2430 }
2431 }
2432
2433 #[rstest]
2436 fn prop_quantity_checked_add_matches_spec(
2437 a in quantity_value_strategy(),
2438 b in quantity_value_strategy(),
2439 precision in precision_strategy()
2440 ) {
2441 let q_a = Quantity::new(a, precision);
2442 let q_b = Quantity::new(b, precision);
2443 let expected = q_a.raw
2444 .checked_add(q_b.raw)
2445 .filter(|r| *r <= QUANTITY_RAW_MAX)
2446 .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
2447 .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
2448 prop_assert_eq!(q_a.checked_add(q_b), expected);
2449 }
2450
2451 #[rstest]
2454 fn prop_quantity_checked_sub_matches_spec(
2455 a in quantity_value_strategy(),
2456 b in quantity_value_strategy(),
2457 precision in precision_strategy()
2458 ) {
2459 let q_a = Quantity::new(a, precision);
2460 let q_b = Quantity::new(b, precision);
2461 let expected = q_a.raw
2462 .checked_sub(q_b.raw)
2463 .filter(|_| q_a.raw != QUANTITY_UNDEF && q_b.raw != QUANTITY_UNDEF)
2464 .map(|raw| Quantity { raw, precision: q_a.precision.max(q_b.precision) });
2465 prop_assert_eq!(q_a.checked_sub(q_b), expected);
2466 }
2467
2468 #[rstest]
2470 fn prop_quantity_ordering_transitive(
2471 a in quantity_value_strategy(),
2472 b in quantity_value_strategy(),
2473 c in quantity_value_strategy(),
2474 precision in precision_strategy()
2475 ) {
2476 let q_a = Quantity::new(a, precision);
2477 let q_b = Quantity::new(b, precision);
2478 let q_c = Quantity::new(c, precision);
2479
2480 if q_a <= q_b && q_b <= q_c {
2482 prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
2483 q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
2484 }
2485 }
2486
2487 #[rstest]
2489 fn prop_quantity_string_parsing_precision(
2490 integral in 0u32..1_000_000,
2491 fractional in 0u32..1_000_000,
2492 precision in precision_strategy_non_zero()
2493 ) {
2494 let pow = 10u128.pow(u32::from(precision));
2496 let fractional_mod = u128::from(fractional) % pow;
2497 let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
2498 let quantity_str = format!("{integral}.{fractional_str}");
2499
2500 let parsed: Quantity = quantity_str.parse().unwrap();
2501 prop_assert_eq!(parsed.precision, precision);
2502
2503 let round_trip = parsed.to_string();
2505 let expected_value = format!("{integral}.{fractional_str}");
2506 prop_assert_eq!(round_trip, expected_value);
2507 }
2508
2509 #[rstest]
2511 fn prop_quantity_arithmetic_bounds(
2512 a in quantity_value_strategy(),
2513 b in quantity_value_strategy(),
2514 precision in precision_strategy()
2515 ) {
2516 let q_a = Quantity::new(a, precision);
2517 let q_b = Quantity::new(b, precision);
2518
2519 let sum_f64 = q_a.as_f64() + q_b.as_f64();
2521 if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
2522 let sum = q_a + q_b;
2523 prop_assert!(sum.as_f64().is_finite());
2524 prop_assert!(!sum.is_undefined());
2525 }
2526
2527 let diff_f64 = q_a.as_f64() - q_b.as_f64();
2529 if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
2530 let diff = q_a - q_b;
2531 prop_assert!(diff.as_f64().is_finite());
2532 prop_assert!(!diff.is_undefined());
2533 }
2534 }
2535
2536 #[rstest]
2538 fn prop_quantity_multiplication_non_negative(
2539 a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2540 b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
2541 precision in precision_strategy()
2542 ) {
2543 let q_a = Quantity::new(a, precision);
2544 let q_b = Quantity::new(b, precision);
2545
2546 let raw_product_check = q_a.raw.checked_mul(q_b.raw);
2548
2549 if let Some(raw_product) = raw_product_check {
2550 let scaled_raw = raw_product / FIXED_SCALAR_RAW;
2552 if scaled_raw <= QUANTITY_RAW_MAX {
2553 let product = q_a * q_b;
2555 prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
2556 }
2557 }
2558 }
2559
2560 #[rstest]
2562 fn prop_quantity_zero_addition_identity(
2563 value in quantity_value_strategy(),
2564 precision in precision_strategy()
2565 ) {
2566 let q = Quantity::new(value, precision);
2567 let zero = Quantity::zero(precision);
2568
2569 prop_assert_eq!(q + zero, q);
2571 prop_assert_eq!(zero + q, q);
2572 }
2573 }
2574
2575 proptest! {
2576 #[rstest]
2578 fn prop_quantity_as_decimal_preserves_precision(
2579 (raw, precision) in raw_for_precision_strategy()
2580 ) {
2581 prop_assume!(decimal_compatible(raw, precision));
2582 let quantity = Quantity::from_raw(raw, precision);
2583 let decimal = quantity.as_decimal();
2584 prop_assert_eq!(decimal.scale(), u32::from(precision));
2585 }
2586
2587 #[rstest]
2589 fn prop_quantity_as_decimal_matches_display(
2590 (raw, precision) in raw_for_precision_strategy()
2591 ) {
2592 prop_assume!(decimal_compatible(raw, precision));
2593 let quantity = Quantity::from_raw(raw, precision);
2594 let display_str = format!("{quantity}");
2595 let decimal_str = quantity.as_decimal().to_string();
2596 prop_assert_eq!(display_str, decimal_str);
2597 }
2598
2599 #[rstest]
2601 fn prop_quantity_from_decimal_roundtrip(
2602 (raw, precision) in raw_for_precision_strategy()
2603 ) {
2604 prop_assume!(decimal_compatible(raw, precision));
2605 let original = Quantity::from_raw(raw, precision);
2606 let decimal = original.as_decimal();
2607 let reconstructed = Quantity::from_decimal(decimal).unwrap();
2608 prop_assert_eq!(original.raw, reconstructed.raw);
2609 prop_assert_eq!(original.precision, reconstructed.precision);
2610 }
2611
2612 #[rstest]
2614 fn prop_quantity_from_raw_round_trip(
2615 (raw, precision) in raw_for_precision_strategy()
2616 ) {
2617 let quantity = Quantity::from_raw(raw, precision);
2618 prop_assert_eq!(quantity.raw, raw);
2619 prop_assert_eq!(quantity.precision, precision);
2620 }
2621 }
2622}