finance_solution/stocks/
risk.rs1use crate::stocks::returns::{mean_return, simple_returns};
8use crate::util::error::{require_finite, FinanceError, FinanceResult};
9
10pub fn volatility(returns: &[f64]) -> FinanceResult<f64> {
23 if returns.len() < 2 {
24 return Err(FinanceError::Unsolvable {
25 message: "volatility requires at least two returns",
26 });
27 }
28 let mean = mean_return(returns)?;
29 let mut sum_sq = 0.0;
30 for r in returns {
31 require_finite("returns", *r)?;
32 let d = r - mean;
33 sum_sq += d * d;
34 }
35 Ok((sum_sq / (returns.len() - 1) as f64).sqrt())
36}
37
38pub fn volatility_annualized(returns: &[f64], periods_per_year: f64) -> FinanceResult<f64> {
51 require_finite("periods_per_year", periods_per_year)?;
52 if periods_per_year <= 0.0 {
53 return Err(FinanceError::Unsolvable {
54 message: "periods_per_year must be positive",
55 });
56 }
57 Ok(volatility(returns)? * periods_per_year.sqrt())
58}
59
60pub fn sharpe_ratio(returns: &[f64], risk_free_rate: f64) -> FinanceResult<f64> {
74 require_finite("risk_free_rate", risk_free_rate)?;
75 let vol = volatility(returns)?;
76 if vol == 0.0 {
77 return Err(FinanceError::Unsolvable {
78 message: "sharpe_ratio undefined when volatility is zero",
79 });
80 }
81 let mean = mean_return(returns)?;
82 Ok((mean - risk_free_rate) / vol)
83}
84
85pub fn sortino_ratio(returns: &[f64], target: f64) -> FinanceResult<f64> {
99 require_finite("target", target)?;
100 if returns.len() < 2 {
101 return Err(FinanceError::Unsolvable {
102 message: "sortino_ratio requires at least two returns",
103 });
104 }
105 for r in returns {
106 require_finite("returns", *r)?;
107 }
108 let mut sum_sq = 0.0;
109 let mut downside_count = 0usize;
110 for &r in returns {
111 let shortfall = r - target;
112 if shortfall < 0.0 {
113 sum_sq += shortfall * shortfall;
114 downside_count += 1;
115 }
116 }
117 if downside_count == 0 {
118 return Err(FinanceError::Unsolvable {
119 message: "sortino_ratio undefined when no returns fall below target",
120 });
121 }
122 let dd = (sum_sq / (returns.len() - 1) as f64).sqrt();
124 if dd == 0.0 {
125 return Err(FinanceError::Unsolvable {
126 message: "sortino_ratio undefined when downside deviation is zero",
127 });
128 }
129 let mean = mean_return(returns)?;
130 Ok((mean - target) / dd)
131}
132
133pub fn max_drawdown(prices: &[f64]) -> FinanceResult<f64> {
147 if prices.len() < 2 {
148 return Err(FinanceError::Unsolvable {
149 message: "max_drawdown requires at least two prices",
150 });
151 }
152 let series = drawdown_series(prices)?;
153 Ok(series.into_iter().fold(0.0_f64, f64::max))
154}
155
156pub fn drawdown_series(prices: &[f64]) -> FinanceResult<Vec<f64>> {
169 if prices.is_empty() {
170 return Err(FinanceError::Unsolvable {
171 message: "drawdown_series requires at least one price",
172 });
173 }
174 let mut peak = prices[0];
175 require_finite("prices", peak)?;
176 if peak <= 0.0 {
177 return Err(FinanceError::InvalidCashflow {
178 message: "drawdown_series requires positive prices",
179 });
180 }
181 let mut out = Vec::with_capacity(prices.len());
182 for &p in prices {
183 require_finite("prices", p)?;
184 if p <= 0.0 {
185 return Err(FinanceError::InvalidCashflow {
186 message: "drawdown_series requires positive prices",
187 });
188 }
189 if p > peak {
190 peak = p;
191 }
192 out.push((peak - p) / peak);
193 }
194 Ok(out)
195}
196
197pub fn rolling_max_drawdown(prices: &[f64]) -> FinanceResult<Vec<f64>> {
211 let dd = drawdown_series(prices)?;
212 let mut out = Vec::with_capacity(dd.len());
213 let mut running = 0.0_f64;
214 for d in dd {
215 running = running.max(d);
216 out.push(running);
217 }
218 Ok(out)
219}
220
221pub fn beta(asset_returns: &[f64], market_returns: &[f64]) -> FinanceResult<f64> {
242 if asset_returns.len() != market_returns.len() {
243 return Err(FinanceError::Unsolvable {
244 message: "beta requires asset and market return series of equal length",
245 });
246 }
247 if asset_returns.len() < 2 {
248 return Err(FinanceError::Unsolvable {
249 message: "beta requires at least two paired returns",
250 });
251 }
252 for r in asset_returns.iter().chain(market_returns.iter()) {
253 require_finite("returns", *r)?;
254 }
255 let mean_a = mean_return(asset_returns)?;
256 let mean_m = mean_return(market_returns)?;
257 let n = asset_returns.len() as f64;
258 let mut cov = 0.0;
259 let mut var_m = 0.0;
260 for i in 0..asset_returns.len() {
261 let da = asset_returns[i] - mean_a;
262 let dm = market_returns[i] - mean_m;
263 cov += da * dm;
264 var_m += dm * dm;
265 }
266 cov /= n - 1.0;
267 var_m /= n - 1.0;
268 if var_m == 0.0 {
269 return Err(FinanceError::Unsolvable {
270 message: "beta undefined when market variance is zero",
271 });
272 }
273 Ok(cov / var_m)
274}
275
276pub fn price_volatility(prices: &[f64]) -> FinanceResult<f64> {
290 let rets = simple_returns(prices)?;
291 volatility(&rets)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use crate::*;
298
299 #[test]
300 fn test_volatility_constant_zero() {
301 let returns = [0.01, 0.01, 0.01, 0.01];
302 assert_approx_equal!(volatility(&returns).unwrap(), 0.0);
303 }
304
305 #[test]
306 fn test_max_drawdown() {
307 let prices = [100.0, 120.0, 90.0, 95.0];
308 assert_approx_equal!(max_drawdown(&prices).unwrap(), 0.25);
309 }
310
311 #[test]
312 fn test_sharpe() {
313 let returns = [0.02, 0.01, 0.03, -0.01, 0.02];
314 let s = sharpe_ratio(&returns, 0.0).unwrap();
315 assert!(s.is_finite() && s > 0.0);
316 }
317
318 #[test]
319 fn test_sortino_has_downside() {
320 let returns = [0.02, -0.03, 0.01, -0.01, 0.02];
321 let s = sortino_ratio(&returns, 0.0).unwrap();
322 assert!(s.is_finite());
323 }
324
325 #[test]
326 fn test_sortino_no_downside_errs() {
327 assert!(sortino_ratio(&[0.01, 0.02, 0.03], 0.0).is_err());
328 }
329
330 #[test]
331 fn test_beta_double() {
332 let market = [0.01, 0.02, -0.01, 0.03];
333 let asset: Vec<f64> = market.iter().map(|r| 2.0 * r).collect();
334 assert!((beta(&asset, &market).unwrap() - 2.0).abs() < 1e-9);
335 }
336
337 #[test]
338 fn test_rolling_max_drawdown() {
339 let prices = [100.0, 120.0, 90.0, 95.0, 130.0];
340 let r = rolling_max_drawdown(&prices).unwrap();
341 assert_eq!(r.len(), 5);
342 assert_approx_equal!(r[2], 0.25);
343 assert_approx_equal!(r[4], 0.25);
344 }
345
346 #[test]
347 fn test_drawdown_series() {
348 let prices = [100.0, 120.0, 90.0];
349 let d = drawdown_series(&prices).unwrap();
350 assert_approx_equal!(d[0], 0.0);
351 assert_approx_equal!(d[1], 0.0);
352 assert_approx_equal!(d[2], 0.25);
353 }
354}