1use std::{
19 cmp::Ordering,
20 fmt::{Debug, Display},
21 hash::{Hash, Hasher},
22 ops::{Add, AddAssign, Deref, Mul, Neg, Sub, SubAssign},
23 str::FromStr,
24};
25
26use nautilus_core::correctness::{FAILED, check_in_range_inclusive_f64, check_predicate_true};
27use rust_decimal::{Decimal, prelude::ToPrimitive};
28use serde::{Deserialize, Deserializer, Serialize};
29use thousands::Separable;
30
31use super::fixed::{FIXED_PRECISION, FIXED_SCALAR, check_fixed_precision};
32#[cfg(feature = "high-precision")]
33use super::fixed::{PRECISION_DIFF_SCALAR, f64_to_fixed_i128, fixed_i128_to_f64};
34#[cfg(not(feature = "high-precision"))]
35use super::fixed::{f64_to_fixed_i64, fixed_i64_to_f64};
36#[cfg(feature = "defi")]
37use crate::types::fixed::MAX_FLOAT_PRECISION;
38
39#[cfg(feature = "high-precision")]
47pub type PriceRaw = i128;
48
49#[cfg(not(feature = "high-precision"))]
50pub type PriceRaw = i64;
51
52#[unsafe(no_mangle)]
63#[allow(unsafe_code)]
64pub static PRICE_RAW_MAX: PriceRaw = (PRICE_MAX * FIXED_SCALAR) as PriceRaw;
65
66#[unsafe(no_mangle)]
75#[allow(unsafe_code)]
76pub static PRICE_RAW_MIN: PriceRaw = (PRICE_MIN * FIXED_SCALAR) as PriceRaw;
77
78pub const PRICE_UNDEF: PriceRaw = PriceRaw::MAX;
80
81pub const PRICE_ERROR: PriceRaw = PriceRaw::MIN;
83
84#[cfg(feature = "high-precision")]
90pub const PRICE_MAX: f64 = 17_014_118_346_046.0;
91
92#[cfg(not(feature = "high-precision"))]
93pub const PRICE_MAX: f64 = 9_223_372_036.0;
95
96#[cfg(feature = "high-precision")]
101pub const PRICE_MIN: f64 = -17_014_118_346_046.0;
103
104#[cfg(not(feature = "high-precision"))]
105pub const PRICE_MIN: f64 = -9_223_372_036.0;
107
108pub const ERROR_PRICE: Price = Price {
112 raw: 0,
113 precision: 255,
114};
115
116#[repr(C)]
127#[derive(Clone, Copy, Default, Eq)]
128#[cfg_attr(
129 feature = "python",
130 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", frozen)
131)]
132pub struct Price {
133 pub raw: PriceRaw,
135 pub precision: u8,
137}
138
139impl Price {
140 pub fn new_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
152 check_in_range_inclusive_f64(value, PRICE_MIN, PRICE_MAX, "value")?;
153
154 #[cfg(feature = "defi")]
155 if precision > MAX_FLOAT_PRECISION {
156 anyhow::bail!(
158 "`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Price::from_wei()` for wei values instead"
159 );
160 }
161
162 check_fixed_precision(precision)?;
163
164 #[cfg(feature = "high-precision")]
165 let raw = f64_to_fixed_i128(value, precision);
166
167 #[cfg(not(feature = "high-precision"))]
168 let raw = f64_to_fixed_i64(value, precision);
169
170 Ok(Self { raw, precision })
171 }
172
173 pub fn new(value: f64, precision: u8) -> Self {
179 Self::new_checked(value, precision).expect(FAILED)
180 }
181
182 pub fn from_raw(raw: PriceRaw, precision: u8) -> Self {
188 if raw == PRICE_UNDEF {
189 check_predicate_true(
190 precision == 0,
191 "`precision` must be 0 when `raw` is PRICE_UNDEF",
192 )
193 .expect(FAILED);
194 }
195 check_predicate_true(
196 raw == PRICE_ERROR
197 || raw == PRICE_UNDEF
198 || (raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX),
199 &format!("raw value outside valid range, was {raw}"),
200 )
201 .expect(FAILED);
202 check_fixed_precision(precision).expect(FAILED);
203 Self { raw, precision }
204 }
205
206 #[must_use]
212 pub fn zero(precision: u8) -> Self {
213 check_fixed_precision(precision).expect(FAILED);
214 Self { raw: 0, precision }
215 }
216
217 #[must_use]
223 pub fn max(precision: u8) -> Self {
224 check_fixed_precision(precision).expect(FAILED);
225 Self {
226 raw: PRICE_RAW_MAX,
227 precision,
228 }
229 }
230
231 #[must_use]
237 pub fn min(precision: u8) -> Self {
238 check_fixed_precision(precision).expect(FAILED);
239 Self {
240 raw: PRICE_RAW_MIN,
241 precision,
242 }
243 }
244
245 #[must_use]
247 pub fn is_undefined(&self) -> bool {
248 self.raw == PRICE_UNDEF
249 }
250
251 #[must_use]
253 pub fn is_zero(&self) -> bool {
254 self.raw == 0
255 }
256
257 #[must_use]
259 pub fn is_positive(&self) -> bool {
260 self.raw != PRICE_UNDEF && self.raw > 0
261 }
262
263 #[cfg(feature = "high-precision")]
264 #[must_use]
270 pub fn as_f64(&self) -> f64 {
271 #[cfg(feature = "defi")]
272 if self.precision > MAX_FLOAT_PRECISION {
273 panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
274 }
275
276 fixed_i128_to_f64(self.raw)
277 }
278
279 #[cfg(not(feature = "high-precision"))]
280 #[must_use]
286 pub fn as_f64(&self) -> f64 {
287 #[cfg(feature = "defi")]
288 if self.precision > MAX_FLOAT_PRECISION {
289 panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
290 }
291
292 fixed_i64_to_f64(self.raw)
293 }
294
295 #[must_use]
297 pub fn as_decimal(&self) -> Decimal {
298 let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
300 let rescaled_raw = self.raw / PriceRaw::pow(10, u32::from(precision_diff));
301 #[allow(clippy::unnecessary_cast, reason = "Required for precision modes")]
302 Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
303 }
304
305 #[must_use]
307 pub fn to_formatted_string(&self) -> String {
308 format!("{self}").separate_with_underscores()
309 }
310
311 pub fn from_decimal(decimal: Decimal, precision: u8) -> anyhow::Result<Self> {
323 check_fixed_precision(precision)?;
324
325 let scale_factor = Decimal::from(10_i64.pow(precision as u32));
327 let scaled = decimal * scale_factor;
328 let rounded = scaled.round();
329
330 #[cfg(feature = "high-precision")]
331 let raw_at_precision: PriceRaw = rounded.to_i128().ok_or_else(|| {
332 anyhow::anyhow!("Decimal value '{decimal}' cannot be converted to i128")
333 })?;
334 #[cfg(not(feature = "high-precision"))]
335 let raw_at_precision: PriceRaw = rounded.to_i64().ok_or_else(|| {
336 anyhow::anyhow!("Decimal value '{decimal}' cannot be converted to i64")
337 })?;
338
339 let scale_up = 10_i64.pow((FIXED_PRECISION - precision) as u32) as PriceRaw;
340 let raw = raw_at_precision
341 .checked_mul(scale_up)
342 .ok_or_else(|| anyhow::anyhow!("Overflow when scaling to fixed precision"))?;
343
344 check_predicate_true(
345 raw == PRICE_UNDEF || (raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX),
346 &format!("raw value outside valid range, was {raw}"),
347 )?;
348
349 Ok(Self { raw, precision })
350 }
351}
352
353impl FromStr for Price {
354 type Err = String;
355
356 fn from_str(value: &str) -> Result<Self, Self::Err> {
357 let clean_value = value.replace('_', "");
358
359 let decimal = if clean_value.contains('e') || clean_value.contains('E') {
360 Decimal::from_scientific(&clean_value)
361 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
362 } else {
363 Decimal::from_str(&clean_value)
364 .map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
365 };
366
367 let decimal_str = decimal.to_string();
369 let precision = if let Some(dot_pos) = decimal_str.find('.') {
370 let decimal_part = &decimal_str[dot_pos + 1..];
371 decimal_part.len().min(u8::MAX as usize) as u8
372 } else {
373 0
374 };
375
376 Self::from_decimal(decimal, precision).map_err(|e| e.to_string())
377 }
378}
379
380impl<T: AsRef<str>> From<T> for Price {
381 fn from(value: T) -> Self {
382 Self::from_str(value.as_ref()).expect(FAILED)
383 }
384}
385
386impl From<Price> for f64 {
387 fn from(price: Price) -> Self {
388 price.as_f64()
389 }
390}
391
392impl From<&Price> for f64 {
393 fn from(price: &Price) -> Self {
394 price.as_f64()
395 }
396}
397
398impl Hash for Price {
399 fn hash<H: Hasher>(&self, state: &mut H) {
400 self.raw.hash(state);
401 }
402}
403
404impl PartialEq for Price {
405 fn eq(&self, other: &Self) -> bool {
406 self.raw == other.raw
407 }
408}
409
410impl PartialOrd for Price {
411 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
412 Some(self.cmp(other))
413 }
414
415 fn lt(&self, other: &Self) -> bool {
416 self.raw.lt(&other.raw)
417 }
418
419 fn le(&self, other: &Self) -> bool {
420 self.raw.le(&other.raw)
421 }
422
423 fn gt(&self, other: &Self) -> bool {
424 self.raw.gt(&other.raw)
425 }
426
427 fn ge(&self, other: &Self) -> bool {
428 self.raw.ge(&other.raw)
429 }
430}
431
432impl Ord for Price {
433 fn cmp(&self, other: &Self) -> Ordering {
434 self.raw.cmp(&other.raw)
435 }
436}
437
438impl Deref for Price {
439 type Target = PriceRaw;
440
441 fn deref(&self) -> &Self::Target {
442 &self.raw
443 }
444}
445
446impl Neg for Price {
447 type Output = Self;
448 fn neg(self) -> Self::Output {
449 Self {
450 raw: -self.raw,
451 precision: self.precision,
452 }
453 }
454}
455
456impl Add for Price {
457 type Output = Self;
458 fn add(self, rhs: Self) -> Self::Output {
459 assert!(
465 self.precision >= rhs.precision,
466 "Precision mismatch: cannot add precision {} to precision {} (precision loss)",
467 rhs.precision,
468 self.precision,
469 );
470 Self {
471 raw: self
472 .raw
473 .checked_add(rhs.raw)
474 .expect("Overflow occurred when adding `Price`"),
475 precision: self.precision,
476 }
477 }
478}
479
480impl Sub for Price {
481 type Output = Self;
482 fn sub(self, rhs: Self) -> Self::Output {
483 assert!(
489 self.precision >= rhs.precision,
490 "Precision mismatch: cannot subtract precision {} from precision {} (precision loss)",
491 rhs.precision,
492 self.precision,
493 );
494 Self {
495 raw: self
496 .raw
497 .checked_sub(rhs.raw)
498 .expect("Underflow occurred when subtracting `Price`"),
499 precision: self.precision,
500 }
501 }
502}
503
504impl AddAssign for Price {
505 fn add_assign(&mut self, other: Self) {
506 assert!(
507 self.precision >= other.precision,
508 "Precision mismatch: cannot add precision {} to precision {} (precision loss)",
509 other.precision,
510 self.precision,
511 );
512 self.raw = self
513 .raw
514 .checked_add(other.raw)
515 .expect("Overflow occurred when adding `Price`");
516 }
517}
518
519impl SubAssign for Price {
520 fn sub_assign(&mut self, other: Self) {
521 assert!(
522 self.precision >= other.precision,
523 "Precision mismatch: cannot subtract precision {} from precision {} (precision loss)",
524 other.precision,
525 self.precision,
526 );
527 self.raw = self
528 .raw
529 .checked_sub(other.raw)
530 .expect("Underflow occurred when subtracting `Price`");
531 }
532}
533
534impl Add<f64> for Price {
535 type Output = f64;
536 fn add(self, rhs: f64) -> Self::Output {
537 self.as_f64() + rhs
538 }
539}
540
541impl Sub<f64> for Price {
542 type Output = f64;
543 fn sub(self, rhs: f64) -> Self::Output {
544 self.as_f64() - rhs
545 }
546}
547
548impl Mul<f64> for Price {
549 type Output = f64;
550 fn mul(self, rhs: f64) -> Self::Output {
551 self.as_f64() * rhs
552 }
553}
554
555impl Debug for Price {
556 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
557 if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
558 write!(f, "{}({})", stringify!(Price), self.raw)
559 } else {
560 write!(f, "{}({})", stringify!(Price), self.as_decimal())
561 }
562 }
563}
564
565impl Display for Price {
566 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
567 if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
568 write!(f, "{}", self.raw)
569 } else {
570 write!(f, "{}", self.as_decimal())
571 }
572 }
573}
574
575impl Serialize for Price {
576 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
577 where
578 S: serde::Serializer,
579 {
580 serializer.serialize_str(&self.to_string())
581 }
582}
583
584impl<'de> Deserialize<'de> for Price {
585 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
586 where
587 D: Deserializer<'de>,
588 {
589 let price_str: &str = Deserialize::deserialize(_deserializer)?;
590 let price: Self = price_str.into();
591 Ok(price)
592 }
593}
594
595pub fn check_positive_price(value: Price, param: &str) -> anyhow::Result<()> {
601 if value.raw == PRICE_UNDEF {
602 anyhow::bail!("invalid `Price` for '{param}', was PRICE_UNDEF")
603 }
604 if !value.is_positive() {
605 anyhow::bail!("invalid `Price` for '{param}' not positive, was {value}")
606 }
607 Ok(())
608}
609
610#[cfg(feature = "high-precision")]
611pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
614 value as PriceRaw * PRECISION_DIFF_SCALAR as PriceRaw
615}
616
617#[cfg(not(feature = "high-precision"))]
618pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
619 value
620}
621
622#[cfg(test)]
626mod tests {
627 use nautilus_core::approx_eq;
628 use rstest::rstest;
629 use rust_decimal_macros::dec;
630
631 use super::*;
632
633 #[rstest]
634 #[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
635 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 50")]
636 fn test_invalid_precision_new() {
637 let _ = Price::new(1.0, 50);
639 }
640
641 #[rstest]
642 #[cfg(all(not(feature = "defi"), feature = "high-precision"))]
643 #[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 50")]
644 fn test_invalid_precision_new() {
645 let _ = Price::new(1.0, 50);
647 }
648
649 #[rstest]
650 #[cfg(not(feature = "defi"))]
651 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
652 fn test_invalid_precision_from_raw() {
653 let _ = Price::from_raw(1, FIXED_PRECISION + 1);
655 }
656
657 #[rstest]
658 #[cfg(not(feature = "defi"))]
659 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
660 fn test_invalid_precision_max() {
661 let _ = Price::max(FIXED_PRECISION + 1);
663 }
664
665 #[rstest]
666 #[cfg(not(feature = "defi"))]
667 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
668 fn test_invalid_precision_min() {
669 let _ = Price::min(FIXED_PRECISION + 1);
671 }
672
673 #[rstest]
674 #[cfg(not(feature = "defi"))]
675 #[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
676 fn test_invalid_precision_zero() {
677 let _ = Price::zero(FIXED_PRECISION + 1);
679 }
680
681 #[rstest]
682 #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
683 fn test_max_value_exceeded() {
684 Price::new(PRICE_MAX + 0.1, FIXED_PRECISION);
685 }
686
687 #[rstest]
688 #[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
689 fn test_min_value_exceeded() {
690 Price::new(PRICE_MIN - 0.1, FIXED_PRECISION);
691 }
692
693 #[rstest]
694 fn test_is_positive_ok() {
695 let price = Price::new(42.0, 2);
697 assert!(price.is_positive());
698
699 check_positive_price(price, "price").unwrap();
701 }
702
703 #[rstest]
704 #[should_panic(expected = "invalid `Price` for 'price' not positive")]
705 fn test_is_positive_rejects_non_positive() {
706 let zero = Price::zero(2);
708 check_positive_price(zero, "price").unwrap();
709 }
710
711 #[rstest]
712 #[should_panic(expected = "invalid `Price` for 'price', was PRICE_UNDEF")]
713 fn test_is_positive_rejects_undefined() {
714 let undef = Price::from_raw(PRICE_UNDEF, 0);
716 check_positive_price(undef, "price").unwrap();
717 }
718
719 #[rstest]
720 fn test_construction() {
721 let price = Price::new_checked(1.23456, 4);
722 assert!(price.is_ok());
723 let price = price.unwrap();
724 assert_eq!(price.precision, 4);
725 assert!(approx_eq!(f64, price.as_f64(), 1.23456, epsilon = 0.0001));
726 }
727
728 #[rstest]
729 fn test_negative_price_in_range() {
730 let neg_price = Price::new(PRICE_MIN / 2.0, FIXED_PRECISION);
732 assert!(neg_price.raw < 0);
733 }
734
735 #[rstest]
736 fn test_new_checked() {
737 assert!(Price::new_checked(1.0, FIXED_PRECISION).is_ok());
739 assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
740 assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
741 }
742
743 #[rstest]
744 fn test_from_raw() {
745 let raw = 100 * FIXED_SCALAR as PriceRaw;
746 let price = Price::from_raw(raw, 2);
747 assert_eq!(price.raw, raw);
748 assert_eq!(price.precision, 2);
749 }
750
751 #[rstest]
752 fn test_zero_constructor() {
753 let zero = Price::zero(3);
754 assert!(zero.is_zero());
755 assert_eq!(zero.precision, 3);
756 }
757
758 #[rstest]
759 fn test_max_constructor() {
760 let max = Price::max(4);
761 assert_eq!(max.raw, PRICE_RAW_MAX);
762 assert_eq!(max.precision, 4);
763 }
764
765 #[rstest]
766 fn test_min_constructor() {
767 let min = Price::min(4);
768 assert_eq!(min.raw, PRICE_RAW_MIN);
769 assert_eq!(min.precision, 4);
770 }
771
772 #[rstest]
773 fn test_nan_validation() {
774 assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
775 }
776
777 #[rstest]
778 fn test_infinity_validation() {
779 assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
780 assert!(Price::new_checked(f64::NEG_INFINITY, FIXED_PRECISION).is_err());
781 }
782
783 #[rstest]
784 fn test_special_values() {
785 let zero = Price::zero(5);
786 assert!(zero.is_zero());
787 assert_eq!(zero.to_string(), "0.00000");
788
789 let undef = Price::from_raw(PRICE_UNDEF, 0);
790 assert!(undef.is_undefined());
791
792 let error = ERROR_PRICE;
793 assert_eq!(error.precision, 255);
794 }
795
796 #[rstest]
797 fn test_string_parsing() {
798 let price: Price = "123.456".into();
799 assert_eq!(price.precision, 3);
800 assert_eq!(price, Price::from("123.456"));
801 }
802
803 #[rstest]
804 fn test_negative_price_from_str() {
805 let price: Price = "-123.45".parse().unwrap();
806 assert_eq!(price.precision, 2);
807 assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-9));
808 }
809
810 #[rstest]
811 fn test_string_parsing_errors() {
812 assert!(Price::from_str("invalid").is_err());
813 }
814
815 #[rstest]
816 #[case("1e7", 0, 10_000_000.0)]
817 #[case("1.5e3", 0, 1_500.0)]
818 #[case("1.234e-2", 5, 0.01234)]
819 #[case("5E-3", 3, 0.005)]
820 fn test_from_str_scientific_notation(
821 #[case] input: &str,
822 #[case] expected_precision: u8,
823 #[case] expected_value: f64,
824 ) {
825 let price = Price::from_str(input).unwrap();
826 assert_eq!(price.precision, expected_precision);
827 assert!(approx_eq!(
828 f64,
829 price.as_f64(),
830 expected_value,
831 epsilon = 1e-10
832 ));
833 }
834
835 #[rstest]
836 #[case("1_234.56", 2, 1234.56)]
837 #[case("1_000_000", 0, 1_000_000.0)]
838 #[case("99_999.999_99", 5, 99_999.999_99)]
839 fn test_from_str_with_underscores(
840 #[case] input: &str,
841 #[case] expected_precision: u8,
842 #[case] expected_value: f64,
843 ) {
844 let price = Price::from_str(input).unwrap();
845 assert_eq!(price.precision, expected_precision);
846 assert!(approx_eq!(
847 f64,
848 price.as_f64(),
849 expected_value,
850 epsilon = 1e-10
851 ));
852 }
853
854 #[rstest]
855 fn test_from_decimal_precision_preservation() {
856 use rust_decimal::Decimal;
857
858 let decimal = Decimal::from_str("123.456789").unwrap();
860 let price = Price::from_decimal(decimal, 6).unwrap();
861 assert_eq!(price.precision, 6);
862 assert!(approx_eq!(f64, price.as_f64(), 123.456789, epsilon = 1e-10));
863
864 let expected_raw = 123456789 * 10_i64.pow((FIXED_PRECISION - 6) as u32);
866 assert_eq!(price.raw, expected_raw as PriceRaw);
867 }
868
869 #[rstest]
870 fn test_from_decimal_rounding() {
871 use rust_decimal::Decimal;
872
873 let decimal = Decimal::from_str("1.005").unwrap();
875 let price = Price::from_decimal(decimal, 2).unwrap();
876 assert_eq!(price.as_f64(), 1.0); let decimal = Decimal::from_str("1.015").unwrap();
879 let price = Price::from_decimal(decimal, 2).unwrap();
880 assert_eq!(price.as_f64(), 1.02); }
882
883 #[rstest]
884 fn test_string_formatting() {
885 assert_eq!(format!("{}", Price::new(1234.5678, 4)), "1234.5678");
886 assert_eq!(
887 format!("{:?}", Price::new(1234.5678, 4)),
888 "Price(1234.5678)"
889 );
890 assert_eq!(Price::new(1234.5678, 4).to_formatted_string(), "1_234.5678");
891 }
892
893 #[rstest]
894 #[case(1234.5678, 4, "Price(1234.5678)", "1234.5678")] #[case(123.456789012345, 8, "Price(123.45678901)", "123.45678901")] #[cfg_attr(
897 feature = "defi",
898 case(
899 2_000_000_000_000_000_000.0,
900 18,
901 "Price(2000000000000000000)",
902 "2000000000000000000"
903 )
904 )] fn test_string_formatting_precision_handling(
906 #[case] value: f64,
907 #[case] precision: u8,
908 #[case] expected_debug: &str,
909 #[case] expected_display: &str,
910 ) {
911 let price = if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
912 Price::from_raw(value as PriceRaw, precision)
913 } else {
914 Price::new(value, precision)
915 };
916
917 assert_eq!(format!("{price:?}"), expected_debug);
918 assert_eq!(format!("{price}"), expected_display);
919 assert_eq!(
920 price.to_formatted_string().replace("_", ""),
921 expected_display
922 );
923 }
924
925 #[rstest]
926 fn test_decimal_conversions() {
927 let price = Price::new(123.456, 3);
928 assert_eq!(price.as_decimal(), dec!(123.456));
929
930 let price = Price::new(0.000001, 6);
931 assert_eq!(price.as_decimal(), dec!(0.000001));
932 }
933
934 #[rstest]
935 fn test_basic_arithmetic() {
936 let p1 = Price::new(10.5, 2);
937 let p2 = Price::new(5.25, 2);
938 assert_eq!(p1 + p2, Price::from("15.75"));
939 assert_eq!(p1 - p2, Price::from("5.25"));
940 assert_eq!(-p1, Price::from("-10.5"));
941 }
942
943 #[rstest]
944 #[should_panic(expected = "Precision mismatch: cannot add precision 2 to precision 1")]
945 fn test_precision_mismatch_add() {
946 let p1 = Price::new(10.5, 1);
947 let p2 = Price::new(5.25, 2);
948 let _ = p1 + p2;
949 }
950
951 #[rstest]
952 #[should_panic(expected = "Precision mismatch: cannot subtract precision 2 from precision 1")]
953 fn test_precision_mismatch_sub() {
954 let p1 = Price::new(10.5, 1);
955 let p2 = Price::new(5.25, 2);
956 let _ = p1 - p2;
957 }
958
959 #[rstest]
960 fn test_f64_operations() {
961 let p = Price::new(10.5, 2);
962 assert_eq!(p + 1.0, 11.5);
963 assert_eq!(p - 1.0, 9.5);
964 assert_eq!(p * 2.0, 21.0);
965 }
966
967 #[rstest]
968 fn test_assignment_operators() {
969 let mut p = Price::new(10.5, 2);
970 p += Price::new(5.25, 2);
971 assert_eq!(p, Price::from("15.75"));
972 p -= Price::new(5.25, 2);
973 assert_eq!(p, Price::from("10.5"));
974 }
975
976 #[rstest]
977 fn test_equality_and_comparisons() {
978 let p1 = Price::new(10.0, 1);
979 let p2 = Price::new(20.0, 1);
980 let p3 = Price::new(10.0, 1);
981
982 assert!(p1 < p2);
983 assert!(p2 > p1);
984 assert!(p1 <= p3);
985 assert!(p1 >= p3);
986 assert_eq!(p1, p3);
987 assert_ne!(p1, p2);
988
989 assert_eq!(Price::from("1.0"), Price::from("1.0"));
990 assert_ne!(Price::from("1.1"), Price::from("1.0"));
991 assert!(Price::from("1.0") <= Price::from("1.0"));
992 assert!(Price::from("1.1") > Price::from("1.0"));
993 assert!(Price::from("1.0") >= Price::from("1.0"));
994 assert!(Price::from("1.0") >= Price::from("1.0"));
995 assert!(Price::from("1.0") >= Price::from("1.0"));
996 assert!(Price::from("0.9") < Price::from("1.0"));
997 assert!(Price::from("0.9") <= Price::from("1.0"));
998 assert!(Price::from("0.9") <= Price::from("1.0"));
999 }
1000
1001 #[rstest]
1002 fn test_deref() {
1003 let price = Price::new(10.0, 1);
1004 assert_eq!(*price, price.raw);
1005 }
1006
1007 #[rstest]
1008 fn test_decode_raw_price_i64() {
1009 let raw_scaled_by_1e9 = 42_000_000_000i64; let decoded = decode_raw_price_i64(raw_scaled_by_1e9);
1011 let price = Price::from_raw(decoded, FIXED_PRECISION);
1012 assert!(
1013 approx_eq!(f64, price.as_f64(), 42.0, epsilon = 1e-9),
1014 "Expected 42.0 f64, was {} (precision = {})",
1015 price.as_f64(),
1016 price.precision
1017 );
1018 }
1019
1020 #[rstest]
1021 fn test_hash() {
1022 use std::{
1023 collections::hash_map::DefaultHasher,
1024 hash::{Hash, Hasher},
1025 };
1026
1027 let price1 = Price::new(1.0, 2);
1028 let price2 = Price::new(1.0, 2);
1029 let price3 = Price::new(1.1, 2);
1030
1031 let mut hasher1 = DefaultHasher::new();
1032 let mut hasher2 = DefaultHasher::new();
1033 let mut hasher3 = DefaultHasher::new();
1034
1035 price1.hash(&mut hasher1);
1036 price2.hash(&mut hasher2);
1037 price3.hash(&mut hasher3);
1038
1039 assert_eq!(hasher1.finish(), hasher2.finish());
1040 assert_ne!(hasher1.finish(), hasher3.finish());
1041 }
1042
1043 #[rstest]
1044 fn test_price_serde_json_round_trip() {
1045 let price = Price::new(1.0500, 4);
1046 let json = serde_json::to_string(&price).unwrap();
1047 let deserialized: Price = serde_json::from_str(&json).unwrap();
1048 assert_eq!(deserialized, price);
1049 }
1050}
1051
1052#[cfg(test)]
1056mod property_tests {
1057 use proptest::prelude::*;
1058 use rstest::rstest;
1059
1060 use super::*;
1061
1062 fn price_value_strategy() -> impl Strategy<Value = f64> {
1064 prop_oneof![
1067 0.00001..1.0,
1069 1.0..100_000.0,
1071 100_000.0..1_000_000.0,
1073 -1_000.0..0.0,
1075 Just(PRICE_MIN / 2.0),
1077 Just(PRICE_MAX / 2.0),
1078 ]
1079 }
1080
1081 fn float_precision_upper_bound() -> u8 {
1082 FIXED_PRECISION.min(crate::types::fixed::MAX_FLOAT_PRECISION)
1083 }
1084
1085 fn precision_strategy() -> impl Strategy<Value = u8> {
1087 let upper = float_precision_upper_bound();
1088 prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
1089 }
1090
1091 fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
1092 let upper = float_precision_upper_bound().max(1);
1093 prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
1094 }
1095
1096 fn price_raw_strategy() -> impl Strategy<Value = PriceRaw> {
1097 prop_oneof![
1098 Just(PRICE_RAW_MIN),
1099 Just(PRICE_RAW_MAX),
1100 PRICE_RAW_MIN..=PRICE_RAW_MAX,
1101 ]
1102 }
1103
1104 fn float_precision_strategy() -> impl Strategy<Value = u8> {
1106 precision_strategy()
1107 }
1108
1109 proptest! {
1110 #[rstest]
1112 fn prop_price_serde_round_trip(
1113 value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1114 precision in precision_strategy()
1115 ) {
1116 let original = Price::new(value, precision);
1117
1118 let string_repr = original.to_string();
1120 let from_string: Price = string_repr.parse().unwrap();
1121 prop_assert_eq!(from_string.raw, original.raw);
1122 prop_assert_eq!(from_string.precision, original.precision);
1123
1124 let json = serde_json::to_string(&original).unwrap();
1126 let from_json: Price = serde_json::from_str(&json).unwrap();
1127 prop_assert_eq!(from_json.precision, original.precision);
1128 }
1130
1131 #[rstest]
1133 fn prop_price_arithmetic_associative(
1134 a in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1135 b in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1136 c in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1137 precision in precision_strategy()
1138 ) {
1139 let p_a = Price::new(a, precision);
1140 let p_b = Price::new(b, precision);
1141 let p_c = Price::new(c, precision);
1142
1143 let ab_raw = p_a.raw.checked_add(p_b.raw);
1145 let bc_raw = p_b.raw.checked_add(p_c.raw);
1146
1147 if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
1148 let ab_c_raw = ab_raw.checked_add(p_c.raw);
1149 let a_bc_raw = p_a.raw.checked_add(bc_raw);
1150
1151 if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
1152 prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
1154 }
1155 }
1156 }
1157
1158 #[rstest]
1160 fn prop_price_addition_subtraction_inverse(
1161 base in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1162 delta in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
1163 precision in precision_strategy()
1164 ) {
1165 let p_base = Price::new(base, precision);
1166 let p_delta = Price::new(delta, precision);
1167
1168 if let Some(added_raw) = p_base.raw.checked_add(p_delta.raw)
1170 && let Some(result_raw) = added_raw.checked_sub(p_delta.raw) {
1171 prop_assert_eq!(result_raw, p_base.raw, "Inverse operation failed in raw arithmetic");
1173 }
1174 }
1175
1176 #[rstest]
1178 fn prop_price_ordering_transitive(
1179 a in price_value_strategy(),
1180 b in price_value_strategy(),
1181 c in price_value_strategy(),
1182 precision in float_precision_strategy()
1183 ) {
1184 let p_a = Price::new(a, precision);
1185 let p_b = Price::new(b, precision);
1186 let p_c = Price::new(c, precision);
1187
1188 if p_a <= p_b && p_b <= p_c {
1190 prop_assert!(p_a <= p_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
1191 p_a.as_f64(), p_b.as_f64(), p_c.as_f64(), p_a.as_f64(), p_c.as_f64());
1192 }
1193 }
1194
1195 #[rstest]
1197 fn prop_price_string_parsing_precision(
1198 integral in 0u32..1000000,
1199 fractional in 0u32..1000000,
1200 precision in precision_strategy_non_zero()
1201 ) {
1202 let pow = 10u128.pow(u32::from(precision));
1204 let fractional_mod = (fractional as u128) % pow;
1205 let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
1206 let price_str = format!("{integral}.{fractional_str}");
1207
1208 let parsed: Price = price_str.parse().unwrap();
1209 prop_assert_eq!(parsed.precision, precision);
1210
1211 let round_trip = parsed.to_string();
1213 let expected_value = format!("{integral}.{fractional_str}");
1214 prop_assert_eq!(round_trip, expected_value);
1215 }
1216
1217 #[rstest]
1219 fn prop_price_precision_information_preservation(
1220 value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
1221 precision1 in precision_strategy_non_zero(),
1222 precision2 in precision_strategy_non_zero()
1223 ) {
1224 prop_assume!(precision1 != precision2);
1226
1227 let _p1 = Price::new(value, precision1);
1228 let _p2 = Price::new(value, precision2);
1229
1230 let min_precision = precision1.min(precision2);
1233
1234 let scale = 10.0_f64.powi(min_precision as i32);
1236 let rounded_value = (value * scale).round() / scale;
1237
1238 let p1_reduced = Price::new(rounded_value, min_precision);
1239 let p2_reduced = Price::new(rounded_value, min_precision);
1240
1241 prop_assert_eq!(p1_reduced.raw, p2_reduced.raw, "Precision reduction inconsistent");
1243 }
1244
1245 #[rstest]
1247 fn prop_price_arithmetic_bounds(
1248 a in price_value_strategy(),
1249 b in price_value_strategy(),
1250 precision in float_precision_strategy()
1251 ) {
1252 let p_a = Price::new(a, precision);
1253 let p_b = Price::new(b, precision);
1254
1255 let sum_f64 = p_a.as_f64() + p_b.as_f64();
1257 if sum_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&sum_f64) {
1258 let sum = p_a + p_b;
1259 prop_assert!(sum.as_f64().is_finite());
1260 prop_assert!(!sum.is_undefined());
1261 }
1262
1263 let diff_f64 = p_a.as_f64() - p_b.as_f64();
1265 if diff_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&diff_f64) {
1266 let diff = p_a - p_b;
1267 prop_assert!(diff.as_f64().is_finite());
1268 prop_assert!(!diff.is_undefined());
1269 }
1270 }
1271 }
1272
1273 proptest! {
1274 #[rstest]
1276 fn prop_price_from_raw_round_trip(
1277 raw in price_raw_strategy(),
1278 precision in precision_strategy()
1279 ) {
1280 let price = Price::from_raw(raw, precision);
1281 prop_assert_eq!(price.raw, raw);
1282 prop_assert_eq!(price.precision, precision);
1283 }
1284 }
1285}