1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
9 pub use crate::{
10 RiskBudget, RiskError, RiskLevel, RiskLevelParseError, RiskLimit, RiskMeasure,
11 RiskMeasureParseError,
12 };
13}
14
15#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum RiskMeasure {
18 Volatility,
20 ValueAtRisk,
22 ExpectedShortfall,
24 Beta,
26 TrackingError,
28 Drawdown,
30 Exposure,
32 Leverage,
34 Unknown,
36 Custom(String),
38}
39
40impl fmt::Display for RiskMeasure {
41 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42 formatter.write_str(match self {
43 Self::Volatility => "volatility",
44 Self::ValueAtRisk => "value-at-risk",
45 Self::ExpectedShortfall => "expected-shortfall",
46 Self::Beta => "beta",
47 Self::TrackingError => "tracking-error",
48 Self::Drawdown => "drawdown",
49 Self::Exposure => "exposure",
50 Self::Leverage => "leverage",
51 Self::Unknown => "unknown",
52 Self::Custom(value) => value.as_str(),
53 })
54 }
55}
56
57impl FromStr for RiskMeasure {
58 type Err = RiskMeasureParseError;
59
60 fn from_str(value: &str) -> Result<Self, Self::Err> {
61 let trimmed = value.trim();
62 if trimmed.is_empty() {
63 return Err(RiskMeasureParseError::Empty);
64 }
65
66 match normalized_token(trimmed).as_str() {
67 "volatility" => Ok(Self::Volatility),
68 "value-at-risk" | "var" => Ok(Self::ValueAtRisk),
69 "expected-shortfall" | "es" => Ok(Self::ExpectedShortfall),
70 "beta" => Ok(Self::Beta),
71 "tracking-error" => Ok(Self::TrackingError),
72 "drawdown" => Ok(Self::Drawdown),
73 "exposure" => Ok(Self::Exposure),
74 "leverage" => Ok(Self::Leverage),
75 "unknown" => Ok(Self::Unknown),
76 _ => Ok(Self::Custom(trimmed.to_string())),
77 }
78 }
79}
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum RiskMeasureParseError {
84 Empty,
86}
87
88impl fmt::Display for RiskMeasureParseError {
89 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self {
91 Self::Empty => formatter.write_str("risk measure cannot be empty"),
92 }
93 }
94}
95
96impl Error for RiskMeasureParseError {}
97
98#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
100pub enum RiskLevel {
101 Low,
103 Medium,
105 High,
107 Critical,
109 Unknown,
111 Custom(String),
113}
114
115impl fmt::Display for RiskLevel {
116 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
117 formatter.write_str(match self {
118 Self::Low => "low",
119 Self::Medium => "medium",
120 Self::High => "high",
121 Self::Critical => "critical",
122 Self::Unknown => "unknown",
123 Self::Custom(value) => value.as_str(),
124 })
125 }
126}
127
128impl FromStr for RiskLevel {
129 type Err = RiskLevelParseError;
130
131 fn from_str(value: &str) -> Result<Self, Self::Err> {
132 let trimmed = value.trim();
133 if trimmed.is_empty() {
134 return Err(RiskLevelParseError::Empty);
135 }
136
137 match normalized_token(trimmed).as_str() {
138 "low" => Ok(Self::Low),
139 "medium" => Ok(Self::Medium),
140 "high" => Ok(Self::High),
141 "critical" => Ok(Self::Critical),
142 "unknown" => Ok(Self::Unknown),
143 _ => Ok(Self::Custom(trimmed.to_string())),
144 }
145 }
146}
147
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub enum RiskLevelParseError {
151 Empty,
153}
154
155impl fmt::Display for RiskLevelParseError {
156 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
157 match self {
158 Self::Empty => formatter.write_str("risk level cannot be empty"),
159 }
160 }
161}
162
163impl Error for RiskLevelParseError {}
164
165#[derive(Clone, Debug, PartialEq)]
167pub struct RiskLimit {
168 measure: RiskMeasure,
169 threshold: f64,
170 level: RiskLevel,
171}
172
173impl RiskLimit {
174 pub fn new(measure: RiskMeasure, threshold: f64) -> Result<Self, RiskError> {
180 Ok(Self {
181 measure,
182 threshold: validate_non_negative(
183 threshold,
184 RiskError::NonFiniteThreshold,
185 RiskError::NegativeThreshold,
186 )?,
187 level: RiskLevel::Unknown,
188 })
189 }
190
191 #[must_use]
193 pub fn with_level(mut self, level: RiskLevel) -> Self {
194 self.level = level;
195 self
196 }
197
198 #[must_use]
200 pub const fn measure(&self) -> &RiskMeasure {
201 &self.measure
202 }
203
204 #[must_use]
206 pub const fn threshold(&self) -> f64 {
207 self.threshold
208 }
209
210 #[must_use]
212 pub const fn level(&self) -> &RiskLevel {
213 &self.level
214 }
215}
216
217#[derive(Clone, Debug, PartialEq)]
219pub struct RiskBudget {
220 measure: RiskMeasure,
221 amount: f64,
222}
223
224impl RiskBudget {
225 pub fn new(measure: RiskMeasure, amount: f64) -> Result<Self, RiskError> {
231 Ok(Self {
232 measure,
233 amount: validate_non_negative(
234 amount,
235 RiskError::NonFiniteAmount,
236 RiskError::NegativeAmount,
237 )?,
238 })
239 }
240
241 #[must_use]
243 pub const fn measure(&self) -> &RiskMeasure {
244 &self.measure
245 }
246
247 #[must_use]
249 pub const fn amount(&self) -> f64 {
250 self.amount
251 }
252}
253
254#[derive(Clone, Copy, Debug, Eq, PartialEq)]
256pub enum RiskError {
257 NonFiniteThreshold,
259 NegativeThreshold,
261 NonFiniteAmount,
263 NegativeAmount,
265}
266
267impl fmt::Display for RiskError {
268 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
269 match self {
270 Self::NonFiniteThreshold => formatter.write_str("risk limit threshold must be finite"),
271 Self::NegativeThreshold => {
272 formatter.write_str("risk limit threshold cannot be negative")
273 },
274 Self::NonFiniteAmount => formatter.write_str("risk budget amount must be finite"),
275 Self::NegativeAmount => formatter.write_str("risk budget amount cannot be negative"),
276 }
277 }
278}
279
280impl Error for RiskError {}
281
282fn validate_non_negative(
283 value: f64,
284 non_finite: RiskError,
285 negative: RiskError,
286) -> Result<f64, RiskError> {
287 if !value.is_finite() {
288 return Err(non_finite);
289 }
290
291 if value < 0.0 {
292 return Err(negative);
293 }
294
295 Ok(value)
296}
297
298fn normalized_token(value: &str) -> String {
299 value
300 .trim()
301 .chars()
302 .map(|character| match character {
303 '_' | ' ' => '-',
304 other => other.to_ascii_lowercase(),
305 })
306 .collect()
307}
308
309#[cfg(test)]
310mod tests {
311 use super::{RiskBudget, RiskLevel, RiskLimit, RiskMeasure};
312
313 #[test]
314 fn displays_and_parses_risk_measure() {
315 let measure: RiskMeasure = "value at risk".parse().expect("measure should parse");
316
317 assert_eq!(measure, RiskMeasure::ValueAtRisk);
318 assert_eq!(measure.to_string(), "value-at-risk");
319 }
320
321 #[test]
322 fn supports_custom_risk_measure() {
323 let measure: RiskMeasure = "liquidity".parse().expect("measure should parse");
324
325 assert_eq!(measure, RiskMeasure::Custom("liquidity".to_string()));
326 }
327
328 #[test]
329 fn displays_and_parses_risk_level() {
330 let level: RiskLevel = "critical".parse().expect("level should parse");
331
332 assert_eq!(level, RiskLevel::Critical);
333 assert_eq!(level.to_string(), "critical");
334 }
335
336 #[test]
337 fn constructs_risk_limit() {
338 let limit = RiskLimit::new(RiskMeasure::Volatility, 0.20)
339 .expect("limit should be valid")
340 .with_level(RiskLevel::Medium);
341
342 assert!((limit.threshold() - 0.20).abs() < f64::EPSILON);
343 assert_eq!(limit.level(), &RiskLevel::Medium);
344 }
345
346 #[test]
347 fn constructs_risk_budget() {
348 let budget = RiskBudget::new(RiskMeasure::Drawdown, 0.10).expect("budget should be valid");
349
350 assert!((budget.amount() - 0.10).abs() < f64::EPSILON);
351 }
352}