1use std::{
48 cmp::Ordering,
49 fmt::{Debug, Display},
50 hash::{Hash, Hasher},
51 ops::{Add, Deref, Div, Mul, Neg, Sub},
52 str::FromStr,
53};
54
55use nautilus_core::{
56 correctness::{
57 CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
58 check_in_range_inclusive_f64,
59 },
60 string::formatting::Separable,
61};
62use rust_decimal::Decimal;
63use serde::{Deserialize, Deserializer, Serialize};
64
65#[cfg(feature = "defi")]
66use super::fixed::compare_raw_signed;
67use super::fixed::{
68 FIXED_PRECISION, FIXED_SCALAR, canonical_raw, check_fixed_precision, format_scaled_i128,
69 mantissa_exponent_to_fixed_i128, mantissa_exponent_to_raw_checked, parse_decimal_mantissa,
70 raw_scales_match, scaled_raw_to_decimal,
71};
72#[cfg(feature = "high-precision")]
73use super::fixed::{PRECISION_DIFF_SCALAR, f64_to_fixed_i128, fixed_i128_to_f64};
74#[cfg(not(feature = "high-precision"))]
75use super::fixed::{f64_to_fixed_i64, fixed_i64_to_f64};
76#[cfg(feature = "defi")]
77use crate::types::fixed::MAX_FLOAT_PRECISION;
78
79#[cfg(feature = "high-precision")]
87pub type PriceRaw = i128;
88
89#[cfg(not(feature = "high-precision"))]
90pub type PriceRaw = i64;
91
92#[unsafe(no_mangle)]
103#[allow(unsafe_code)]
104pub static PRICE_RAW_MAX: PriceRaw = (PRICE_MAX as PriceRaw) * (FIXED_SCALAR as PriceRaw);
105
106#[unsafe(no_mangle)]
115#[allow(unsafe_code)]
116pub static PRICE_RAW_MIN: PriceRaw = (PRICE_MIN as PriceRaw) * (FIXED_SCALAR as PriceRaw);
117
118pub const PRICE_UNDEF: PriceRaw = PriceRaw::MAX;
120
121pub const PRICE_ERROR: PriceRaw = PriceRaw::MIN;
123
124#[cfg(feature = "high-precision")]
130pub const PRICE_MAX: f64 = 17_014_118_346_046.0;
131
132#[cfg(not(feature = "high-precision"))]
133pub const PRICE_MAX: f64 = 9_223_372_036.0;
135
136#[cfg(feature = "high-precision")]
141pub const PRICE_MIN: f64 = -17_014_118_346_046.0;
143
144#[cfg(not(feature = "high-precision"))]
145pub const PRICE_MIN: f64 = -9_223_372_036.0;
147
148pub const ERROR_PRICE: Price = Price {
153 raw: 0,
154 precision: 255,
155};
156
157#[repr(C)]
168#[derive(Clone, Copy, Default, Eq)]
169#[cfg_attr(
170 feature = "python",
171 pyo3::pyclass(module = "nautilus_trader.model", frozen, from_py_object)
172)]
173#[cfg_attr(
174 feature = "python",
175 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
176)]
177pub struct Price {
178 pub(crate) raw: PriceRaw,
179 pub precision: u8,
181}
182
183impl Price {
184 pub fn new_checked(value: f64, precision: u8) -> CorrectnessResult<Self> {
196 check_in_range_inclusive_f64(value, PRICE_MIN, PRICE_MAX, "value")?;
197
198 #[cfg(feature = "defi")]
199 if precision > MAX_FLOAT_PRECISION {
200 return Err(CorrectnessError::PredicateViolation {
202 message: format!(
203 "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Price::from_wei()` for wei values instead"
204 ),
205 });
206 }
207
208 check_fixed_precision(precision)?;
209
210 #[cfg(feature = "high-precision")]
211 let raw = f64_to_fixed_i128(value, precision);
212
213 #[cfg(not(feature = "high-precision"))]
214 let raw = f64_to_fixed_i64(value, precision);
215
216 Ok(Self { raw, precision })
217 }
218
219 #[must_use]
225 pub fn new(value: f64, precision: u8) -> Self {
226 Self::new_checked(value, precision).expect_display(FAILED)
227 }
228
229 #[must_use]
236 pub fn from_raw(raw: PriceRaw, precision: u8) -> Self {
237 assert!(
238 raw == PRICE_ERROR
239 || raw == PRICE_UNDEF
240 || (raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX),
241 "`raw` value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
242 );
243
244 if raw == PRICE_UNDEF {
245 assert!(
246 precision == 0,
247 "`precision` must be 0 when `raw` is PRICE_UNDEF"
248 );
249 }
250 check_fixed_precision(precision).expect_display(FAILED);
251
252 Self { raw, precision }
261 }
262
263 pub fn from_raw_checked(raw: PriceRaw, precision: u8) -> CorrectnessResult<Self> {
274 if raw == PRICE_UNDEF && precision != 0 {
275 return Err(CorrectnessError::PredicateViolation {
276 message: "`precision` must be 0 when `raw` is PRICE_UNDEF".to_string(),
277 });
278 }
279
280 if raw != PRICE_ERROR && raw != PRICE_UNDEF && (raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX)
281 {
282 return Err(CorrectnessError::PredicateViolation {
283 message: format!(
284 "raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
285 ),
286 });
287 }
288
289 check_fixed_precision(precision)?;
290
291 Ok(Self { raw, precision })
292 }
293
294 #[must_use]
300 pub fn zero(precision: u8) -> Self {
301 check_fixed_precision(precision).expect_display(FAILED);
302 Self { raw: 0, precision }
303 }
304
305 #[must_use]
311 pub fn max(precision: u8) -> Self {
312 check_fixed_precision(precision).expect_display(FAILED);
313 Self {
314 raw: PRICE_RAW_MAX,
315 precision,
316 }
317 }
318
319 #[must_use]
325 pub fn min(precision: u8) -> Self {
326 check_fixed_precision(precision).expect_display(FAILED);
327 Self {
328 raw: PRICE_RAW_MIN,
329 precision,
330 }
331 }
332
333 #[must_use]
341 pub fn checked_add(self, rhs: Self) -> Option<Self> {
342 if self.is_sentinel() || rhs.is_sentinel() {
343 return None;
344 }
345
346 if !raw_scales_match(self.precision, rhs.precision) {
347 return None;
348 }
349
350 let raw = self.raw.checked_add(rhs.raw)?;
351 if raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX {
352 return None;
353 }
354
355 Some(Self {
356 raw,
357 precision: self.precision.max(rhs.precision),
358 })
359 }
360
361 #[must_use]
369 pub fn checked_sub(self, rhs: Self) -> Option<Self> {
370 if self.is_sentinel() || rhs.is_sentinel() {
371 return None;
372 }
373
374 if !raw_scales_match(self.precision, rhs.precision) {
375 return None;
376 }
377
378 let raw = self.raw.checked_sub(rhs.raw)?;
379 if raw < PRICE_RAW_MIN || raw > PRICE_RAW_MAX {
380 return None;
381 }
382
383 Some(Self {
384 raw,
385 precision: self.precision.max(rhs.precision),
386 })
387 }
388
389 #[inline]
390 fn is_sentinel(self) -> bool {
391 self.raw == PRICE_UNDEF || self.raw == PRICE_ERROR || self.precision == u8::MAX
395 }
396
397 #[must_use]
399 pub fn is_undefined(&self) -> bool {
400 self.raw == PRICE_UNDEF
401 }
402
403 #[must_use]
405 #[inline]
406 pub fn is_error(&self) -> bool {
407 self.raw == PRICE_ERROR
408 }
409
410 #[must_use]
423 #[inline]
424 pub const fn raw(&self) -> PriceRaw {
425 self.raw
426 }
427
428 #[must_use]
430 #[inline]
431 pub fn is_zero(&self) -> bool {
432 self.raw == 0
433 }
434
435 #[must_use]
437 #[inline]
438 pub fn is_positive(&self) -> bool {
439 self.raw != PRICE_UNDEF && self.raw > 0
440 }
441
442 #[must_use]
444 #[inline]
445 pub fn is_negative(&self) -> bool {
446 self.raw != PRICE_UNDEF && self.raw < 0
447 }
448
449 #[cfg(feature = "high-precision")]
450 #[must_use]
456 pub fn as_f64(&self) -> f64 {
457 #[cfg(feature = "defi")]
458 assert!(
459 self.precision <= MAX_FLOAT_PRECISION,
460 "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
461 );
462
463 fixed_i128_to_f64(self.raw)
464 }
465
466 #[cfg(not(feature = "high-precision"))]
467 #[must_use]
469 pub fn as_f64(&self) -> f64 {
470 fixed_i64_to_f64(self.raw)
471 }
472
473 #[must_use]
475 pub fn as_decimal(&self) -> Decimal {
476 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
478 let rescaled_raw = self.raw / PriceRaw::pow(10, u32::from(precision_diff));
479 #[allow(
480 clippy::unnecessary_cast,
481 clippy::cast_lossless,
482 reason = "cast is real when PriceRaw is i64, no-op when i128"
483 )]
484 scaled_raw_to_decimal(rescaled_raw as i128, self.precision)
485 }
486
487 #[must_use]
489 pub fn to_formatted_string(&self) -> String {
490 format!("{self}").separate_with_underscores()
491 }
492
493 fn raw_at_precision(&self) -> i128 {
494 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
495 let rescaled_raw = self.raw / PriceRaw::pow(10, u32::from(precision_diff));
496 Self::raw_as_i128(rescaled_raw)
497 }
498
499 fn raw_as_i128(raw: PriceRaw) -> i128 {
500 #[allow(
501 clippy::useless_conversion,
502 reason = "i128::from is a widening conversion when PriceRaw is i64"
503 )]
504 i128::from(raw)
505 }
506
507 pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> CorrectnessResult<Self> {
519 let exponent = -(decimal.scale() as i8);
520 let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
521
522 #[allow(
523 clippy::useless_conversion,
524 reason = "i128 to PriceRaw is real when not high-precision"
525 )]
526 let raw: PriceRaw =
527 raw_i128
528 .try_into()
529 .map_err(|_| CorrectnessError::PredicateViolation {
530 message: format!(
531 "Decimal value exceeds PriceRaw range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
532 ),
533 })?;
534
535 if !(raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX) {
536 return Err(CorrectnessError::PredicateViolation {
537 message: format!(
538 "Raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
539 ),
540 });
541 }
542
543 Ok(Self { raw, precision })
544 }
545
546 pub fn from_decimal(decimal: Decimal) -> CorrectnessResult<Self> {
558 let precision = decimal.scale() as u8;
559 Self::from_decimal_dp(decimal, precision)
560 }
561
562 #[must_use]
571 pub fn from_mantissa_exponent(mantissa: i64, exponent: i8, precision: u8) -> Self {
572 check_fixed_precision(precision).expect_display(FAILED);
573
574 if mantissa == 0 {
575 return Self { raw: 0, precision };
576 }
577
578 let raw_i128 = mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, precision)
579 .expect("Overflow in Price::from_mantissa_exponent");
580
581 #[allow(
582 clippy::useless_conversion,
583 reason = "i128 to PriceRaw is real when not high-precision"
584 )]
585 let raw: PriceRaw = raw_i128
586 .try_into()
587 .expect("Raw value exceeds PriceRaw range in Price::from_mantissa_exponent");
588 assert!(
589 raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX,
590 "`raw` value {raw} exceeded bounds [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
591 );
592
593 Self { raw, precision }
594 }
595
596 pub fn from_mantissa_exponent_checked(
603 mantissa: i64,
604 exponent: i8,
605 precision: u8,
606 ) -> CorrectnessResult<Self> {
607 let raw = mantissa_exponent_to_raw_checked::<PriceRaw>(
608 i128::from(mantissa),
609 exponent,
610 precision,
611 "Price::from_mantissa_exponent",
612 "PriceRaw",
613 "Price",
614 )?;
615
616 Self::from_raw_checked(raw, precision)
617 }
618}
619
620impl FromStr for Price {
621 type Err = String;
622
623 fn from_str(value: &str) -> Result<Self, Self::Err> {
624 let clean_value = value.replace('_', "");
625
626 if clean_value.contains('e') || clean_value.contains('E') {
627 let decimal = Decimal::from_scientific(&clean_value)
628 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?;
629 let precision = decimal.scale() as u8;
630 return Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string());
631 }
632
633 let (mantissa, precision) = parse_decimal_mantissa(&clean_value)
634 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?;
635 let exponent = -i8::try_from(precision).map_err(|e| e.to_string())?;
636 let raw = mantissa_exponent_to_raw_checked::<PriceRaw>(
637 mantissa,
638 exponent,
639 precision,
640 "Price::from_str",
641 "PriceRaw",
642 "Price",
643 )
644 .map_err(|e| e.to_string())?;
645 Self::from_raw_checked(raw, precision).map_err(|e| e.to_string())
646 }
647}
648
649impl<T: AsRef<str>> From<T> for Price {
650 fn from(value: T) -> Self {
651 Self::from_str(value.as_ref()).expect(FAILED)
652 }
653}
654
655impl From<Price> for f64 {
656 fn from(price: Price) -> Self {
657 price.as_f64()
658 }
659}
660
661impl From<&Price> for f64 {
662 fn from(price: &Price) -> Self {
663 price.as_f64()
664 }
665}
666
667impl From<Price> for Decimal {
668 fn from(value: Price) -> Self {
669 value.as_decimal()
670 }
671}
672
673impl From<&Price> for Decimal {
674 fn from(value: &Price) -> Self {
675 value.as_decimal()
676 }
677}
678
679impl Hash for Price {
680 fn hash<H: Hasher>(&self, state: &mut H) {
681 self.raw.signum().hash(state);
682 if self.raw == PRICE_ERROR {
683 self.raw.hash(state);
684 } else {
685 canonical_raw(self.raw.unsigned_abs(), self.precision).hash(state);
686 }
687 }
688}
689
690impl PartialEq for Price {
691 #[inline]
692 fn eq(&self, other: &Self) -> bool {
693 self.cmp(other) == Ordering::Equal
694 }
695}
696
697impl PartialOrd for Price {
698 #[inline]
699 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
700 Some(self.cmp(other))
701 }
702}
703
704impl Ord for Price {
705 #[inline]
706 fn cmp(&self, other: &Self) -> Ordering {
707 if self.raw == PRICE_ERROR || other.raw == PRICE_ERROR {
709 return self.raw.cmp(&other.raw);
710 }
711
712 #[cfg(feature = "defi")]
713 {
714 compare_raw_signed(self.raw, self.precision, other.raw, other.precision)
715 }
716
717 #[cfg(not(feature = "defi"))]
718 {
719 self.raw.cmp(&other.raw)
720 }
721 }
722}
723
724impl Deref for Price {
725 type Target = PriceRaw;
726
727 fn deref(&self) -> &Self::Target {
728 &self.raw
729 }
730}
731
732impl Neg for Price {
733 type Output = Self;
734 fn neg(self) -> Self::Output {
735 if self.raw == PRICE_ERROR || self.raw == PRICE_UNDEF {
737 return self;
738 }
739 Self {
740 raw: -self.raw,
741 precision: self.precision,
742 }
743 }
744}
745
746impl Add for Price {
747 type Output = Self;
748 #[inline]
749 fn add(self, rhs: Self) -> Self::Output {
750 assert!(
751 raw_scales_match(self.precision, rhs.precision),
752 "Cannot add `Price` values with mismatched decimal scales"
753 );
754 Self {
755 raw: self
756 .raw
757 .checked_add(rhs.raw)
758 .expect("Overflow occurred when adding `Price`"),
759 precision: self.precision.max(rhs.precision),
760 }
761 }
762}
763
764impl Sub for Price {
765 type Output = Self;
766 #[inline]
767 fn sub(self, rhs: Self) -> Self::Output {
768 assert!(
769 raw_scales_match(self.precision, rhs.precision),
770 "Cannot subtract `Price` values with mismatched decimal scales"
771 );
772 Self {
773 raw: self
774 .raw
775 .checked_sub(rhs.raw)
776 .expect("Underflow occurred when subtracting `Price`"),
777 precision: self.precision.max(rhs.precision),
778 }
779 }
780}
781
782impl Add<Decimal> for Price {
783 type Output = Decimal;
784 fn add(self, rhs: Decimal) -> Self::Output {
785 self.as_decimal() + rhs
786 }
787}
788
789impl Sub<Decimal> for Price {
790 type Output = Decimal;
791 fn sub(self, rhs: Decimal) -> Self::Output {
792 self.as_decimal() - rhs
793 }
794}
795
796impl Mul<Decimal> for Price {
797 type Output = Decimal;
798 fn mul(self, rhs: Decimal) -> Self::Output {
799 self.as_decimal() * rhs
800 }
801}
802
803impl Div<Decimal> for Price {
804 type Output = Decimal;
805 fn div(self, rhs: Decimal) -> Self::Output {
806 self.as_decimal() / rhs
807 }
808}
809
810impl Add<f64> for Price {
811 type Output = f64;
812 fn add(self, rhs: f64) -> Self::Output {
813 self.as_f64() + rhs
814 }
815}
816
817impl Sub<f64> for Price {
818 type Output = f64;
819 fn sub(self, rhs: f64) -> Self::Output {
820 self.as_f64() - rhs
821 }
822}
823
824impl Mul<f64> for Price {
825 type Output = f64;
826 fn mul(self, rhs: f64) -> Self::Output {
827 self.as_f64() * rhs
828 }
829}
830
831impl Div<f64> for Price {
832 type Output = f64;
833 fn div(self, rhs: f64) -> Self::Output {
834 self.as_f64() / rhs
835 }
836}
837
838impl Debug for Price {
839 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
840 write!(f, "{}({self})", stringify!(Price))
841 }
842}
843
844impl Display for Price {
845 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846 if self.precision == ERROR_PRICE.precision {
847 return write!(f, "{}", self.raw);
848 }
849
850 write!(
851 f,
852 "{}",
853 format_scaled_i128(self.raw_at_precision(), self.precision),
854 )
855 }
856}
857
858impl Serialize for Price {
859 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
860 where
861 S: serde::Serializer,
862 {
863 serializer.serialize_str(&self.to_string())
864 }
865}
866
867impl<'de> Deserialize<'de> for Price {
868 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
869 where
870 D: Deserializer<'de>,
871 {
872 let price_str: std::borrow::Cow<'de, str> = Deserialize::deserialize(deserializer)?;
873 Self::from_str(price_str.as_ref()).map_err(serde::de::Error::custom)
874 }
875}
876
877pub fn check_positive_price(value: Price, param: &str) -> CorrectnessResult<()> {
883 if value.raw == PRICE_UNDEF {
884 return Err(CorrectnessError::InvalidValue {
885 param: param.to_string(),
886 value: "PRICE_UNDEF".to_string(),
887 type_name: "`Price`",
888 });
889 }
890
891 if !value.is_positive() {
892 return Err(CorrectnessError::NotPositive {
893 param: param.to_string(),
894 value: value.to_string(),
895 type_name: "`Price`",
896 });
897 }
898 Ok(())
899}
900
901#[cfg(feature = "high-precision")]
902#[must_use]
905pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
906 PriceRaw::from(value) * PRECISION_DIFF_SCALAR as PriceRaw
907}
908
909#[cfg(not(feature = "high-precision"))]
910#[must_use]
911pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
912 value
913}
914
915#[cfg(test)]
916mod tests {
917 use nautilus_core::{approx_eq, correctness::CorrectnessError};
918 use rstest::rstest;
919 use rust_decimal_macros::dec;
920
921 use super::*;
922
923 #[cfg(feature = "high-precision")]
924 #[rstest]
925 #[case(PRICE_RAW_MAX, dec!(17014118346046))]
926 #[case(PRICE_RAW_MIN, dec!(-17014118346046))]
927 fn test_as_decimal_above_decimal_mantissa(#[case] raw: PriceRaw, #[case] expected: Decimal) {
928 let price = Price::from_raw(raw, 16);
931
932 assert_eq!(price.as_decimal(), expected);
933 }
934
935 #[rstest]
936 fn test_error_sentinel_formatting() {
937 assert_eq!(ERROR_PRICE.to_string(), "0");
938 assert_eq!(format!("{ERROR_PRICE:?}"), "Price(0)");
939 assert_eq!(ERROR_PRICE.to_formatted_string(), "0");
940 }
941
942 #[rstest]
943 fn test_error_sentinel_comparisons_preserve_raw_zero_semantics() {
944 let zero = Price::zero(FIXED_PRECISION);
945 let positive = Price::from_mantissa_exponent(1, 0, FIXED_PRECISION);
946 let negative = -positive;
947
948 assert_eq!(ERROR_PRICE, zero);
949 assert_eq!(ERROR_PRICE.cmp(&zero), Ordering::Equal);
950 assert_eq!(ERROR_PRICE.cmp(&positive), Ordering::Less);
951 assert_eq!(negative.cmp(&ERROR_PRICE), Ordering::Less);
952 }
953
954 #[rstest]
955 fn test_extreme_prices_round_trip_through_raw() {
956 let max = Price::new(PRICE_MAX, 0);
959 let min = Price::new(PRICE_MIN, 0);
960
961 assert_eq!(max.raw, PRICE_RAW_MAX);
962 assert_eq!(min.raw, PRICE_RAW_MIN);
963 assert!(Price::from_raw_checked(max.raw, 0).is_ok());
964 assert!(Price::from_raw_checked(min.raw, 0).is_ok());
965 }
966
967 #[rstest]
968 #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
969 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 50")]
970 fn test_invalid_precision_new() {
971 let _ = Price::new(1.0, 50);
973 }
974
975 #[rstest]
976 #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
977 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 50")]
978 fn test_invalid_precision_new() {
979 let _ = Price::new(1.0, 50);
981 }
982
983 #[rstest]
984 #[cfg(not(feature = "defi"))]
985 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
986 fn test_invalid_precision_from_raw() {
987 let _ = Price::from_raw(1, FIXED_PRECISION + 1);
989 }
990
991 #[rstest]
992 #[cfg(not(feature = "defi"))]
993 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
994 fn test_invalid_precision_max() {
995 let _ = Price::max(FIXED_PRECISION + 1);
997 }
998
999 #[rstest]
1000 #[cfg(not(feature = "defi"))]
1001 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
1002 fn test_invalid_precision_min() {
1003 let _ = Price::min(FIXED_PRECISION + 1);
1005 }
1006
1007 #[rstest]
1008 #[cfg(not(feature = "defi"))]
1009 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
1010 fn test_invalid_precision_zero() {
1011 let _ = Price::zero(FIXED_PRECISION + 1);
1013 }
1014
1015 #[rstest]
1016 #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
1017 fn test_max_value_exceeded() {
1018 let _ = Price::new(PRICE_MAX + 0.1, FIXED_PRECISION);
1019 }
1020
1021 #[rstest]
1022 #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
1023 fn test_min_value_exceeded() {
1024 let _ = Price::new(PRICE_MIN - 0.1, FIXED_PRECISION);
1025 }
1026
1027 #[rstest]
1028 fn test_is_positive_ok() {
1029 let price = Price::new(42.0, 2);
1031 assert!(price.is_positive());
1032
1033 check_positive_price(price, "price").unwrap();
1035 }
1036
1037 #[rstest]
1038 fn test_is_positive_rejects_non_positive() {
1039 let zero = Price::zero(2);
1041 let error = check_positive_price(zero, "price").unwrap_err();
1042
1043 assert_eq!(
1044 error,
1045 CorrectnessError::NotPositive {
1046 param: "price".to_string(),
1047 value: "0.00".to_string(),
1048 type_name: "`Price`",
1049 }
1050 );
1051 assert_eq!(
1052 error.to_string(),
1053 "invalid `Price` for 'price' not positive, was 0.00"
1054 );
1055 }
1056
1057 #[rstest]
1058 fn test_is_positive_rejects_undefined() {
1059 let undef = Price::from_raw(PRICE_UNDEF, 0);
1061 let error = check_positive_price(undef, "price").unwrap_err();
1062
1063 assert_eq!(
1064 error,
1065 CorrectnessError::InvalidValue {
1066 param: "price".to_string(),
1067 value: "PRICE_UNDEF".to_string(),
1068 type_name: "`Price`",
1069 }
1070 );
1071 assert_eq!(
1072 error.to_string(),
1073 "invalid `Price` for 'price', was PRICE_UNDEF"
1074 );
1075 }
1076
1077 #[rstest]
1078 fn test_construction() {
1079 let price = Price::new_checked(1.23456, 4);
1080 assert!(price.is_ok());
1081 let price = price.unwrap();
1082 assert_eq!(price.precision, 4);
1083 assert!(approx_eq!(f64, price.as_f64(), 1.23456, epsilon = 0.0001));
1084 }
1085
1086 #[rstest]
1087 fn test_negative_price_in_range() {
1088 let neg_price = Price::new(PRICE_MIN / 2.0, FIXED_PRECISION);
1090 assert!(neg_price.raw < 0);
1091 }
1092
1093 #[rstest]
1094 fn test_new_checked() {
1095 assert!(Price::new_checked(1.0, FIXED_PRECISION).is_ok());
1097 assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
1098 assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
1099 }
1100
1101 #[rstest]
1102 fn test_new_checked_returns_typed_error_with_stable_display() {
1103 let error = Price::new_checked(PRICE_MAX + 1.0, FIXED_PRECISION).unwrap_err();
1104
1105 assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
1106 assert_eq!(
1107 error.to_string(),
1108 format!(
1109 "invalid f64 for 'value' not in range [{PRICE_MIN}, {PRICE_MAX}], was {}",
1110 PRICE_MAX + 1.0
1111 )
1112 );
1113 }
1114
1115 #[rstest]
1116 fn test_from_raw_checked_returns_typed_error_with_stable_display() {
1117 let error = Price::from_raw_checked(PRICE_UNDEF, 3).unwrap_err();
1118
1119 assert_eq!(
1120 error,
1121 CorrectnessError::PredicateViolation {
1122 message: "`precision` must be 0 when `raw` is PRICE_UNDEF".to_string(),
1123 }
1124 );
1125 assert_eq!(
1126 error.to_string(),
1127 "`precision` must be 0 when `raw` is PRICE_UNDEF"
1128 );
1129 }
1130
1131 #[rstest]
1132 #[case::below_minimum(PRICE_RAW_MIN - 1)]
1133 #[case::above_maximum(PRICE_RAW_MAX + 1)]
1134 fn test_from_raw_checked_rejects_out_of_range_value(#[case] raw: PriceRaw) {
1135 let error = Price::from_raw_checked(raw, 0).unwrap_err();
1136
1137 assert_eq!(
1138 error,
1139 CorrectnessError::PredicateViolation {
1140 message: format!(
1141 "raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
1142 ),
1143 }
1144 );
1145 }
1146
1147 #[rstest]
1148 #[should_panic(expected = "outside valid range")]
1149 fn test_from_raw_out_of_range_panics() {
1150 let _ = Price::from_raw(PRICE_RAW_MAX + 1, 0);
1151 }
1152
1153 #[rstest]
1154 fn test_from_raw() {
1155 let raw = 100 * FIXED_SCALAR as PriceRaw;
1156 let price = Price::from_raw(raw, 2);
1157 assert_eq!(price.raw, raw);
1158 assert_eq!(price.precision, 2);
1159 }
1160
1161 #[rstest]
1162 fn test_zero_constructor() {
1163 let zero = Price::zero(3);
1164 assert!(zero.is_zero());
1165 assert_eq!(zero.precision, 3);
1166 }
1167
1168 #[rstest]
1169 fn test_max_constructor() {
1170 let max = Price::max(4);
1171 assert_eq!(max.raw, PRICE_RAW_MAX);
1172 assert_eq!(max.precision, 4);
1173 }
1174
1175 #[rstest]
1176 fn test_min_constructor() {
1177 let min = Price::min(4);
1178 assert_eq!(min.raw, PRICE_RAW_MIN);
1179 assert_eq!(min.precision, 4);
1180 }
1181
1182 #[rstest]
1183 fn test_nan_validation() {
1184 assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
1185 }
1186
1187 #[rstest]
1188 fn test_infinity_validation() {
1189 assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
1190 assert!(Price::new_checked(f64::NEG_INFINITY, FIXED_PRECISION).is_err());
1191 }
1192
1193 #[rstest]
1194 fn test_special_values() {
1195 let zero = Price::zero(5);
1196 assert!(zero.is_zero());
1197 assert_eq!(zero.to_string(), "0.00000");
1198
1199 let undef = Price::from_raw(PRICE_UNDEF, 0);
1200 assert!(undef.is_undefined());
1201
1202 let error = ERROR_PRICE;
1203 assert_eq!(error.precision, 255);
1204 }
1205
1206 #[rstest]
1207 fn test_string_parsing() {
1208 let price: Price = "123.456".into();
1209 assert_eq!(price.precision, 3);
1210 assert_eq!(price, Price::from("123.456"));
1211 }
1212
1213 #[rstest]
1214 #[case(PRICE_RAW_MIN)]
1215 #[case(PRICE_RAW_MAX)]
1216 fn test_from_str_raw_limits(#[case] raw: PriceRaw) {
1217 let original = Price::from_raw(raw, FIXED_PRECISION);
1218 let decoded = original.to_string().parse::<Price>().unwrap();
1219
1220 assert_eq!(decoded.raw, raw);
1221 assert_eq!(decoded.precision, FIXED_PRECISION);
1222 }
1223
1224 #[rstest]
1225 fn test_negative_price_from_str() {
1226 let price: Price = "-123.45".parse().unwrap();
1227 assert_eq!(price.precision, 2);
1228 assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-9));
1229 }
1230
1231 #[rstest]
1232 fn test_string_parsing_errors() {
1233 assert!(Price::from_str("invalid").is_err());
1234 }
1235
1236 #[rstest]
1237 #[case("1e7", 0, 10_000_000.0)]
1238 #[case("1.5e3", 0, 1_500.0)]
1239 #[case("1.234e-2", 5, 0.01234)]
1240 #[case("5E-3", 3, 0.005)]
1241 fn test_from_str_scientific_notation(
1242 #[case] input: &str,
1243 #[case] expected_precision: u8,
1244 #[case] expected_value: f64,
1245 ) {
1246 let price = Price::from_str(input).unwrap();
1247 assert_eq!(price.precision, expected_precision);
1248 assert!(approx_eq!(
1249 f64,
1250 price.as_f64(),
1251 expected_value,
1252 epsilon = 1e-10
1253 ));
1254 }
1255
1256 #[rstest]
1257 #[case("1_234.56", 2, 1234.56)]
1258 #[case("1000000", 0, 1_000_000.0)]
1259 #[case("99_999.999_99", 5, 99_999.999_99)]
1260 fn test_from_str_with_underscores(
1261 #[case] input: &str,
1262 #[case] expected_precision: u8,
1263 #[case] expected_value: f64,
1264 ) {
1265 let price = Price::from_str(input).unwrap();
1266 assert_eq!(price.precision, expected_precision);
1267 assert!(approx_eq!(
1268 f64,
1269 price.as_f64(),
1270 expected_value,
1271 epsilon = 1e-10
1272 ));
1273 }
1274
1275 #[rstest]
1276 fn test_from_decimal_dp_preservation() {
1277 let decimal = dec!(123.456789);
1279 let price = Price::from_decimal_dp(decimal, 6).unwrap();
1280 assert_eq!(price.precision, 6);
1281 assert!(approx_eq!(
1282 f64,
1283 price.as_f64(),
1284 123.456_789,
1285 epsilon = 1e-10
1286 ));
1287
1288 let expected_raw = 123_456_789 * 10_i64.pow(u32::from(FIXED_PRECISION - 6));
1290 assert_eq!(price.raw, PriceRaw::from(expected_raw));
1291 }
1292
1293 #[rstest]
1294 fn test_from_decimal_dp_rounding() {
1295 let decimal = dec!(1.005);
1297 let price = Price::from_decimal_dp(decimal, 2).unwrap();
1298 assert_eq!(price.as_f64(), 1.0); let decimal = dec!(1.015);
1301 let price = Price::from_decimal_dp(decimal, 2).unwrap();
1302 assert_eq!(price.as_f64(), 1.02); }
1304
1305 #[rstest]
1306 fn test_from_decimal_infers_precision() {
1307 let decimal = dec!(123.456);
1309 let price = Price::from_decimal(decimal).unwrap();
1310 assert_eq!(price.precision, 3);
1311 assert!(approx_eq!(f64, price.as_f64(), 123.456, epsilon = 1e-10));
1312
1313 let decimal = dec!(100);
1315 let price = Price::from_decimal(decimal).unwrap();
1316 assert_eq!(price.precision, 0);
1317 assert_eq!(price.as_f64(), 100.0);
1318
1319 let decimal = dec!(1.23456789);
1321 let price = Price::from_decimal(decimal).unwrap();
1322 assert_eq!(price.precision, 8);
1323 assert!(approx_eq!(
1324 f64,
1325 price.as_f64(),
1326 1.234_567_89,
1327 epsilon = 1e-10
1328 ));
1329 }
1330
1331 #[rstest]
1332 fn test_from_decimal_trailing_zeros() {
1333 let decimal = dec!(1.230);
1335 assert_eq!(decimal.scale(), 3); let price = Price::from_decimal(decimal).unwrap();
1339 assert_eq!(price.precision, 3);
1340 assert!(approx_eq!(f64, price.as_f64(), 1.23, epsilon = 1e-10));
1341
1342 let normalized = decimal.normalize();
1344 assert_eq!(normalized.scale(), 2);
1345 let price_normalized = Price::from_decimal(normalized).unwrap();
1346 assert_eq!(price_normalized.precision, 2);
1347 }
1348
1349 #[rstest]
1350 #[case("1.00", 2)]
1351 #[case("1.0", 1)]
1352 #[case("1.000", 3)]
1353 #[case("100.00", 2)]
1354 #[case("0.10", 2)]
1355 #[case("0.100", 3)]
1356 fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
1357 let price = Price::from_str(input).unwrap();
1358 assert_eq!(price.precision, expected_precision);
1359 }
1360
1361 #[rstest]
1362 fn test_from_decimal_excessive_precision_inference() {
1363 let decimal = dec!(1.1234567890123456789012345678);
1366
1367 if decimal.scale() > u32::from(FIXED_PRECISION) {
1369 assert!(Price::from_decimal(decimal).is_err());
1370 }
1371 }
1372
1373 #[rstest]
1374 fn test_from_decimal_dp_rejects_raw_between_price_and_raw_bounds() {
1375 let at_max = Decimal::try_from(PRICE_MAX).unwrap();
1376 let above_max = at_max + dec!(0.5);
1377 let expected_raw = PRICE_RAW_MAX + 5 * PriceRaw::pow(10, u32::from(FIXED_PRECISION - 1));
1378
1379 let error = Price::from_decimal_dp(above_max, 1).unwrap_err();
1380
1381 assert_eq!(
1382 error.to_string(),
1383 format!(
1384 "Raw value {expected_raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
1385 )
1386 );
1387 assert_eq!(
1388 Price::from_decimal_dp(at_max, 1).unwrap().raw,
1389 PRICE_RAW_MAX
1390 );
1391 }
1392
1393 #[rstest]
1394 fn test_from_decimal_dp_out_of_range_returns_typed_error_with_stable_display() {
1395 let huge = Decimal::from_str("99999999999999999999.99").unwrap();
1396 let error = Price::from_decimal_dp(huge, 2).unwrap_err();
1397 match error {
1398 CorrectnessError::PredicateViolation { ref message } => {
1399 assert!(
1400 message.contains("PriceRaw range") || message.contains("for Price"),
1401 "unexpected message: {message:?}",
1402 );
1403 }
1404 _ => panic!("expected PredicateViolation, was {error:?}"),
1405 }
1406 }
1407
1408 #[rstest]
1409 fn test_from_decimal_negative_price() {
1410 let decimal = dec!(-123.45);
1412 let price = Price::from_decimal(decimal).unwrap();
1413 assert_eq!(price.precision, 2);
1414 assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-10));
1415 assert!(price.raw < 0);
1416 }
1417
1418 #[rstest]
1419 fn test_string_formatting() {
1420 assert_eq!(format!("{}", Price::new(1234.5678, 4)), "1234.5678");
1421 assert_eq!(
1422 format!("{:?}", Price::new(1234.5678, 4)),
1423 "Price(1234.5678)"
1424 );
1425 assert_eq!(Price::new(1234.5678, 4).to_formatted_string(), "1_234.5678");
1426 }
1427
1428 #[rstest]
1429 #[case(1234.5678, 4, "Price(1234.5678)", "1234.5678")] #[case(123.456_789_012_345, 8, "Price(123.45678901)", "123.45678901")] #[cfg_attr(
1432 feature = "defi",
1433 case(
1434 2_000_000_000_000_000_000.0,
1435 18,
1436 "Price(2.000000000000000000)",
1437 "2.000000000000000000"
1438 )
1439 )] fn test_string_formatting_precision_handling(
1441 #[case] value: f64,
1442 #[case] precision: u8,
1443 #[case] expected_debug: &str,
1444 #[case] expected_display: &str,
1445 ) {
1446 let price = if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
1447 Price::from_raw(value as PriceRaw, precision)
1448 } else {
1449 Price::new(value, precision)
1450 };
1451
1452 assert_eq!(format!("{price:?}"), expected_debug);
1453 assert_eq!(format!("{price}"), expected_display);
1454 assert_eq!(
1455 price.to_formatted_string().replace('_', ""),
1456 expected_display
1457 );
1458 }
1459
1460 #[rstest]
1461 fn test_decimal_conversions() {
1462 let price = Price::new(123.456, 3);
1463 assert_eq!(price.as_decimal(), dec!(123.456));
1464
1465 let price = Price::new(0.000_001, 6);
1466 assert_eq!(price.as_decimal(), dec!(0.000001));
1467 }
1468
1469 #[rstest]
1470 #[case(PRICE_ERROR, true)]
1471 #[case(PRICE_UNDEF, false)]
1472 #[case(-1, false)]
1473 #[case(0, false)]
1474 #[case(1, false)]
1475 fn test_is_error(#[case] raw: PriceRaw, #[case] expected: bool) {
1476 let price = Price::from_raw(raw, 0);
1477 assert_eq!(price.is_error(), expected);
1478 }
1479
1480 #[rstest]
1481 fn test_basic_arithmetic() {
1482 let p1 = Price::new(10.5, 2);
1483 let p2 = Price::new(5.25, 2);
1484 assert_eq!(p1 + p2, Price::from("15.75"));
1485 assert_eq!(p1 - p2, Price::from("5.25"));
1486 assert_eq!(-p1, Price::from("-10.5"));
1487 }
1488
1489 #[rstest]
1490 #[case::error(PRICE_ERROR)]
1491 #[case::undefined(PRICE_UNDEF)]
1492 fn test_neg_preserves_sentinel(#[case] raw: PriceRaw) {
1493 let price = Price::from_raw(raw, 0);
1494
1495 assert_eq!(-price, price);
1496 }
1497
1498 #[rstest]
1499 fn test_price_checked_add_within_bounds() {
1500 let a = Price::new(10.0, 2);
1501 let b = Price::new(5.0, 2);
1502 assert_eq!(a.checked_add(b), Some(Price::new(15.0, 2)));
1503
1504 let neg = Price::new(-3.0, 2);
1505 assert_eq!(a.checked_add(neg), Some(Price::new(7.0, 2)));
1506 }
1507
1508 #[rstest]
1509 fn test_price_checked_add_above_max_returns_none() {
1510 let near_max = Price::from_raw(PRICE_RAW_MAX, 0);
1511 let one = Price::new(1.0, 0);
1512 assert_eq!(near_max.checked_add(one), None);
1513 }
1514
1515 #[rstest]
1516 fn test_price_checked_sub_within_bounds() {
1517 let a = Price::new(10.0, 2);
1518 let b = Price::new(3.0, 2);
1519 assert_eq!(a.checked_sub(b), Some(Price::new(7.0, 2)));
1520 assert_eq!(b.checked_sub(a), Some(Price::new(-7.0, 2)));
1521 }
1522
1523 #[rstest]
1524 fn test_price_checked_sub_below_min_returns_none() {
1525 let near_min = Price::from_raw(PRICE_RAW_MIN, 0);
1526 let one = Price::new(1.0, 0);
1527 assert_eq!(near_min.checked_sub(one), None);
1528 }
1529
1530 #[rstest]
1531 fn test_price_checked_arith_uses_max_precision() {
1532 let a = Price::new(10.5, 1);
1533 let b = Price::new(5.25, 2);
1534 let sum = a.checked_add(b).unwrap();
1535 assert_eq!(sum.precision, 2);
1536 assert_eq!(sum.as_f64(), 15.75);
1537 }
1538
1539 #[rstest]
1540 fn test_price_checked_add_rejects_sentinel_undef() {
1541 let undef = Price::from_raw(PRICE_UNDEF, 0);
1542 let one = Price::new(1.0, 0);
1543 assert_eq!(undef.checked_add(one), None);
1544 assert_eq!(one.checked_add(undef), None);
1545 }
1546
1547 #[rstest]
1548 fn test_price_checked_sub_rejects_sentinel_undef() {
1549 let undef = Price::from_raw(PRICE_UNDEF, 0);
1550 let neg_one = Price::new(-1.0, 0);
1551 assert_eq!(undef.checked_sub(neg_one), None);
1552 }
1553
1554 #[rstest]
1555 fn test_price_is_zero() {
1556 assert!(!Price::new(1.5, 2).is_zero());
1557 assert!(Price::new(0.0, 2).is_zero());
1558 }
1559
1560 #[rstest]
1561 fn test_price_as_f64() {
1562 assert_eq!(Price::new(1.5, 2).as_f64(), 1.5);
1563 assert_eq!(Price::new(0.0, 2).as_f64(), 0.0);
1564 }
1565
1566 #[rstest]
1567 fn test_price_checked_arith_rejects_out_of_bounds_without_integer_overflow() {
1568 let one_unit = Price::from_raw(1, 0);
1569
1570 assert_eq!(
1571 Price::from_raw(PRICE_RAW_MAX, 0).checked_add(one_unit),
1572 None
1573 );
1574 assert_eq!(
1575 Price::from_raw(PRICE_RAW_MIN, 0).checked_sub(one_unit),
1576 None
1577 );
1578 }
1579
1580 #[rstest]
1581 fn test_price_checked_sub_rejects_sentinel_before_bounds_check() {
1582 let undef = Price::from_raw(PRICE_UNDEF, 0);
1583 let max = Price::from_raw(PRICE_RAW_MAX, 0);
1584
1585 assert_eq!(undef.checked_sub(max), None);
1586 }
1587
1588 #[rstest]
1589 fn test_price_checked_arith_rejects_error_price() {
1590 let one = Price::new(1.0, 0);
1591 assert_eq!(ERROR_PRICE.checked_add(one), None);
1592 assert_eq!(one.checked_sub(ERROR_PRICE), None);
1593 }
1594
1595 #[rstest]
1596 fn test_price_checked_arith_rejects_raw_error() {
1597 let error = Price::from_raw(PRICE_ERROR, 0);
1598 let one = Price::new(1.0, 0);
1599 assert_eq!(error.checked_add(one), None);
1600 assert_eq!(one.checked_add(error), None);
1601 assert_eq!(error.checked_sub(one), None);
1602 assert_eq!(one.checked_sub(error), None);
1603 }
1604
1605 #[rstest]
1606 fn test_price_checked_add_at_exact_max_returns_some() {
1607 let near_max = Price::from_raw(PRICE_RAW_MAX - 1, 0);
1608 let one_unit = Price::from_raw(1, 0);
1609 assert_eq!(
1610 near_max.checked_add(one_unit),
1611 Some(Price::from_raw(PRICE_RAW_MAX, 0)),
1612 );
1613 }
1614
1615 #[rstest]
1616 fn test_price_checked_sub_at_exact_min_returns_some() {
1617 let near_min = Price::from_raw(PRICE_RAW_MIN + 1, 0);
1618 let one_unit = Price::from_raw(1, 0);
1619 assert_eq!(
1620 near_min.checked_sub(one_unit),
1621 Some(Price::from_raw(PRICE_RAW_MIN, 0)),
1622 );
1623 }
1624
1625 #[rstest]
1626 fn test_mixed_precision_add() {
1627 let p1 = Price::new(10.5, 1);
1628 let p2 = Price::new(5.25, 2);
1629 let result = p1 + p2;
1630 assert_eq!(result.precision, 2);
1631 assert_eq!(result.as_f64(), 15.75);
1632 }
1633
1634 #[rstest]
1635 fn test_mixed_precision_sub() {
1636 let p1 = Price::new(10.5, 1);
1637 let p2 = Price::new(5.25, 2);
1638 let result = p1 - p2;
1639 assert_eq!(result.precision, 2);
1640 assert_eq!(result.as_f64(), 5.25);
1641 }
1642
1643 #[rstest]
1644 fn test_f64_operations() {
1645 let p = Price::new(10.5, 2);
1646 assert_eq!(p + 1.0, 11.5);
1647 assert_eq!(p - 1.0, 9.5);
1648 assert_eq!(p * 2.0, 21.0);
1649 assert_eq!(p / 2.0, 5.25);
1650 }
1651
1652 #[rstest]
1653 fn test_equality_and_comparisons() {
1654 let p1 = Price::new(10.0, 1);
1655 let p2 = Price::new(20.0, 1);
1656 let p3 = Price::new(10.0, 1);
1657
1658 assert!(p1 < p2);
1659 assert!(p2 > p1);
1660 assert!(p1 <= p3);
1661 assert!(p1 >= p3);
1662 assert_eq!(p1, p3);
1663 assert_ne!(p1, p2);
1664
1665 assert_eq!(Price::from("1.0"), Price::from("1.0"));
1666 assert_ne!(Price::from("1.1"), Price::from("1.0"));
1667 assert!(Price::from("1.0") <= Price::from("1.0"));
1668 assert!(Price::from("1.1") > Price::from("1.0"));
1669 assert!(Price::from("1.0") >= Price::from("1.0"));
1670 assert!(Price::from("1.0") >= Price::from("1.0"));
1671 assert!(Price::from("1.0") >= Price::from("1.0"));
1672 assert!(Price::from("0.9") < Price::from("1.0"));
1673 assert!(Price::from("0.9") <= Price::from("1.0"));
1674 assert!(Price::from("0.9") <= Price::from("1.0"));
1675 }
1676
1677 #[rstest]
1678 fn test_deref() {
1679 let price = Price::new(10.0, 1);
1680 assert_eq!(*price, price.raw);
1681 }
1682
1683 #[rstest]
1684 fn test_decode_raw_price_i64() {
1685 let raw_scaled_by_1e9 = 42_000_000_000i64; let decoded = decode_raw_price_i64(raw_scaled_by_1e9);
1687 let price = Price::from_raw(decoded, FIXED_PRECISION);
1688 assert!(
1689 approx_eq!(f64, price.as_f64(), 42.0, epsilon = 1e-9),
1690 "Expected 42.0 f64, was {} (precision = {})",
1691 price.as_f64(),
1692 price.precision
1693 );
1694 }
1695
1696 #[rstest]
1697 fn test_hash() {
1698 use std::{
1699 collections::hash_map::DefaultHasher,
1700 hash::{Hash, Hasher},
1701 };
1702
1703 let price1 = Price::new(1.0, 2);
1704 let price2 = Price::new(1.0, 2);
1705 let price3 = Price::new(1.1, 2);
1706
1707 let mut hasher1 = DefaultHasher::new();
1708 let mut hasher2 = DefaultHasher::new();
1709 let mut hasher3 = DefaultHasher::new();
1710
1711 price1.hash(&mut hasher1);
1712 price2.hash(&mut hasher2);
1713 price3.hash(&mut hasher3);
1714
1715 assert_eq!(hasher1.finish(), hasher2.finish());
1716 assert_ne!(hasher1.finish(), hasher3.finish());
1717 }
1718
1719 #[rstest]
1720 fn test_price_serde_json_round_trip() {
1721 let price = Price::new(1.0500, 4);
1722 let json = serde_json::to_string(&price).unwrap();
1723 let deserialized: Price = serde_json::from_str(&json).unwrap();
1724 assert_eq!(deserialized, price);
1725 }
1726
1727 #[rstest]
1728 fn test_price_serde_json_from_value_round_trip() {
1729 let price = Price::new(1.0500, 4);
1730 let value = serde_json::to_value(price).unwrap();
1731
1732 let deserialized: Price = serde_json::from_value(value).unwrap();
1733 assert_eq!(deserialized, price);
1734 assert_eq!(deserialized.precision, 4);
1735 }
1736
1737 #[cfg(feature = "high-precision")]
1738 #[rstest]
1739 fn test_high_precision_16_serde_json_round_trip() {
1740 let price = Price::from_raw(1_234_567_890_123_456_789_i128, 16);
1741 let json = serde_json::to_string(&price).unwrap();
1742 let deserialized: Price = serde_json::from_str(&json).unwrap();
1743
1744 assert_eq!(deserialized, price);
1745 assert_eq!(deserialized.precision, 16);
1746 assert_eq!(deserialized.raw, 1_234_567_890_123_456_789_i128);
1747 }
1748
1749 #[cfg(all(feature = "high-precision", feature = "defi"))]
1750 #[rstest]
1751 #[case(17, 12_345_678_901_234_567_891_i128)]
1752 #[case(18, 123_456_789_012_345_678_901_i128)]
1753 fn test_defi_precision_serde_json_round_trip(#[case] precision: u8, #[case] raw: PriceRaw) {
1754 let price = Price::from_raw(raw, precision);
1755 let json = serde_json::to_string(&price).unwrap();
1756 let deserialized: Price = serde_json::from_str(&json).unwrap();
1757
1758 assert_eq!(deserialized, price);
1759 assert_eq!(deserialized.precision, precision);
1760 assert_eq!(deserialized.raw, raw);
1761 }
1762
1763 #[rstest]
1764 fn test_price_deserialize_invalid_string_returns_error() {
1765 let result = serde_json::from_str::<Price>("\"not-a-price\"");
1766 let error = result.unwrap_err();
1767 assert!(
1768 error.to_string().contains("Error parsing"),
1769 "unexpected message: {error}"
1770 );
1771 }
1772
1773 #[rstest]
1774 fn test_price_deserialize_out_of_range_returns_error() {
1775 let result = serde_json::from_str::<Price>("\"99999999999999999999.99\"");
1776 assert!(result.is_err());
1777 }
1778
1779 #[rstest]
1780 fn test_from_mantissa_exponent_exact_precision() {
1781 let price = Price::from_mantissa_exponent(12345, -2, 2);
1782 assert_eq!(price.as_f64(), 123.45);
1783 }
1784
1785 #[rstest]
1786 fn test_from_mantissa_exponent_excess_rounds_down() {
1787 let price = Price::from_mantissa_exponent(12345, -3, 2);
1789 assert_eq!(price.as_f64(), 12.34);
1790 }
1791
1792 #[rstest]
1793 fn test_from_mantissa_exponent_excess_rounds_up() {
1794 let price = Price::from_mantissa_exponent(12355, -3, 2);
1796 assert_eq!(price.as_f64(), 12.36);
1797 }
1798
1799 #[rstest]
1800 fn test_from_mantissa_exponent_positive_exponent() {
1801 let price = Price::from_mantissa_exponent(5, 2, 0);
1802 assert_eq!(price.as_f64(), 500.0);
1803 }
1804
1805 #[rstest]
1806 fn test_from_mantissa_exponent_negative_mantissa() {
1807 let price = Price::from_mantissa_exponent(-12345, -2, 2);
1808 assert_eq!(price.as_f64(), -123.45);
1809 }
1810
1811 #[rstest]
1812 fn test_from_mantissa_exponent_zero() {
1813 let price = Price::from_mantissa_exponent(0, 2, 2);
1814 assert_eq!(price.as_f64(), 0.0);
1815 }
1816
1817 #[cfg(all(feature = "defi", feature = "high-precision"))]
1818 #[rstest]
1819 fn test_wei_above_decimal_mantissa_formats_exactly() {
1820 let raw = 80_000_000_000_000_000_250_000_000_000_i128;
1821 let price = Price::from_raw(raw, 18);
1822
1823 assert_eq!(price.to_string(), "80000000000.000000250000000000");
1824 assert_eq!(Price::from_str(&price.to_string()).unwrap(), price);
1825 assert_eq!(
1826 serde_json::from_str::<Price>(&serde_json::to_string(&price).unwrap()).unwrap(),
1827 price,
1828 );
1829 }
1830
1831 #[rstest]
1832 fn test_from_mantissa_exponent_checked_exact_precision() {
1833 let price = Price::from_mantissa_exponent_checked(12345, -2, 2).unwrap();
1834 assert_eq!(price.as_decimal(), dec!(123.45));
1835 }
1836
1837 #[rstest]
1838 fn test_from_mantissa_exponent_checked_zero_with_large_exponent() {
1839 let price = Price::from_mantissa_exponent_checked(0, 119, 2).unwrap();
1840 assert_eq!(price.as_decimal(), dec!(0.00));
1841 }
1842
1843 #[rstest]
1844 fn test_from_mantissa_exponent_checked_invalid_precision() {
1845 #[cfg(feature = "defi")]
1846 let invalid_precision = crate::defi::WEI_PRECISION + 1;
1847 #[cfg(not(feature = "defi"))]
1848 let invalid_precision = FIXED_PRECISION + 1;
1849
1850 let error = Price::from_mantissa_exponent_checked(1, 0, invalid_precision).unwrap_err();
1851 assert!(error.to_string().contains("`precision` exceeded maximum"));
1852 }
1853
1854 #[rstest]
1855 fn test_from_mantissa_exponent_checked_overflow_returns_error() {
1856 let error = Price::from_mantissa_exponent_checked(i64::MAX, 100, 0).unwrap_err();
1857 assert!(
1858 error
1859 .to_string()
1860 .contains("Overflow in Price::from_mantissa_exponent")
1861 );
1862 }
1863
1864 #[rstest]
1865 #[should_panic(expected = "Price::from_mantissa_exponent")]
1866 fn test_from_mantissa_exponent_overflow_panics() {
1867 let _ = Price::from_mantissa_exponent(i64::MAX, 9, 0);
1868 }
1869
1870 #[rstest]
1871 #[should_panic(expected = "exceeds i128 range")]
1872 fn test_from_mantissa_exponent_large_exponent_panics() {
1873 let _ = Price::from_mantissa_exponent(1, 119, 0);
1874 }
1875
1876 #[rstest]
1877 fn test_from_mantissa_exponent_zero_with_large_exponent() {
1878 let price = Price::from_mantissa_exponent(0, 119, 0);
1879 assert_eq!(price.as_f64(), 0.0);
1880 }
1881
1882 #[rstest]
1883 fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
1884 let price = Price::from_mantissa_exponent(12345, -120, 2);
1885 assert_eq!(price.as_f64(), 0.0);
1886 }
1887
1888 #[rstest]
1889 fn test_decimal_arithmetic_operations() {
1890 let price = Price::new(100.0, 2);
1891 assert_eq!(price + dec!(50.25), dec!(150.25));
1892 assert_eq!(price - dec!(30.50), dec!(69.50));
1893 assert_eq!(price * dec!(1.5), dec!(150.00));
1894 assert_eq!(price / dec!(4), dec!(25.00));
1895 }
1896}
1897
1898#[cfg(test)]
1899mod property_tests {
1900 use proptest::prelude::*;
1901 use rstest::rstest;
1902
1903 use super::*;
1904
1905 fn price_value_strategy() -> impl Strategy<Value = f64> {
1907 prop_oneof![
1910 0.00001..1.0,
1912 1.0..100_000.0,
1914 100_000.0..1_000_000.0,
1916 -1_000.0..0.0,
1918 Just(PRICE_MIN / 2.0),
1920 Just(PRICE_MAX / 2.0),
1921 ]
1922 }
1923
1924 fn float_precision_upper_bound() -> u8 {
1925 FIXED_PRECISION.min(crate::types::fixed::MAX_FLOAT_PRECISION)
1926 }
1927
1928 fn precision_strategy() -> impl Strategy<Value = u8> {
1930 let upper = float_precision_upper_bound();
1931 prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1932 }
1933
1934 fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1935 let upper = float_precision_upper_bound().max(1);
1936 prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1937 }
1938
1939 fn valid_precision_raw_strategy() -> impl Strategy<Value = (u8, PriceRaw)> {
1943 precision_strategy().prop_flat_map(|precision| {
1944 let scale: PriceRaw = if precision >= FIXED_PRECISION {
1945 1
1946 } else {
1947 (10 as PriceRaw).pow(u32::from(FIXED_PRECISION - precision))
1948 };
1949 let max_base = PRICE_RAW_MAX / scale;
1951 let min_base = PRICE_RAW_MIN / scale;
1952 (min_base..=max_base).prop_map(move |base| (precision, base * scale))
1953 })
1954 }
1955
1956 fn float_precision_strategy() -> impl Strategy<Value = u8> {
1958 precision_strategy()
1959 }
1960
1961 const DECIMAL_MAX_MANTISSA: i128 = 79_228_162_514_264_337_593_543_950_335;
1962
1963 #[allow(
1964 clippy::useless_conversion,
1965 reason = "PriceRaw is i64 or i128 depending on feature; the conversion is only useless in high-precision builds"
1966 )]
1967 fn decimal_compatible(raw: PriceRaw, precision: u8) -> bool {
1968 if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
1969 return false;
1970 }
1971 let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
1972 let divisor = (10 as PriceRaw).pow(precision_diff);
1973 let rescaled_raw = raw / divisor;
1974 i128::from(rescaled_raw.abs()) <= DECIMAL_MAX_MANTISSA
1975 }
1976
1977 proptest! {
1978 #[rstest]
1980 fn prop_price_serde_round_trip(
1981 (precision, raw) in valid_precision_raw_strategy()
1982 ) {
1983 let original = Price::from_raw(raw, precision);
1984
1985 let string_repr = original.to_string();
1987 let from_string: Price = string_repr.parse().unwrap();
1988 prop_assert_eq!(from_string.raw, original.raw);
1989 prop_assert_eq!(from_string.precision, original.precision);
1990
1991 let json = serde_json::to_string(&original).unwrap();
1993 let from_json: Price = serde_json::from_str(&json).unwrap();
1994 prop_assert_eq!(from_json.precision, original.precision);
1995 prop_assert_eq!(from_json.raw, original.raw);
1996 }
1997
1998 #[rstest]
2000 fn prop_price_arithmetic_associative(
2001 a in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
2002 b in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
2003 c in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
2004 precision in precision_strategy()
2005 ) {
2006 let p_a = Price::new(a, precision);
2007 let p_b = Price::new(b, precision);
2008 let p_c = Price::new(c, precision);
2009
2010 let expected = p_a
2011 .raw
2012 .checked_add(p_b.raw)
2013 .and_then(|sum| sum.checked_add(p_c.raw))
2014 .filter(|sum| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(sum));
2015
2016 if let Some(expected) = expected {
2017 let left = (p_a + p_b) + p_c;
2018 let right = p_a + (p_b + p_c);
2019 prop_assert_eq!(left.raw, expected);
2020 prop_assert_eq!(right.raw, expected);
2021 }
2022 }
2023
2024 #[rstest]
2026 fn prop_price_addition_subtraction_inverse(
2027 base in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
2028 delta in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
2029 precision in precision_strategy()
2030 ) {
2031 let p_base = Price::new(base, precision);
2032 let p_delta = Price::new(delta, precision);
2033
2034 if p_base
2035 .raw
2036 .checked_add(p_delta.raw)
2037 .is_some_and(|sum| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(&sum))
2038 {
2039 prop_assert_eq!((p_base + p_delta) - p_delta, p_base);
2040 }
2041 }
2042
2043 #[rstest]
2045 fn prop_price_ordering_transitive(
2046 a in price_value_strategy(),
2047 b in price_value_strategy(),
2048 c in price_value_strategy(),
2049 precision in float_precision_strategy()
2050 ) {
2051 let p_a = Price::new(a, precision);
2052 let p_b = Price::new(b, precision);
2053 let p_c = Price::new(c, precision);
2054
2055 if p_a <= p_b && p_b <= p_c {
2057 prop_assert!(p_a <= p_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
2058 p_a.as_f64(), p_b.as_f64(), p_c.as_f64(), p_a.as_f64(), p_c.as_f64());
2059 }
2060 }
2061
2062 #[rstest]
2064 fn prop_price_string_parsing_precision(
2065 integral in 0u32..1_000_000,
2066 fractional in 0u32..1_000_000,
2067 precision in precision_strategy_non_zero()
2068 ) {
2069 let pow = 10u128.pow(u32::from(precision));
2071 let fractional_mod = u128::from(fractional) % pow;
2072 let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
2073 let price_str = format!("{integral}.{fractional_str}");
2074
2075 let parsed: Price = price_str.parse().unwrap();
2076 prop_assert_eq!(parsed.precision, precision);
2077
2078 let round_trip = parsed.to_string();
2080 let expected_value = format!("{integral}.{fractional_str}");
2081 prop_assert_eq!(round_trip, expected_value);
2082 }
2083
2084 #[rstest]
2086 fn prop_price_arithmetic_bounds(
2087 a in price_value_strategy(),
2088 b in price_value_strategy(),
2089 precision in float_precision_strategy()
2090 ) {
2091 let p_a = Price::new(a, precision);
2092 let p_b = Price::new(b, precision);
2093
2094 let sum_f64 = p_a.as_f64() + p_b.as_f64();
2096 if sum_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&sum_f64) {
2097 let sum = p_a + p_b;
2098 prop_assert!(sum.as_f64().is_finite());
2099 prop_assert!(!sum.is_undefined());
2100 }
2101
2102 let diff_f64 = p_a.as_f64() - p_b.as_f64();
2104 if diff_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&diff_f64) {
2105 let diff = p_a - p_b;
2106 prop_assert!(diff.as_f64().is_finite());
2107 prop_assert!(!diff.is_undefined());
2108 }
2109 }
2110
2111 #[rstest]
2114 fn prop_price_checked_add_matches_spec(
2115 a in price_value_strategy(),
2116 b in price_value_strategy(),
2117 precision in float_precision_strategy()
2118 ) {
2119 let p_a = Price::new(a, precision);
2120 let p_b = Price::new(b, precision);
2121 let expected = p_a.raw
2122 .checked_add(p_b.raw)
2123 .filter(|r| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(r))
2124 .filter(|_| !p_a.is_sentinel() && !p_b.is_sentinel())
2125 .map(|raw| Price { raw, precision: p_a.precision.max(p_b.precision) });
2126 prop_assert_eq!(p_a.checked_add(p_b), expected);
2127 }
2128
2129 #[rstest]
2132 fn prop_price_checked_sub_matches_spec(
2133 a in price_value_strategy(),
2134 b in price_value_strategy(),
2135 precision in float_precision_strategy()
2136 ) {
2137 let p_a = Price::new(a, precision);
2138 let p_b = Price::new(b, precision);
2139 let expected = p_a.raw
2140 .checked_sub(p_b.raw)
2141 .filter(|r| (PRICE_RAW_MIN..=PRICE_RAW_MAX).contains(r))
2142 .filter(|_| !p_a.is_sentinel() && !p_b.is_sentinel())
2143 .map(|raw| Price { raw, precision: p_a.precision.max(p_b.precision) });
2144 prop_assert_eq!(p_a.checked_sub(p_b), expected);
2145 }
2146 }
2147
2148 proptest! {
2149 #[rstest]
2151 fn prop_price_as_decimal_preserves_precision(
2152 (precision, raw) in valid_precision_raw_strategy()
2153 ) {
2154 prop_assume!(decimal_compatible(raw, precision));
2155 let price = Price::from_raw(raw, precision);
2156 let decimal = price.as_decimal();
2157 prop_assert_eq!(decimal.scale(), u32::from(precision));
2158 }
2159
2160 #[rstest]
2162 fn prop_price_as_decimal_matches_display(
2163 value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
2164 precision in float_precision_strategy()
2165 ) {
2166 let price = Price::new(value, precision);
2167 prop_assume!(decimal_compatible(price.raw, precision));
2168 let display_str = format!("{price}");
2169 let decimal_str = price.as_decimal().to_string();
2170 prop_assert_eq!(display_str, decimal_str);
2171 }
2172
2173 #[rstest]
2175 fn prop_price_from_decimal_roundtrip(
2176 (precision, raw) in valid_precision_raw_strategy()
2177 ) {
2178 prop_assume!(decimal_compatible(raw, precision));
2179 let original = Price::from_raw(raw, precision);
2180 let decimal = original.as_decimal();
2181 let reconstructed = Price::from_decimal(decimal).unwrap();
2182 prop_assert_eq!(original.raw, reconstructed.raw);
2183 prop_assert_eq!(original.precision, reconstructed.precision);
2184 }
2185
2186 #[rstest]
2188 fn prop_price_from_raw_round_trip(
2189 (precision, raw) in valid_precision_raw_strategy()
2190 ) {
2191 let price = Price::from_raw(raw, precision);
2192 prop_assert_eq!(price.raw, raw);
2193 prop_assert_eq!(price.precision, precision);
2194 }
2195 }
2196}