1mod beta;
29mod cvar;
30mod drawdown;
31mod ratios;
32mod var;
33
34pub use self::beta::beta;
35pub use self::cvar::{historical_cvar, parametric_cvar};
36pub use self::drawdown::max_drawdown;
37pub use self::ratios::{
38 calmar_ratio, information_ratio, kelly_criterion, omega_ratio, sharpe_ratio, sortino_ratio,
39 tracking_error, ulcer_index, win_loss_stats,
40};
41pub use self::var::{historical_var, parametric_var};
42
43use crate::models::chart::Candle;
44use serde::{Deserialize, Serialize};
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
50#[non_exhaustive]
51pub struct RiskSummary {
52 pub var_95: f64,
54 pub var_99: f64,
56 pub parametric_var_95: f64,
58 pub cvar_95: f64,
61 pub cvar_99: f64,
63 pub parametric_cvar_95: f64,
66 pub omega: f64,
70 pub kelly: f64,
75 pub sharpe: Option<f64>,
78 pub sortino: Option<f64>,
81 pub calmar: Option<f64>,
84 pub beta: Option<f64>,
86 pub max_drawdown: f64,
88 pub max_drawdown_recovery_periods: Option<u64>,
91 pub ulcer_index: f64,
95 pub information_ratio: Option<f64>,
98 pub tracking_error: Option<f64>,
102}
103
104pub(crate) fn candles_to_returns(candles: &[Candle]) -> Vec<f64> {
106 candles
107 .windows(2)
108 .map(|w| (w[1].close - w[0].close) / w[0].close)
109 .collect()
110}
111
112#[allow(dead_code)]
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub(crate) enum TradingCalendar {
119 Exchange,
122 Forex,
124 Crypto,
126}
127
128#[allow(dead_code)]
134impl TradingCalendar {
135 fn trading_days(self) -> f64 {
136 match self {
137 TradingCalendar::Exchange => 252.0,
138 TradingCalendar::Forex => 260.0,
139 TradingCalendar::Crypto => 365.0,
140 }
141 }
142
143 fn session_hours(self) -> f64 {
144 match self {
145 TradingCalendar::Exchange => 6.5,
146 TradingCalendar::Forex | TradingCalendar::Crypto => 24.0,
147 }
148 }
149}
150
151#[allow(dead_code)]
155pub(crate) fn periods_per_year(interval: crate::Interval, cal: TradingCalendar) -> f64 {
156 use crate::Interval;
157 let days = cal.trading_days();
158 match interval {
159 Interval::OneDay => days,
160 Interval::FiveDays | Interval::OneWeek => 52.0,
161 Interval::OneMonth => 12.0,
162 Interval::ThreeMonths => 4.0,
163 Interval::NinetyMinutes => days * cal.session_hours() / 1.5,
164 Interval::OneHour => days * cal.session_hours(),
165 Interval::ThirtyMinutes => days * cal.session_hours() * 2.0,
166 Interval::FifteenMinutes => days * cal.session_hours() * 4.0,
167 Interval::FiveMinutes => days * cal.session_hours() * 12.0,
168 Interval::TwoMinutes => days * cal.session_hours() * 30.0,
169 Interval::OneMinute => days * cal.session_hours() * 60.0,
170 }
171}
172
173pub(crate) fn compute_risk_summary(
176 candles: &[Candle],
177 benchmark_returns: Option<&[f64]>,
178) -> RiskSummary {
179 compute_risk_summary_with_periods(candles, benchmark_returns, 252.0)
180}
181
182pub(crate) fn compute_risk_summary_with_periods(
186 candles: &[Candle],
187 benchmark_returns: Option<&[f64]>,
188 periods_per_year: f64,
189) -> RiskSummary {
190 let returns = candles_to_returns(candles);
191
192 let mut sorted = returns.clone();
193 sorted.sort_by(|a, b| a.total_cmp(b));
194 let stats = ratios::mean_and_std(&returns);
195
196 let var_95 = var::historical_var_sorted(&sorted, 0.95).unwrap_or(0.0);
197 let var_99 = var::historical_var_sorted(&sorted, 0.99).unwrap_or(0.0);
198 let parametric_var_95 = stats
199 .map(|(m, s)| var::parametric_var_with_stats(m, s, 0.95))
200 .unwrap_or(0.0);
201
202 let cvar_95 = cvar::historical_cvar_sorted(&sorted, 0.95).unwrap_or(0.0);
203 let cvar_99 = cvar::historical_cvar_sorted(&sorted, 0.99).unwrap_or(0.0);
204 let parametric_cvar_95 = stats
205 .map(|(m, s)| cvar::parametric_cvar_with_stats(m, s, 0.95))
206 .unwrap_or(0.0);
207
208 let omega = omega_ratio(&returns);
209 let (win_rate, avg_win_pct, avg_loss_pct) = win_loss_stats(&returns);
210 let kelly = kelly_criterion(win_rate, avg_win_pct, avg_loss_pct);
211
212 let sharpe = stats.and_then(|(m, s)| ratios::sharpe_with_stats(m, s, 0.0, periods_per_year));
213 let sortino = sortino_ratio(&returns, 0.0, periods_per_year);
214
215 let dd = max_drawdown(&returns);
216 let total_return = returns.iter().fold(1.0_f64, |acc, r| acc * (1.0 + r)) - 1.0;
217 let years = returns.len() as f64 / periods_per_year;
218 let calmar = calmar_ratio(total_return, years, dd.max_drawdown);
219 let ulcer_index_val = ulcer_index(&returns);
220
221 let beta_val = benchmark_returns.and_then(|br| beta(&returns, br));
222 let information_ratio_val =
223 benchmark_returns.and_then(|br| information_ratio(&returns, br, periods_per_year));
224 let tracking_error_val =
225 benchmark_returns.and_then(|br| tracking_error(&returns, br, periods_per_year));
226
227 RiskSummary {
228 var_95,
229 var_99,
230 parametric_var_95,
231 cvar_95,
232 cvar_99,
233 parametric_cvar_95,
234 omega,
235 kelly,
236 sharpe,
237 sortino,
238 calmar,
239 beta: beta_val,
240 max_drawdown: dd.max_drawdown,
241 max_drawdown_recovery_periods: dd.recovery_periods,
242 ulcer_index: ulcer_index_val,
243 information_ratio: information_ratio_val,
244 tracking_error: tracking_error_val,
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 fn make_candle(close: f64) -> Candle {
253 Candle {
254 timestamp: 0,
255 open: close,
256 high: close,
257 low: close,
258 close,
259 volume: 1_000_000,
260 adj_close: None,
261 provider_id: None,
262 }
263 }
264
265 #[test]
266 fn test_compute_risk_summary_flat() {
267 let candles: Vec<Candle> = (0..=252).map(|_| make_candle(100.0)).collect();
269 let summary = compute_risk_summary(&candles, None);
270 assert_eq!(summary.var_95, 0.0);
271 assert_eq!(summary.max_drawdown, 0.0);
272 assert!(summary.sharpe.is_none());
273 assert_eq!(summary.cvar_95, 0.0);
275 assert_eq!(summary.cvar_99, 0.0);
276 assert_eq!(summary.parametric_cvar_95, 0.0);
277 assert_eq!(summary.omega, 0.0);
278 assert_eq!(summary.kelly, 0.0);
279 assert_eq!(summary.ulcer_index, 0.0);
280 assert!(summary.information_ratio.is_none());
281 assert!(summary.tracking_error.is_none());
282 }
283
284 #[test]
285 fn test_cvar_at_least_as_severe_as_var() {
286 let closes: Vec<f64> = (0..60)
289 .map(|i| 100.0 - i as f64 * 0.5 + if i % 5 == 0 { -8.0 } else { 0.0 })
290 .collect();
291 let candles: Vec<Candle> = closes.into_iter().map(make_candle).collect();
292 let summary = compute_risk_summary(&candles, None);
293 assert!(
294 summary.cvar_95 >= summary.var_95,
295 "cvar_95 ({}) should be >= var_95 ({})",
296 summary.cvar_95,
297 summary.var_95
298 );
299 assert!(summary.parametric_cvar_95 >= summary.parametric_var_95);
300 }
301
302 #[test]
303 fn test_drawdown_produces_positive_ulcer_index() {
304 let closes = [100.0, 110.0, 80.0, 85.0, 105.0, 115.0];
307 let candles: Vec<Candle> = closes.into_iter().map(make_candle).collect();
308 let summary = compute_risk_summary(&candles, None);
309 assert!(summary.ulcer_index > 0.0);
310 assert!(summary.max_drawdown > 0.0);
311 }
312
313 #[test]
314 fn test_information_ratio_and_tracking_error_with_benchmark() {
315 let asset_closes: Vec<f64> = (0..30).map(|i| 100.0 + i as f64 * 1.2).collect();
316 let bench_closes: Vec<f64> = (0..30).map(|i| 100.0 + i as f64 * 0.8).collect();
317 let candles: Vec<Candle> = asset_closes.into_iter().map(make_candle).collect();
318 let bench_candles: Vec<Candle> = bench_closes.into_iter().map(make_candle).collect();
319 let bench_returns = candles_to_returns(&bench_candles);
320
321 let summary = compute_risk_summary(&candles, Some(&bench_returns));
322 assert!(summary.information_ratio.is_some());
323 assert!(summary.tracking_error.is_some());
324 assert!(summary.tracking_error.unwrap() > 0.0);
325 assert!(summary.information_ratio.unwrap() > 0.0);
327 }
328
329 #[test]
330 fn test_candles_to_returns_basic() {
331 let candles = vec![make_candle(100.0), make_candle(110.0), make_candle(99.0)];
332 let returns = candles_to_returns(&candles);
333 assert_eq!(returns.len(), 2);
334 assert!((returns[0] - 0.10).abs() < 1e-9);
335 assert!((returns[1] - (-0.1)).abs() < 0.01);
336 }
337
338 #[test]
339 fn test_candles_to_returns_empty_and_single() {
340 assert!(candles_to_returns(&[]).is_empty());
341 assert!(candles_to_returns(&[make_candle(100.0)]).is_empty());
342
343 let empty_summary = compute_risk_summary(&[], None);
344 assert_eq!(empty_summary.var_95, 0.0);
345 assert_eq!(empty_summary.var_99, 0.0);
346 assert_eq!(empty_summary.parametric_var_95, 0.0);
347 assert!(empty_summary.sharpe.is_none());
348
349 let single_summary = compute_risk_summary(&[make_candle(100.0)], None);
350 assert_eq!(single_summary.var_95, 0.0);
351 assert_eq!(single_summary.var_99, 0.0);
352 assert_eq!(single_summary.parametric_var_95, 0.0);
353 assert!(single_summary.sharpe.is_none());
354 }
355
356 #[test]
357 fn test_periods_per_year_by_calendar() {
358 use crate::Interval;
359 assert_eq!(
361 periods_per_year(Interval::OneDay, TradingCalendar::Exchange),
362 252.0
363 );
364 assert_eq!(
365 periods_per_year(Interval::OneDay, TradingCalendar::Forex),
366 260.0
367 );
368 assert_eq!(
369 periods_per_year(Interval::OneDay, TradingCalendar::Crypto),
370 365.0
371 );
372 assert_eq!(
373 periods_per_year(Interval::OneWeek, TradingCalendar::Crypto),
374 52.0
375 );
376 assert!(
378 periods_per_year(Interval::OneHour, TradingCalendar::Crypto)
379 > periods_per_year(Interval::OneHour, TradingCalendar::Exchange)
380 );
381 }
382
383 #[test]
384 fn test_annualization_factor_changes_sharpe() {
385 let candles: Vec<Candle> = (0..50).map(|i| make_candle(100.0 + i as f64)).collect();
387 let daily = compute_risk_summary_with_periods(&candles, None, 252.0);
388 let crypto = compute_risk_summary_with_periods(&candles, None, 365.0);
389 assert!(daily.sharpe.is_some() && crypto.sharpe.is_some());
390 assert!(crypto.sharpe.unwrap() > daily.sharpe.unwrap());
391 }
392
393 #[test]
394 fn shared_stats_match_standalone_functions() {
395 let returns: Vec<f64> = (0..2_000)
396 .map(|i| ((i as f64 * 0.37).sin()) * 0.02 - 0.0001)
397 .collect();
398
399 let mut sorted = returns.clone();
400 sorted.sort_by(|a, b| a.total_cmp(b));
401 let (mean, std_dev) = crate::risk::ratios::mean_and_std(&returns).unwrap();
402
403 assert_eq!(
404 crate::risk::var::historical_var_sorted(&sorted, 0.95),
405 crate::risk::historical_var(&returns, 0.95)
406 );
407 assert_eq!(
408 crate::risk::var::historical_var_sorted(&sorted, 0.99),
409 crate::risk::historical_var(&returns, 0.99)
410 );
411 assert_eq!(
412 Some(crate::risk::var::parametric_var_with_stats(
413 mean, std_dev, 0.95
414 )),
415 crate::risk::parametric_var(&returns, 0.95)
416 );
417 assert_eq!(
418 crate::risk::ratios::sharpe_with_stats(mean, std_dev, 0.0, 252.0),
419 crate::risk::sharpe_ratio(&returns, 0.0, 252.0)
420 );
421 }
422}