1mod beta;
27mod drawdown;
28mod ratios;
29mod var;
30
31pub use self::beta::beta;
32pub use self::drawdown::max_drawdown;
33pub use self::ratios::{calmar_ratio, sharpe_ratio, sortino_ratio};
34pub use self::var::{historical_var, parametric_var};
35
36use crate::models::chart::Candle;
37use serde::{Deserialize, Serialize};
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
43#[non_exhaustive]
44pub struct RiskSummary {
45 pub var_95: f64,
47 pub var_99: f64,
49 pub parametric_var_95: f64,
51 pub sharpe: Option<f64>,
54 pub sortino: Option<f64>,
57 pub calmar: Option<f64>,
60 pub beta: Option<f64>,
62 pub max_drawdown: f64,
64 pub max_drawdown_recovery_periods: Option<u64>,
67}
68
69pub(crate) fn candles_to_returns(candles: &[Candle]) -> Vec<f64> {
71 candles
72 .windows(2)
73 .map(|w| (w[1].close - w[0].close) / w[0].close)
74 .collect()
75}
76
77#[allow(dead_code)]
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub(crate) enum TradingCalendar {
84 Exchange,
87 Forex,
89 Crypto,
91}
92
93impl TradingCalendar {
94 fn trading_days(self) -> f64 {
95 match self {
96 TradingCalendar::Exchange => 252.0,
97 TradingCalendar::Forex => 260.0,
98 TradingCalendar::Crypto => 365.0,
99 }
100 }
101
102 fn session_hours(self) -> f64 {
103 match self {
104 TradingCalendar::Exchange => 6.5,
105 TradingCalendar::Forex | TradingCalendar::Crypto => 24.0,
106 }
107 }
108}
109
110pub(crate) fn periods_per_year(interval: crate::Interval, cal: TradingCalendar) -> f64 {
114 use crate::Interval;
115 let days = cal.trading_days();
116 match interval {
117 Interval::OneDay => days,
118 Interval::OneWeek => 52.0,
119 Interval::OneMonth => 12.0,
120 Interval::ThreeMonths => 4.0,
121 Interval::OneHour => days * cal.session_hours(),
122 Interval::ThirtyMinutes => days * cal.session_hours() * 2.0,
123 Interval::FifteenMinutes => days * cal.session_hours() * 4.0,
124 Interval::FiveMinutes => days * cal.session_hours() * 12.0,
125 Interval::OneMinute => days * cal.session_hours() * 60.0,
126 }
127}
128
129pub(crate) fn compute_risk_summary(
132 candles: &[Candle],
133 benchmark_returns: Option<&[f64]>,
134) -> RiskSummary {
135 compute_risk_summary_with_periods(candles, benchmark_returns, 252.0)
136}
137
138pub(crate) fn compute_risk_summary_with_periods(
142 candles: &[Candle],
143 benchmark_returns: Option<&[f64]>,
144 periods_per_year: f64,
145) -> RiskSummary {
146 let returns = candles_to_returns(candles);
147
148 let var_95 = historical_var(&returns, 0.95).unwrap_or(0.0);
149 let var_99 = historical_var(&returns, 0.99).unwrap_or(0.0);
150 let parametric_var_95 = parametric_var(&returns, 0.95).unwrap_or(0.0);
151
152 let sharpe = sharpe_ratio(&returns, 0.0, periods_per_year);
153 let sortino = sortino_ratio(&returns, 0.0, periods_per_year);
154
155 let dd = max_drawdown(&returns);
156 let total_return = returns.iter().fold(1.0_f64, |acc, r| acc * (1.0 + r)) - 1.0;
157 let years = returns.len() as f64 / periods_per_year;
158 let calmar = calmar_ratio(total_return, years, dd.max_drawdown);
159
160 let beta_val = benchmark_returns.and_then(|br| beta(&returns, br));
161
162 RiskSummary {
163 var_95,
164 var_99,
165 parametric_var_95,
166 sharpe,
167 sortino,
168 calmar,
169 beta: beta_val,
170 max_drawdown: dd.max_drawdown,
171 max_drawdown_recovery_periods: dd.recovery_periods,
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn make_candle(close: f64) -> Candle {
180 Candle {
181 timestamp: 0,
182 open: close,
183 high: close,
184 low: close,
185 close,
186 volume: 1_000_000,
187 adj_close: None,
188 provider_id: None,
189 }
190 }
191
192 #[test]
193 fn test_compute_risk_summary_flat() {
194 let candles: Vec<Candle> = (0..=252).map(|_| make_candle(100.0)).collect();
196 let summary = compute_risk_summary(&candles, None);
197 assert_eq!(summary.var_95, 0.0);
198 assert_eq!(summary.max_drawdown, 0.0);
199 assert!(summary.sharpe.is_none());
200 }
201
202 #[test]
203 fn test_candles_to_returns_basic() {
204 let candles = vec![make_candle(100.0), make_candle(110.0), make_candle(99.0)];
205 let returns = candles_to_returns(&candles);
206 assert_eq!(returns.len(), 2);
207 assert!((returns[0] - 0.10).abs() < 1e-9);
208 assert!((returns[1] - (-0.1)).abs() < 0.01);
209 }
210
211 #[test]
212 fn test_periods_per_year_by_calendar() {
213 use crate::Interval;
214 assert_eq!(
216 periods_per_year(Interval::OneDay, TradingCalendar::Exchange),
217 252.0
218 );
219 assert_eq!(
220 periods_per_year(Interval::OneDay, TradingCalendar::Forex),
221 260.0
222 );
223 assert_eq!(
224 periods_per_year(Interval::OneDay, TradingCalendar::Crypto),
225 365.0
226 );
227 assert_eq!(
228 periods_per_year(Interval::OneWeek, TradingCalendar::Crypto),
229 52.0
230 );
231 assert!(
233 periods_per_year(Interval::OneHour, TradingCalendar::Crypto)
234 > periods_per_year(Interval::OneHour, TradingCalendar::Exchange)
235 );
236 }
237
238 #[test]
239 fn test_annualization_factor_changes_sharpe() {
240 let candles: Vec<Candle> = (0..50).map(|i| make_candle(100.0 + i as f64)).collect();
242 let daily = compute_risk_summary_with_periods(&candles, None, 252.0);
243 let crypto = compute_risk_summary_with_periods(&candles, None, 365.0);
244 assert!(daily.sharpe.is_some() && crypto.sharpe.is_some());
245 assert!(crypto.sharpe.unwrap() > daily.sharpe.unwrap());
246 }
247}