betex 0.5.3

Betfair / Prediction Market Exchange
Documentation
use serde::{Deserialize, Serialize};
use std::ops::{Add, Sub};

/// Integral amount type used by the engine/book for stake/size/remaining.
///
/// # Units (Quanta)
/// `Money` is an integer count of **quanta**, not cents.
///
/// - `Money(1)` is **$0.0001** (one ten-thousandth of a dollar).
/// - This makes probability-tick prediction markets with `MAX=10_000` exact:
///   `cost_quanta = qty_shares * price_ticks` (no division, no rounding).
///
/// The engine avoids floating point to keep matching deterministic.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Serialize,
    Deserialize,
    PartialOrd,
    Ord,
    Default,
    Hash,
    rkyv::Archive,
    rkyv::Serialize,
    rkyv::Deserialize,
)]
pub struct Money(pub i64);

impl Money {
    /// Quanta per USD (i.e. `Money(MONEY_SCALE_PER_USD)` == $1.00).
    pub const MONEY_SCALE_PER_USD: i64 = 10_000;

    /// Quanta per cent (i.e. `Money(MONEY_SCALE_PER_CENT)` == $0.01).
    pub const MONEY_SCALE_PER_CENT: i64 = Self::MONEY_SCALE_PER_USD / 100;

    pub fn zero() -> Self {
        Money(0)
    }
    pub fn is_positive(self) -> bool {
        self.0 > 0
    }
    pub fn saturating_sub(self, other: Money) -> Money {
        Money(self.0.saturating_sub(other.0))
    }
    pub fn saturating_add(self, other: Money) -> Money {
        Money(self.0.saturating_add(other.0))
    }
    pub fn clamp_non_negative(self) -> Money {
        Money(self.0.max(0))
    }

    /// Convert cents to `Money` quanta.
    pub fn from_cents(cents: i64) -> Self {
        Money(cents.saturating_mul(Self::MONEY_SCALE_PER_CENT))
    }

    /// Convert `Money` quanta to cents by truncation towards zero.
    pub fn to_cents_trunc(self) -> i64 {
        self.0 / Self::MONEY_SCALE_PER_CENT
    }
}

impl Add for Money {
    type Output = Money;
    fn add(self, rhs: Money) -> Money {
        Money(self.0.saturating_add(rhs.0))
    }
}

impl Sub for Money {
    type Output = Money;
    fn sub(self, rhs: Money) -> Money {
        Money(self.0.saturating_sub(rhs.0))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn add_saturates_at_max() {
        let max = Money(i64::MAX);
        let one = Money(1);
        assert_eq!(max + one, Money(i64::MAX));
        assert_eq!(max + max, Money(i64::MAX));
    }

    #[test]
    fn sub_saturates_at_min() {
        let min = Money(i64::MIN);
        let one = Money(1);
        assert_eq!(min - one, Money(i64::MIN));
        assert_eq!(min - Money(i64::MAX), Money(i64::MIN));
    }

    #[test]
    fn add_normal_case() {
        assert_eq!(Money(100) + Money(200), Money(300));
        assert_eq!(Money(-50) + Money(100), Money(50));
    }

    #[test]
    fn sub_normal_case() {
        assert_eq!(Money(300) - Money(100), Money(200));
        assert_eq!(Money(50) - Money(100), Money(-50));
    }

    #[test]
    fn saturating_add_method_matches_trait() {
        let a = Money(i64::MAX - 10);
        let b = Money(100);
        assert_eq!(a.saturating_add(b), a + b);
    }

    #[test]
    fn saturating_sub_method_matches_trait() {
        let a = Money(i64::MIN + 10);
        let b = Money(100);
        assert_eq!(a.saturating_sub(b), a - b);
    }
}