Skip to main content

use_volatility/
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 volatility primitives.
8pub mod prelude {
9    pub use crate::{
10        Volatility, VolatilityError, VolatilityKind, VolatilityKindParseError, VolatilityWindow,
11    };
12}
13
14/// A finite non-negative volatility value.
15#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
16pub struct Volatility {
17    value: f64,
18}
19
20impl Volatility {
21    /// Creates a volatility value.
22    ///
23    /// # Errors
24    ///
25    /// Returns [`VolatilityError::NonFinite`] or [`VolatilityError::Negative`] when `value` is
26    /// invalid.
27    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    /// Computes sample standard-deviation volatility from return values.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`VolatilityError::InsufficientReturns`] for fewer than two returns and
44    /// [`VolatilityError::NonFinite`] for non-finite inputs.
45    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    /// Returns the volatility value.
70    #[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/// Descriptive volatility kind vocabulary.
83#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
84pub enum VolatilityKind {
85    /// Historical volatility.
86    Historical,
87    /// Realized volatility.
88    Realized,
89    /// Implied volatility.
90    Implied,
91    /// Forecast volatility.
92    Forecast,
93    /// Unknown volatility kind.
94    Unknown,
95    /// Caller-defined volatility kind.
96    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/// Errors returned while parsing volatility kinds.
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub enum VolatilityKindParseError {
135    /// The input was empty after trimming whitespace.
136    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/// A simple observation-count volatility window.
150#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
151pub struct VolatilityWindow {
152    length: usize,
153}
154
155impl VolatilityWindow {
156    /// Creates a non-zero volatility window length.
157    ///
158    /// # Errors
159    ///
160    /// Returns [`VolatilityError::ZeroWindow`] when `length` is zero.
161    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    /// Returns the window length.
170    #[must_use]
171    pub const fn length(self) -> usize {
172        self.length
173    }
174}
175
176/// Errors returned by volatility helpers.
177#[derive(Clone, Copy, Debug, Eq, PartialEq)]
178pub enum VolatilityError {
179    /// Volatility and return inputs must be finite.
180    NonFinite,
181    /// Volatility must not be negative.
182    Negative,
183    /// Sample volatility requires at least two return observations.
184    InsufficientReturns,
185    /// Sample volatility only supports observation counts representable as `u32`.
186    TooManyReturns,
187    /// Window lengths must be non-zero.
188    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}