1use std::fmt::Display;
62
63use nautilus_core::correctness::{
64 CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
65};
66use rust_decimal::Decimal;
67
68use crate::types::{price::PriceRaw, quantity::QuantityRaw};
69
70#[unsafe(no_mangle)]
78#[allow(unsafe_code)]
79pub static HIGH_PRECISION_MODE: u8 = cfg!(feature = "high-precision") as u8;
80
81#[cfg(feature = "high-precision")]
86pub const FIXED_PRECISION: u8 = 16;
88
89#[cfg(not(feature = "high-precision"))]
90pub const FIXED_PRECISION: u8 = 9;
92
93#[cfg(feature = "high-precision")]
98pub const PRECISION_BYTES: i32 = 16;
100
101#[cfg(not(feature = "high-precision"))]
102pub const PRECISION_BYTES: i32 = 8;
104
105#[cfg(feature = "high-precision")]
110pub const FIXED_SIZE_BINARY: &str = "FixedSizeBinary(16)";
112
113#[cfg(not(feature = "high-precision"))]
114pub const FIXED_SIZE_BINARY: &str = "FixedSizeBinary(8)";
116
117#[cfg(feature = "high-precision")]
122pub(crate) const FIXED_SCALAR_RAW: QuantityRaw = 10_000_000_000_000_000;
123
124#[cfg(not(feature = "high-precision"))]
125pub(crate) const FIXED_SCALAR_RAW: QuantityRaw = 1_000_000_000;
126
127#[cfg(feature = "high-precision")]
128pub const FIXED_SCALAR: f64 = 10_000_000_000_000_000.0;
130
131#[cfg(not(feature = "high-precision"))]
132pub const FIXED_SCALAR: f64 = 1_000_000_000.0;
134
135#[cfg(feature = "high-precision")]
140pub const PRECISION_DIFF_SCALAR: f64 = 10_000_000.0; #[cfg(not(feature = "high-precision"))]
144pub const PRECISION_DIFF_SCALAR: f64 = 1.0;
146
147const POWERS_OF_10: [u64; 17] = [
156 1, 10, 100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000, 10_000_000_000, 100_000_000_000, 1_000_000_000_000, 10_000_000_000_000, 100_000_000_000_000, 1_000_000_000_000_000, 10_000_000_000_000_000, ];
174
175const _: () = assert!(
178 (FIXED_PRECISION as usize) < POWERS_OF_10.len(),
179 "FIXED_PRECISION exceeds POWERS_OF_10 table size"
180);
181
182pub const MAX_FLOAT_PRECISION: u8 = 16;
193
194pub fn check_fixed_precision(precision: u8) -> CorrectnessResult<()> {
202 #[cfg(feature = "defi")]
203 if precision > crate::defi::WEI_PRECISION {
204 return Err(CorrectnessError::PredicateViolation {
205 message: format!("`precision` exceeded maximum `WEI_PRECISION` (18), was {precision}"),
206 });
207 }
208
209 #[cfg(not(feature = "defi"))]
210 if precision > FIXED_PRECISION {
211 return Err(CorrectnessError::PredicateViolation {
212 message: format!(
213 "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {precision}"
214 ),
215 });
216 }
217
218 Ok(())
219}
220
221#[inline]
232#[must_use]
233pub fn raw_scales_match(a: u8, b: u8) -> bool {
234 a.max(FIXED_PRECISION) == b.max(FIXED_PRECISION)
235}
236
237#[inline]
239#[must_use]
240pub(crate) fn raw_scale(precision: u8) -> u128 {
241 10_u128.pow(u32::from(precision.max(FIXED_PRECISION)))
242}
243
244#[must_use]
253pub(crate) fn scaled_raw_to_decimal(scaled_raw: i128, precision: u8) -> Decimal {
254 let scale = u32::from(precision);
255
256 Decimal::try_from_i128_with_scale(scaled_raw, scale).unwrap_or_else(|_| {
257 let divisor = 10_i128.pow(scale);
258
259 Decimal::from(scaled_raw / divisor)
260 + Decimal::from_i128_with_scale(scaled_raw % divisor, scale)
261 })
262}
263
264#[must_use]
268pub(crate) fn checked_mul_div_fixed(lhs: QuantityRaw, rhs: QuantityRaw) -> Option<QuantityRaw> {
269 let lhs_whole = lhs / FIXED_SCALAR_RAW;
270 let lhs_remainder = lhs % FIXED_SCALAR_RAW;
271 let rhs_whole = rhs / FIXED_SCALAR_RAW;
272 let rhs_remainder = rhs % FIXED_SCALAR_RAW;
273
274 lhs_whole
275 .checked_mul(rhs)
276 .and_then(|whole| {
277 lhs_remainder
278 .checked_mul(rhs_whole)
279 .and_then(|mixed| whole.checked_add(mixed))
280 })
281 .and_then(|whole_and_mixed| {
282 lhs_remainder
283 .checked_mul(rhs_remainder)
284 .map(|fractional| fractional / FIXED_SCALAR_RAW)
285 .and_then(|fractional| whole_and_mixed.checked_add(fractional))
286 })
287}
288
289const _: () = {
290 assert!(FIXED_SCALAR_RAW > 0);
291 assert!((FIXED_SCALAR_RAW as f64).to_bits() == FIXED_SCALAR.to_bits());
292 let max_remainder = FIXED_SCALAR_RAW - 1;
293 assert!(max_remainder.checked_mul(max_remainder).is_some());
294};
295
296#[inline(always)]
306fn should_skip_validation(precision: u8) -> bool {
307 #[cfg(not(feature = "defi"))]
308 debug_assert!(
309 precision <= FIXED_PRECISION,
310 "precision {precision} exceeds FIXED_PRECISION {FIXED_PRECISION}: \
311 raw value validation is not possible at this precision"
312 );
313
314 precision >= FIXED_PRECISION
315}
316
317#[cold]
319fn invalid_raw_error(
320 raw: impl Display,
321 precision: u8,
322 remainder: impl Display,
323 scale: impl Display,
324) -> anyhow::Error {
325 anyhow::anyhow!(
326 "Invalid fixed-point raw value {raw} for precision {precision}: \
327 remainder {remainder} when divided by scale {scale}. \
328 Raw value should be a multiple of {scale}. \
329 This indicates data corruption or incorrect precision/scaling upstream"
330 )
331}
332
333#[inline(always)]
363pub fn check_fixed_raw_u128(raw: u128, precision: u8) -> anyhow::Result<()> {
364 if should_skip_validation(precision) {
365 return Ok(());
366 }
367
368 let exp = usize::from(FIXED_PRECISION - precision);
369 let scale = u128::from(POWERS_OF_10[exp]);
370 let remainder = raw % scale;
371
372 if remainder != 0 {
373 return Err(invalid_raw_error(raw, precision, remainder, scale));
374 }
375
376 Ok(())
377}
378
379#[inline(always)]
388pub fn check_fixed_raw_u64(raw: u64, precision: u8) -> anyhow::Result<()> {
389 if should_skip_validation(precision) {
390 return Ok(());
391 }
392
393 let exp = usize::from(FIXED_PRECISION - precision);
394 let scale = POWERS_OF_10[exp];
395 let remainder = raw % scale;
396
397 if remainder != 0 {
398 return Err(invalid_raw_error(raw, precision, remainder, scale));
399 }
400
401 Ok(())
402}
403
404#[inline(always)]
434pub fn check_fixed_raw_i128(raw: i128, precision: u8) -> anyhow::Result<()> {
435 if should_skip_validation(precision) {
436 return Ok(());
437 }
438
439 let exp = usize::from(FIXED_PRECISION - precision);
440 let scale = i128::from(POWERS_OF_10[exp]);
441 let remainder = raw % scale;
442
443 if remainder != 0 {
444 return Err(invalid_raw_error(raw, precision, remainder, scale));
445 }
446
447 Ok(())
448}
449
450#[inline(always)]
459pub fn check_fixed_raw_i64(raw: i64, precision: u8) -> anyhow::Result<()> {
460 if should_skip_validation(precision) {
461 return Ok(());
462 }
463
464 let exp = usize::from(FIXED_PRECISION - precision);
465 let scale = POWERS_OF_10[exp].cast_signed();
466 let remainder = raw % scale;
467
468 if remainder != 0 {
469 return Err(invalid_raw_error(raw, precision, remainder, scale));
470 }
471
472 Ok(())
473}
474
475#[must_use]
492pub fn correct_raw_u128(raw: u128, precision: u8) -> u128 {
493 if precision >= FIXED_PRECISION {
494 return raw;
495 }
496 let exp = usize::from(FIXED_PRECISION - precision);
497 let scale = u128::from(POWERS_OF_10[exp]);
498 let half_scale = scale / 2;
499 let remainder = raw % scale;
500 if remainder == 0 {
501 raw
502 } else if remainder >= half_scale {
503 raw.checked_add(scale - remainder)
504 .unwrap_or(raw - remainder)
505 } else {
506 raw - remainder
507 }
508}
509
510#[must_use]
518pub fn correct_raw_u64(raw: u64, precision: u8) -> u64 {
519 if precision >= FIXED_PRECISION {
520 return raw;
521 }
522 let exp = usize::from(FIXED_PRECISION - precision);
523 let scale = POWERS_OF_10[exp];
524 let half_scale = scale / 2;
525 let remainder = raw % scale;
526 if remainder == 0 {
527 raw
528 } else if remainder >= half_scale {
529 raw.checked_add(scale - remainder)
530 .unwrap_or(raw - remainder)
531 } else {
532 raw - remainder
533 }
534}
535
536#[must_use]
544pub fn correct_raw_i128(raw: i128, precision: u8) -> i128 {
545 if precision >= FIXED_PRECISION {
546 return raw;
547 }
548 let exp = usize::from(FIXED_PRECISION - precision);
549 let scale = i128::from(POWERS_OF_10[exp]);
550 let half_scale = scale / 2;
551 let remainder = raw % scale;
552 if remainder == 0 {
553 raw
554 } else if raw >= 0 {
555 if remainder >= half_scale {
556 raw.checked_add(scale - remainder)
557 .unwrap_or(raw - remainder)
558 } else {
559 raw - remainder
560 }
561 } else {
562 if remainder.abs() >= half_scale {
564 raw.checked_sub(scale + remainder)
565 .unwrap_or(raw - remainder)
566 } else {
567 raw - remainder
568 }
569 }
570}
571
572#[must_use]
580pub fn correct_raw_i64(raw: i64, precision: u8) -> i64 {
581 if precision >= FIXED_PRECISION {
582 return raw;
583 }
584 let exp = usize::from(FIXED_PRECISION - precision);
585 let scale = POWERS_OF_10[exp].cast_signed();
586 let half_scale = scale / 2;
587 let remainder = raw % scale;
588 if remainder == 0 {
589 raw
590 } else if raw >= 0 {
591 if remainder >= half_scale {
592 raw.checked_add(scale - remainder)
593 .unwrap_or(raw - remainder)
594 } else {
595 raw - remainder
596 }
597 } else {
598 if remainder.abs() >= half_scale {
600 raw.checked_sub(scale + remainder)
601 .unwrap_or(raw - remainder)
602 } else {
603 raw - remainder
604 }
605 }
606}
607
608#[must_use]
614#[inline]
615pub fn correct_price_raw(raw: PriceRaw, precision: u8) -> PriceRaw {
616 #[cfg(feature = "high-precision")]
617 {
618 correct_raw_i128(raw, precision)
619 }
620 #[cfg(not(feature = "high-precision"))]
621 {
622 correct_raw_i64(raw, precision)
623 }
624}
625
626#[must_use]
632#[inline]
633pub fn correct_quantity_raw(raw: QuantityRaw, precision: u8) -> QuantityRaw {
634 #[cfg(feature = "high-precision")]
635 {
636 correct_raw_u128(raw, precision)
637 }
638 #[cfg(not(feature = "high-precision"))]
639 {
640 correct_raw_u64(raw, precision)
641 }
642}
643
644#[must_use]
649#[inline]
650pub fn bankers_round(mantissa: i128, excess: u32) -> i128 {
651 if excess == 0 {
652 return mantissa;
653 }
654
655 if excess >= 39 {
657 return 0;
658 }
659
660 let divisor = 10i128.pow(excess);
661 let quotient = mantissa / divisor;
662 let remainder = mantissa % divisor;
663 let half = divisor / 2;
664
665 if remainder.abs() > half || (remainder.abs() == half && quotient % 2 != 0) {
666 quotient + mantissa.signum()
667 } else {
668 quotient
669 }
670}
671
672pub fn mantissa_exponent_to_fixed_i128(
687 mantissa: i128,
688 exponent: i8,
689 precision: u8,
690) -> CorrectnessResult<i128> {
691 check_fixed_precision(precision)?;
692
693 let precision_i16 = i16::from(precision);
694 let target_scale = i16::from(FIXED_PRECISION).max(precision_i16);
695 let frac_digits = -i16::from(exponent);
696
697 let mantissa = if frac_digits > precision_i16 {
698 let excess = u32::from((frac_digits - precision_i16).cast_unsigned());
699 bankers_round(mantissa, excess)
700 } else {
701 mantissa
702 };
703
704 let scale_after_rounding = frac_digits.min(precision_i16);
705 let scale_exp = target_scale - scale_after_rounding;
706 if scale_exp > 38 {
707 return Err(CorrectnessError::PredicateViolation {
708 message: format!(
709 "Exponent {exponent} produces scale factor 10^{scale_exp} which exceeds i128 range"
710 ),
711 });
712 }
713
714 if scale_exp >= 0 {
715 mantissa.checked_mul(10i128.pow(u32::from(scale_exp.cast_unsigned())))
716 } else {
717 Some(mantissa / 10i128.pow(u32::from((-scale_exp).cast_unsigned())))
718 }
719 .ok_or_else(|| CorrectnessError::PredicateViolation {
720 message: "Overflow when scaling mantissa to fixed precision".to_string(),
721 })
722}
723
724pub(crate) fn mantissa_exponent_to_raw_checked<R>(
725 mantissa: i128,
726 exponent: i8,
727 precision: u8,
728 context: &'static str,
729 raw_type_name: &'static str,
730 value_type_name: &'static str,
731) -> CorrectnessResult<R>
732where
733 R: TryFrom<i128>,
734{
735 check_fixed_precision(precision)?;
736
737 let raw_i128 = if mantissa == 0 {
738 0
739 } else {
740 mantissa_exponent_to_fixed_i128(mantissa, exponent, precision).map_err(|_| {
741 CorrectnessError::PredicateViolation {
742 message: format!(
743 "Overflow in {context} (mantissa={mantissa}, exponent={exponent}, precision={precision})"
744 ),
745 }
746 })?
747 };
748
749 raw_i128
750 .try_into()
751 .map_err(|_| CorrectnessError::PredicateViolation {
752 message: format!(
753 "Raw value {raw_i128} exceeds {raw_type_name} range for {value_type_name}"
754 ),
755 })
756}
757
758#[must_use]
775#[expect(
776 clippy::cast_precision_loss,
777 clippy::cast_possible_truncation,
778 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
779)]
780pub fn f64_to_fixed_i64(value: f64, precision: u8) -> i64 {
781 check_fixed_precision(precision).expect_display(FAILED);
782 let pow1 = 10_i64.pow(u32::from(precision));
783 let pow2 = 10_i64.pow(u32::from(FIXED_PRECISION - precision));
784 let rounded = (value * pow1 as f64).round() as i64;
785 rounded
786 .checked_mul(pow2)
787 .expect("Overflow when scaling f64 to fixed-point i64")
788}
789
790#[must_use]
800#[expect(
801 clippy::cast_precision_loss,
802 clippy::cast_possible_truncation,
803 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
804)]
805pub fn f64_to_fixed_i128(value: f64, precision: u8) -> i128 {
806 check_fixed_precision(precision).expect_display(FAILED);
807 let pow1 = 10_i128.pow(u32::from(precision));
808 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
809 let rounded = (value * pow1 as f64).round() as i128;
810 rounded
811 .checked_mul(pow2)
812 .expect("Overflow when scaling f64 to fixed-point i128")
813}
814
815#[must_use]
825#[expect(
826 clippy::cast_precision_loss,
827 clippy::cast_possible_truncation,
828 clippy::cast_sign_loss,
829 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
830)]
831pub fn f64_to_fixed_u64(value: f64, precision: u8) -> u64 {
832 check_fixed_precision(precision).expect_display(FAILED);
833 let pow1 = 10_u64.pow(u32::from(precision));
834 let pow2 = 10_u64.pow(u32::from(FIXED_PRECISION - precision));
835 let rounded = (value * pow1 as f64).round() as u64;
836 rounded
837 .checked_mul(pow2)
838 .expect("Overflow when scaling f64 to fixed-point u64")
839}
840
841#[must_use]
851#[expect(
852 clippy::cast_precision_loss,
853 clippy::cast_possible_truncation,
854 clippy::cast_sign_loss,
855 reason = "f64 to fixed-point conversion is inherently lossy; callers validate range and finiteness"
856)]
857pub fn f64_to_fixed_u128(value: f64, precision: u8) -> u128 {
858 check_fixed_precision(precision).expect_display(FAILED);
859 let pow1 = 10_u128.pow(u32::from(precision));
860 let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
861 let rounded = (value * pow1 as f64).round() as u128;
862 rounded
863 .checked_mul(pow2)
864 .expect("Overflow when scaling f64 to fixed-point u128")
865}
866
867#[must_use]
869#[expect(
870 clippy::cast_precision_loss,
871 reason = "i64 to f64 is inherently lossy above 2^53; accepted for float interop"
872)]
873pub fn fixed_i64_to_f64(value: i64) -> f64 {
874 (value as f64) / FIXED_SCALAR
875}
876
877#[must_use]
879#[expect(
880 clippy::cast_precision_loss,
881 reason = "i128 to f64 is inherently lossy above 2^53; accepted for float interop"
882)]
883pub fn fixed_i128_to_f64(value: i128) -> f64 {
884 (value as f64) / FIXED_SCALAR
885}
886
887#[must_use]
889#[expect(
890 clippy::cast_precision_loss,
891 reason = "u64 to f64 is inherently lossy above 2^53; accepted for float interop"
892)]
893pub fn fixed_u64_to_f64(value: u64) -> f64 {
894 (value as f64) / FIXED_SCALAR
895}
896
897#[must_use]
899#[expect(
900 clippy::cast_precision_loss,
901 reason = "u128 to f64 is inherently lossy above 2^53; accepted for float interop"
902)]
903pub fn fixed_u128_to_f64(value: u128) -> f64 {
904 (value as f64) / FIXED_SCALAR
905}
906
907#[cfg(feature = "high-precision")]
908#[cfg(test)]
909mod tests {
910 use nautilus_core::approx_eq;
911 use rstest::rstest;
912
913 use super::*;
914
915 #[cfg(not(feature = "defi"))]
916 #[rstest]
917 fn test_precision_boundaries() {
918 assert!(check_fixed_precision(0).is_ok());
919 assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
920 assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
921 }
922
923 #[rstest]
924 #[case(0, 0, "0")]
925 #[case(125, 2, "1.25")]
926 #[case(-1234, 2, "-12.34")]
927 #[case(1, 16, "0.0000000000000001")]
928 #[case(-1, 16, "-0.0000000000000001")]
929 #[case(1_000_000_000_000_000_000, 18, "1.000000000000000000")]
930 fn test_scaled_raw_to_decimal_matches_plain_conversion(
931 #[case] raw: i128,
932 #[case] precision: u8,
933 #[case] expected: &str,
934 ) {
935 let plain = Decimal::from_i128_with_scale(raw, u32::from(precision));
936 let result = scaled_raw_to_decimal(raw, precision);
937
938 assert_eq!(result, plain);
939 assert_eq!(result.scale(), plain.scale());
940 assert_eq!(result.to_string(), expected);
941 }
942
943 #[rstest]
944 #[case(80_000_000_000_000_000_000_000_000_000, 16, "8000000000000")]
945 #[case(340_282_366_920_930_000_000_000_000_000, 16, "34028236692093")]
946 #[case(170_141_183_460_460_000_000_000_000_000, 16, "17014118346046")]
947 #[case(-170_141_183_460_460_000_000_000_000_000, 16, "-17014118346046")]
948 #[case(
951 80_000_000_000_000_005_000_000_000_000,
952 16,
953 "8000000000000.000500000000000"
954 )]
955 #[case(-80_000_000_000_000_005_000_000_000_000, 16, "-8000000000000.000500000000000")]
956 #[case(
957 80_000_000_000_000_000_000_000_000_001,
958 16,
959 "8000000000000.000000000000000"
960 )]
961 #[case(
962 80_000_000_000_000_000_250_000_000_000,
963 18,
964 "80000000000.00000025000000000"
965 )]
966 #[case(-80_000_000_000_000_000_250_000_000_000, 18, "-80000000000.00000025000000000")]
967 fn test_scaled_raw_to_decimal_beyond_mantissa_rounds_rather_than_panics(
968 #[case] raw: i128,
969 #[case] precision: u8,
970 #[case] expected: &str,
971 ) {
972 assert_eq!(scaled_raw_to_decimal(raw, precision).to_string(), expected);
976 }
977
978 #[cfg(feature = "defi")]
979 #[rstest]
980 fn test_precision_boundaries() {
981 use crate::defi::WEI_PRECISION;
982
983 assert!(check_fixed_precision(0).is_ok());
984 assert!(check_fixed_precision(WEI_PRECISION).is_ok());
985 assert!(check_fixed_precision(WEI_PRECISION + 1).is_err());
986 }
987
988 #[rstest]
989 #[case(0.0)]
990 #[case(1.0)]
991 #[case(-1.0)]
992 fn test_basic_roundtrip(#[case] value: f64) {
993 for precision in 0..=FIXED_PRECISION {
994 let fixed = f64_to_fixed_i128(value, precision);
995 let result = fixed_i128_to_f64(fixed);
996 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
997 }
998 }
999
1000 #[rstest]
1001 #[case(1_000_000.0)]
1002 #[case(-1_000_000.0)]
1003 fn test_large_value_roundtrip(#[case] value: f64) {
1004 for precision in 0..=FIXED_PRECISION {
1005 let fixed = f64_to_fixed_i128(value, precision);
1006 let result = fixed_i128_to_f64(fixed);
1007 assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1008 }
1009 }
1010
1011 #[rstest]
1012 #[case(0, 123_456.0)]
1013 #[case(0, 123_456.7)]
1014 #[case(1, 123_456.7)]
1015 #[case(2, 123_456.78)]
1016 #[case(8, 123_456.123_456_78)]
1017 fn test_precision_specific_values_basic(#[case] precision: u8, #[case] value: f64) {
1018 let result = f64_to_fixed_i128(value, precision);
1019 let back_converted = fixed_i128_to_f64(result);
1020 let scale = 10.0_f64.powi(i32::from(precision));
1022 let expected_rounded = (value * scale).round() / scale;
1023 assert!((back_converted - expected_rounded).abs() < 1e-10);
1024 }
1025
1026 #[rstest]
1027 fn test_max_precision_values() {
1028 let test_value = 123_456.123_456_789;
1030 let result = f64_to_fixed_i128(test_value, FIXED_PRECISION);
1031 let back_converted = fixed_i128_to_f64(result);
1032 assert!((back_converted - test_value).abs() < 1e-6);
1034 }
1035
1036 #[rstest]
1037 #[case(0.0)]
1038 #[case(1.0)]
1039 #[case(1_000_000.0)]
1040 fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1041 for precision in 0..=FIXED_PRECISION {
1042 let fixed = f64_to_fixed_u128(value, precision);
1043 let result = fixed_u128_to_f64(fixed);
1044 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1045 }
1046 }
1047
1048 #[rstest]
1049 #[case(0)]
1050 #[case(FIXED_PRECISION)]
1051 fn test_valid_precision(#[case] precision: u8) {
1052 let result = check_fixed_precision(precision);
1053 assert!(result.is_ok());
1054 }
1055
1056 #[cfg(not(feature = "defi"))]
1057 #[rstest]
1058 fn test_invalid_precision() {
1059 let precision = FIXED_PRECISION + 1;
1060 let result = check_fixed_precision(precision);
1061 assert!(result.is_err());
1062 }
1063
1064 #[cfg(feature = "defi")]
1065 #[rstest]
1066 fn test_invalid_precision() {
1067 use crate::defi::WEI_PRECISION;
1068 let precision = WEI_PRECISION + 1;
1069 let result = check_fixed_precision(precision);
1070 assert!(result.is_err());
1071 }
1072
1073 #[cfg(not(feature = "defi"))]
1074 #[rstest]
1075 fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1076 let error = check_fixed_precision(FIXED_PRECISION + 1).unwrap_err();
1077
1078 assert_eq!(
1079 error,
1080 CorrectnessError::PredicateViolation {
1081 message: format!(
1082 "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1083 FIXED_PRECISION + 1
1084 ),
1085 }
1086 );
1087 assert_eq!(
1088 error.to_string(),
1089 format!(
1090 "`precision` exceeded maximum `FIXED_PRECISION` ({FIXED_PRECISION}), was {}",
1091 FIXED_PRECISION + 1
1092 )
1093 );
1094 }
1095
1096 #[cfg(feature = "defi")]
1097 #[rstest]
1098 fn test_check_fixed_precision_returns_typed_error_with_stable_display() {
1099 use crate::defi::WEI_PRECISION;
1100
1101 let error = check_fixed_precision(WEI_PRECISION + 1).unwrap_err();
1102
1103 assert_eq!(
1104 error,
1105 CorrectnessError::PredicateViolation {
1106 message: format!(
1107 "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1108 WEI_PRECISION + 1
1109 ),
1110 }
1111 );
1112 assert_eq!(
1113 error.to_string(),
1114 format!(
1115 "`precision` exceeded maximum `WEI_PRECISION` (18), was {}",
1116 WEI_PRECISION + 1
1117 )
1118 );
1119 }
1120
1121 #[rstest]
1122 #[case(0, 0.0)]
1123 #[case(1, 1.0)]
1124 #[case(1, 1.1)]
1125 #[case(9, 0.000_000_001)]
1126 #[case(16, 0.000_000_000_000_000_1)]
1127 #[case(0, -0.0)]
1128 #[case(1, -1.0)]
1129 #[case(1, -1.1)]
1130 #[case(9, -0.000_000_001)]
1131 #[case(16, -0.000_000_000_000_000_1)]
1132 fn test_f64_to_fixed_i128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1133 let fixed = f64_to_fixed_i128(value, precision);
1134 let result = fixed_i128_to_f64(fixed);
1135 assert_eq!(result, value);
1136 }
1137
1138 #[rstest]
1139 #[case(0, 0.0)]
1140 #[case(1, 1.0)]
1141 #[case(1, 1.1)]
1142 #[case(9, 0.000_000_001)]
1143 #[case(16, 0.000_000_000_000_000_1)]
1144 fn test_f64_to_fixed_u128_to_fixed(#[case] precision: u8, #[case] value: f64) {
1145 let fixed = f64_to_fixed_u128(value, precision);
1146 let result = fixed_u128_to_f64(fixed);
1147 assert_eq!(result, value);
1148 }
1149
1150 #[rstest]
1151 #[case(0, 123_456.0)]
1152 #[case(0, 123_456.7)]
1153 #[case(0, 123_456.4)]
1154 #[case(1, 123_456.0)]
1155 #[case(1, 123_456.7)]
1156 #[case(1, 123_456.4)]
1157 #[case(2, 123_456.0)]
1158 #[case(2, 123_456.7)]
1159 #[case(2, 123_456.4)]
1160 fn test_f64_to_fixed_i128_with_precision(#[case] precision: u8, #[case] value: f64) {
1161 let result = f64_to_fixed_i128(value, precision);
1162
1163 let pow1 = 10_i128.pow(u32::from(precision));
1165 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1166 let rounded = (value * pow1 as f64).round() as i128;
1167 let expected = rounded * pow2;
1168
1169 assert_eq!(
1170 result, expected,
1171 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1172 );
1173 }
1174
1175 #[rstest]
1176 #[case(0, 5.555_555_555_555_555)]
1177 #[case(1, 5.555_555_555_555_555)]
1178 #[case(2, 5.555_555_555_555_555)]
1179 #[case(3, 5.555_555_555_555_555)]
1180 #[case(4, 5.555_555_555_555_555)]
1181 #[case(5, 5.555_555_555_555_555)]
1182 #[case(6, 5.555_555_555_555_555)]
1183 #[case(7, 5.555_555_555_555_555)]
1184 #[case(8, 5.555_555_555_555_555)]
1185 #[case(9, 5.555_555_555_555_555)]
1186 #[case(10, 5.555_555_555_555_555)]
1187 #[case(11, 5.555_555_555_555_555)]
1188 #[case(12, 5.555_555_555_555_555)]
1189 #[case(13, 5.555_555_555_555_555)]
1190 #[case(14, 5.555_555_555_555_555)]
1191 #[case(15, 5.555_555_555_555_555)]
1192 #[case(0, -5.555_555_555_555_555)]
1193 #[case(1, -5.555_555_555_555_555)]
1194 #[case(2, -5.555_555_555_555_555)]
1195 #[case(3, -5.555_555_555_555_555)]
1196 #[case(4, -5.555_555_555_555_555)]
1197 #[case(5, -5.555_555_555_555_555)]
1198 #[case(6, -5.555_555_555_555_555)]
1199 #[case(7, -5.555_555_555_555_555)]
1200 #[case(8, -5.555_555_555_555_555)]
1201 #[case(9, -5.555_555_555_555_555)]
1202 #[case(10, -5.555_555_555_555_555)]
1203 #[case(11, -5.555_555_555_555_555)]
1204 #[case(12, -5.555_555_555_555_555)]
1205 #[case(13, -5.555_555_555_555_555)]
1206 #[case(14, -5.555_555_555_555_555)]
1207 #[case(15, -5.555_555_555_555_555)]
1208 fn test_f64_to_fixed_i128(#[case] precision: u8, #[case] value: f64) {
1209 if precision > FIXED_PRECISION {
1211 return;
1212 }
1213
1214 let result = f64_to_fixed_i128(value, precision);
1215
1216 let pow1 = 10_i128.pow(u32::from(precision));
1218 let pow2 = 10_i128.pow(u32::from(FIXED_PRECISION - precision));
1219 let rounded = (value * pow1 as f64).round() as i128;
1220 let expected = rounded * pow2;
1221
1222 assert_eq!(
1223 result, expected,
1224 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1225 );
1226 }
1227
1228 #[rstest]
1229 #[case(0, 5.555_555_555_555_555)]
1230 #[case(1, 5.555_555_555_555_555)]
1231 #[case(2, 5.555_555_555_555_555)]
1232 #[case(3, 5.555_555_555_555_555)]
1233 #[case(4, 5.555_555_555_555_555)]
1234 #[case(5, 5.555_555_555_555_555)]
1235 #[case(6, 5.555_555_555_555_555)]
1236 #[case(7, 5.555_555_555_555_555)]
1237 #[case(8, 5.555_555_555_555_555)]
1238 #[case(9, 5.555_555_555_555_555)]
1239 #[case(10, 5.555_555_555_555_555)]
1240 #[case(11, 5.555_555_555_555_555)]
1241 #[case(12, 5.555_555_555_555_555)]
1242 #[case(13, 5.555_555_555_555_555)]
1243 #[case(14, 5.555_555_555_555_555)]
1244 #[case(15, 5.555_555_555_555_555)]
1245 #[case(16, 5.555_555_555_555_555)]
1246 fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64) {
1247 if precision > FIXED_PRECISION {
1249 return;
1250 }
1251
1252 let result = f64_to_fixed_u128(value, precision);
1253
1254 let pow1 = 10_u128.pow(u32::from(precision));
1256 let pow2 = 10_u128.pow(u32::from(FIXED_PRECISION - precision));
1257 let rounded = (value * pow1 as f64).round() as u128;
1258 let expected = rounded * pow2;
1259
1260 assert_eq!(
1261 result, expected,
1262 "Failed for precision {precision}, value {value}: got {result}, expected {expected}"
1263 );
1264 }
1265
1266 #[rstest]
1267 fn test_fixed_i128_to_f64(
1268 #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000, -10_000, -100_000)] value: i128,
1269 ) {
1270 assert_eq!(fixed_i128_to_f64(value), value as f64 / FIXED_SCALAR);
1271 }
1272
1273 #[rstest]
1274 fn test_fixed_u128_to_f64(
1275 #[values(
1276 0,
1277 1,
1278 2,
1279 3,
1280 10,
1281 100,
1282 1_000,
1283 10_000,
1284 100_000,
1285 1_000_000,
1286 10_000_000,
1287 100_000_000,
1288 1_000_000_000,
1289 10_000_000_000,
1290 100_000_000_000,
1291 1_000_000_000_000,
1292 10_000_000_000_000,
1293 100_000_000_000_000,
1294 1_000_000_000_000_000,
1295 10_000_000_000_000_000,
1296 100_000_000_000_000_000,
1297 1_000_000_000_000_000_000,
1298 10_000_000_000_000_000_000,
1299 100_000_000_000_000_000_000
1300 )]
1301 value: u128,
1302 ) {
1303 let result = fixed_u128_to_f64(value);
1304 assert_eq!(result, (value as f64) / FIXED_SCALAR);
1305 }
1306
1307 #[rstest]
1312 #[case(0, 0)] #[case(0, 10_000_000_000_000_000)] #[case(0, 1_200_000_000_000_000_000)] #[case(8, 12_345_678_900_000_000)] #[case(15, 1_234_567_890_123_450)] fn test_check_fixed_raw_u128_valid(#[case] precision: u8, #[case] raw: u128) {
1318 assert!(check_fixed_raw_u128(raw, precision).is_ok());
1319 }
1320
1321 #[rstest]
1322 #[case(0, 1)] #[case(0, 9_999_999_999_999_999)] #[case(0, 10_000_000_000_000_001)] #[case(8, 12_345_678_900_000_001)] #[case(15, 1_234_567_890_123_451)] fn test_check_fixed_raw_u128_invalid(#[case] precision: u8, #[case] raw: u128) {
1328 assert!(check_fixed_raw_u128(raw, precision).is_err());
1329 }
1330
1331 #[rstest]
1332 fn test_check_fixed_raw_u128_at_max_precision() {
1333 assert!(check_fixed_raw_u128(0, FIXED_PRECISION).is_ok());
1335 assert!(check_fixed_raw_u128(1, FIXED_PRECISION).is_ok());
1336 assert!(check_fixed_raw_u128(123_456_789, FIXED_PRECISION).is_ok());
1337 assert!(check_fixed_raw_u128(u128::MAX, FIXED_PRECISION).is_ok());
1338 }
1339
1340 #[rstest]
1341 #[case(0, 0)]
1342 #[case(0, 10_000_000_000_000_000)]
1343 #[case(0, -10_000_000_000_000_000)]
1344 #[case(8, 12_345_678_900_000_000)]
1345 #[case(8, -12_345_678_900_000_000)]
1346 fn test_check_fixed_raw_i128_valid(#[case] precision: u8, #[case] raw: i128) {
1347 assert!(check_fixed_raw_i128(raw, precision).is_ok());
1348 }
1349
1350 #[rstest]
1351 #[case(0, 1)]
1352 #[case(0, -1)]
1353 #[case(0, 9_999_999_999_999_999)]
1354 #[case(0, -9_999_999_999_999_999)]
1355 fn test_check_fixed_raw_i128_invalid(#[case] precision: u8, #[case] raw: i128) {
1356 assert!(check_fixed_raw_i128(raw, precision).is_err());
1357 }
1358
1359 #[rstest]
1360 fn test_check_fixed_raw_i128_at_max_precision() {
1361 assert!(check_fixed_raw_i128(0, FIXED_PRECISION).is_ok());
1362 assert!(check_fixed_raw_i128(1, FIXED_PRECISION).is_ok());
1363 assert!(check_fixed_raw_i128(-1, FIXED_PRECISION).is_ok());
1364 assert!(check_fixed_raw_i128(i128::MAX, FIXED_PRECISION).is_ok());
1365 assert!(check_fixed_raw_i128(i128::MIN, FIXED_PRECISION).is_ok());
1366 }
1367
1368 #[rstest]
1369 #[should_panic(expected = "Overflow when scaling f64 to fixed-point i128")]
1370 fn test_f64_to_fixed_i128_overflow_panics() {
1371 let _ = f64_to_fixed_i128(1e30, 0);
1372 }
1373
1374 #[rstest]
1375 #[should_panic(expected = "Overflow when scaling f64 to fixed-point u128")]
1376 fn test_f64_to_fixed_u128_overflow_panics() {
1377 let _ = f64_to_fixed_u128(1e30, 0);
1378 }
1379}
1380
1381#[cfg(not(feature = "high-precision"))]
1382#[cfg(test)]
1383mod tests {
1384 use nautilus_core::approx_eq;
1385 use rstest::rstest;
1386
1387 use super::*;
1388
1389 #[rstest]
1390 fn test_precision_boundaries() {
1391 assert!(check_fixed_precision(0).is_ok());
1392 assert!(check_fixed_precision(FIXED_PRECISION).is_ok());
1393 assert!(check_fixed_precision(FIXED_PRECISION + 1).is_err());
1394 }
1395
1396 #[rstest]
1397 #[case(0.0)]
1398 #[case(1.0)]
1399 #[case(-1.0)]
1400 fn test_basic_roundtrip(#[case] value: f64) {
1401 for precision in 0..=FIXED_PRECISION {
1402 let fixed = f64_to_fixed_i64(value, precision);
1403 let result = fixed_i64_to_f64(fixed);
1404 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1405 }
1406 }
1407
1408 #[rstest]
1409 #[case(1_000_000.0)]
1410 #[case(-1_000_000.0)]
1411 fn test_large_value_roundtrip(#[case] value: f64) {
1412 for precision in 0..=FIXED_PRECISION {
1413 let fixed = f64_to_fixed_i64(value, precision);
1414 let result = fixed_i64_to_f64(fixed);
1415 assert!(approx_eq!(f64, value, result, epsilon = 0.000_1));
1416 }
1417 }
1418
1419 #[rstest]
1420 #[case(0, 123_456.0, 123_456_000_000_000)]
1421 #[case(0, 123_456.7, 123_457_000_000_000)]
1422 #[case(1, 123_456.7, 123_456_700_000_000)]
1423 #[case(2, 123_456.78, 123_456_780_000_000)]
1424 #[case(8, 123_456.123_456_78, 123_456_123_456_780)]
1425 #[case(9, 123_456.123_456_789, 123_456_123_456_789)]
1426 fn test_precision_specific_values(
1427 #[case] precision: u8,
1428 #[case] value: f64,
1429 #[case] expected: i64,
1430 ) {
1431 assert_eq!(f64_to_fixed_i64(value, precision), expected);
1432 }
1433
1434 #[rstest]
1435 #[case(0.0)]
1436 #[case(1.0)]
1437 #[case(1_000_000.0)]
1438 fn test_unsigned_basic_roundtrip(#[case] value: f64) {
1439 for precision in 0..=FIXED_PRECISION {
1440 let fixed = f64_to_fixed_u64(value, precision);
1441 let result = fixed_u64_to_f64(fixed);
1442 assert!(approx_eq!(f64, value, result, epsilon = 0.001));
1443 }
1444 }
1445
1446 #[rstest]
1447 #[case(0, 1.4, 1.0)]
1448 #[case(0, 1.5, 2.0)]
1449 #[case(0, 1.6, 2.0)]
1450 #[case(1, 1.44, 1.4)]
1451 #[case(1, 1.45, 1.5)]
1452 #[case(1, 1.46, 1.5)]
1453 #[case(2, 1.444, 1.44)]
1454 #[case(2, 1.445, 1.45)]
1455 #[case(2, 1.446, 1.45)]
1456 fn test_rounding(#[case] precision: u8, #[case] input: f64, #[case] expected: f64) {
1457 let fixed = f64_to_fixed_i128(input, precision);
1458 assert!(approx_eq!(
1459 f64,
1460 fixed_i128_to_f64(fixed),
1461 expected,
1462 epsilon = 0.000_000_001
1463 ));
1464 }
1465
1466 #[rstest]
1467 fn test_special_values() {
1468 assert_eq!(f64_to_fixed_i128(0.0, FIXED_PRECISION), 0);
1470 assert_eq!(f64_to_fixed_i128(-0.0, FIXED_PRECISION), 0);
1471
1472 let smallest_positive = 1.0 / FIXED_SCALAR;
1474 let fixed_smallest = f64_to_fixed_i128(smallest_positive, FIXED_PRECISION);
1475 assert_eq!(fixed_smallest, 1);
1476
1477 let large_int = 1_000_000_000.0;
1479 let fixed_large = f64_to_fixed_i128(large_int, 0);
1480 assert_eq!(fixed_i128_to_f64(fixed_large), large_int);
1481 }
1482
1483 #[rstest]
1484 #[case(0)]
1485 #[case(FIXED_PRECISION)]
1486 fn test_valid_precision(#[case] precision: u8) {
1487 let result = check_fixed_precision(precision);
1488 assert!(result.is_ok());
1489 }
1490
1491 #[rstest]
1492 fn test_invalid_precision() {
1493 let precision = FIXED_PRECISION + 1;
1494 let result = check_fixed_precision(precision);
1495 assert!(result.is_err());
1496 }
1497
1498 #[rstest]
1499 #[case(0, 0.0)]
1500 #[case(1, 1.0)]
1501 #[case(1, 1.1)]
1502 #[case(9, 0.000_000_001)]
1503 #[case(0, -0.0)]
1504 #[case(1, -1.0)]
1505 #[case(1, -1.1)]
1506 #[case(9, -0.000_000_001)]
1507 fn test_f64_to_fixed_i64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1508 let fixed = f64_to_fixed_i64(value, precision);
1509 let result = fixed_i64_to_f64(fixed);
1510 assert_eq!(result, value);
1511 }
1512
1513 #[rstest]
1514 #[case(0, 0.0)]
1515 #[case(1, 1.0)]
1516 #[case(1, 1.1)]
1517 #[case(9, 0.000_000_001)]
1518 fn test_f64_to_fixed_u64_to_fixed(#[case] precision: u8, #[case] value: f64) {
1519 let fixed = f64_to_fixed_u64(value, precision);
1520 let result = fixed_u64_to_f64(fixed);
1521 assert_eq!(result, value);
1522 }
1523
1524 #[rstest]
1525 #[case(0, 123_456.0, 123_456_000_000_000)]
1526 #[case(0, 123_456.7, 123_457_000_000_000)]
1527 #[case(0, 123_456.4, 123_456_000_000_000)]
1528 #[case(1, 123_456.0, 123_456_000_000_000)]
1529 #[case(1, 123_456.7, 123_456_700_000_000)]
1530 #[case(1, 123_456.4, 123_456_400_000_000)]
1531 #[case(2, 123_456.0, 123_456_000_000_000)]
1532 #[case(2, 123_456.7, 123_456_700_000_000)]
1533 #[case(2, 123_456.4, 123_456_400_000_000)]
1534 fn test_f64_to_fixed_i64_with_precision(
1535 #[case] precision: u8,
1536 #[case] value: f64,
1537 #[case] expected: i64,
1538 ) {
1539 assert_eq!(f64_to_fixed_i64(value, precision), expected);
1540 }
1541
1542 #[rstest]
1543 #[case(0, 5.5, 6_000_000_000)]
1544 #[case(1, 5.55, 5_600_000_000)]
1545 #[case(2, 5.555, 5_560_000_000)]
1546 #[case(3, 5.5555, 5_556_000_000)]
1547 #[case(4, 5.55555, 5_555_600_000)]
1548 #[case(5, 5.555_555, 5_555_560_000)]
1549 #[case(6, 5.555_555_5, 5_555_556_000)]
1550 #[case(7, 5.555_555_55, 5_555_555_600)]
1551 #[case(8, 5.555_555_555, 5_555_555_560)]
1552 #[case(9, 5.555_555_555_5, 5_555_555_556)]
1553 #[case(0, -5.5, -6_000_000_000)]
1554 #[case(1, -5.55, -5_600_000_000)]
1555 #[case(2, -5.555, -5_560_000_000)]
1556 #[case(3, -5.5555, -5_556_000_000)]
1557 #[case(4, -5.55555, -5_555_600_000)]
1558 #[case(5, -5.555_555, -5_555_560_000)]
1559 #[case(6, -5.555_555_5, -5_555_556_000)]
1560 #[case(7, -5.555_555_55, -5_555_555_600)]
1561 #[case(8, -5.555_555_555, -5_555_555_560)]
1562 #[case(9, -5.555_555_555_5, -5_555_555_556)]
1563 fn test_f64_to_fixed_i64(#[case] precision: u8, #[case] value: f64, #[case] expected: i64) {
1564 assert_eq!(f64_to_fixed_i64(value, precision), expected);
1565 }
1566
1567 #[rstest]
1568 #[case(0, 5.5, 6_000_000_000)]
1569 #[case(1, 5.55, 5_600_000_000)]
1570 #[case(2, 5.555, 5_560_000_000)]
1571 #[case(3, 5.5555, 5_556_000_000)]
1572 #[case(4, 5.55555, 5_555_600_000)]
1573 #[case(5, 5.555_555, 5_555_560_000)]
1574 #[case(6, 5.555_555_5, 5_555_556_000)]
1575 #[case(7, 5.555_555_55, 5_555_555_600)]
1576 #[case(8, 5.555_555_555, 5_555_555_560)]
1577 #[case(9, 5.555_555_555_5, 5_555_555_556)]
1578 fn test_f64_to_fixed_u64(#[case] precision: u8, #[case] value: f64, #[case] expected: u64) {
1579 assert_eq!(f64_to_fixed_u64(value, precision), expected);
1580 }
1581
1582 #[rstest]
1583 fn test_fixed_i64_to_f64(
1584 #[values(1, -1, 2, -2, 10, -10, 100, -100, 1_000, -1_000)] value: i64,
1585 ) {
1586 assert_eq!(fixed_i64_to_f64(value), value as f64 / FIXED_SCALAR);
1587 }
1588
1589 #[rstest]
1590 fn test_fixed_u64_to_f64(
1591 #[values(
1592 0,
1593 1,
1594 2,
1595 3,
1596 10,
1597 100,
1598 1_000,
1599 10_000,
1600 100_000,
1601 1_000_000,
1602 10_000_000,
1603 100_000_000,
1604 1_000_000_000,
1605 10_000_000_000,
1606 100_000_000_000,
1607 1_000_000_000_000,
1608 10_000_000_000_000,
1609 100_000_000_000_000,
1610 1_000_000_000_000_000
1611 )]
1612 value: u64,
1613 ) {
1614 let result = fixed_u64_to_f64(value);
1615 assert_eq!(result, (value as f64) / FIXED_SCALAR);
1616 }
1617
1618 #[rstest]
1619 #[case(0, 0)] #[case(0, 1_000_000_000)] #[case(0, 120_000_000_000)] #[case(2, 123_450_000_000)] #[case(8, 1_234_567_890)] fn test_check_fixed_raw_u64_valid(#[case] precision: u8, #[case] raw: u64) {
1625 assert!(check_fixed_raw_u64(raw, precision).is_ok());
1626 }
1627
1628 #[rstest]
1629 #[case(0, 1)] #[case(0, 999_999_999)] #[case(0, 1_000_000_001)] #[case(0, 119_582_001_968_421_736)] #[case(2, 123_456_789_000)] #[case(8, 1_234_567_891)] fn test_check_fixed_raw_u64_invalid(#[case] precision: u8, #[case] raw: u64) {
1636 assert!(check_fixed_raw_u64(raw, precision).is_err());
1637 }
1638
1639 #[rstest]
1640 fn test_check_fixed_raw_u64_at_max_precision() {
1641 assert!(check_fixed_raw_u64(0, FIXED_PRECISION).is_ok());
1643 assert!(check_fixed_raw_u64(1, FIXED_PRECISION).is_ok());
1644 assert!(check_fixed_raw_u64(123_456_789, FIXED_PRECISION).is_ok());
1645 assert!(check_fixed_raw_u64(u64::MAX, FIXED_PRECISION).is_ok());
1646 }
1647
1648 #[rstest]
1649 #[case(0, 0)]
1650 #[case(0, 1_000_000_000)]
1651 #[case(0, -1_000_000_000)]
1652 #[case(2, 123_450_000_000)]
1653 #[case(2, -123_450_000_000)]
1654 fn test_check_fixed_raw_i64_valid(#[case] precision: u8, #[case] raw: i64) {
1655 assert!(check_fixed_raw_i64(raw, precision).is_ok());
1656 }
1657
1658 #[rstest]
1659 #[case(0, 1)]
1660 #[case(0, -1)]
1661 #[case(0, 999_999_999)]
1662 #[case(0, -999_999_999)]
1663 fn test_check_fixed_raw_i64_invalid(#[case] precision: u8, #[case] raw: i64) {
1664 assert!(check_fixed_raw_i64(raw, precision).is_err());
1665 }
1666
1667 #[rstest]
1668 fn test_check_fixed_raw_i64_at_max_precision() {
1669 assert!(check_fixed_raw_i64(0, FIXED_PRECISION).is_ok());
1670 assert!(check_fixed_raw_i64(1, FIXED_PRECISION).is_ok());
1671 assert!(check_fixed_raw_i64(-1, FIXED_PRECISION).is_ok());
1672 assert!(check_fixed_raw_i64(i64::MAX, FIXED_PRECISION).is_ok());
1673 assert!(check_fixed_raw_i64(i64::MIN, FIXED_PRECISION).is_ok());
1674 }
1675
1676 #[rstest]
1677 #[should_panic(expected = "Overflow when scaling f64 to fixed-point i64")]
1678 fn test_f64_to_fixed_i64_overflow_panics() {
1679 let _ = f64_to_fixed_i64(2e18, 0);
1680 }
1681
1682 #[rstest]
1683 #[should_panic(expected = "Overflow when scaling f64 to fixed-point u64")]
1684 fn test_f64_to_fixed_u64_overflow_panics() {
1685 let _ = f64_to_fixed_u64(2e19, 0);
1686 }
1687}
1688
1689#[cfg(test)]
1690mod bankers_round_tests {
1691 use std::str::FromStr;
1692
1693 use rstest::rstest;
1694 use rust_decimal::{Decimal, RoundingStrategy};
1695
1696 use super::*;
1697
1698 #[rstest]
1699 #[case(0, 0, 0)]
1701 #[case(1, 0, 1)]
1702 #[case(5, 0, 5)]
1703 #[case(99, 0, 99)]
1704 #[case(-7, 0, -7)]
1705 #[case(12345, 39, 0)]
1707 #[case(i128::from(i64::MAX), 100, 0)]
1708 #[case(-99999, 50, 0)]
1709 #[case(15, 1, 2)] #[case(25, 1, 2)] #[case(35, 1, 4)] #[case(45, 1, 4)] #[case(55, 1, 6)] #[case(65, 1, 6)] #[case(75, 1, 8)] #[case(85, 1, 8)] #[case(95, 1, 10)] #[case(105, 1, 10)] #[case(14, 1, 1)] #[case(16, 1, 2)] #[case(24, 1, 2)] #[case(26, 1, 3)] #[case(11, 1, 1)] #[case(19, 1, 2)] #[case(150, 2, 2)] #[case(250, 2, 2)] #[case(350, 2, 4)] #[case(450, 2, 4)] #[case(550, 2, 6)] #[case(1050, 2, 10)] #[case(1150, 2, 12)] #[case(149, 2, 1)] #[case(151, 2, 2)] #[case(199, 2, 2)] #[case(101, 2, 1)] #[case(1500, 3, 2)] #[case(2500, 3, 2)] #[case(3500, 3, 4)] #[case(10500, 3, 10)] #[case(11500, 3, 12)] #[case(1499, 3, 1)] #[case(1501, 3, 2)] #[case(-15, 1, -2)] #[case(-25, 1, -2)] #[case(-35, 1, -4)] #[case(-45, 1, -4)] #[case(-55, 1, -6)] #[case(-65, 1, -6)] #[case(-150, 2, -2)] #[case(-250, 2, -2)] #[case(-350, 2, -4)] #[case(-14, 1, -1)] #[case(-16, 1, -2)] #[case(-24, 1, -2)] #[case(-26, 1, -3)] #[case(0, 1, 0)]
1766 #[case(0, 2, 0)]
1767 #[case(0, 5, 0)]
1768 #[case(123_456_789, 3, 123_457)] #[case(123_456_500, 3, 123_456)] #[case(123_457_500, 3, 123_458)] #[case(100_005, 1, 10_000)] #[case(100_015, 1, 10_002)] #[case(999_999_999_999_999_995, 1, 100_000_000_000_000_000)]
1776 #[case(1_000_000_000_000_000_005, 1, 100_000_000_000_000_000)]
1777 fn test_bankers_round(#[case] mantissa: i128, #[case] excess: u32, #[case] expected: i128) {
1778 assert_eq!(
1779 bankers_round(mantissa, excess),
1780 expected,
1781 "bankers_round({mantissa}, {excess}) expected {expected}"
1782 );
1783 }
1784
1785 #[rstest]
1787 #[case(15, 1)]
1788 #[case(25, 1)]
1789 #[case(35, 1)]
1790 #[case(150, 2)]
1791 #[case(250, 2)]
1792 #[case(1500, 3)]
1793 #[case(2500, 3)]
1794 #[case(123_456_789, 3)]
1795 #[case(14, 1)]
1796 #[case(16, 1)]
1797 fn test_bankers_round_negative_symmetry(#[case] mantissa: i128, #[case] excess: u32) {
1798 assert_eq!(
1799 bankers_round(-mantissa, excess),
1800 -bankers_round(mantissa, excess),
1801 "Negative symmetry failed for mantissa={mantissa}, excess={excess}"
1802 );
1803 }
1804
1805 #[rstest]
1807 #[case("1.005", 2, "1.00")] #[case("1.015", 2, "1.02")] #[case("1.025", 2, "1.02")] #[case("1.035", 2, "1.04")] #[case("1.045", 2, "1.04")] #[case("2.5", 0, "2")] #[case("3.5", 0, "4")] #[case("-2.5", 0, "-2")]
1815 #[case("-3.5", 0, "-4")]
1816 #[case("123.456", 2, "123.46")]
1817 #[case("123.455", 2, "123.46")] #[case("123.445", 2, "123.44")] fn test_bankers_round_matches_decimal(
1820 #[case] input: &str,
1821 #[case] target_precision: u8,
1822 #[case] expected: &str,
1823 ) {
1824 let dec = Decimal::from_str(input).unwrap();
1825 let expected_dec = Decimal::from_str(expected).unwrap();
1826
1827 let decimal_rounded = dec.round_dp_with_strategy(
1828 u32::from(target_precision),
1829 RoundingStrategy::MidpointNearestEven,
1830 );
1831 assert_eq!(
1832 decimal_rounded, expected_dec,
1833 "Decimal rounding sanity check failed for {input}"
1834 );
1835
1836 let mantissa = dec.mantissa();
1837 let scale = dec.scale() as u8;
1838 let excess = u32::from(scale.saturating_sub(target_precision));
1839 if excess > 0 {
1840 let rounded = bankers_round(mantissa, excess);
1841
1842 let expected_mantissa = expected_dec.mantissa();
1844 let expected_scale = expected_dec.scale() as u8;
1845 let scale_diff = u32::from(target_precision.saturating_sub(expected_scale));
1846 let normalized_expected = expected_mantissa * 10i128.pow(scale_diff);
1847
1848 assert_eq!(
1849 rounded, normalized_expected,
1850 "bankers_round disagrees with Decimal for {input} at precision {target_precision}"
1851 );
1852 }
1853 }
1854}
1855
1856#[cfg(test)]
1857mod correct_raw_tests {
1858 use rstest::rstest;
1859
1860 use super::*;
1861
1862 #[rstest]
1866 #[case(0, 0)]
1867 #[case(10, 10)] #[case(14, 10)] #[case(15, 20)] #[case(16, 20)] #[case(u64::MAX, u64::MAX - 5)] fn test_correct_raw_u64(#[case] raw: u64, #[case] expected: u64) {
1873 assert_eq!(correct_raw_u64(raw, FIXED_PRECISION - 1), expected);
1874 }
1875
1876 #[rstest]
1877 #[case(0, 0)]
1878 #[case(14, 10)]
1879 #[case(15, 20)]
1880 #[case(-14, -10)] #[case(-15, -20)] #[case(-16, -20)] #[case(i64::MAX, i64::MAX - 7)] #[case(i64::MIN, i64::MIN + 8)] fn test_correct_raw_i64(#[case] raw: i64, #[case] expected: i64) {
1886 assert_eq!(correct_raw_i64(raw, FIXED_PRECISION - 1), expected);
1887 }
1888
1889 #[rstest]
1890 #[case(0, 0)]
1891 #[case(14, 10)]
1892 #[case(15, 20)]
1893 #[case(u128::MAX, u128::MAX - 5)] fn test_correct_raw_u128(#[case] raw: u128, #[case] expected: u128) {
1895 assert_eq!(correct_raw_u128(raw, FIXED_PRECISION - 1), expected);
1896 }
1897
1898 #[rstest]
1899 #[case(0, 0)]
1900 #[case(14, 10)]
1901 #[case(15, 20)]
1902 #[case(-15, -20)]
1903 #[case(i128::MAX, i128::MAX - 7)] #[case(i128::MIN, i128::MIN + 8)] fn test_correct_raw_i128(#[case] raw: i128, #[case] expected: i128) {
1906 assert_eq!(correct_raw_i128(raw, FIXED_PRECISION - 1), expected);
1907 }
1908
1909 #[rstest]
1910 fn test_correct_raw_identity_at_max_precision() {
1911 assert_eq!(correct_raw_u64(12_345, FIXED_PRECISION), 12_345);
1912 assert_eq!(correct_raw_i64(-12_345, FIXED_PRECISION), -12_345);
1913 assert_eq!(correct_raw_u128(12_345, FIXED_PRECISION), 12_345);
1914 assert_eq!(correct_raw_i128(-12_345, FIXED_PRECISION), -12_345);
1915 }
1916}
1917
1918#[cfg(test)]
1919mod checked_mul_div_tests {
1920 #[cfg(feature = "defi")]
1921 use alloy_primitives::U256;
1922 use proptest::{prelude::*, test_runner::Config as ProptestConfig};
1923 use rstest::rstest;
1924
1925 use super::{FIXED_SCALAR_RAW, checked_mul_div_fixed};
1926 use crate::types::quantity::QuantityRaw;
1927
1928 #[rstest]
1929 fn test_checked_mul_div_fixed_exact_boundaries() {
1930 let scalar = FIXED_SCALAR_RAW;
1931
1932 assert_eq!(checked_mul_div_fixed(0, QuantityRaw::MAX), Some(0));
1933 assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, 0), Some(0));
1934 assert_eq!(checked_mul_div_fixed(scalar, scalar), Some(scalar));
1935 assert_eq!(
1936 checked_mul_div_fixed(scalar - 1, scalar - 1),
1937 Some(scalar - 2)
1938 );
1939 assert_eq!(
1940 checked_mul_div_fixed(scalar + 1, scalar + 1),
1941 Some(scalar + 2)
1942 );
1943 assert_eq!(
1944 checked_mul_div_fixed(QuantityRaw::MAX, scalar),
1945 Some(QuantityRaw::MAX)
1946 );
1947 assert_eq!(
1948 checked_mul_div_fixed(scalar, QuantityRaw::MAX),
1949 Some(QuantityRaw::MAX)
1950 );
1951 assert_eq!(checked_mul_div_fixed(QuantityRaw::MAX, scalar + 1), None);
1952 assert_eq!(checked_mul_div_fixed(scalar + 1, QuantityRaw::MAX), None);
1953 }
1954
1955 #[cfg(not(feature = "high-precision"))]
1956 proptest! {
1957 #![proptest_config(ProptestConfig::with_cases(4_096))]
1958
1959 #[rstest]
1960 fn prop_checked_mul_div_fixed_matches_u128_full_range(
1961 lhs in any::<QuantityRaw>(),
1962 rhs in any::<QuantityRaw>(),
1963 ) {
1964 let expected = u128::from(lhs)
1965 .checked_mul(u128::from(rhs))
1966 .map(|product| product / u128::from(FIXED_SCALAR_RAW))
1967 .and_then(|result| QuantityRaw::try_from(result).ok());
1968
1969 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
1970 }
1971
1972 #[rstest]
1973 fn prop_checked_mul_div_fixed_matches_u128_final_fit(
1974 (lhs, rhs) in standard_final_fit_strategy(),
1975 ) {
1976 let expected =
1977 u128::from(lhs) * u128::from(rhs) / u128::from(FIXED_SCALAR_RAW);
1978 let expected = QuantityRaw::try_from(expected).expect("strategy result fits u64");
1979
1980 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
1981 }
1982
1983 #[rstest]
1984 fn prop_checked_mul_div_fixed_avoids_u64_phantom_overflow(
1985 (lhs, rhs, expected) in standard_phantom_overflow_strategy(),
1986 ) {
1987 prop_assert!(lhs.checked_mul(rhs).is_none());
1988 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
1989 }
1990 }
1991
1992 #[cfg(feature = "high-precision")]
1993 proptest! {
1994 #![proptest_config(ProptestConfig::with_cases(4_096))]
1995
1996 #[rstest]
1997 fn prop_checked_mul_div_fixed_matches_u128_ordinary(
1998 (lhs, rhs) in high_precision_ordinary_strategy(),
1999 ) {
2000 let expected = lhs
2001 .checked_mul(rhs)
2002 .expect("ordinary strategy product fits u128")
2003 / FIXED_SCALAR_RAW;
2004
2005 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2006 }
2007
2008 #[rstest]
2009 fn prop_checked_mul_div_fixed_avoids_u128_phantom_overflow(
2010 (lhs, rhs, expected) in high_precision_phantom_overflow_strategy(),
2011 ) {
2012 prop_assert!(lhs.checked_mul(rhs).is_none());
2013 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2014 }
2015
2016 #[rstest]
2017 fn prop_checked_mul_div_fixed_handles_remainders_after_u128_overflow(
2018 rhs in high_precision_remainder_overflow_strategy(),
2019 ) {
2020 let lhs = 2 * FIXED_SCALAR_RAW - 1;
2021 let expected = 2 * rhs - rhs.div_ceil(FIXED_SCALAR_RAW);
2022
2023 prop_assert!(lhs.checked_mul(rhs).is_none());
2024 prop_assert_ne!(lhs % FIXED_SCALAR_RAW, 0);
2025 prop_assert_ne!(rhs % FIXED_SCALAR_RAW, 0);
2026 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), Some(expected));
2027 prop_assert_eq!(checked_mul_div_fixed(rhs, lhs), Some(expected));
2028 }
2029
2030 #[rstest]
2031 fn prop_checked_mul_div_fixed_is_commutative(
2032 lhs in any::<QuantityRaw>(),
2033 rhs in any::<QuantityRaw>(),
2034 ) {
2035 prop_assert_eq!(
2036 checked_mul_div_fixed(lhs, rhs),
2037 checked_mul_div_fixed(rhs, lhs)
2038 );
2039 }
2040 }
2041
2042 #[cfg(feature = "defi")]
2043 proptest! {
2044 #![proptest_config(ProptestConfig::with_cases(4_096))]
2045
2046 #[rstest]
2047 fn prop_checked_mul_div_fixed_matches_u256_full_range(
2048 lhs in any::<QuantityRaw>(),
2049 rhs in any::<QuantityRaw>(),
2050 ) {
2051 let expected = U256::from(lhs)
2052 .checked_mul(U256::from(rhs))
2053 .expect("u128 product fits U256")
2054 / U256::from(FIXED_SCALAR_RAW);
2055 let expected = QuantityRaw::try_from(expected).ok();
2056
2057 prop_assert_eq!(checked_mul_div_fixed(lhs, rhs), expected);
2058 }
2059 }
2060
2061 #[cfg(not(feature = "high-precision"))]
2062 fn standard_final_fit_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2063 let scalar = FIXED_SCALAR_RAW;
2064
2065 (0_u64..=1_000, 0_u64..=1_000, 0_u64..scalar, 0_u64..scalar).prop_map(
2066 move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2067 (
2068 lhs_whole * scalar + lhs_remainder,
2069 rhs_whole * scalar + rhs_remainder,
2070 )
2071 },
2072 )
2073 }
2074
2075 #[cfg(not(feature = "high-precision"))]
2076 fn standard_phantom_overflow_strategy()
2077 -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2078 let scalar = FIXED_SCALAR_RAW;
2079
2080 (8_000_000_000_u64..=9_000_000_000, 0_u64..scalar).prop_map(
2081 move |(lhs_whole, rhs_remainder)| {
2082 let lhs = lhs_whole * scalar;
2083 let rhs = scalar + rhs_remainder;
2084 (lhs, rhs, lhs_whole * rhs)
2085 },
2086 )
2087 }
2088
2089 #[cfg(feature = "high-precision")]
2090 fn high_precision_ordinary_strategy() -> impl Strategy<Value = (QuantityRaw, QuantityRaw)> {
2091 let scalar = FIXED_SCALAR_RAW;
2092
2093 (
2094 0_u128..=1_000,
2095 0_u128..=1_000,
2096 0_u128..scalar,
2097 0_u128..scalar,
2098 )
2099 .prop_map(
2100 move |(lhs_whole, rhs_whole, lhs_remainder, rhs_remainder)| {
2101 (
2102 lhs_whole * scalar + lhs_remainder,
2103 rhs_whole * scalar + rhs_remainder,
2104 )
2105 },
2106 )
2107 }
2108
2109 #[cfg(feature = "high-precision")]
2110 fn high_precision_phantom_overflow_strategy()
2111 -> impl Strategy<Value = (QuantityRaw, QuantityRaw, QuantityRaw)> {
2112 let scalar = FIXED_SCALAR_RAW;
2113
2114 (10_000_u128..=1_000_000, 1_000_u128..=10_000, 0_u128..scalar).prop_map(
2115 move |(lhs_whole, rhs_whole, rhs_remainder)| {
2116 let lhs = lhs_whole * scalar;
2117 let rhs = rhs_whole * scalar + rhs_remainder;
2118 (lhs, rhs, lhs_whole * rhs)
2119 },
2120 )
2121 }
2122
2123 #[cfg(feature = "high-precision")]
2124 fn high_precision_remainder_overflow_strategy() -> impl Strategy<Value = QuantityRaw> {
2125 let lhs = 2 * FIXED_SCALAR_RAW - 1;
2126 let min = QuantityRaw::MAX / lhs + 1;
2127
2128 (min..=QuantityRaw::MAX / 2).prop_filter("rhs remainder is nonzero", |rhs| {
2129 rhs % FIXED_SCALAR_RAW != 0
2130 })
2131 }
2132}