@ebowwa/quant-rust 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +161 -0
  2. package/bun-ffi.d.ts +54 -0
  3. package/dist/index.js +576 -0
  4. package/dist/src/index.d.ts +324 -0
  5. package/dist/src/index.d.ts.map +1 -0
  6. package/dist/types/index.d.ts +403 -0
  7. package/dist/types/index.d.ts.map +1 -0
  8. package/native/README.md +62 -0
  9. package/native/darwin-arm64/libquant_rust.dylib +0 -0
  10. package/package.json +70 -0
  11. package/scripts/postinstall.cjs +85 -0
  12. package/src/ffi.rs +496 -0
  13. package/src/index.ts +1073 -0
  14. package/src/indicators/ma.rs +222 -0
  15. package/src/indicators/mod.rs +18 -0
  16. package/src/indicators/momentum.rs +353 -0
  17. package/src/indicators/sr.rs +195 -0
  18. package/src/indicators/trend.rs +351 -0
  19. package/src/indicators/volatility.rs +270 -0
  20. package/src/indicators/volume.rs +213 -0
  21. package/src/lib.rs +130 -0
  22. package/src/patterns/breakout.rs +431 -0
  23. package/src/patterns/chart.rs +772 -0
  24. package/src/patterns/mod.rs +394 -0
  25. package/src/patterns/sr.rs +423 -0
  26. package/src/prediction/amm.rs +338 -0
  27. package/src/prediction/arbitrage.rs +230 -0
  28. package/src/prediction/calibration.rs +317 -0
  29. package/src/prediction/kelly.rs +232 -0
  30. package/src/prediction/lmsr.rs +194 -0
  31. package/src/prediction/mod.rs +59 -0
  32. package/src/prediction/odds.rs +229 -0
  33. package/src/prediction/pnl.rs +254 -0
  34. package/src/prediction/risk.rs +228 -0
  35. package/src/risk/beta.rs +257 -0
  36. package/src/risk/drawdown.rs +256 -0
  37. package/src/risk/leverage.rs +201 -0
  38. package/src/risk/mod.rs +388 -0
  39. package/src/risk/portfolio.rs +287 -0
  40. package/src/risk/ratios.rs +290 -0
  41. package/src/risk/sizing.rs +194 -0
  42. package/src/risk/var.rs +222 -0
  43. package/src/stats/cdf.rs +257 -0
  44. package/src/stats/correlation.rs +225 -0
  45. package/src/stats/distribution.rs +194 -0
  46. package/src/stats/hypothesis.rs +177 -0
  47. package/src/stats/matrix.rs +346 -0
  48. package/src/stats/mod.rs +257 -0
  49. package/src/stats/regression.rs +239 -0
  50. package/src/stats/rolling.rs +193 -0
  51. package/src/stats/timeseries.rs +263 -0
  52. package/src/types.rs +224 -0
  53. package/src/utils/mod.rs +215 -0
  54. package/src/utils/normalize.rs +192 -0
  55. package/src/utils/price.rs +167 -0
  56. package/src/utils/quantiles.rs +177 -0
  57. package/src/utils/returns.rs +158 -0
  58. package/src/utils/rolling.rs +97 -0
  59. package/src/utils/stats.rs +154 -0
  60. package/types/index.ts +513 -0
@@ -0,0 +1,290 @@
1
+ //! Risk-Adjusted Return Ratios
2
+ //!
3
+ //! Sharpe, Sortino, Calmar, Treynor, and Information ratios.
4
+
5
+ use serde::{Deserialize, Serialize};
6
+
7
+ use crate::utils::{mean, std_dev, sum};
8
+
9
+ use super::drawdown::calculate_drawdown;
10
+
11
+ /// Calculate Sharpe Ratio
12
+ ///
13
+ /// Measures risk-adjusted returns using total volatility.
14
+ ///
15
+ /// # Arguments
16
+ /// * `returns` - Array of periodic returns
17
+ /// * `risk_free_rate` - Annual risk-free rate (e.g., 0.04 for 4%)
18
+ /// * `periods_per_year` - Number of periods in a year (252 for daily, 12 for monthly)
19
+ ///
20
+ /// # Returns
21
+ /// Annualized Sharpe ratio
22
+ ///
23
+ /// # Example
24
+ /// ```
25
+ /// use quant_rust::risk::ratios::sharpe_ratio;
26
+ /// let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
27
+ /// let sharpe = sharpe_ratio(&returns, 0.04, 252);
28
+ /// assert!(sharpe.is_finite());
29
+ /// ```
30
+ pub fn sharpe_ratio(returns: &[f64], risk_free_rate: f64, periods_per_year: usize) -> f64 {
31
+ if returns.is_empty() {
32
+ return 0.0;
33
+ }
34
+
35
+ let avg_return = mean(returns);
36
+ let sd = std_dev(returns, false); // Sample std dev
37
+
38
+ if sd == 0.0 {
39
+ return 0.0;
40
+ }
41
+
42
+ // Annualize
43
+ let annualized_return = avg_return * periods_per_year as f64;
44
+ let annualized_vol = sd * (periods_per_year as f64).sqrt();
45
+
46
+ (annualized_return - risk_free_rate) / annualized_vol
47
+ }
48
+
49
+ /// Calculate Sortino Ratio
50
+ ///
51
+ /// Like Sharpe, but only penalizes downside volatility.
52
+ ///
53
+ /// # Arguments
54
+ /// * `returns` - Array of periodic returns
55
+ /// * `risk_free_rate` - Annual risk-free rate
56
+ /// * `periods_per_year` - Number of periods in a year
57
+ ///
58
+ /// # Returns
59
+ /// Annualized Sortino ratio
60
+ ///
61
+ /// # Example
62
+ /// ```
63
+ /// use quant_rust::risk::ratios::sortino_ratio;
64
+ /// let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
65
+ /// let sortino = sortino_ratio(&returns, 0.04, 252);
66
+ /// assert!(sortino.is_finite() || sortino.is_infinite());
67
+ /// ```
68
+ pub fn sortino_ratio(returns: &[f64], risk_free_rate: f64, periods_per_year: usize) -> f64 {
69
+ if returns.is_empty() {
70
+ return 0.0;
71
+ }
72
+
73
+ let avg_return = mean(returns);
74
+ let target_return = risk_free_rate / periods_per_year as f64;
75
+
76
+ // Downside deviation
77
+ let downside_returns: Vec<f64> = returns.iter().filter(|&&r| r < target_return).cloned().collect();
78
+
79
+ if downside_returns.is_empty() {
80
+ return f64::INFINITY;
81
+ }
82
+
83
+ let squared_downside: Vec<f64> = downside_returns.iter().map(|r| (r - target_return).powi(2)).collect();
84
+ let downside_deviation = (sum(&squared_downside) / returns.len() as f64).sqrt();
85
+
86
+ if downside_deviation == 0.0 {
87
+ return 0.0;
88
+ }
89
+
90
+ let annualized_return = avg_return * periods_per_year as f64;
91
+ let annualized_downside = downside_deviation * (periods_per_year as f64).sqrt();
92
+
93
+ (annualized_return - risk_free_rate) / annualized_downside
94
+ }
95
+
96
+ /// Calculate Calmar Ratio
97
+ ///
98
+ /// Return divided by maximum drawdown.
99
+ ///
100
+ /// # Arguments
101
+ /// * `returns` - Array of periodic returns
102
+ /// * `equity_curve` - Array of equity values over time
103
+ /// * `periods_per_year` - Number of periods in a year
104
+ ///
105
+ /// # Returns
106
+ /// Calmar ratio
107
+ ///
108
+ /// # Example
109
+ /// ```
110
+ /// use quant_rust::risk::ratios::calmar_ratio;
111
+ /// let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
112
+ /// let equity = vec![100.0, 101.0, 103.0, 102.0, 105.0, 103.0, 104.0, 106.0, 107.0, 106.0, 108.0];
113
+ /// let calmar = calmar_ratio(&returns, &equity, 252);
114
+ /// assert!(calmar.is_finite());
115
+ /// ```
116
+ pub fn calmar_ratio(returns: &[f64], equity_curve: &[f64], periods_per_year: usize) -> f64 {
117
+ if returns.is_empty() || equity_curve.is_empty() {
118
+ return 0.0;
119
+ }
120
+
121
+ let dd_result = calculate_drawdown(equity_curve);
122
+ if dd_result.max_drawdown == 0.0 {
123
+ return 0.0;
124
+ }
125
+
126
+ let annualized_return = mean(returns) * periods_per_year as f64;
127
+
128
+ annualized_return / dd_result.max_drawdown
129
+ }
130
+
131
+ /// Calculate Treynor Ratio
132
+ ///
133
+ /// Risk-adjusted return relative to systematic risk (beta).
134
+ ///
135
+ /// # Arguments
136
+ /// * `returns` - Array of portfolio periodic returns
137
+ /// * `beta` - Portfolio beta relative to benchmark
138
+ /// * `risk_free_rate` - Annual risk-free rate
139
+ /// * `periods_per_year` - Number of periods in a year
140
+ ///
141
+ /// # Returns
142
+ /// Treynor ratio
143
+ ///
144
+ /// # Example
145
+ /// ```
146
+ /// use quant_rust::risk::ratios::treynor_ratio;
147
+ /// let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
148
+ /// let treynor = treynor_ratio(&returns, 1.2, 0.04, 252);
149
+ /// assert!(treynor.is_finite());
150
+ /// ```
151
+ pub fn treynor_ratio(returns: &[f64], beta: f64, risk_free_rate: f64, periods_per_year: usize) -> f64 {
152
+ if returns.is_empty() || beta == 0.0 {
153
+ return 0.0;
154
+ }
155
+
156
+ let annualized_return = mean(returns) * periods_per_year as f64;
157
+
158
+ (annualized_return - risk_free_rate) / beta
159
+ }
160
+
161
+ /// Calculate Information Ratio
162
+ ///
163
+ /// Active return divided by tracking error.
164
+ ///
165
+ /// # Arguments
166
+ /// * `portfolio_returns` - Array of portfolio periodic returns
167
+ /// * `benchmark_returns` - Array of benchmark periodic returns
168
+ ///
169
+ /// # Returns
170
+ /// Information ratio
171
+ ///
172
+ /// # Example
173
+ /// ```
174
+ /// use quant_rust::risk::ratios::information_ratio;
175
+ /// let portfolio = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
176
+ /// let benchmark = vec![0.005, 0.015, -0.005, 0.02, -0.01, 0.005, 0.015, 0.01, -0.005, 0.015];
177
+ /// let ir = information_ratio(&portfolio, &benchmark);
178
+ /// assert!(ir.is_finite());
179
+ /// ```
180
+ pub fn information_ratio(portfolio_returns: &[f64], benchmark_returns: &[f64]) -> f64 {
181
+ if portfolio_returns.len() != benchmark_returns.len() || portfolio_returns.is_empty() {
182
+ return 0.0;
183
+ }
184
+
185
+ // Active returns
186
+ let active_returns: Vec<f64> = portfolio_returns
187
+ .iter()
188
+ .zip(benchmark_returns.iter())
189
+ .map(|(p, b)| p - b)
190
+ .collect();
191
+
192
+ let avg_active = mean(&active_returns);
193
+ let tracking_error = std_dev(&active_returns, false); // Sample std dev
194
+
195
+ if tracking_error == 0.0 {
196
+ return 0.0;
197
+ }
198
+
199
+ avg_active / tracking_error
200
+ }
201
+
202
+ /// Comprehensive risk-adjusted metrics
203
+ #[derive(Debug, Clone, Serialize, Deserialize)]
204
+ pub struct RiskAdjustedMetrics {
205
+ /// Sharpe ratio
206
+ pub sharpe_ratio: f64,
207
+ /// Sortino ratio
208
+ pub sortino_ratio: f64,
209
+ /// Calmar ratio
210
+ pub calmar_ratio: f64,
211
+ }
212
+
213
+ /// Calculate all risk-adjusted metrics
214
+ ///
215
+ /// # Arguments
216
+ /// * `returns` - Array of periodic returns
217
+ /// * `equity_curve` - Array of equity values over time
218
+ /// * `risk_free_rate` - Annual risk-free rate
219
+ /// * `periods_per_year` - Number of periods in a year
220
+ ///
221
+ /// # Returns
222
+ /// RiskAdjustedMetrics struct
223
+ pub fn calculate_risk_adjusted_metrics(
224
+ returns: &[f64],
225
+ equity_curve: &[f64],
226
+ risk_free_rate: f64,
227
+ periods_per_year: usize,
228
+ ) -> RiskAdjustedMetrics {
229
+ RiskAdjustedMetrics {
230
+ sharpe_ratio: sharpe_ratio(returns, risk_free_rate, periods_per_year),
231
+ sortino_ratio: sortino_ratio(returns, risk_free_rate, periods_per_year),
232
+ calmar_ratio: calmar_ratio(returns, equity_curve, periods_per_year),
233
+ }
234
+ }
235
+
236
+ #[cfg(test)]
237
+ mod tests {
238
+ use super::*;
239
+
240
+ #[test]
241
+ fn test_sharpe_ratio() {
242
+ let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
243
+ let sharpe = sharpe_ratio(&returns, 0.04, 252);
244
+ assert!(sharpe.is_finite());
245
+ }
246
+
247
+ #[test]
248
+ fn test_sharpe_ratio_empty() {
249
+ let sharpe = sharpe_ratio(&[], 0.04, 252);
250
+ assert_eq!(sharpe, 0.0);
251
+ }
252
+
253
+ #[test]
254
+ fn test_sortino_ratio() {
255
+ let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
256
+ let sortino = sortino_ratio(&returns, 0.04, 252);
257
+ assert!(sortino.is_finite() || sortino.is_infinite());
258
+ }
259
+
260
+ #[test]
261
+ fn test_calmar_ratio() {
262
+ let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
263
+ let equity = vec![100.0, 101.0, 103.0, 102.0, 105.0, 103.0, 104.0, 106.0, 107.0, 106.0, 108.0];
264
+ let calmar = calmar_ratio(&returns, &equity, 252);
265
+ assert!(calmar.is_finite());
266
+ }
267
+
268
+ #[test]
269
+ fn test_treynor_ratio() {
270
+ let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
271
+ let treynor = treynor_ratio(&returns, 1.2, 0.04, 252);
272
+ assert!(treynor.is_finite());
273
+ }
274
+
275
+ #[test]
276
+ fn test_information_ratio() {
277
+ let portfolio = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
278
+ let benchmark = vec![0.005, 0.015, -0.005, 0.02, -0.01, 0.005, 0.015, 0.01, -0.005, 0.015];
279
+ let ir = information_ratio(&portfolio, &benchmark);
280
+ assert!(ir.is_finite());
281
+ }
282
+
283
+ #[test]
284
+ fn test_information_ratio_mismatched_lengths() {
285
+ let portfolio = vec![0.01, 0.02, -0.01];
286
+ let benchmark = vec![0.005, 0.015];
287
+ let ir = information_ratio(&portfolio, &benchmark);
288
+ assert_eq!(ir, 0.0);
289
+ }
290
+ }
@@ -0,0 +1,194 @@
1
+ //! Position Sizing Functions
2
+ //!
3
+ //! Methods for calculating appropriate position sizes based on risk parameters.
4
+
5
+ use serde::{Deserialize, Serialize};
6
+
7
+ /// Result of position sizing calculation
8
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
9
+ pub struct PositionSizeResult {
10
+ /// Number of shares/contracts
11
+ pub shares: usize,
12
+ /// Total position value
13
+ pub position_value: f64,
14
+ /// Amount of capital at risk
15
+ pub risk_amount: f64,
16
+ }
17
+
18
+ /// Calculate position size based on fixed fractional method
19
+ ///
20
+ /// Determines the number of shares to buy based on the percentage of capital
21
+ /// you're willing to risk and the distance to your stop loss.
22
+ ///
23
+ /// # Arguments
24
+ /// * `capital` - Total trading capital
25
+ /// * `risk_percent` - Percent of capital to risk (e.g., 0.02 for 2%)
26
+ /// * `entry_price` - Planned entry price
27
+ /// * `stop_loss` - Stop loss price
28
+ ///
29
+ /// # Returns
30
+ /// PositionSizeResult with shares, position value, and risk amount
31
+ ///
32
+ /// # Example
33
+ /// ```
34
+ /// use quant_rust::risk::sizing::fixed_fractional_size;
35
+ /// let result = fixed_fractional_size(100000.0, 0.02, 50.0, 45.0);
36
+ /// assert_eq!(result.shares, 400);
37
+ /// assert_eq!(result.risk_amount, 2000.0);
38
+ /// ```
39
+ pub fn fixed_fractional_size(
40
+ capital: f64,
41
+ risk_percent: f64,
42
+ entry_price: f64,
43
+ stop_loss: f64,
44
+ ) -> PositionSizeResult {
45
+ let risk_amount = capital * risk_percent;
46
+ let risk_per_share = (entry_price - stop_loss).abs();
47
+
48
+ if risk_per_share == 0.0 {
49
+ return PositionSizeResult {
50
+ shares: 0,
51
+ position_value: 0.0,
52
+ risk_amount: 0.0,
53
+ };
54
+ }
55
+
56
+ let shares = (risk_amount / risk_per_share).floor() as usize;
57
+ let position_value = shares as f64 * entry_price;
58
+
59
+ PositionSizeResult {
60
+ shares,
61
+ position_value,
62
+ risk_amount,
63
+ }
64
+ }
65
+
66
+ /// Calculate position size using volatility-based sizing
67
+ ///
68
+ /// Sizes positions such that a 1 standard deviation move equals the risk amount.
69
+ /// This normalizes risk across assets with different volatilities.
70
+ ///
71
+ /// # Arguments
72
+ /// * `capital` - Total trading capital
73
+ /// * `risk_percent` - Percent of capital to risk
74
+ /// * `volatility` - Asset volatility (daily standard deviation as decimal)
75
+ /// * `price` - Current price
76
+ ///
77
+ /// # Returns
78
+ /// PositionSizeResult with shares, position value, and risk amount
79
+ ///
80
+ /// # Example
81
+ /// ```
82
+ /// use quant_rust::risk::sizing::volatility_based_size;
83
+ /// let result = volatility_based_size(100000.0, 0.02, 0.02, 100.0);
84
+ /// assert_eq!(result.shares, 100);
85
+ /// assert_eq!(result.risk_amount, 2000.0);
86
+ /// ```
87
+ pub fn volatility_based_size(
88
+ capital: f64,
89
+ risk_percent: f64,
90
+ volatility: f64,
91
+ price: f64,
92
+ ) -> PositionSizeResult {
93
+ let risk_amount = capital * risk_percent;
94
+
95
+ if volatility == 0.0 || price == 0.0 {
96
+ return PositionSizeResult {
97
+ shares: 0,
98
+ position_value: 0.0,
99
+ risk_amount: 0.0,
100
+ };
101
+ }
102
+
103
+ // Size such that expected 1 SD move = risk amount
104
+ let shares = (risk_amount / (price * volatility)).floor() as usize;
105
+ let position_value = shares as f64 * price;
106
+
107
+ PositionSizeResult {
108
+ shares,
109
+ position_value,
110
+ risk_amount,
111
+ }
112
+ }
113
+
114
+ /// Calculate optimal position size using Kelly criterion (for continuous returns)
115
+ ///
116
+ /// The Kelly criterion maximizes long-term growth rate.
117
+ /// Kelly = expected_return / variance
118
+ ///
119
+ /// # Arguments
120
+ /// * `expected_return` - Expected return of the asset
121
+ /// * `variance` - Variance of returns
122
+ /// * `capital` - Total trading capital
123
+ /// * `fraction` - Fraction of Kelly to use (1.0 = full Kelly, 0.5 = half Kelly)
124
+ ///
125
+ /// # Returns
126
+ /// Optimal position size in currency units
127
+ ///
128
+ /// # Example
129
+ /// ```
130
+ /// use quant_rust::risk::sizing::kelly_size;
131
+ /// let size = kelly_size(0.10, 0.04, 100000.0, 0.5);
132
+ /// assert!((size - 125000.0).abs() < 1e-10);
133
+ /// ```
134
+ pub fn kelly_size(expected_return: f64, variance: f64, capital: f64, fraction: f64) -> f64 {
135
+ if variance <= 0.0 {
136
+ return 0.0;
137
+ }
138
+
139
+ let kelly = expected_return / variance;
140
+ capital * kelly * fraction
141
+ }
142
+
143
+ #[cfg(test)]
144
+ mod tests {
145
+ use super::*;
146
+
147
+ #[test]
148
+ fn test_fixed_fractional_size() {
149
+ let result = fixed_fractional_size(100000.0, 0.02, 50.0, 45.0);
150
+ assert_eq!(result.shares, 400);
151
+ assert_eq!(result.risk_amount, 2000.0);
152
+ assert!((result.position_value - 20000.0).abs() < 1e-10);
153
+ }
154
+
155
+ #[test]
156
+ fn test_fixed_fractional_size_zero_risk() {
157
+ let result = fixed_fractional_size(100000.0, 0.02, 50.0, 50.0);
158
+ assert_eq!(result.shares, 0);
159
+ }
160
+
161
+ #[test]
162
+ fn test_volatility_based_size() {
163
+ let result = volatility_based_size(100000.0, 0.02, 0.02, 100.0);
164
+ // risk_amount = 100000 * 0.02 = 2000
165
+ // shares = floor(2000 / (100 * 0.02)) = floor(2000 / 2) = 1000
166
+ assert_eq!(result.shares, 1000);
167
+ assert_eq!(result.risk_amount, 2000.0);
168
+ assert!((result.position_value - 100000.0).abs() < 1e-10);
169
+ }
170
+
171
+ #[test]
172
+ fn test_volatility_based_size_zero_vol() {
173
+ let result = volatility_based_size(100000.0, 0.02, 0.0, 100.0);
174
+ assert_eq!(result.shares, 0);
175
+ }
176
+
177
+ #[test]
178
+ fn test_kelly_size() {
179
+ let size = kelly_size(0.10, 0.04, 100000.0, 1.0);
180
+ assert!((size - 250000.0).abs() < 1e-10);
181
+ }
182
+
183
+ #[test]
184
+ fn test_kelly_size_half() {
185
+ let size = kelly_size(0.10, 0.04, 100000.0, 0.5);
186
+ assert!((size - 125000.0).abs() < 1e-10);
187
+ }
188
+
189
+ #[test]
190
+ fn test_kelly_size_zero_variance() {
191
+ let size = kelly_size(0.10, 0.0, 100000.0, 1.0);
192
+ assert_eq!(size, 0.0);
193
+ }
194
+ }
@@ -0,0 +1,222 @@
1
+ //! Value at Risk (VaR) Functions
2
+ //!
3
+ //! Historical and parametric VaR calculations with Expected Shortfall (CVaR).
4
+
5
+ use serde::{Deserialize, Serialize};
6
+
7
+ use crate::utils::{mean, std_dev};
8
+
9
+ /// Configuration for VaR calculation
10
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
11
+ pub struct VaRConfig {
12
+ /// Confidence level for VaR (default: 0.95)
13
+ pub confidence_level: f64,
14
+ }
15
+
16
+ impl Default for VaRConfig {
17
+ fn default() -> Self {
18
+ VaRConfig {
19
+ confidence_level: 0.95,
20
+ }
21
+ }
22
+ }
23
+
24
+ /// Result of VaR calculation containing both 95% and 99% confidence levels
25
+ #[derive(Debug, Clone, Serialize, Deserialize)]
26
+ pub struct VaRResult {
27
+ /// Value at Risk at 95% confidence
28
+ pub var95: f64,
29
+ /// Value at Risk at 99% confidence
30
+ pub var99: f64,
31
+ /// Expected Shortfall (CVaR) at 95% confidence
32
+ pub expected_shortfall95: f64,
33
+ /// Expected Shortfall (CVaR) at 99% confidence
34
+ pub expected_shortfall99: f64,
35
+ }
36
+
37
+ /// Calculate historical Value at Risk
38
+ ///
39
+ /// Uses historical simulation to estimate potential losses at a given confidence level.
40
+ ///
41
+ /// # Arguments
42
+ /// * `returns` - Array of historical returns
43
+ /// * `confidence` - Confidence level (e.g., 0.95 for 95%)
44
+ ///
45
+ /// # Returns
46
+ /// VaR as a positive number representing potential loss
47
+ ///
48
+ /// # Example
49
+ /// ```
50
+ /// use quant_rust::risk::var::historical_var;
51
+ /// let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
52
+ /// let var = historical_var(&returns, 0.95);
53
+ /// assert!(var > 0.0);
54
+ /// ```
55
+ pub fn historical_var(returns: &[f64], confidence: f64) -> f64 {
56
+ if returns.is_empty() {
57
+ return 0.0;
58
+ }
59
+
60
+ let mut sorted = returns.to_vec();
61
+ sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
62
+
63
+ let index = ((1.0 - confidence) * returns.len() as f64).floor() as usize;
64
+ let index = index.min(sorted.len() - 1);
65
+
66
+ sorted[index].abs()
67
+ }
68
+
69
+ /// Calculate parametric Value at Risk (assuming normal distribution)
70
+ ///
71
+ /// Uses the assumption of normally distributed returns to estimate VaR.
72
+ ///
73
+ /// # Arguments
74
+ /// * `returns` - Array of historical returns
75
+ /// * `confidence` - Confidence level (e.g., 0.95 for 95%)
76
+ ///
77
+ /// # Returns
78
+ /// VaR as a positive number representing potential loss
79
+ ///
80
+ /// # Example
81
+ /// ```
82
+ /// use quant_rust::risk::var::parametric_var;
83
+ /// let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
84
+ /// let var = parametric_var(&returns, 0.95);
85
+ /// assert!(var > 0.0);
86
+ /// ```
87
+ pub fn parametric_var(returns: &[f64], confidence: f64) -> f64 {
88
+ if returns.is_empty() {
89
+ return 0.0;
90
+ }
91
+
92
+ let avg = mean(returns);
93
+ let sd = std_dev(returns, false); // Sample std dev
94
+
95
+ // Z-scores for common confidence levels
96
+ let z = match confidence {
97
+ c if (c - 0.90).abs() < 1e-6 => 1.282,
98
+ c if (c - 0.95).abs() < 1e-6 => 1.645,
99
+ c if (c - 0.99).abs() < 1e-6 => 2.326,
100
+ _ => 1.645, // Default to 95%
101
+ };
102
+
103
+ (avg - z * sd).abs()
104
+ }
105
+
106
+ /// Calculate VaR and Expected Shortfall (CVaR) at 95% and 99% confidence
107
+ ///
108
+ /// Comprehensive VaR calculation returning both VaR and Expected Shortfall
109
+ /// at two common confidence levels.
110
+ ///
111
+ /// # Arguments
112
+ /// * `returns` - Array of historical returns
113
+ /// * `config` - Optional configuration (uses defaults if not provided)
114
+ ///
115
+ /// # Returns
116
+ /// VaRResult containing VaR and CVaR at 95% and 99%
117
+ ///
118
+ /// # Example
119
+ /// ```
120
+ /// use quant_rust::risk::var::{calculate_var, VaRConfig};
121
+ /// let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
122
+ /// let result = calculate_var(&returns, VaRConfig::default());
123
+ /// assert!(result.var95 > 0.0);
124
+ /// assert!(result.var99 >= result.var95);
125
+ /// assert!(result.expected_shortfall95 >= result.var95);
126
+ /// ```
127
+ pub fn calculate_var(returns: &[f64], _config: VaRConfig) -> VaRResult {
128
+ if returns.is_empty() {
129
+ return VaRResult {
130
+ var95: 0.0,
131
+ var99: 0.0,
132
+ expected_shortfall95: 0.0,
133
+ expected_shortfall99: 0.0,
134
+ };
135
+ }
136
+
137
+ let mut sorted = returns.to_vec();
138
+ sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
139
+
140
+ let index95 = ((1.0 - 0.95) * returns.len() as f64).floor() as usize;
141
+ let index99 = ((1.0 - 0.99) * returns.len() as f64).floor() as usize;
142
+
143
+ let index95 = index95.min(sorted.len() - 1);
144
+ let index99 = index99.min(sorted.len() - 1);
145
+
146
+ let var95 = sorted[index95].abs();
147
+ let var99 = sorted[index99].abs();
148
+
149
+ // Expected Shortfall (average of returns below VaR)
150
+ let tail95: Vec<f64> = sorted.iter().take(index95 + 1).cloned().collect();
151
+ let tail99: Vec<f64> = sorted.iter().take(index99 + 1).cloned().collect();
152
+
153
+ let expected_shortfall95 = if tail95.is_empty() {
154
+ var95
155
+ } else {
156
+ tail95.iter().sum::<f64>().abs() / tail95.len() as f64
157
+ };
158
+
159
+ let expected_shortfall99 = if tail99.is_empty() {
160
+ var99
161
+ } else {
162
+ tail99.iter().sum::<f64>().abs() / tail99.len() as f64
163
+ };
164
+
165
+ VaRResult {
166
+ var95,
167
+ var99,
168
+ expected_shortfall95,
169
+ expected_shortfall99,
170
+ }
171
+ }
172
+
173
+ #[cfg(test)]
174
+ mod tests {
175
+ use super::*;
176
+
177
+ #[test]
178
+ fn test_historical_var() {
179
+ let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
180
+ let var = historical_var(&returns, 0.95);
181
+ assert!(var > 0.0);
182
+ assert!(var <= 0.1);
183
+ }
184
+
185
+ #[test]
186
+ fn test_historical_var_empty() {
187
+ let var = historical_var(&[], 0.95);
188
+ assert_eq!(var, 0.0);
189
+ }
190
+
191
+ #[test]
192
+ fn test_parametric_var() {
193
+ let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
194
+ let var = parametric_var(&returns, 0.95);
195
+ assert!(var > 0.0);
196
+ }
197
+
198
+ #[test]
199
+ fn test_parametric_var_99() {
200
+ let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
201
+ let var95 = parametric_var(&returns, 0.95);
202
+ let var99 = parametric_var(&returns, 0.99);
203
+ assert!(var99 >= var95);
204
+ }
205
+
206
+ #[test]
207
+ fn test_calculate_var() {
208
+ let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
209
+ let result = calculate_var(&returns, VaRConfig::default());
210
+ assert!(result.var95 > 0.0);
211
+ assert!(result.var99 >= result.var95);
212
+ assert!(result.expected_shortfall95 >= result.var95);
213
+ assert!(result.expected_shortfall99 >= result.var99);
214
+ }
215
+
216
+ #[test]
217
+ fn test_calculate_var_empty() {
218
+ let result = calculate_var(&[], VaRConfig::default());
219
+ assert_eq!(result.var95, 0.0);
220
+ assert_eq!(result.var99, 0.0);
221
+ }
222
+ }