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 Volatility, VolatilityError, VolatilityKind, VolatilityKindParseError, VolatilityWindow,
11 };
12}
13
14#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
16pub struct Volatility {
17 value: f64,
18}
19
20impl Volatility {
21 pub fn new(value: f64) -> Result<Self, VolatilityError> {
28 if !value.is_finite() {
29 return Err(VolatilityError::NonFinite);
30 }
31
32 if value < 0.0 {
33 return Err(VolatilityError::Negative);
34 }
35
36 Ok(Self { value })
37 }
38
39 pub fn sample_from_returns(returns: &[f64]) -> Result<Self, VolatilityError> {
46 if returns.len() < 2 {
47 return Err(VolatilityError::InsufficientReturns);
48 }
49
50 if returns.iter().any(|value| !value.is_finite()) {
51 return Err(VolatilityError::NonFinite);
52 }
53
54 let count = observation_count_to_f64(returns.len())?;
55 let mean = returns.iter().sum::<f64>() / count;
56 let sum_squared_deviation = returns
57 .iter()
58 .map(|value| {
59 let deviation = value - mean;
60 deviation * deviation
61 })
62 .sum::<f64>();
63 let sample_count = observation_count_to_f64(returns.len() - 1)?;
64 let variance = sum_squared_deviation / sample_count;
65
66 Self::new(variance.sqrt())
67 }
68
69 #[must_use]
71 pub const fn value(self) -> f64 {
72 self.value
73 }
74}
75
76impl fmt::Display for Volatility {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 self.value.fmt(formatter)
79 }
80}
81
82#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
84pub enum VolatilityKind {
85 Historical,
87 Realized,
89 Implied,
91 Forecast,
93 Unknown,
95 Custom(String),
97}
98
99impl fmt::Display for VolatilityKind {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 formatter.write_str(match self {
102 Self::Historical => "historical",
103 Self::Realized => "realized",
104 Self::Implied => "implied",
105 Self::Forecast => "forecast",
106 Self::Unknown => "unknown",
107 Self::Custom(value) => value.as_str(),
108 })
109 }
110}
111
112impl FromStr for VolatilityKind {
113 type Err = VolatilityKindParseError;
114
115 fn from_str(value: &str) -> Result<Self, Self::Err> {
116 let trimmed = value.trim();
117 if trimmed.is_empty() {
118 return Err(VolatilityKindParseError::Empty);
119 }
120
121 match normalized_token(trimmed).as_str() {
122 "historical" => Ok(Self::Historical),
123 "realized" => Ok(Self::Realized),
124 "implied" => Ok(Self::Implied),
125 "forecast" => Ok(Self::Forecast),
126 "unknown" => Ok(Self::Unknown),
127 _ => Ok(Self::Custom(trimmed.to_string())),
128 }
129 }
130}
131
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub enum VolatilityKindParseError {
135 Empty,
137}
138
139impl fmt::Display for VolatilityKindParseError {
140 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141 match self {
142 Self::Empty => formatter.write_str("volatility kind cannot be empty"),
143 }
144 }
145}
146
147impl Error for VolatilityKindParseError {}
148
149#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
151pub struct VolatilityWindow {
152 length: usize,
153}
154
155impl VolatilityWindow {
156 pub const fn new(length: usize) -> Result<Self, VolatilityError> {
162 if length == 0 {
163 Err(VolatilityError::ZeroWindow)
164 } else {
165 Ok(Self { length })
166 }
167 }
168
169 #[must_use]
171 pub const fn length(self) -> usize {
172 self.length
173 }
174}
175
176#[derive(Clone, Copy, Debug, Eq, PartialEq)]
178pub enum VolatilityError {
179 NonFinite,
181 Negative,
183 InsufficientReturns,
185 TooManyReturns,
187 ZeroWindow,
189}
190
191impl fmt::Display for VolatilityError {
192 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
193 match self {
194 Self::NonFinite => formatter.write_str("volatility values must be finite"),
195 Self::Negative => formatter.write_str("volatility cannot be negative"),
196 Self::InsufficientReturns => {
197 formatter.write_str("sample volatility requires at least two returns")
198 },
199 Self::TooManyReturns => {
200 formatter.write_str("sample volatility observation count exceeds supported range")
201 },
202 Self::ZeroWindow => formatter.write_str("volatility window length must be non-zero"),
203 }
204 }
205}
206
207impl Error for VolatilityError {}
208
209fn observation_count_to_f64(count: usize) -> Result<f64, VolatilityError> {
210 let count = u32::try_from(count).map_err(|_| VolatilityError::TooManyReturns)?;
211 Ok(f64::from(count))
212}
213
214fn normalized_token(value: &str) -> String {
215 value
216 .trim()
217 .chars()
218 .map(|character| match character {
219 '_' | ' ' => '-',
220 other => other.to_ascii_lowercase(),
221 })
222 .collect()
223}
224
225#[cfg(test)]
226mod tests {
227 use super::{Volatility, VolatilityError, VolatilityKind};
228
229 #[test]
230 fn accepts_valid_volatility() {
231 let volatility = Volatility::new(0.20).expect("volatility should be valid");
232
233 assert!((volatility.value() - 0.20).abs() < f64::EPSILON);
234 }
235
236 #[test]
237 fn rejects_negative_volatility() {
238 assert_eq!(Volatility::new(-0.01), Err(VolatilityError::Negative));
239 }
240
241 #[test]
242 fn displays_and_parses_volatility_kind() {
243 let kind: VolatilityKind = "realized".parse().expect("kind should parse");
244
245 assert_eq!(kind, VolatilityKind::Realized);
246 assert_eq!(kind.to_string(), "realized");
247 }
248
249 #[test]
250 fn supports_custom_volatility_kind() {
251 let kind: VolatilityKind = "intraday".parse().expect("kind should parse");
252
253 assert_eq!(kind, VolatilityKind::Custom("intraday".to_string()));
254 }
255
256 #[test]
257 fn computes_sample_volatility() {
258 let volatility = Volatility::sample_from_returns(&[0.01, -0.02, 0.015])
259 .expect("volatility should compute");
260
261 assert!((volatility.value() - 0.018_929_694_486).abs() < 1.0e-12);
262 }
263}