use std::{
cmp::Ordering,
fmt::{Debug, Display},
hash::{Hash, Hasher},
ops::{Add, Deref, Div, Mul, Sub},
str::FromStr,
};
#[cfg(feature = "defi")]
use alloy_primitives::U256;
use nautilus_core::{
correctness::{FAILED, check_in_range_inclusive_f64, check_predicate_true},
formatting::Separable,
};
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize};
use super::fixed::{
FIXED_PRECISION, FIXED_SCALAR, MAX_FLOAT_PRECISION, check_fixed_precision,
mantissa_exponent_to_fixed_i128,
};
#[cfg(not(feature = "high-precision"))]
use super::fixed::{f64_to_fixed_u64, fixed_u64_to_f64};
#[cfg(feature = "high-precision")]
use super::fixed::{f64_to_fixed_u128, fixed_u128_to_f64};
#[cfg(feature = "high-precision")]
pub type QuantityRaw = u128;
#[cfg(not(feature = "high-precision"))]
pub type QuantityRaw = u64;
#[unsafe(no_mangle)]
#[allow(unsafe_code)]
pub static QUANTITY_RAW_MAX: QuantityRaw = (QUANTITY_MAX * FIXED_SCALAR) as QuantityRaw;
pub const QUANTITY_UNDEF: QuantityRaw = QuantityRaw::MAX;
#[cfg(feature = "high-precision")]
pub const QUANTITY_MAX: f64 = 34_028_236_692_093.0;
#[cfg(not(feature = "high-precision"))]
pub const QUANTITY_MAX: f64 = 18_446_744_073.0;
pub const QUANTITY_MIN: f64 = 0.0;
#[repr(C)]
#[derive(Clone, Copy, Default, Eq)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
module = "nautilus_trader.core.nautilus_pyo3.model",
frozen,
from_py_object
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct Quantity {
pub raw: QuantityRaw,
pub precision: u8,
}
impl Quantity {
pub fn new_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
check_in_range_inclusive_f64(value, QUANTITY_MIN, QUANTITY_MAX, "value")?;
#[cfg(feature = "defi")]
if precision > MAX_FLOAT_PRECISION {
anyhow::bail!(
"`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Quantity::from_wei()` for wei values instead"
);
}
check_fixed_precision(precision)?;
#[cfg(feature = "high-precision")]
let raw = f64_to_fixed_u128(value, precision);
#[cfg(not(feature = "high-precision"))]
let raw = f64_to_fixed_u64(value, precision);
Ok(Self { raw, precision })
}
pub fn non_zero_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
check_predicate_true(value != 0.0, "value was zero")?;
check_fixed_precision(precision)?;
let rounded_value =
(value * 10.0_f64.powi(precision as i32)).round() / 10.0_f64.powi(precision as i32);
check_predicate_true(
rounded_value != 0.0,
&format!("value {value} was zero after rounding to precision {precision}"),
)?;
Self::new_checked(value, precision)
}
pub fn new(value: f64, precision: u8) -> Self {
Self::new_checked(value, precision).expect(FAILED)
}
pub fn non_zero(value: f64, precision: u8) -> Self {
Self::non_zero_checked(value, precision).expect(FAILED)
}
pub fn from_raw(raw: QuantityRaw, precision: u8) -> Self {
assert!(
raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
"`raw` value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
);
if raw == QUANTITY_UNDEF {
assert!(
precision == 0,
"`precision` must be 0 when `raw` is QUANTITY_UNDEF"
);
}
check_fixed_precision(precision).expect(FAILED);
Self { raw, precision }
}
pub fn from_raw_checked(raw: QuantityRaw, precision: u8) -> anyhow::Result<Self> {
if raw == QUANTITY_UNDEF {
anyhow::ensure!(
precision == 0,
"`precision` must be 0 when `raw` is QUANTITY_UNDEF"
);
}
anyhow::ensure!(
raw == QUANTITY_UNDEF || raw <= QUANTITY_RAW_MAX,
"raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}"
);
check_fixed_precision(precision)?;
Ok(Self { raw, precision })
}
#[must_use]
pub fn saturating_sub(self, rhs: Self) -> Self {
let precision = self.precision.max(rhs.precision);
let raw = self.raw.saturating_sub(rhs.raw);
if raw == 0 && self.raw < rhs.raw {
log::warn!(
"Saturating Quantity subtraction: {self} - {rhs} < 0, clamped to 0 (precision={precision})"
);
}
Self { raw, precision }
}
#[must_use]
pub fn zero(precision: u8) -> Self {
check_fixed_precision(precision).expect(FAILED);
Self::new(0.0, precision)
}
#[must_use]
pub fn is_undefined(&self) -> bool {
self.raw == QUANTITY_UNDEF
}
#[must_use]
pub fn is_zero(&self) -> bool {
self.raw == 0
}
#[must_use]
pub fn is_positive(&self) -> bool {
self.raw != QUANTITY_UNDEF && self.raw > 0
}
#[cfg(feature = "high-precision")]
#[must_use]
pub fn as_f64(&self) -> f64 {
#[cfg(feature = "defi")]
assert!(
self.precision <= MAX_FLOAT_PRECISION,
"Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
);
fixed_u128_to_f64(self.raw)
}
#[cfg(not(feature = "high-precision"))]
#[must_use]
pub fn as_f64(&self) -> f64 {
#[cfg(feature = "defi")]
if self.precision > MAX_FLOAT_PRECISION {
panic!("Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)");
}
fixed_u64_to_f64(self.raw)
}
#[must_use]
pub fn as_decimal(&self) -> Decimal {
let precision_diff = FIXED_PRECISION.saturating_sub(self.precision);
let rescaled_raw = self.raw / QuantityRaw::pow(10, u32::from(precision_diff));
#[allow(clippy::useless_conversion)]
Decimal::from_i128_with_scale(rescaled_raw as i128, u32::from(self.precision))
}
#[must_use]
pub fn to_formatted_string(&self) -> String {
format!("{self}").separate_with_underscores()
}
pub fn from_decimal_dp(decimal: Decimal, precision: u8) -> anyhow::Result<Self> {
anyhow::ensure!(
decimal.mantissa() >= 0,
"Decimal value '{decimal}' is negative, Quantity must be non-negative"
);
let exponent = -(decimal.scale() as i8);
let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
let raw: QuantityRaw = raw_i128.try_into().map_err(|_| {
anyhow::anyhow!("Decimal value exceeds QuantityRaw range [0, {QUANTITY_RAW_MAX}]")
})?;
anyhow::ensure!(
raw <= QUANTITY_RAW_MAX,
"Raw value {raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
);
Ok(Self { raw, precision })
}
pub fn from_decimal(decimal: Decimal) -> anyhow::Result<Self> {
let precision = decimal.scale() as u8;
Self::from_decimal_dp(decimal, precision)
}
#[must_use]
pub fn from_mantissa_exponent(mantissa: u64, exponent: i8, precision: u8) -> Self {
check_fixed_precision(precision).expect(FAILED);
if mantissa == 0 {
return Self { raw: 0, precision };
}
let raw_i128 = mantissa_exponent_to_fixed_i128(mantissa as i128, exponent, precision)
.expect("Overflow in Quantity::from_mantissa_exponent");
let raw: QuantityRaw = raw_i128
.try_into()
.expect("Raw value exceeds QuantityRaw range in Quantity::from_mantissa_exponent");
assert!(
raw <= QUANTITY_RAW_MAX,
"`raw` value {raw} exceeded QUANTITY_RAW_MAX={QUANTITY_RAW_MAX} for Quantity"
);
Self { raw, precision }
}
#[cfg(feature = "defi")]
pub fn from_u256(amount: U256, precision: u8) -> anyhow::Result<Self> {
let scaled_amount = if precision < FIXED_PRECISION {
amount
.checked_mul(U256::from(10u128.pow((FIXED_PRECISION - precision) as u32)))
.ok_or_else(|| {
anyhow::anyhow!(
"Amount overflow during scaling to fixed precision: {} * 10^{}",
amount,
FIXED_PRECISION - precision
)
})?
} else {
amount
};
let raw = QuantityRaw::try_from(scaled_amount).map_err(|_| {
anyhow::anyhow!("U256 scaled amount {scaled_amount} exceeds QuantityRaw range")
})?;
Ok(Self::from_raw(raw, precision))
}
}
impl From<Quantity> for f64 {
fn from(qty: Quantity) -> Self {
qty.as_f64()
}
}
impl From<&Quantity> for f64 {
fn from(qty: &Quantity) -> Self {
qty.as_f64()
}
}
impl From<i32> for Quantity {
fn from(value: i32) -> Self {
assert!(
value >= 0,
"Cannot create Quantity from negative i32: {value}. Use u32 or check value is non-negative."
);
Self::new(value as f64, 0)
}
}
impl From<i64> for Quantity {
fn from(value: i64) -> Self {
assert!(
value >= 0,
"Cannot create Quantity from negative i64: {value}. Use u64 or check value is non-negative."
);
Self::new(value as f64, 0)
}
}
impl From<u32> for Quantity {
fn from(value: u32) -> Self {
Self::new(value as f64, 0)
}
}
impl From<u64> for Quantity {
fn from(value: u64) -> Self {
Self::new(value as f64, 0)
}
}
impl Hash for Quantity {
fn hash<H: Hasher>(&self, state: &mut H) {
self.raw.hash(state);
}
}
impl PartialEq for Quantity {
fn eq(&self, other: &Self) -> bool {
self.raw == other.raw
}
}
impl PartialOrd for Quantity {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
fn lt(&self, other: &Self) -> bool {
self.raw.lt(&other.raw)
}
fn le(&self, other: &Self) -> bool {
self.raw.le(&other.raw)
}
fn gt(&self, other: &Self) -> bool {
self.raw.gt(&other.raw)
}
fn ge(&self, other: &Self) -> bool {
self.raw.ge(&other.raw)
}
}
impl Ord for Quantity {
fn cmp(&self, other: &Self) -> Ordering {
self.raw.cmp(&other.raw)
}
}
impl Deref for Quantity {
type Target = QuantityRaw;
fn deref(&self) -> &Self::Target {
&self.raw
}
}
impl Add for Quantity {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
raw: self
.raw
.checked_add(rhs.raw)
.expect("Overflow occurred when adding `Quantity`"),
precision: self.precision.max(rhs.precision),
}
}
}
impl Sub for Quantity {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self {
raw: self
.raw
.checked_sub(rhs.raw)
.expect("Underflow occurred when subtracting `Quantity`"),
precision: self.precision.max(rhs.precision),
}
}
}
#[allow(
clippy::suspicious_arithmetic_impl,
reason = "Can use division to scale back"
)]
impl Mul for Quantity {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
let result_raw = self
.raw
.checked_mul(rhs.raw)
.expect("Overflow occurred when multiplying `Quantity`");
Self {
raw: result_raw / (FIXED_SCALAR as QuantityRaw),
precision: self.precision.max(rhs.precision),
}
}
}
impl Add<Decimal> for Quantity {
type Output = Decimal;
fn add(self, rhs: Decimal) -> Self::Output {
self.as_decimal() + rhs
}
}
impl Sub<Decimal> for Quantity {
type Output = Decimal;
fn sub(self, rhs: Decimal) -> Self::Output {
self.as_decimal() - rhs
}
}
impl Mul<Decimal> for Quantity {
type Output = Decimal;
fn mul(self, rhs: Decimal) -> Self::Output {
self.as_decimal() * rhs
}
}
impl Div<Decimal> for Quantity {
type Output = Decimal;
fn div(self, rhs: Decimal) -> Self::Output {
self.as_decimal() / rhs
}
}
impl Add<f64> for Quantity {
type Output = f64;
fn add(self, rhs: f64) -> Self::Output {
self.as_f64() + rhs
}
}
impl Sub<f64> for Quantity {
type Output = f64;
fn sub(self, rhs: f64) -> Self::Output {
self.as_f64() - rhs
}
}
impl Mul<f64> for Quantity {
type Output = f64;
fn mul(self, rhs: f64) -> Self::Output {
self.as_f64() * rhs
}
}
impl Div<f64> for Quantity {
type Output = f64;
fn div(self, rhs: f64) -> Self::Output {
self.as_f64() / rhs
}
}
impl From<Quantity> for QuantityRaw {
fn from(value: Quantity) -> Self {
value.raw
}
}
impl From<&Quantity> for QuantityRaw {
fn from(value: &Quantity) -> Self {
value.raw
}
}
impl From<Quantity> for Decimal {
fn from(value: Quantity) -> Self {
value.as_decimal()
}
}
impl From<&Quantity> for Decimal {
fn from(value: &Quantity) -> Self {
value.as_decimal()
}
}
impl FromStr for Quantity {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let clean_value = value.replace('_', "");
let decimal = if clean_value.contains('e') || clean_value.contains('E') {
Decimal::from_scientific(&clean_value)
.map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
} else {
Decimal::from_str(&clean_value)
.map_err(|e| format!("Error parsing `input` string '{value}' as Decimal: {e}"))?
};
let precision = decimal.scale() as u8;
Self::from_decimal_dp(decimal, precision).map_err(|e| e.to_string())
}
}
impl From<&str> for Quantity {
fn from(value: &str) -> Self {
Self::from_str(value).expect(FAILED)
}
}
impl From<String> for Quantity {
fn from(value: String) -> Self {
Self::from_str(&value).expect(FAILED)
}
}
impl From<&String> for Quantity {
fn from(value: &String) -> Self {
Self::from_str(value).expect(FAILED)
}
}
impl Debug for Quantity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.precision > MAX_FLOAT_PRECISION {
write!(f, "{}({})", stringify!(Quantity), self.raw)
} else {
write!(f, "{}({})", stringify!(Quantity), self.as_decimal())
}
}
}
impl Display for Quantity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.precision > MAX_FLOAT_PRECISION {
write!(f, "{}", self.raw)
} else {
write!(f, "{}", self.as_decimal())
}
}
}
impl Serialize for Quantity {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Quantity {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let qty_str: &str = Deserialize::deserialize(_deserializer)?;
let qty: Self = qty_str.into();
Ok(qty)
}
}
pub fn check_positive_quantity(value: Quantity, param: &str) -> anyhow::Result<()> {
if !value.is_positive() {
anyhow::bail!("invalid `Quantity` for '{param}' not positive, was {value}")
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use nautilus_core::approx_eq;
use rstest::rstest;
use rust_decimal_macros::dec;
use super::*;
#[rstest]
#[should_panic(expected = "invalid `Quantity` for 'qty' not positive, was 0")]
fn test_check_quantity_positive() {
let qty = Quantity::new(0.0, 0);
check_positive_quantity(qty, "qty").unwrap();
}
#[rstest]
#[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
#[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 17")]
fn test_invalid_precision_new() {
let _ = Quantity::new(1.0, 17);
}
#[rstest]
#[cfg(all(not(feature = "defi"), feature = "high-precision"))]
#[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 17")]
fn test_invalid_precision_new() {
let _ = Quantity::new(1.0, 17);
}
#[rstest]
#[cfg(not(feature = "defi"))]
#[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
fn test_invalid_precision_from_raw() {
let _ = Quantity::from_raw(1, FIXED_PRECISION + 1);
}
#[rstest]
#[cfg(not(feature = "defi"))]
#[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
fn test_invalid_precision_zero() {
let _ = Quantity::zero(FIXED_PRECISION + 1);
}
#[rstest]
fn test_mixed_precision_add() {
let q1 = Quantity::new(1.0, 1);
let q2 = Quantity::new(1.0, 2);
let result = q1 + q2;
assert_eq!(result.precision, 2);
assert_eq!(result.as_f64(), 2.0);
}
#[rstest]
fn test_mixed_precision_sub() {
let q1 = Quantity::new(2.0, 1);
let q2 = Quantity::new(1.0, 2);
let result = q1 - q2;
assert_eq!(result.precision, 2);
assert_eq!(result.as_f64(), 1.0);
}
#[rstest]
fn test_mixed_precision_mul() {
let q1 = Quantity::new(2.0, 1);
let q2 = Quantity::new(3.0, 2);
let result = q1 * q2;
assert_eq!(result.precision, 2);
assert_eq!(result.as_f64(), 6.0);
}
#[rstest]
fn test_new_non_zero_ok() {
let qty = Quantity::non_zero_checked(123.456, 3).unwrap();
assert_eq!(qty.raw, Quantity::new(123.456, 3).raw);
assert!(qty.is_positive());
}
#[rstest]
fn test_new_non_zero_zero_input() {
assert!(Quantity::non_zero_checked(0.0, 0).is_err());
}
#[rstest]
fn test_new_non_zero_rounds_to_zero() {
assert!(Quantity::non_zero_checked(0.0004, 3).is_err());
}
#[rstest]
fn test_new_non_zero_negative() {
assert!(Quantity::non_zero_checked(-1.0, 0).is_err());
}
#[rstest]
fn test_new_non_zero_exceeds_max() {
assert!(Quantity::non_zero_checked(QUANTITY_MAX * 10.0, 0).is_err());
}
#[rstest]
fn test_new_non_zero_invalid_precision() {
assert!(Quantity::non_zero_checked(1.0, FIXED_PRECISION + 1).is_err());
}
#[rstest]
fn test_new() {
let value = 0.00812;
let qty = Quantity::new(value, 8);
assert_eq!(qty, qty);
assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
assert_eq!(qty.precision, 8);
assert_eq!(qty, Quantity::from("0.00812000"));
assert_eq!(qty.as_decimal(), dec!(0.00812000));
assert_eq!(qty.to_string(), "0.00812000");
assert!(!qty.is_zero());
assert!(qty.is_positive());
assert!(approx_eq!(f64, qty.as_f64(), 0.00812, epsilon = 0.000_001));
}
#[rstest]
fn test_check_quantity_positive_ok() {
let qty = Quantity::new(10.0, 0);
check_positive_quantity(qty, "qty").unwrap();
}
#[rstest]
fn test_negative_quantity_validation() {
assert!(Quantity::new_checked(-1.0, FIXED_PRECISION).is_err());
}
#[rstest]
fn test_undefined() {
let qty = Quantity::from_raw(QUANTITY_UNDEF, 0);
assert_eq!(qty.raw, QUANTITY_UNDEF);
assert!(qty.is_undefined());
}
#[rstest]
fn test_zero() {
let qty = Quantity::zero(8);
assert_eq!(qty.raw, 0);
assert_eq!(qty.precision, 8);
assert!(qty.is_zero());
assert!(!qty.is_positive());
}
#[rstest]
fn test_from_i32() {
let value = 100_000i32;
let qty = Quantity::from(value);
assert_eq!(qty, qty);
assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
assert_eq!(qty.precision, 0);
}
#[rstest]
fn test_from_u32() {
let value: u32 = 5000;
let qty = Quantity::from(value);
assert_eq!(qty.raw, Quantity::from(format!("{value}")).raw);
assert_eq!(qty.precision, 0);
}
#[rstest]
fn test_from_i64() {
let value = 100_000i64;
let qty = Quantity::from(value);
assert_eq!(qty, qty);
assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
assert_eq!(qty.precision, 0);
}
#[rstest]
fn test_from_u64() {
let value = 100_000u64;
let qty = Quantity::from(value);
assert_eq!(qty, qty);
assert_eq!(qty.raw, Quantity::from(&format!("{value}")).raw);
assert_eq!(qty.precision, 0);
}
#[rstest] fn test_with_maximum_value() {
let qty = Quantity::new_checked(QUANTITY_MAX, 0);
assert!(qty.is_ok());
}
#[rstest]
fn test_with_minimum_positive_value() {
let value = 0.000_000_001;
let qty = Quantity::new(value, 9);
assert_eq!(qty.raw, Quantity::from("0.000000001").raw);
assert_eq!(qty.as_decimal(), dec!(0.000000001));
assert_eq!(qty.to_string(), "0.000000001");
}
#[rstest]
fn test_with_minimum_value() {
let qty = Quantity::new(QUANTITY_MIN, 9);
assert_eq!(qty.raw, 0);
assert_eq!(qty.as_decimal(), dec!(0));
assert_eq!(qty.to_string(), "0.000000000");
}
#[rstest]
fn test_is_zero() {
let qty = Quantity::zero(8);
assert_eq!(qty, qty);
assert_eq!(qty.raw, 0);
assert_eq!(qty.precision, 8);
assert_eq!(qty, Quantity::from("0.00000000"));
assert_eq!(qty.as_decimal(), dec!(0));
assert_eq!(qty.to_string(), "0.00000000");
assert!(qty.is_zero());
}
#[rstest]
fn test_precision() {
let value = 1.001;
let qty = Quantity::new(value, 2);
assert_eq!(qty.to_string(), "1.00");
}
#[rstest]
fn test_new_from_str() {
let qty = Quantity::new(0.00812000, 8);
assert_eq!(qty, qty);
assert_eq!(qty.precision, 8);
assert_eq!(qty, Quantity::from("0.00812000"));
assert_eq!(qty.to_string(), "0.00812000");
}
#[rstest]
#[case("0", 0)]
#[case("1.1", 1)]
#[case("1.123456789", 9)]
fn test_from_str_valid_input(#[case] input: &str, #[case] expected_prec: u8) {
let qty = Quantity::from(input);
assert_eq!(qty.precision, expected_prec);
assert_eq!(qty.as_decimal(), Decimal::from_str(input).unwrap());
}
#[rstest]
#[should_panic]
fn test_from_str_invalid_input() {
let input = "invalid";
Quantity::new(f64::from_str(input).unwrap(), 8);
}
#[rstest]
fn test_from_str_errors() {
assert!(Quantity::from_str("invalid").is_err());
assert!(Quantity::from_str("12.34.56").is_err());
assert!(Quantity::from_str("").is_err());
assert!(Quantity::from_str("-1").is_err()); assert!(Quantity::from_str("-0.001").is_err());
}
#[rstest]
#[case("1e7", 0, 10_000_000.0)]
#[case("2.5e3", 0, 2_500.0)]
#[case("1.234e-2", 5, 0.01234)]
#[case("5E-3", 3, 0.005)]
#[case("1.0e6", 0, 1_000_000.0)]
fn test_from_str_scientific_notation(
#[case] input: &str,
#[case] expected_precision: u8,
#[case] expected_value: f64,
) {
let qty = Quantity::from_str(input).unwrap();
assert_eq!(qty.precision, expected_precision);
assert!(approx_eq!(
f64,
qty.as_f64(),
expected_value,
epsilon = 1e-10
));
}
#[rstest]
#[case("1_234.56", 2, 1234.56)]
#[case("1_000_000", 0, 1_000_000.0)]
#[case("99_999.999_99", 5, 99_999.999_99)]
fn test_from_str_with_underscores(
#[case] input: &str,
#[case] expected_precision: u8,
#[case] expected_value: f64,
) {
let qty = Quantity::from_str(input).unwrap();
assert_eq!(qty.precision, expected_precision);
assert!(approx_eq!(
f64,
qty.as_f64(),
expected_value,
epsilon = 1e-10
));
}
#[rstest]
fn test_from_decimal_dp_preservation() {
let decimal = dec!(123.456789);
let qty = Quantity::from_decimal_dp(decimal, 6).unwrap();
assert_eq!(qty.precision, 6);
assert!(approx_eq!(f64, qty.as_f64(), 123.456789, epsilon = 1e-10));
let expected_raw = 123456789_u64 * 10_u64.pow((FIXED_PRECISION - 6) as u32);
assert_eq!(qty.raw, expected_raw as QuantityRaw);
}
#[rstest]
fn test_from_decimal_dp_rounding() {
let decimal = dec!(1.005);
let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
assert_eq!(qty.as_f64(), 1.0);
let decimal = dec!(1.015);
let qty = Quantity::from_decimal_dp(decimal, 2).unwrap();
assert_eq!(qty.as_f64(), 1.02); }
#[rstest]
fn test_from_decimal_infers_precision() {
let decimal = dec!(123.456);
let qty = Quantity::from_decimal(decimal).unwrap();
assert_eq!(qty.precision, 3);
assert!(approx_eq!(f64, qty.as_f64(), 123.456, epsilon = 1e-10));
let decimal = dec!(100);
let qty = Quantity::from_decimal(decimal).unwrap();
assert_eq!(qty.precision, 0);
assert_eq!(qty.as_f64(), 100.0);
let decimal = dec!(1.23456789);
let qty = Quantity::from_decimal(decimal).unwrap();
assert_eq!(qty.precision, 8);
assert!(approx_eq!(f64, qty.as_f64(), 1.23456789, epsilon = 1e-10));
}
#[rstest]
fn test_from_decimal_trailing_zeros() {
let decimal = dec!(5.670);
assert_eq!(decimal.scale(), 3);
let qty = Quantity::from_decimal(decimal).unwrap();
assert_eq!(qty.precision, 3);
assert!(approx_eq!(f64, qty.as_f64(), 5.67, epsilon = 1e-10));
let normalized = decimal.normalize();
assert_eq!(normalized.scale(), 2);
let qty_normalized = Quantity::from_decimal(normalized).unwrap();
assert_eq!(qty_normalized.precision, 2);
}
#[rstest]
#[case("1.00", 2)]
#[case("1.0", 1)]
#[case("1.000", 3)]
#[case("100.00", 2)]
#[case("0.10", 2)]
#[case("0.100", 3)]
fn test_from_str_preserves_trailing_zeros(#[case] input: &str, #[case] expected_precision: u8) {
let qty = Quantity::from_str(input).unwrap();
assert_eq!(qty.precision, expected_precision);
}
#[rstest]
fn test_from_decimal_excessive_precision_inference() {
let decimal = dec!(1.1234567890123456789012345678);
if decimal.scale() > FIXED_PRECISION as u32 {
assert!(Quantity::from_decimal(decimal).is_err());
}
}
#[rstest]
fn test_from_decimal_negative_quantity_errors() {
let decimal = dec!(-123.45);
let result = Quantity::from_decimal(decimal);
assert!(result.is_err());
let result = Quantity::from_decimal_dp(decimal, 2);
assert!(result.is_err());
}
#[rstest]
fn test_add() {
let a = 1.0;
let b = 2.0;
let quantity1 = Quantity::new(1.0, 0);
let quantity2 = Quantity::new(2.0, 0);
let quantity3 = quantity1 + quantity2;
assert_eq!(quantity3.raw, Quantity::new(a + b, 0).raw);
}
#[rstest]
fn test_sub() {
let a = 3.0;
let b = 2.0;
let quantity1 = Quantity::new(a, 0);
let quantity2 = Quantity::new(b, 0);
let quantity3 = quantity1 - quantity2;
assert_eq!(quantity3.raw, Quantity::new(a - b, 0).raw);
}
#[rstest]
fn test_mul() {
let value = 2.0;
let quantity1 = Quantity::new(value, 1);
let quantity2 = Quantity::new(value, 1);
let quantity3 = quantity1 * quantity2;
assert_eq!(quantity3.raw, Quantity::new(value * value, 0).raw);
}
#[rstest]
fn test_comparisons() {
assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 1));
assert_eq!(Quantity::new(1.0, 1), Quantity::new(1.0, 2));
assert_ne!(Quantity::new(1.1, 1), Quantity::new(1.0, 1));
assert!(Quantity::new(1.0, 1) <= Quantity::new(1.0, 2));
assert!(Quantity::new(1.1, 1) > Quantity::new(1.0, 1));
assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 1));
assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
assert!(Quantity::new(1.0, 1) >= Quantity::new(1.0, 2));
assert!(Quantity::new(0.9, 1) < Quantity::new(1.0, 1));
assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 2));
assert!(Quantity::new(0.9, 1) <= Quantity::new(1.0, 1));
}
#[rstest]
fn test_debug() {
let quantity = Quantity::from_str("44.12").unwrap();
let result = format!("{quantity:?}");
assert_eq!(result, "Quantity(44.12)");
}
#[rstest]
fn test_display() {
let quantity = Quantity::from_str("44.12").unwrap();
let result = format!("{quantity}");
assert_eq!(result, "44.12");
}
#[rstest]
#[case(44.12, 2, "Quantity(44.12)", "44.12")] #[case(1234.567, 8, "Quantity(1234.56700000)", "1234.56700000")] #[cfg_attr(
feature = "defi",
case(
1_000_000_000_000_000_000.0,
18,
"Quantity(1000000000000000000)",
"1000000000000000000"
)
)] fn test_debug_display_precision_handling(
#[case] value: f64,
#[case] precision: u8,
#[case] expected_debug: &str,
#[case] expected_display: &str,
) {
let quantity = if precision > MAX_FLOAT_PRECISION {
Quantity::from_raw(value as QuantityRaw, precision)
} else {
Quantity::new(value, precision)
};
assert_eq!(format!("{quantity:?}"), expected_debug);
assert_eq!(format!("{quantity}"), expected_display);
}
#[rstest]
fn test_to_formatted_string() {
let qty = Quantity::new(1234.5678, 4);
let formatted = qty.to_formatted_string();
assert_eq!(formatted, "1_234.5678");
assert_eq!(qty.to_string(), "1234.5678");
}
#[rstest]
fn test_saturating_sub() {
let q1 = Quantity::new(100.0, 2);
let q2 = Quantity::new(50.0, 2);
let q3 = Quantity::new(150.0, 2);
let result = q1.saturating_sub(q2);
assert_eq!(result, Quantity::new(50.0, 2));
let result = q1.saturating_sub(q3);
assert_eq!(result, Quantity::zero(2));
assert_eq!(result.raw, 0);
}
#[rstest]
fn test_saturating_sub_overflow_bug() {
use crate::types::fixed::FIXED_PRECISION;
let precision = 3;
let scale = 10u64.pow(u32::from(FIXED_PRECISION - precision)) as QuantityRaw;
let peak_qty = Quantity::from_raw(79 * scale, precision);
let order_qty = Quantity::from_raw(80 * scale, precision);
let result = peak_qty.saturating_sub(order_qty);
assert_eq!(result.raw, 0);
assert_eq!(result, Quantity::zero(precision));
}
#[rstest]
fn test_hash() {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
let q1 = Quantity::new(100.0, 1);
let q2 = Quantity::new(100.0, 1);
let q3 = Quantity::new(200.0, 1);
let mut s1 = DefaultHasher::new();
let mut s2 = DefaultHasher::new();
let mut s3 = DefaultHasher::new();
q1.hash(&mut s1);
q2.hash(&mut s2);
q3.hash(&mut s3);
assert_eq!(
s1.finish(),
s2.finish(),
"Equal quantities must hash equally"
);
assert_ne!(
s1.finish(),
s3.finish(),
"Different quantities must hash differently"
);
}
#[rstest]
fn test_quantity_serde_json_round_trip() {
let original = Quantity::new(123.456, 3);
let json_str = serde_json::to_string(&original).unwrap();
assert_eq!(json_str, "\"123.456\"");
let deserialized: Quantity = serde_json::from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
assert_eq!(deserialized.precision, 3);
}
#[rstest]
fn test_from_mantissa_exponent_exact_precision() {
let qty = Quantity::from_mantissa_exponent(12345, -2, 2);
assert_eq!(qty.as_f64(), 123.45);
}
#[rstest]
fn test_from_mantissa_exponent_excess_rounds_down() {
let qty = Quantity::from_mantissa_exponent(12345, -3, 2);
assert_eq!(qty.as_f64(), 12.34);
}
#[rstest]
fn test_from_mantissa_exponent_excess_rounds_up() {
let qty = Quantity::from_mantissa_exponent(12355, -3, 2);
assert_eq!(qty.as_f64(), 12.36);
}
#[rstest]
fn test_from_mantissa_exponent_positive_exponent() {
let qty = Quantity::from_mantissa_exponent(5, 2, 0);
assert_eq!(qty.as_f64(), 500.0);
}
#[rstest]
fn test_from_mantissa_exponent_zero() {
let qty = Quantity::from_mantissa_exponent(0, 2, 2);
assert_eq!(qty.as_f64(), 0.0);
}
#[rstest]
#[should_panic]
fn test_from_mantissa_exponent_overflow_panics() {
let _ = Quantity::from_mantissa_exponent(u64::MAX, 9, 0);
}
#[rstest]
#[should_panic(expected = "exceeds i128 range")]
fn test_from_mantissa_exponent_large_exponent_panics() {
let _ = Quantity::from_mantissa_exponent(1, 119, 0);
}
#[rstest]
fn test_from_mantissa_exponent_zero_with_large_exponent() {
let qty = Quantity::from_mantissa_exponent(0, 119, 0);
assert_eq!(qty.as_f64(), 0.0);
}
#[rstest]
fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
let qty = Quantity::from_mantissa_exponent(12345, -120, 2);
assert_eq!(qty.as_f64(), 0.0);
}
#[rstest]
fn test_f64_operations() {
let q = Quantity::new(10.5, 2);
assert_eq!(q + 1.0, 11.5);
assert_eq!(q - 1.0, 9.5);
assert_eq!(q * 2.0, 21.0);
assert_eq!(q / 2.0, 5.25);
}
#[rstest]
fn test_decimal_arithmetic_operations() {
let qty = Quantity::new(100.0, 2);
assert_eq!(qty + dec!(50.25), dec!(150.25));
assert_eq!(qty - dec!(30.50), dec!(69.50));
assert_eq!(qty * dec!(1.5), dec!(150.00));
assert_eq!(qty / dec!(4), dec!(25.00));
}
#[rstest]
#[cfg(feature = "defi")]
#[case::sell_tx_rain_amount(
U256::from_str_radix("42193532365637161405123", 10).unwrap(),
18,
"42193.532365637161405123"
)]
#[case::sell_tx_weth_amount(
U256::from_str_radix("112633187203033110", 10).unwrap(),
18,
"0.112633187203033110"
)]
fn test_from_u256_real_swap_data(
#[case] amount: U256,
#[case] precision: u8,
#[case] expected_str: &str,
) {
let qty = Quantity::from_u256(amount, precision).unwrap();
assert_eq!(qty.precision, precision);
assert_eq!(qty.as_decimal().to_string(), expected_str);
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
use rstest::rstest;
use super::*;
fn quantity_value_strategy() -> impl Strategy<Value = f64> {
prop_oneof![
0.00001..1.0,
1.0..100_000.0,
100_000.0..1_000_000.0,
Just(0.0),
Just(QUANTITY_MAX / 2.0),
]
}
fn precision_strategy() -> impl Strategy<Value = u8> {
let upper = FIXED_PRECISION.min(MAX_FLOAT_PRECISION);
prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
}
fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
let upper = FIXED_PRECISION.clamp(1, MAX_FLOAT_PRECISION);
prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
}
fn raw_for_precision_strategy() -> impl Strategy<Value = (QuantityRaw, u8)> {
precision_strategy().prop_flat_map(|precision| {
let step_u128 = 10u128.pow(u32::from(FIXED_PRECISION.saturating_sub(precision)));
#[cfg(feature = "high-precision")]
let max_steps_u128 = QUANTITY_RAW_MAX / step_u128;
#[cfg(not(feature = "high-precision"))]
let max_steps_u128 = (QUANTITY_RAW_MAX as u128) / step_u128;
(0u128..=max_steps_u128).prop_map(move |steps_u128| {
let raw_u128 = steps_u128 * step_u128;
#[cfg(feature = "high-precision")]
let raw = raw_u128;
#[cfg(not(feature = "high-precision"))]
let raw = raw_u128
.try_into()
.expect("raw value should fit in QuantityRaw");
(raw, precision)
})
})
}
const DECIMAL_MAX_MANTISSA: u128 = 79_228_162_514_264_337_593_543_950_335;
fn decimal_compatible(raw: QuantityRaw, precision: u8) -> bool {
if precision > MAX_FLOAT_PRECISION {
return false;
}
let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
let divisor = 10u128.pow(precision_diff);
#[cfg(feature = "high-precision")]
let rescaled_raw = raw / divisor;
#[cfg(not(feature = "high-precision"))]
let rescaled_raw = (raw as u128) / divisor;
rescaled_raw <= DECIMAL_MAX_MANTISSA
}
proptest! {
#[rstest]
fn prop_quantity_serde_round_trip(
(raw, precision) in raw_for_precision_strategy()
) {
prop_assume!(decimal_compatible(raw, precision));
let original = Quantity::from_raw(raw, precision);
let string_repr = original.to_string();
let from_string: Quantity = string_repr.parse().unwrap();
prop_assert_eq!(from_string.raw, original.raw);
prop_assert_eq!(from_string.precision, original.precision);
let json = serde_json::to_string(&original).unwrap();
let from_json: Quantity = serde_json::from_str(&json).unwrap();
prop_assert_eq!(from_json.precision, original.precision);
prop_assert_eq!(from_json.raw, original.raw);
}
#[rstest]
fn prop_quantity_arithmetic_associative(
a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
c in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
precision in precision_strategy()
) {
let q_a = Quantity::new(a, precision);
let q_b = Quantity::new(b, precision);
let q_c = Quantity::new(c, precision);
let ab_raw = q_a.raw.checked_add(q_b.raw);
let bc_raw = q_b.raw.checked_add(q_c.raw);
if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
let ab_c_raw = ab_raw.checked_add(q_c.raw);
let a_bc_raw = q_a.raw.checked_add(bc_raw);
if let (Some(ab_c_raw), Some(a_bc_raw)) = (ab_c_raw, a_bc_raw) {
prop_assert_eq!(ab_c_raw, a_bc_raw, "Associativity failed in raw arithmetic");
}
}
}
#[rstest]
fn prop_quantity_addition_subtraction_inverse(
base in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
delta in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 1e-3 && x < 1e6),
precision in precision_strategy()
) {
let q_base = Quantity::new(base, precision);
let q_delta = Quantity::new(delta, precision);
if let Some(added_raw) = q_base.raw.checked_add(q_delta.raw)
&& let Some(result_raw) = added_raw.checked_sub(q_delta.raw) {
prop_assert_eq!(result_raw, q_base.raw, "Inverse operation failed in raw arithmetic");
}
}
#[rstest]
fn prop_quantity_ordering_transitive(
a in quantity_value_strategy(),
b in quantity_value_strategy(),
c in quantity_value_strategy(),
precision in precision_strategy()
) {
let q_a = Quantity::new(a, precision);
let q_b = Quantity::new(b, precision);
let q_c = Quantity::new(c, precision);
if q_a <= q_b && q_b <= q_c {
prop_assert!(q_a <= q_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
q_a.as_f64(), q_b.as_f64(), q_c.as_f64(), q_a.as_f64(), q_c.as_f64());
}
}
#[rstest]
fn prop_quantity_string_parsing_precision(
integral in 0u32..1000000,
fractional in 0u32..1000000,
precision in precision_strategy_non_zero()
) {
let pow = 10u128.pow(u32::from(precision));
let fractional_mod = (fractional as u128) % pow;
let fractional_str = format!("{:0width$}", fractional_mod, width = precision as usize);
let quantity_str = format!("{integral}.{fractional_str}");
let parsed: Quantity = quantity_str.parse().unwrap();
prop_assert_eq!(parsed.precision, precision);
let round_trip = parsed.to_string();
let expected_value = format!("{integral}.{fractional_str}");
prop_assert_eq!(round_trip, expected_value);
}
#[rstest]
fn prop_quantity_precision_information_preservation(
value in quantity_value_strategy().prop_filter("Reasonable values", |&x| x < 1e6),
precision1 in precision_strategy_non_zero(),
precision2 in precision_strategy_non_zero()
) {
prop_assume!(precision1 != precision2);
let _q1 = Quantity::new(value, precision1);
let _q2 = Quantity::new(value, precision2);
let min_precision = precision1.min(precision2);
let scale = 10.0_f64.powi(min_precision as i32);
let rounded_value = (value * scale).round() / scale;
let q1_reduced = Quantity::new(rounded_value, min_precision);
let q2_reduced = Quantity::new(rounded_value, min_precision);
prop_assert_eq!(q1_reduced.raw, q2_reduced.raw, "Precision reduction inconsistent");
}
#[rstest]
fn prop_quantity_arithmetic_bounds(
a in quantity_value_strategy(),
b in quantity_value_strategy(),
precision in precision_strategy()
) {
let q_a = Quantity::new(a, precision);
let q_b = Quantity::new(b, precision);
let sum_f64 = q_a.as_f64() + q_b.as_f64();
if sum_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&sum_f64) {
let sum = q_a + q_b;
prop_assert!(sum.as_f64().is_finite());
prop_assert!(!sum.is_undefined());
}
let diff_f64 = q_a.as_f64() - q_b.as_f64();
if diff_f64.is_finite() && (QUANTITY_MIN..=QUANTITY_MAX).contains(&diff_f64) {
let diff = q_a - q_b;
prop_assert!(diff.as_f64().is_finite());
prop_assert!(!diff.is_undefined());
}
}
#[rstest]
fn prop_quantity_multiplication_non_negative(
a in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
b in quantity_value_strategy().prop_filter("Reasonable values", |&x| x > 0.0 && x < 10.0),
precision in precision_strategy()
) {
let q_a = Quantity::new(a, precision);
let q_b = Quantity::new(b, precision);
let raw_product_check = q_a.raw.checked_mul(q_b.raw);
if let Some(raw_product) = raw_product_check {
let scaled_raw = raw_product / (FIXED_SCALAR as QuantityRaw);
if scaled_raw <= QUANTITY_RAW_MAX {
let product = q_a * q_b;
prop_assert!(product.as_f64() >= 0.0, "Quantity multiplication produced negative value: {}", product.as_f64());
}
}
}
#[rstest]
fn prop_quantity_zero_addition_identity(
value in quantity_value_strategy(),
precision in precision_strategy()
) {
let q = Quantity::new(value, precision);
let zero = Quantity::zero(precision);
prop_assert_eq!(q + zero, q);
prop_assert_eq!(zero + q, q);
}
}
proptest! {
#[rstest]
fn prop_quantity_as_decimal_preserves_precision(
(raw, precision) in raw_for_precision_strategy()
) {
prop_assume!(decimal_compatible(raw, precision));
let quantity = Quantity::from_raw(raw, precision);
let decimal = quantity.as_decimal();
prop_assert_eq!(decimal.scale(), u32::from(precision));
}
#[rstest]
fn prop_quantity_as_decimal_matches_display(
(raw, precision) in raw_for_precision_strategy()
) {
prop_assume!(decimal_compatible(raw, precision));
let quantity = Quantity::from_raw(raw, precision);
let display_str = format!("{quantity}");
let decimal_str = quantity.as_decimal().to_string();
prop_assert_eq!(display_str, decimal_str);
}
#[rstest]
fn prop_quantity_from_decimal_roundtrip(
(raw, precision) in raw_for_precision_strategy()
) {
prop_assume!(decimal_compatible(raw, precision));
let original = Quantity::from_raw(raw, precision);
let decimal = original.as_decimal();
let reconstructed = Quantity::from_decimal(decimal).unwrap();
prop_assert_eq!(original.raw, reconstructed.raw);
prop_assert_eq!(original.precision, reconstructed.precision);
}
#[rstest]
fn prop_quantity_from_raw_round_trip(
(raw, precision) in raw_for_precision_strategy()
) {
let quantity = Quantity::from_raw(raw, precision);
prop_assert_eq!(quantity.raw, raw);
prop_assert_eq!(quantity.precision, precision);
}
}
}