quant-primitives 0.7.0

Pure trading primitives — candles, intervals, symbols, currencies, asset taxonomy
Documentation
//! Validated basis-points value object.
//!
//! Replaces raw `Decimal` fields like `slippage_bps`, `commission_bps`, etc.
//! with a type that guarantees the value is in [0, 10000].
//! 1 basis point = 0.01% = 0.0001 as a fraction.

use std::fmt;

use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

use crate::Fraction;

/// A basis-points value validated to the range [0, 10000].
///
/// 10000 bps = 100% = 1.0 as a fraction.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Bps(Decimal);

/// Error for invalid basis-points construction.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum BpsError {
    /// Value is outside the valid [0, 10000] range.
    #[error("basis points {0} out of range [0, 10000]")]
    OutOfRange(Decimal),
}

impl Bps {
    /// Create a new basis-points value, validating that the value is in [0, 10000].
    pub fn new(value: Decimal) -> Result<Self, BpsError> {
        if value < Decimal::ZERO || value > Decimal::from(10000) {
            return Err(BpsError::OutOfRange(value));
        }
        Ok(Self(value))
    }

    /// Create basis points from a trusted integer value.
    ///
    /// # Panics
    ///
    /// Panics if `value > 10000`.
    pub fn from_trusted(value: u32) -> Self {
        assert!(value <= 10000, "from_trusted: {value} > 10000");
        Self(Decimal::from(value))
    }

    /// The raw basis-points value (0-10000).
    pub fn value(&self) -> Decimal {
        self.0
    }

    /// Convert to a [`Fraction`] in [0.0, 1.0].
    ///
    /// 100 bps → 0.01, 10000 bps → 1.0.
    pub fn as_fraction(&self) -> Decimal {
        self.0 / Decimal::from(10000)
    }

    /// Convert to a validated [`Fraction`] value.
    pub fn to_fraction(&self) -> Fraction {
        // Safety: Bps is [0, 10000], so / 10000 is always [0, 1].
        Fraction(self.0 / Decimal::from(10000))
    }

    /// The zero basis points.
    pub fn zero() -> Self {
        Self(Decimal::ZERO)
    }
}

impl fmt::Display for Bps {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}bps", self.0.normalize())
    }
}

#[cfg(test)]
#[path = "bps_tests.rs"]
mod tests;