use std::{
cmp::Ordering,
fmt::{Debug, Display},
hash::{Hash, Hasher},
ops::{Add, Deref, Div, Mul, Neg, Sub},
str::FromStr,
};
use nautilus_core::{
correctness::{FAILED, check_in_range_inclusive_f64},
formatting::Separable,
};
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize};
use super::fixed::{
FIXED_PRECISION, FIXED_SCALAR, check_fixed_precision, mantissa_exponent_to_fixed_i128,
};
#[cfg(feature = "high-precision")]
use super::fixed::{PRECISION_DIFF_SCALAR, f64_to_fixed_i128, fixed_i128_to_f64};
#[cfg(not(feature = "high-precision"))]
use super::fixed::{f64_to_fixed_i64, fixed_i64_to_f64};
#[cfg(feature = "defi")]
use crate::types::fixed::MAX_FLOAT_PRECISION;
#[cfg(feature = "high-precision")]
pub type PriceRaw = i128;
#[cfg(not(feature = "high-precision"))]
pub type PriceRaw = i64;
#[unsafe(no_mangle)]
#[allow(unsafe_code)]
pub static PRICE_RAW_MAX: PriceRaw = (PRICE_MAX * FIXED_SCALAR) as PriceRaw;
#[unsafe(no_mangle)]
#[allow(unsafe_code)]
pub static PRICE_RAW_MIN: PriceRaw = (PRICE_MIN * FIXED_SCALAR) as PriceRaw;
pub const PRICE_UNDEF: PriceRaw = PriceRaw::MAX;
pub const PRICE_ERROR: PriceRaw = PriceRaw::MIN;
#[cfg(feature = "high-precision")]
pub const PRICE_MAX: f64 = 17_014_118_346_046.0;
#[cfg(not(feature = "high-precision"))]
pub const PRICE_MAX: f64 = 9_223_372_036.0;
#[cfg(feature = "high-precision")]
pub const PRICE_MIN: f64 = -17_014_118_346_046.0;
#[cfg(not(feature = "high-precision"))]
pub const PRICE_MIN: f64 = -9_223_372_036.0;
pub const ERROR_PRICE: Price = Price {
raw: 0,
precision: 255,
};
#[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 Price {
pub raw: PriceRaw,
pub precision: u8,
}
impl Price {
pub fn new_checked(value: f64, precision: u8) -> anyhow::Result<Self> {
check_in_range_inclusive_f64(value, PRICE_MIN, PRICE_MAX, "value")?;
#[cfg(feature = "defi")]
if precision > MAX_FLOAT_PRECISION {
anyhow::bail!(
"`precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Price::from_wei()` for wei values instead"
);
}
check_fixed_precision(precision)?;
#[cfg(feature = "high-precision")]
let raw = f64_to_fixed_i128(value, precision);
#[cfg(not(feature = "high-precision"))]
let raw = f64_to_fixed_i64(value, precision);
Ok(Self { raw, precision })
}
pub fn new(value: f64, precision: u8) -> Self {
Self::new_checked(value, precision).expect(FAILED)
}
pub fn from_raw(raw: PriceRaw, precision: u8) -> Self {
assert!(
raw == PRICE_ERROR
|| raw == PRICE_UNDEF
|| (raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX),
"`raw` value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
);
if raw == PRICE_UNDEF {
assert!(
precision == 0,
"`precision` must be 0 when `raw` is PRICE_UNDEF"
);
}
check_fixed_precision(precision).expect(FAILED);
Self { raw, precision }
}
pub fn from_raw_checked(raw: PriceRaw, precision: u8) -> anyhow::Result<Self> {
if raw == PRICE_UNDEF {
anyhow::ensure!(
precision == 0,
"`precision` must be 0 when `raw` is PRICE_UNDEF"
);
}
anyhow::ensure!(
raw == PRICE_ERROR
|| raw == PRICE_UNDEF
|| (raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX),
"raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
);
check_fixed_precision(precision)?;
Ok(Self { raw, precision })
}
#[must_use]
pub fn zero(precision: u8) -> Self {
check_fixed_precision(precision).expect(FAILED);
Self { raw: 0, precision }
}
#[must_use]
pub fn max(precision: u8) -> Self {
check_fixed_precision(precision).expect(FAILED);
Self {
raw: PRICE_RAW_MAX,
precision,
}
}
#[must_use]
pub fn min(precision: u8) -> Self {
check_fixed_precision(precision).expect(FAILED);
Self {
raw: PRICE_RAW_MIN,
precision,
}
}
#[must_use]
pub fn is_undefined(&self) -> bool {
self.raw == PRICE_UNDEF
}
#[must_use]
pub fn is_zero(&self) -> bool {
self.raw == 0
}
#[must_use]
pub fn is_positive(&self) -> bool {
self.raw != PRICE_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_i128_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_i64_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 / PriceRaw::pow(10, u32::from(precision_diff));
#[allow(clippy::unnecessary_cast)]
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> {
let exponent = -(decimal.scale() as i8);
let raw_i128 = mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, precision)?;
#[allow(clippy::useless_conversion)]
let raw: PriceRaw = raw_i128.try_into().map_err(|_| {
anyhow::anyhow!(
"Decimal value exceeds PriceRaw range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}]"
)
})?;
anyhow::ensure!(
raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX,
"Raw value {raw} outside valid range [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
);
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: i64, 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 Price::from_mantissa_exponent");
#[allow(clippy::useless_conversion)]
let raw: PriceRaw = raw_i128
.try_into()
.expect("Raw value exceeds PriceRaw range in Price::from_mantissa_exponent");
assert!(
raw >= PRICE_RAW_MIN && raw <= PRICE_RAW_MAX,
"`raw` value {raw} exceeded bounds [{PRICE_RAW_MIN}, {PRICE_RAW_MAX}] for Price"
);
Self { raw, precision }
}
}
impl FromStr for Price {
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<T: AsRef<str>> From<T> for Price {
fn from(value: T) -> Self {
Self::from_str(value.as_ref()).expect(FAILED)
}
}
impl From<Price> for f64 {
fn from(price: Price) -> Self {
price.as_f64()
}
}
impl From<&Price> for f64 {
fn from(price: &Price) -> Self {
price.as_f64()
}
}
impl From<Price> for Decimal {
fn from(value: Price) -> Self {
value.as_decimal()
}
}
impl From<&Price> for Decimal {
fn from(value: &Price) -> Self {
value.as_decimal()
}
}
impl Hash for Price {
fn hash<H: Hasher>(&self, state: &mut H) {
self.raw.hash(state);
}
}
impl PartialEq for Price {
fn eq(&self, other: &Self) -> bool {
self.raw == other.raw
}
}
impl PartialOrd for Price {
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 Price {
fn cmp(&self, other: &Self) -> Ordering {
self.raw.cmp(&other.raw)
}
}
impl Deref for Price {
type Target = PriceRaw;
fn deref(&self) -> &Self::Target {
&self.raw
}
}
impl Neg for Price {
type Output = Self;
fn neg(self) -> Self::Output {
if self.raw == PRICE_ERROR || self.raw == PRICE_UNDEF {
return self;
}
Self {
raw: -self.raw,
precision: self.precision,
}
}
}
impl Add for Price {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
raw: self
.raw
.checked_add(rhs.raw)
.expect("Overflow occurred when adding `Price`"),
precision: self.precision.max(rhs.precision),
}
}
}
impl Sub for Price {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self {
raw: self
.raw
.checked_sub(rhs.raw)
.expect("Underflow occurred when subtracting `Price`"),
precision: self.precision.max(rhs.precision),
}
}
}
impl Add<Decimal> for Price {
type Output = Decimal;
fn add(self, rhs: Decimal) -> Self::Output {
self.as_decimal() + rhs
}
}
impl Sub<Decimal> for Price {
type Output = Decimal;
fn sub(self, rhs: Decimal) -> Self::Output {
self.as_decimal() - rhs
}
}
impl Mul<Decimal> for Price {
type Output = Decimal;
fn mul(self, rhs: Decimal) -> Self::Output {
self.as_decimal() * rhs
}
}
impl Div<Decimal> for Price {
type Output = Decimal;
fn div(self, rhs: Decimal) -> Self::Output {
self.as_decimal() / rhs
}
}
impl Add<f64> for Price {
type Output = f64;
fn add(self, rhs: f64) -> Self::Output {
self.as_f64() + rhs
}
}
impl Sub<f64> for Price {
type Output = f64;
fn sub(self, rhs: f64) -> Self::Output {
self.as_f64() - rhs
}
}
impl Mul<f64> for Price {
type Output = f64;
fn mul(self, rhs: f64) -> Self::Output {
self.as_f64() * rhs
}
}
impl Div<f64> for Price {
type Output = f64;
fn div(self, rhs: f64) -> Self::Output {
self.as_f64() / rhs
}
}
impl Debug for Price {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
write!(f, "{}({})", stringify!(Price), self.raw)
} else {
write!(f, "{}({})", stringify!(Price), self.as_decimal())
}
}
}
impl Display for Price {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.precision > crate::types::fixed::MAX_FLOAT_PRECISION {
write!(f, "{}", self.raw)
} else {
write!(f, "{}", self.as_decimal())
}
}
}
impl Serialize for Price {
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 Price {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let price_str: &str = Deserialize::deserialize(_deserializer)?;
let price: Self = price_str.into();
Ok(price)
}
}
pub fn check_positive_price(value: Price, param: &str) -> anyhow::Result<()> {
if value.raw == PRICE_UNDEF {
anyhow::bail!("invalid `Price` for '{param}', was PRICE_UNDEF")
}
if !value.is_positive() {
anyhow::bail!("invalid `Price` for '{param}' not positive, was {value}")
}
Ok(())
}
#[cfg(feature = "high-precision")]
pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
value as PriceRaw * PRECISION_DIFF_SCALAR as PriceRaw
}
#[cfg(not(feature = "high-precision"))]
pub fn decode_raw_price_i64(value: i64) -> PriceRaw {
value
}
#[cfg(test)]
mod tests {
use nautilus_core::approx_eq;
use rstest::rstest;
use rust_decimal_macros::dec;
use super::*;
#[rstest]
#[cfg(all(not(feature = "defi"), not(feature = "high-precision")))]
#[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (9), was 50")]
fn test_invalid_precision_new() {
let _ = Price::new(1.0, 50);
}
#[rstest]
#[cfg(all(not(feature = "defi"), feature = "high-precision"))]
#[should_panic(expected = "`precision` exceeded maximum `FIXED_PRECISION` (16), was 50")]
fn test_invalid_precision_new() {
let _ = Price::new(1.0, 50);
}
#[rstest]
#[cfg(not(feature = "defi"))]
#[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
fn test_invalid_precision_from_raw() {
let _ = Price::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_max() {
let _ = Price::max(FIXED_PRECISION + 1);
}
#[rstest]
#[cfg(not(feature = "defi"))]
#[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
fn test_invalid_precision_min() {
let _ = Price::min(FIXED_PRECISION + 1);
}
#[rstest]
#[cfg(not(feature = "defi"))]
#[should_panic(expected = "Condition failed: `precision` exceeded maximum `FIXED_PRECISION`")]
fn test_invalid_precision_zero() {
let _ = Price::zero(FIXED_PRECISION + 1);
}
#[rstest]
#[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
fn test_max_value_exceeded() {
Price::new(PRICE_MAX + 0.1, FIXED_PRECISION);
}
#[rstest]
#[should_panic(expected = "Condition failed: invalid f64 for 'value' not in range")]
fn test_min_value_exceeded() {
Price::new(PRICE_MIN - 0.1, FIXED_PRECISION);
}
#[rstest]
fn test_is_positive_ok() {
let price = Price::new(42.0, 2);
assert!(price.is_positive());
check_positive_price(price, "price").unwrap();
}
#[rstest]
#[should_panic(expected = "invalid `Price` for 'price' not positive")]
fn test_is_positive_rejects_non_positive() {
let zero = Price::zero(2);
check_positive_price(zero, "price").unwrap();
}
#[rstest]
#[should_panic(expected = "invalid `Price` for 'price', was PRICE_UNDEF")]
fn test_is_positive_rejects_undefined() {
let undef = Price::from_raw(PRICE_UNDEF, 0);
check_positive_price(undef, "price").unwrap();
}
#[rstest]
fn test_construction() {
let price = Price::new_checked(1.23456, 4);
assert!(price.is_ok());
let price = price.unwrap();
assert_eq!(price.precision, 4);
assert!(approx_eq!(f64, price.as_f64(), 1.23456, epsilon = 0.0001));
}
#[rstest]
fn test_negative_price_in_range() {
let neg_price = Price::new(PRICE_MIN / 2.0, FIXED_PRECISION);
assert!(neg_price.raw < 0);
}
#[rstest]
fn test_new_checked() {
assert!(Price::new_checked(1.0, FIXED_PRECISION).is_ok());
assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
}
#[rstest]
fn test_from_raw() {
let raw = 100 * FIXED_SCALAR as PriceRaw;
let price = Price::from_raw(raw, 2);
assert_eq!(price.raw, raw);
assert_eq!(price.precision, 2);
}
#[rstest]
fn test_zero_constructor() {
let zero = Price::zero(3);
assert!(zero.is_zero());
assert_eq!(zero.precision, 3);
}
#[rstest]
fn test_max_constructor() {
let max = Price::max(4);
assert_eq!(max.raw, PRICE_RAW_MAX);
assert_eq!(max.precision, 4);
}
#[rstest]
fn test_min_constructor() {
let min = Price::min(4);
assert_eq!(min.raw, PRICE_RAW_MIN);
assert_eq!(min.precision, 4);
}
#[rstest]
fn test_nan_validation() {
assert!(Price::new_checked(f64::NAN, FIXED_PRECISION).is_err());
}
#[rstest]
fn test_infinity_validation() {
assert!(Price::new_checked(f64::INFINITY, FIXED_PRECISION).is_err());
assert!(Price::new_checked(f64::NEG_INFINITY, FIXED_PRECISION).is_err());
}
#[rstest]
fn test_special_values() {
let zero = Price::zero(5);
assert!(zero.is_zero());
assert_eq!(zero.to_string(), "0.00000");
let undef = Price::from_raw(PRICE_UNDEF, 0);
assert!(undef.is_undefined());
let error = ERROR_PRICE;
assert_eq!(error.precision, 255);
}
#[rstest]
fn test_string_parsing() {
let price: Price = "123.456".into();
assert_eq!(price.precision, 3);
assert_eq!(price, Price::from("123.456"));
}
#[rstest]
fn test_negative_price_from_str() {
let price: Price = "-123.45".parse().unwrap();
assert_eq!(price.precision, 2);
assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-9));
}
#[rstest]
fn test_string_parsing_errors() {
assert!(Price::from_str("invalid").is_err());
}
#[rstest]
#[case("1e7", 0, 10_000_000.0)]
#[case("1.5e3", 0, 1_500.0)]
#[case("1.234e-2", 5, 0.01234)]
#[case("5E-3", 3, 0.005)]
fn test_from_str_scientific_notation(
#[case] input: &str,
#[case] expected_precision: u8,
#[case] expected_value: f64,
) {
let price = Price::from_str(input).unwrap();
assert_eq!(price.precision, expected_precision);
assert!(approx_eq!(
f64,
price.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 price = Price::from_str(input).unwrap();
assert_eq!(price.precision, expected_precision);
assert!(approx_eq!(
f64,
price.as_f64(),
expected_value,
epsilon = 1e-10
));
}
#[rstest]
fn test_from_decimal_dp_preservation() {
let decimal = dec!(123.456789);
let price = Price::from_decimal_dp(decimal, 6).unwrap();
assert_eq!(price.precision, 6);
assert!(approx_eq!(f64, price.as_f64(), 123.456789, epsilon = 1e-10));
let expected_raw = 123456789 * 10_i64.pow((FIXED_PRECISION - 6) as u32);
assert_eq!(price.raw, expected_raw as PriceRaw);
}
#[rstest]
fn test_from_decimal_dp_rounding() {
let decimal = dec!(1.005);
let price = Price::from_decimal_dp(decimal, 2).unwrap();
assert_eq!(price.as_f64(), 1.0);
let decimal = dec!(1.015);
let price = Price::from_decimal_dp(decimal, 2).unwrap();
assert_eq!(price.as_f64(), 1.02); }
#[rstest]
fn test_from_decimal_infers_precision() {
let decimal = dec!(123.456);
let price = Price::from_decimal(decimal).unwrap();
assert_eq!(price.precision, 3);
assert!(approx_eq!(f64, price.as_f64(), 123.456, epsilon = 1e-10));
let decimal = dec!(100);
let price = Price::from_decimal(decimal).unwrap();
assert_eq!(price.precision, 0);
assert_eq!(price.as_f64(), 100.0);
let decimal = dec!(1.23456789);
let price = Price::from_decimal(decimal).unwrap();
assert_eq!(price.precision, 8);
assert!(approx_eq!(f64, price.as_f64(), 1.23456789, epsilon = 1e-10));
}
#[rstest]
fn test_from_decimal_trailing_zeros() {
let decimal = dec!(1.230);
assert_eq!(decimal.scale(), 3);
let price = Price::from_decimal(decimal).unwrap();
assert_eq!(price.precision, 3);
assert!(approx_eq!(f64, price.as_f64(), 1.23, epsilon = 1e-10));
let normalized = decimal.normalize();
assert_eq!(normalized.scale(), 2);
let price_normalized = Price::from_decimal(normalized).unwrap();
assert_eq!(price_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 price = Price::from_str(input).unwrap();
assert_eq!(price.precision, expected_precision);
}
#[rstest]
fn test_from_decimal_excessive_precision_inference() {
let decimal = dec!(1.1234567890123456789012345678);
if decimal.scale() > FIXED_PRECISION as u32 {
assert!(Price::from_decimal(decimal).is_err());
}
}
#[rstest]
fn test_from_decimal_negative_price() {
let decimal = dec!(-123.45);
let price = Price::from_decimal(decimal).unwrap();
assert_eq!(price.precision, 2);
assert!(approx_eq!(f64, price.as_f64(), -123.45, epsilon = 1e-10));
assert!(price.raw < 0);
}
#[rstest]
fn test_string_formatting() {
assert_eq!(format!("{}", Price::new(1234.5678, 4)), "1234.5678");
assert_eq!(
format!("{:?}", Price::new(1234.5678, 4)),
"Price(1234.5678)"
);
assert_eq!(Price::new(1234.5678, 4).to_formatted_string(), "1_234.5678");
}
#[rstest]
#[case(1234.5678, 4, "Price(1234.5678)", "1234.5678")] #[case(123.456789012345, 8, "Price(123.45678901)", "123.45678901")] #[cfg_attr(
feature = "defi",
case(
2_000_000_000_000_000_000.0,
18,
"Price(2000000000000000000)",
"2000000000000000000"
)
)] fn test_string_formatting_precision_handling(
#[case] value: f64,
#[case] precision: u8,
#[case] expected_debug: &str,
#[case] expected_display: &str,
) {
let price = if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
Price::from_raw(value as PriceRaw, precision)
} else {
Price::new(value, precision)
};
assert_eq!(format!("{price:?}"), expected_debug);
assert_eq!(format!("{price}"), expected_display);
assert_eq!(
price.to_formatted_string().replace('_', ""),
expected_display
);
}
#[rstest]
fn test_decimal_conversions() {
let price = Price::new(123.456, 3);
assert_eq!(price.as_decimal(), dec!(123.456));
let price = Price::new(0.000001, 6);
assert_eq!(price.as_decimal(), dec!(0.000001));
}
#[rstest]
fn test_basic_arithmetic() {
let p1 = Price::new(10.5, 2);
let p2 = Price::new(5.25, 2);
assert_eq!(p1 + p2, Price::from("15.75"));
assert_eq!(p1 - p2, Price::from("5.25"));
assert_eq!(-p1, Price::from("-10.5"));
}
#[rstest]
fn test_mixed_precision_add() {
let p1 = Price::new(10.5, 1);
let p2 = Price::new(5.25, 2);
let result = p1 + p2;
assert_eq!(result.precision, 2);
assert_eq!(result.as_f64(), 15.75);
}
#[rstest]
fn test_mixed_precision_sub() {
let p1 = Price::new(10.5, 1);
let p2 = Price::new(5.25, 2);
let result = p1 - p2;
assert_eq!(result.precision, 2);
assert_eq!(result.as_f64(), 5.25);
}
#[rstest]
fn test_f64_operations() {
let p = Price::new(10.5, 2);
assert_eq!(p + 1.0, 11.5);
assert_eq!(p - 1.0, 9.5);
assert_eq!(p * 2.0, 21.0);
assert_eq!(p / 2.0, 5.25);
}
#[rstest]
fn test_equality_and_comparisons() {
let p1 = Price::new(10.0, 1);
let p2 = Price::new(20.0, 1);
let p3 = Price::new(10.0, 1);
assert!(p1 < p2);
assert!(p2 > p1);
assert!(p1 <= p3);
assert!(p1 >= p3);
assert_eq!(p1, p3);
assert_ne!(p1, p2);
assert_eq!(Price::from("1.0"), Price::from("1.0"));
assert_ne!(Price::from("1.1"), Price::from("1.0"));
assert!(Price::from("1.0") <= Price::from("1.0"));
assert!(Price::from("1.1") > Price::from("1.0"));
assert!(Price::from("1.0") >= Price::from("1.0"));
assert!(Price::from("1.0") >= Price::from("1.0"));
assert!(Price::from("1.0") >= Price::from("1.0"));
assert!(Price::from("0.9") < Price::from("1.0"));
assert!(Price::from("0.9") <= Price::from("1.0"));
assert!(Price::from("0.9") <= Price::from("1.0"));
}
#[rstest]
fn test_deref() {
let price = Price::new(10.0, 1);
assert_eq!(*price, price.raw);
}
#[rstest]
fn test_decode_raw_price_i64() {
let raw_scaled_by_1e9 = 42_000_000_000i64; let decoded = decode_raw_price_i64(raw_scaled_by_1e9);
let price = Price::from_raw(decoded, FIXED_PRECISION);
assert!(
approx_eq!(f64, price.as_f64(), 42.0, epsilon = 1e-9),
"Expected 42.0 f64, was {} (precision = {})",
price.as_f64(),
price.precision
);
}
#[rstest]
fn test_hash() {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
let price1 = Price::new(1.0, 2);
let price2 = Price::new(1.0, 2);
let price3 = Price::new(1.1, 2);
let mut hasher1 = DefaultHasher::new();
let mut hasher2 = DefaultHasher::new();
let mut hasher3 = DefaultHasher::new();
price1.hash(&mut hasher1);
price2.hash(&mut hasher2);
price3.hash(&mut hasher3);
assert_eq!(hasher1.finish(), hasher2.finish());
assert_ne!(hasher1.finish(), hasher3.finish());
}
#[rstest]
fn test_price_serde_json_round_trip() {
let price = Price::new(1.0500, 4);
let json = serde_json::to_string(&price).unwrap();
let deserialized: Price = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, price);
}
#[rstest]
fn test_from_mantissa_exponent_exact_precision() {
let price = Price::from_mantissa_exponent(12345, -2, 2);
assert_eq!(price.as_f64(), 123.45);
}
#[rstest]
fn test_from_mantissa_exponent_excess_rounds_down() {
let price = Price::from_mantissa_exponent(12345, -3, 2);
assert_eq!(price.as_f64(), 12.34);
}
#[rstest]
fn test_from_mantissa_exponent_excess_rounds_up() {
let price = Price::from_mantissa_exponent(12355, -3, 2);
assert_eq!(price.as_f64(), 12.36);
}
#[rstest]
fn test_from_mantissa_exponent_positive_exponent() {
let price = Price::from_mantissa_exponent(5, 2, 0);
assert_eq!(price.as_f64(), 500.0);
}
#[rstest]
fn test_from_mantissa_exponent_negative_mantissa() {
let price = Price::from_mantissa_exponent(-12345, -2, 2);
assert_eq!(price.as_f64(), -123.45);
}
#[rstest]
fn test_from_mantissa_exponent_zero() {
let price = Price::from_mantissa_exponent(0, 2, 2);
assert_eq!(price.as_f64(), 0.0);
}
#[rstest]
#[should_panic]
fn test_from_mantissa_exponent_overflow_panics() {
let _ = Price::from_mantissa_exponent(i64::MAX, 9, 0);
}
#[rstest]
#[should_panic(expected = "exceeds i128 range")]
fn test_from_mantissa_exponent_large_exponent_panics() {
let _ = Price::from_mantissa_exponent(1, 119, 0);
}
#[rstest]
fn test_from_mantissa_exponent_zero_with_large_exponent() {
let price = Price::from_mantissa_exponent(0, 119, 0);
assert_eq!(price.as_f64(), 0.0);
}
#[rstest]
fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
let price = Price::from_mantissa_exponent(12345, -120, 2);
assert_eq!(price.as_f64(), 0.0);
}
#[rstest]
fn test_decimal_arithmetic_operations() {
let price = Price::new(100.0, 2);
assert_eq!(price + dec!(50.25), dec!(150.25));
assert_eq!(price - dec!(30.50), dec!(69.50));
assert_eq!(price * dec!(1.5), dec!(150.00));
assert_eq!(price / dec!(4), dec!(25.00));
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
use rstest::rstest;
use super::*;
fn price_value_strategy() -> impl Strategy<Value = f64> {
prop_oneof![
0.00001..1.0,
1.0..100_000.0,
100_000.0..1_000_000.0,
-1_000.0..0.0,
Just(PRICE_MIN / 2.0),
Just(PRICE_MAX / 2.0),
]
}
fn float_precision_upper_bound() -> u8 {
FIXED_PRECISION.min(crate::types::fixed::MAX_FLOAT_PRECISION)
}
fn precision_strategy() -> impl Strategy<Value = u8> {
let upper = float_precision_upper_bound();
prop_oneof![Just(0u8), 0u8..=upper, Just(FIXED_PRECISION),]
}
fn precision_strategy_non_zero() -> impl Strategy<Value = u8> {
let upper = float_precision_upper_bound().max(1);
prop_oneof![Just(upper), Just(FIXED_PRECISION.max(1)), 1u8..=upper,]
}
fn valid_precision_raw_strategy() -> impl Strategy<Value = (u8, PriceRaw)> {
precision_strategy().prop_flat_map(|precision| {
let scale: PriceRaw = if precision >= FIXED_PRECISION {
1
} else {
(10 as PriceRaw).pow(u32::from(FIXED_PRECISION - precision))
};
let max_base = PRICE_RAW_MAX / scale;
let min_base = PRICE_RAW_MIN / scale;
(min_base..=max_base).prop_map(move |base| (precision, base * scale))
})
}
fn float_precision_strategy() -> impl Strategy<Value = u8> {
precision_strategy()
}
const DECIMAL_MAX_MANTISSA: i128 = 79_228_162_514_264_337_593_543_950_335;
#[allow(
clippy::useless_conversion,
reason = "PriceRaw is i64 or i128 depending on feature"
)]
fn decimal_compatible(raw: PriceRaw, precision: u8) -> bool {
if precision > crate::types::fixed::MAX_FLOAT_PRECISION {
return false;
}
let precision_diff = u32::from(FIXED_PRECISION.saturating_sub(precision));
let divisor = (10 as PriceRaw).pow(precision_diff);
let rescaled_raw = raw / divisor;
i128::from(rescaled_raw.abs()) <= DECIMAL_MAX_MANTISSA
}
proptest! {
#[rstest]
fn prop_price_serde_round_trip(
value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
precision in precision_strategy()
) {
let original = Price::new(value, precision);
let string_repr = original.to_string();
let from_string: Price = 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: Price = serde_json::from_str(&json).unwrap();
prop_assert_eq!(from_json.precision, original.precision);
}
#[rstest]
fn prop_price_arithmetic_associative(
a in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
b in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
c in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
precision in precision_strategy()
) {
let p_a = Price::new(a, precision);
let p_b = Price::new(b, precision);
let p_c = Price::new(c, precision);
let ab_raw = p_a.raw.checked_add(p_b.raw);
let bc_raw = p_b.raw.checked_add(p_c.raw);
if let (Some(ab_raw), Some(bc_raw)) = (ab_raw, bc_raw) {
let ab_c_raw = ab_raw.checked_add(p_c.raw);
let a_bc_raw = p_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_price_addition_subtraction_inverse(
base in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
delta in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() > 1e-3 && x.abs() < 1e6),
precision in precision_strategy()
) {
let p_base = Price::new(base, precision);
let p_delta = Price::new(delta, precision);
if let Some(added_raw) = p_base.raw.checked_add(p_delta.raw)
&& let Some(result_raw) = added_raw.checked_sub(p_delta.raw) {
prop_assert_eq!(result_raw, p_base.raw, "Inverse operation failed in raw arithmetic");
}
}
#[rstest]
fn prop_price_ordering_transitive(
a in price_value_strategy(),
b in price_value_strategy(),
c in price_value_strategy(),
precision in float_precision_strategy()
) {
let p_a = Price::new(a, precision);
let p_b = Price::new(b, precision);
let p_c = Price::new(c, precision);
if p_a <= p_b && p_b <= p_c {
prop_assert!(p_a <= p_c, "Transitivity failed: {} <= {} <= {} but {} > {}",
p_a.as_f64(), p_b.as_f64(), p_c.as_f64(), p_a.as_f64(), p_c.as_f64());
}
}
#[rstest]
fn prop_price_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 price_str = format!("{integral}.{fractional_str}");
let parsed: Price = price_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_price_precision_information_preservation(
value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
precision1 in precision_strategy_non_zero(),
precision2 in precision_strategy_non_zero()
) {
prop_assume!(precision1 != precision2);
let _p1 = Price::new(value, precision1);
let _p2 = Price::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 p1_reduced = Price::new(rounded_value, min_precision);
let p2_reduced = Price::new(rounded_value, min_precision);
prop_assert_eq!(p1_reduced.raw, p2_reduced.raw, "Precision reduction inconsistent");
}
#[rstest]
fn prop_price_arithmetic_bounds(
a in price_value_strategy(),
b in price_value_strategy(),
precision in float_precision_strategy()
) {
let p_a = Price::new(a, precision);
let p_b = Price::new(b, precision);
let sum_f64 = p_a.as_f64() + p_b.as_f64();
if sum_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&sum_f64) {
let sum = p_a + p_b;
prop_assert!(sum.as_f64().is_finite());
prop_assert!(!sum.is_undefined());
}
let diff_f64 = p_a.as_f64() - p_b.as_f64();
if diff_f64.is_finite() && (PRICE_MIN..=PRICE_MAX).contains(&diff_f64) {
let diff = p_a - p_b;
prop_assert!(diff.as_f64().is_finite());
prop_assert!(!diff.is_undefined());
}
}
}
proptest! {
#[rstest]
fn prop_price_as_decimal_preserves_precision(
(precision, raw) in valid_precision_raw_strategy()
) {
prop_assume!(decimal_compatible(raw, precision));
let price = Price::from_raw(raw, precision);
let decimal = price.as_decimal();
prop_assert_eq!(decimal.scale(), u32::from(precision));
}
#[rstest]
fn prop_price_as_decimal_matches_display(
value in price_value_strategy().prop_filter("Reasonable values", |&x| x.abs() < 1e6),
precision in float_precision_strategy()
) {
let price = Price::new(value, precision);
prop_assume!(decimal_compatible(price.raw, precision));
let display_str = format!("{price}");
let decimal_str = price.as_decimal().to_string();
prop_assert_eq!(display_str, decimal_str);
}
#[rstest]
fn prop_price_from_decimal_roundtrip(
(precision, raw) in valid_precision_raw_strategy()
) {
prop_assume!(decimal_compatible(raw, precision));
let original = Price::from_raw(raw, precision);
let decimal = original.as_decimal();
let reconstructed = Price::from_decimal(decimal).unwrap();
prop_assert_eq!(original.raw, reconstructed.raw);
prop_assert_eq!(original.precision, reconstructed.precision);
}
#[rstest]
fn prop_price_from_raw_round_trip(
(precision, raw) in valid_precision_raw_strategy()
) {
let price = Price::from_raw(raw, precision);
prop_assert_eq!(price.raw, raw);
prop_assert_eq!(price.precision, precision);
}
}
}