#[macro_use]
mod macros;
pub mod errors;
pub mod traits;
pub mod types;
pub use errors::MathError;
pub use traits::{ScalarBounds, WrapperNum};
pub use types::*;
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
#[test]
fn test_new_constructor_macro() {
let base_lots_1 = BaseLots::new(5);
let base_lots_2 = BaseLots::new(10);
assert_eq!(base_lots_1 + base_lots_2, BaseLots::new(15));
}
#[test]
fn test_multiply_macro() {
let base_units = BaseUnits::new(5);
let base_lots_per_base_unit = BaseLotsPerBaseUnit::new(100);
assert_eq!(base_units * base_lots_per_base_unit, BaseLots::new(500));
}
#[test]
fn test_bounds_range_quote_lots() {
let bounds = QuoteLots::bounds();
assert_eq!(*bounds.start(), 0);
assert_eq!(*bounds.end(), u64::MAX);
assert_eq!(QuoteLots::lower_bound(), 0);
assert_eq!(QuoteLots::upper_bound(), u64::MAX);
}
#[test]
fn test_bounds_range_base_lots() {
let bounds = BaseLots::bounds();
assert_eq!(*bounds.start(), 0);
assert_eq!(*bounds.end(), u32::MAX as u64);
assert_eq!(BaseLots::lower_bound(), 0);
assert_eq!(BaseLots::upper_bound(), u32::MAX as u64);
}
#[test]
fn test_bounds_range_ticks() {
let bounds = Ticks::bounds();
assert_eq!(*bounds.start(), 0);
assert_eq!(*bounds.end(), u32::MAX as u64);
assert_eq!(Ticks::lower_bound(), 0);
assert_eq!(Ticks::upper_bound(), u32::MAX as u64);
}
proptest! {
#[test]
fn test_quote_lots_is_in_bounds(value in 0..=u32::MAX as u64) {
let quote_lots = QuoteLots::new(value);
prop_assert!(quote_lots.is_in_bounds());
prop_assert_eq!(quote_lots.as_inner(), value);
}
#[test]
fn test_base_lots_is_in_bounds(value in 0..=u32::MAX as u64) {
let base_lots = BaseLots::new(value);
prop_assert!(base_lots.is_in_bounds());
prop_assert_eq!(base_lots.as_inner(), value);
}
#[test]
fn test_base_lots_out_of_bounds(value in (u32::MAX as u64 + 1)..=u64::MAX) {
let base_lots = BaseLots::new(value);
prop_assert!(!base_lots.is_in_bounds());
}
#[test]
fn test_ticks_is_in_bounds(value in 0..=u32::MAX as u64) {
let ticks = Ticks::new(value);
prop_assert!(ticks.is_in_bounds());
prop_assert_eq!(ticks.as_inner(), value);
}
#[test]
fn test_ticks_out_of_bounds(value in (u32::MAX as u64 + 1)..=u64::MAX) {
let ticks = Ticks::new(value);
prop_assert!(!ticks.is_in_bounds());
}
#[test]
fn test_arithmetic_preserves_bounds_add(a in 0..=u16::MAX as u64, b in 0..=u16::MAX as u64) {
let quote_lots_a = QuoteLots::new(a);
let quote_lots_b = QuoteLots::new(b);
let sum = quote_lots_a + quote_lots_b;
prop_assert!(sum.is_in_bounds());
prop_assert_eq!(sum.as_inner(), a + b);
}
#[test]
fn test_arithmetic_preserves_bounds_sub(a in 0..=u32::MAX as u64, b in 0..=u32::MAX as u64) {
prop_assume!(b <= a);
let base_lots_a = BaseLots::new(a);
let base_lots_b = BaseLots::new(b);
let diff = base_lots_a - base_lots_b;
prop_assert!(diff.is_in_bounds());
prop_assert_eq!(diff.as_inner(), a - b);
}
#[test]
fn test_saturating_sub_preserves_bounds(a in 0..=u32::MAX as u64, b in 0..=u32::MAX as u64) {
let base_lots_a = BaseLots::new(a);
let base_lots_b = BaseLots::new(b);
let result = base_lots_a.saturating_sub(base_lots_b);
prop_assert!(result.is_in_bounds());
prop_assert_eq!(result.as_inner(), a.saturating_sub(b));
}
#[test]
fn test_multiply_preserves_bounds(ticks in 0..=100u64, base_lots_per_tick in 0..=1000u64) {
let t = Ticks::new(ticks);
let blpt = BaseLotsPerTick::new(base_lots_per_tick);
if ticks * base_lots_per_tick <= u32::MAX as u64 {
let result = blpt * t;
prop_assert!(result.is_in_bounds());
prop_assert_eq!(result.as_inner(), ticks * base_lots_per_tick);
}
}
}
#[test]
fn test_checked_division() {
let a = BaseLots::new(100);
let b = BaseLots::new(10);
let zero = BaseLots::new(0);
assert_eq!(a.checked_div(b), Some(BaseLots::new(10)));
assert_eq!(a.checked_div(zero), None);
}
#[test]
fn test_div_ceil_correct() {
let a = BaseLots::new(10);
let b = BaseLots::new(3);
let c = BaseLots::new(12);
assert_eq!(a.div_ceil(b), BaseLots::new(4));
assert_eq!(c.div_ceil(b), BaseLots::new(4));
assert_eq!(a.checked_div_ceil(b), Some(BaseLots::new(4)));
assert_eq!(c.checked_div_ceil(b), Some(BaseLots::new(4)));
assert_eq!(a.checked_div_ceil(BaseLots::new(0)), None);
}
#[test]
fn test_checked_add_signed() {
let unsigned = QuoteLots::new(100);
let positive = SignedQuoteLots::new(50);
let negative = SignedQuoteLots::new(-30);
let large_negative = SignedQuoteLots::new(-150);
assert_eq!(
unsigned.checked_add_signed(positive),
Some(QuoteLots::new(150))
);
assert_eq!(
unsigned.checked_add_signed(negative),
Some(QuoteLots::new(70))
);
assert_eq!(unsigned.checked_add_signed(large_negative), None);
}
#[test]
fn test_saturating_add_signed() {
let unsigned = QuoteLots::new(100);
let positive = SignedQuoteLots::new(50);
let negative = SignedQuoteLots::new(-30);
let large_negative = SignedQuoteLots::new(-150);
assert_eq!(
unsigned.saturating_add_signed(positive),
QuoteLots::new(150)
);
assert_eq!(unsigned.saturating_add_signed(negative), QuoteLots::new(70));
assert_eq!(
unsigned.saturating_add_signed(large_negative),
QuoteLots::new(0)
);
}
#[test]
fn test_overflow_safe_arithmetic() {
let max = QuoteLots::new(u64::MAX);
let one = QuoteLots::new(1);
assert_eq!(max.checked_add(one), None);
assert_eq!(max.saturating_add(one), max);
let zero = QuoteLots::new(0);
assert_eq!(zero.checked_sub(one), None);
assert_eq!(zero.saturating_sub(one), zero);
assert_eq!(max.wrapping_add(one).as_inner(), 0);
assert_eq!(zero.wrapping_sub(one).as_inner(), u64::MAX);
let mid = QuoteLots::new(1000);
let small = QuoteLots::new(100);
assert_eq!((mid + small).as_inner(), 1100);
assert_eq!((mid - small).as_inner(), 900);
}
#[test]
fn test_checked_constructors() {
assert_eq!(BaseLots::new_checked(100), Ok(BaseLots::new(100)));
assert_eq!(
BaseLots::new_checked(u32::MAX as u64),
Ok(BaseLots::new(u32::MAX as u64))
);
assert!(BaseLots::new_checked(u64::MAX).is_err());
assert_eq!(BaseLots::new_saturating(100).as_inner(), 100);
assert_eq!(
BaseLots::new_saturating(u64::MAX).as_inner(),
u32::MAX as u64
);
assert_eq!(Ticks::new_saturating(u64::MAX).as_inner(), u32::MAX as u64);
assert_eq!(
QuoteLots::new_checked(u64::MAX),
Ok(QuoteLots::new(u64::MAX))
);
}
#[test]
fn test_checked_div_by_types() {
let tick_size = QuoteLotsPerBaseLotPerTick::new(5);
let ticks = Ticks::new(20);
let price = tick_size * ticks;
assert_eq!(
price.checked_div_by_quote_lots_per_base_lot_per_tick(tick_size),
Some(ticks)
);
assert_eq!(price.checked_div_by_ticks(ticks), Some(tick_size));
assert_eq!(
price.checked_div_by_quote_lots_per_base_lot_per_tick(QuoteLotsPerBaseLotPerTick::new(
0
)),
None
);
assert_eq!(price.checked_div_by_ticks(Ticks::new(0)), None);
}
#[test]
#[should_panic(expected = "Underflow in add operation")]
fn test_add_signed_panic_compatibility() {
let unsigned = QuoteLots::new(50);
let large_negative = SignedQuoteLots::new(-100);
let _ = unsigned + large_negative; }
proptest! {
#[test]
fn test_div_ceil_always_gte_regular_division(a in 1u64..=u64::MAX/2, b in 1u64..=u64::MAX/2) {
let regular_div = a / b;
let ceil_div = a.div_ceil(b);
prop_assert!(ceil_div >= regular_div,
"div_ceil({}, {}) = {} should be >= regular division = {}",
a, b, ceil_div, regular_div);
}
#[test]
fn test_div_ceil_exact_when_no_remainder(a in 1u64..=1_000_000u64, b in 1u64..=1_000_000u64) {
if a % b == 0 {
let regular_div = a / b;
let ceil_div = a.div_ceil(b);
prop_assert_eq!(ceil_div, regular_div,
"When {} % {} == 0, div_ceil should equal regular division", a, b);
}
}
#[test]
fn test_div_ceil_plus_one_when_remainder(a in 1u64..=1_000_000u64, b in 1u64..=1_000_000u64) {
if a % b != 0 {
let regular_div = a / b;
let ceil_div = a.div_ceil(b);
prop_assert_eq!(ceil_div, regular_div + 1,
"When {} % {} != 0, div_ceil should be regular division + 1", a, b);
}
}
#[test]
fn test_div_ceil_by_one_identity(a in 0u64..=u64::MAX) {
let result = a.div_ceil(1);
prop_assert_eq!(result, a, "div_ceil({}, 1) should equal {}", a, a);
}
#[test]
fn test_div_ceil_zero_numerator(b in 1u64..=u64::MAX) {
let result = 0u64.div_ceil(b);
prop_assert_eq!(result, 0, "div_ceil(0, {}) should be 0", b);
}
#[test]
fn test_div_ceil_small_numerator(a in 1u64..=1_000_000u64, b in 1u64..=1_000_000u64) {
if a <= b {
let result = a.div_ceil(b);
prop_assert!(result <= 1,
"When {} <= {}, div_ceil should be 0 or 1, got {}", a, b, result);
if a > 0 {
prop_assert_eq!(result, 1);
}
}
}
#[test]
fn test_div_ceil_mathematical_definition(a in 1u64..=u64::MAX/2, b in 1u64..=1_000_000u64) {
let ceil_div = a.div_ceil(b);
if let Some(sum) = a.checked_add(b - 1) {
let alternative = sum / b;
prop_assert_eq!(ceil_div, alternative,
"div_ceil({}, {}) = {} should match alternative formula = {}",
a, b, ceil_div, alternative);
}
}
}
#[test]
fn test_margin_calculation_regression() {
let book_value = BaseLots::new(1_000_000_000); let leverage = Constant::new(1);
let margin = book_value.div_ceil(leverage);
assert_eq!(
margin.as_inner(),
1_000_000_000,
"Margin for $1000 with leverage 1 should be exactly $1000, not $1000.000001"
);
}
#[test]
fn test_old_vs_new_div_ceil_behavior() {
let old_div_ceil = |a: u64, b: u64| -> u64 { a / b + 1 };
let new_div_ceil = |a: u64, b: u64| -> u64 { a.div_ceil(b) };
assert_ne!(
old_div_ceil(10, 5),
new_div_ceil(10, 5),
"Old implementation incorrectly adds 1 for exact divisions"
);
assert_eq!(new_div_ceil(10, 5), 2, "Correct result for 10/5");
assert_eq!(old_div_ceil(10, 5), 3, "Old buggy result for 10/5");
assert_eq!(
old_div_ceil(11, 5),
new_div_ceil(11, 5),
"Both should round up for divisions with remainder"
);
assert_eq!(new_div_ceil(11, 5), 3, "Correct result for 11/5");
}
#[test]
fn test_signed_fee_rate_overflow_safety() {
use crate::math::quantities::types::{
FeeRateMicro, QuoteLots, SignedFeeRateMicro, SignedQuoteLots,
};
let signed_fee = SignedFeeRateMicro::new(100);
let unsigned_fee = FeeRateMicro::new(u32::MAX);
assert!(signed_fee.add_unsigned_checked(unsigned_fee).is_none());
let unsigned_fee_large = FeeRateMicro::new(i32::MAX as u32);
assert!(
signed_fee
.add_unsigned_checked(unsigned_fee_large)
.is_none(),
"Should fail due to addition overflow"
);
let unsigned_fee_small = FeeRateMicro::new(1000);
assert!(
signed_fee
.add_unsigned_checked(unsigned_fee_small)
.is_some()
);
assert_eq!(
signed_fee
.add_unsigned_checked(unsigned_fee_small)
.unwrap()
.as_inner(),
1100
);
let signed_lots = SignedQuoteLots::new(i64::MIN);
let base = QuoteLots::new(1000);
let result = base.saturating_add_signed(signed_lots);
assert_eq!(result, QuoteLots::new(0));
let signed_lots_normal = SignedQuoteLots::new(-500);
let result_normal = base.saturating_add_signed(signed_lots_normal);
assert_eq!(result_normal, QuoteLots::new(500));
}
#[test]
fn test_quote_lots_checked_as_signed_bounds() {
let at_max = QuoteLots::new(i64::MAX as u64);
assert_eq!(
at_max.checked_as_signed(),
Ok(SignedQuoteLots::new(i64::MAX))
);
let overflow = QuoteLots::new(i64::MAX as u64 + 1);
assert_eq!(overflow.checked_as_signed(), Err(MathError::Overflow));
}
#[test]
fn test_signed_quote_lots_checked_as_unsigned() {
let positive = SignedQuoteLots::new(10);
assert_eq!(positive.checked_as_unsigned(), Ok(QuoteLots::new(10)));
let negative = SignedQuoteLots::new(-1);
assert_eq!(negative.checked_as_unsigned(), Err(MathError::Underflow));
}
#[test]
fn test_signed_quote_lots_abs_as_unsigned() {
let value = SignedQuoteLots::new(-10);
assert_eq!(value.abs_as_unsigned(), QuoteLots::new(10));
}
#[test]
fn test_signed_quote_lots_abs_as_unsigned_min_overflow() {
let min_value = SignedQuoteLots::new(i64::MIN);
assert_eq!(
min_value.abs_as_unsigned(),
QuoteLots::new(i64::MIN.unsigned_abs())
);
}
#[test]
#[should_panic(expected = "Overflow in abs for signed 64-bit wrapper")]
fn test_signed_quote_lots_abs_panics_on_min() {
let _ = SignedQuoteLots::new(i64::MIN).abs();
}
#[test]
#[should_panic(expected = "Overflow in neg for signed 64-bit wrapper")]
fn test_signed_quote_lots_neg_panics_on_min() {
let _ = -SignedQuoteLots::new(i64::MIN);
}
#[test]
#[should_panic(expected = "Overflow in abs for signed 32-bit wrapper")]
fn test_signed_fee_rate_abs_panics_on_min() {
let _ = SignedFeeRateMicro::new(i32::MIN).abs();
}
#[test]
#[should_panic(expected = "Overflow in neg for signed 32-bit wrapper")]
fn test_signed_fee_rate_neg_panics_on_min() {
let _ = -SignedFeeRateMicro::new(i32::MIN);
}
#[test]
#[should_panic(expected = "Overflow in add: result exceeds signed bounds")]
fn test_signed_quote_lots_add_unsigned_panics_on_overflow() {
let signed = SignedQuoteLots::new(i64::MAX);
let _ = signed + QuoteLots::new(1);
}
#[test]
#[should_panic(expected = "Overflow in sub: result exceeds signed bounds")]
fn test_signed_quote_lots_sub_unsigned_panics_on_underflow() {
let signed = SignedQuoteLots::new(i64::MIN);
let _ = signed - QuoteLots::new(1);
}
#[test]
fn test_ticks_checked_as_signed_bounds() {
let in_range = Ticks::new(i32::MAX as u64);
assert!(in_range.checked_as_signed().is_ok());
let overflow = Ticks::new(i32::MAX as u64 + 1);
assert_eq!(overflow.checked_as_signed(), Err(MathError::Overflow));
}
#[test]
fn test_base_lots_checked_as_signed_bounds() {
let at_bound = BaseLots::new(u32::MAX as u64);
assert!(at_bound.checked_as_signed().is_ok());
assert_eq!(
at_bound.checked_as_signed(),
Ok(SignedBaseLots::new(u32::MAX as i64))
);
let overflow = BaseLots::new(i64::MAX as u64 + 1);
assert_eq!(overflow.checked_as_signed(), Err(MathError::Overflow));
}
#[test]
fn test_signed_base_lots_checked_as_unsigned_bounds() {
let in_range = SignedBaseLots::new(u32::MAX as i64);
assert_eq!(
in_range.checked_as_unsigned(),
Ok(BaseLots::new(u32::MAX as u64))
);
let overflow = SignedBaseLots::new(u32::MAX as i64 + 1);
assert_eq!(overflow.checked_as_unsigned(), Err(MathError::Overflow));
let large_overflow = SignedBaseLots::new(i64::MAX);
assert_eq!(
large_overflow.checked_as_unsigned(),
Err(MathError::Overflow)
);
let negative = SignedBaseLots::new(-1);
assert_eq!(negative.checked_as_unsigned(), Err(MathError::Underflow));
}
}