Skip to main content

use_risk/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Common risk primitives.
8pub mod prelude {
9    pub use crate::{
10        RiskBudget, RiskError, RiskLevel, RiskLevelParseError, RiskLimit, RiskMeasure,
11        RiskMeasureParseError,
12    };
13}
14
15/// Descriptive risk measure vocabulary.
16#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17pub enum RiskMeasure {
18    /// Volatility measure.
19    Volatility,
20    /// Value-at-risk measure value.
21    ValueAtRisk,
22    /// Expected shortfall measure value.
23    ExpectedShortfall,
24    /// Beta measure.
25    Beta,
26    /// Tracking error measure.
27    TrackingError,
28    /// Drawdown measure.
29    Drawdown,
30    /// Exposure measure.
31    Exposure,
32    /// Leverage measure.
33    Leverage,
34    /// Unknown measure.
35    Unknown,
36    /// Caller-defined risk measure.
37    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/// Errors returned while parsing risk measures.
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum RiskMeasureParseError {
84    /// The input was empty after trimming whitespace.
85    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/// Descriptive risk level vocabulary.
99#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
100pub enum RiskLevel {
101    /// Low risk level.
102    Low,
103    /// Medium risk level.
104    Medium,
105    /// High risk level.
106    High,
107    /// Critical risk level.
108    Critical,
109    /// Unknown risk level.
110    Unknown,
111    /// Caller-defined risk level.
112    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/// Errors returned while parsing risk levels.
149#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub enum RiskLevelParseError {
151    /// The input was empty after trimming whitespace.
152    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/// A simple numeric risk limit threshold.
166#[derive(Clone, Debug, PartialEq)]
167pub struct RiskLimit {
168    measure: RiskMeasure,
169    threshold: f64,
170    level: RiskLevel,
171}
172
173impl RiskLimit {
174    /// Creates a risk limit with unknown level.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`RiskError`] when `threshold` is not finite or is negative.
179    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    /// Sets descriptive risk level vocabulary.
192    #[must_use]
193    pub fn with_level(mut self, level: RiskLevel) -> Self {
194        self.level = level;
195        self
196    }
197
198    /// Returns the risk measure.
199    #[must_use]
200    pub const fn measure(&self) -> &RiskMeasure {
201        &self.measure
202    }
203
204    /// Returns the numeric threshold.
205    #[must_use]
206    pub const fn threshold(&self) -> f64 {
207        self.threshold
208    }
209
210    /// Returns the risk level.
211    #[must_use]
212    pub const fn level(&self) -> &RiskLevel {
213        &self.level
214    }
215}
216
217/// A simple numeric risk budget value.
218#[derive(Clone, Debug, PartialEq)]
219pub struct RiskBudget {
220    measure: RiskMeasure,
221    amount: f64,
222}
223
224impl RiskBudget {
225    /// Creates a risk budget.
226    ///
227    /// # Errors
228    ///
229    /// Returns [`RiskError`] when `amount` is not finite or is negative.
230    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    /// Returns the risk measure.
242    #[must_use]
243    pub const fn measure(&self) -> &RiskMeasure {
244        &self.measure
245    }
246
247    /// Returns the budget amount.
248    #[must_use]
249    pub const fn amount(&self) -> f64 {
250        self.amount
251    }
252}
253
254/// Errors returned by risk wrappers.
255#[derive(Clone, Copy, Debug, Eq, PartialEq)]
256pub enum RiskError {
257    /// Risk limit thresholds must be finite.
258    NonFiniteThreshold,
259    /// Risk limit thresholds must not be negative.
260    NegativeThreshold,
261    /// Risk budget amounts must be finite.
262    NonFiniteAmount,
263    /// Risk budget amounts must not be negative.
264    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}