Skip to main content

quant_primitives/
bps.rs

1//! Validated basis-points value object.
2//!
3//! Replaces raw `Decimal` fields like `slippage_bps`, `commission_bps`, etc.
4//! with a type that guarantees the value is in [0, 10000].
5//! 1 basis point = 0.01% = 0.0001 as a fraction.
6
7use std::fmt;
8
9use rust_decimal::Decimal;
10use serde::{Deserialize, Serialize};
11
12use crate::Fraction;
13
14/// A basis-points value validated to the range [0, 10000].
15///
16/// 10000 bps = 100% = 1.0 as a fraction.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
18pub struct Bps(Decimal);
19
20/// Error for invalid basis-points construction.
21#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22pub enum BpsError {
23    /// Value is outside the valid [0, 10000] range.
24    #[error("basis points {0} out of range [0, 10000]")]
25    OutOfRange(Decimal),
26}
27
28impl Bps {
29    /// Create a new basis-points value, validating that the value is in [0, 10000].
30    pub fn new(value: Decimal) -> Result<Self, BpsError> {
31        if value < Decimal::ZERO || value > Decimal::from(10000) {
32            return Err(BpsError::OutOfRange(value));
33        }
34        Ok(Self(value))
35    }
36
37    /// Create basis points from a trusted integer value.
38    ///
39    /// # Panics
40    ///
41    /// Panics if `value > 10000`.
42    pub fn from_trusted(value: u32) -> Self {
43        assert!(value <= 10000, "from_trusted: {value} > 10000");
44        Self(Decimal::from(value))
45    }
46
47    /// The raw basis-points value (0-10000).
48    pub fn value(&self) -> Decimal {
49        self.0
50    }
51
52    /// Convert to a [`Fraction`] in [0.0, 1.0].
53    ///
54    /// 100 bps → 0.01, 10000 bps → 1.0.
55    pub fn as_fraction(&self) -> Decimal {
56        self.0 / Decimal::from(10000)
57    }
58
59    /// Convert to a validated [`Fraction`] value.
60    pub fn to_fraction(&self) -> Fraction {
61        // Safety: Bps is [0, 10000], so / 10000 is always [0, 1].
62        Fraction(self.0 / Decimal::from(10000))
63    }
64
65    /// The zero basis points.
66    pub fn zero() -> Self {
67        Self(Decimal::ZERO)
68    }
69}
70
71impl fmt::Display for Bps {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        write!(f, "{}bps", self.0.normalize())
74    }
75}
76
77#[cfg(test)]
78#[path = "bps_tests.rs"]
79mod tests;