1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
//! Validated percentage value object.
//!
//! Replaces raw `Decimal` fields like `max_drawdown_pct`, `margin_pct`, etc.
//! with a type that guarantees the value is in [0, 100].
use std::fmt;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::Fraction;
/// A percentage value validated to the range [0, 100].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Percentage(Decimal);
/// Error for invalid percentage construction.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PercentageError {
/// Value is outside the valid [0, 100] range.
#[error("percentage {0} out of range [0, 100]")]
OutOfRange(Decimal),
}
impl Percentage {
/// Create a new percentage, validating that the value is in [0, 100].
pub fn new(value: Decimal) -> Result<Self, PercentageError> {
if value < Decimal::ZERO || value > Decimal::from(100) {
return Err(PercentageError::OutOfRange(value));
}
Ok(Self(value))
}
/// Create a percentage from a trusted `u32` value.
///
/// # Panics
///
/// Panics if `value > 100`.
pub fn from_trusted(value: u32) -> Self {
assert!(value <= 100, "from_trusted: {value} > 100");
Self(Decimal::from(value))
}
/// The raw percentage value (0-100).
pub fn value(&self) -> Decimal {
self.0
}
/// Convert to a fraction in [0.0, 1.0] (raw Decimal).
pub fn as_fraction(&self) -> Decimal {
self.0 / Decimal::from(100)
}
/// Convert to a validated [`Fraction`] value.
pub fn to_fraction(&self) -> Fraction {
// Safety: Percentage is [0, 100], so / 100 is always [0, 1].
Fraction(self.0 / Decimal::from(100))
}
/// Returns `true` if this value looks like it was passed as a fraction (0-1)
/// instead of a percentage (0-100).
///
/// Values < 1.0 are suspiciously small for most percentage use cases
/// (stop loss, margin requirement, risk limit).
///
/// # Example
/// ```
/// use rust_decimal_macros::dec;
/// use quant_primitives::Percentage;
///
/// let pct = Percentage::new(dec!(0.05)).unwrap();
/// assert!(pct.is_likely_fraction()); // 0.05% is suspicious — caller probably meant 5%
///
/// let normal = Percentage::new(dec!(5)).unwrap();
/// assert!(!normal.is_likely_fraction()); // 5% is plausible
/// ```
pub fn is_likely_fraction(&self) -> bool {
self.0 > Decimal::ZERO && self.0 < Decimal::ONE
}
/// The zero percentage.
pub fn zero() -> Self {
Self(Decimal::ZERO)
}
/// The 100% percentage.
pub fn hundred() -> Self {
Self(Decimal::from(100))
}
}
impl fmt::Display for Percentage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}%", self.0.normalize())
}
}
#[cfg(test)]
#[path = "percentage_tests.rs"]
mod tests;