@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,388 @@
1
+ //! Risk Management Module
2
+ //!
3
+ //! Comprehensive risk metrics including VaR, CVaR, Sharpe ratio, drawdown analysis,
4
+ //! position sizing, portfolio risk, leverage/margin, and beta/alpha calculations.
5
+ //!
6
+ //! ## Modules
7
+ //! - `sizing`: Position sizing (fixed fractional, volatility-based, Kelly)
8
+ //! - `var`: Value at Risk (historical, parametric, expected shortfall)
9
+ //! - `drawdown`: Drawdown analysis (max drawdown, underwater periods)
10
+ //! - `ratios`: Risk-adjusted return ratios (Sharpe, Sortino, Calmar, Treynor, Information)
11
+ //! - `portfolio`: Portfolio risk (volatility, covariance, diversification)
12
+ //! - `leverage`: Leverage and margin (liquidation price, effective leverage)
13
+ //! - `beta`: Beta and alpha calculations
14
+
15
+ pub mod beta;
16
+ pub mod drawdown;
17
+ pub mod leverage;
18
+ pub mod portfolio;
19
+ pub mod ratios;
20
+ pub mod sizing;
21
+ pub mod var;
22
+
23
+ // Re-export types and functions from submodules
24
+
25
+ // Position Sizing
26
+ pub use sizing::{
27
+ fixed_fractional_size,
28
+ kelly_size,
29
+ volatility_based_size,
30
+ PositionSizeResult,
31
+ };
32
+
33
+ // Value at Risk
34
+ pub use var::{
35
+ calculate_var,
36
+ historical_var,
37
+ parametric_var,
38
+ VaRConfig,
39
+ VaRResult,
40
+ };
41
+
42
+ // Drawdown
43
+ pub use drawdown::{
44
+ calculate_drawdown,
45
+ underwater_periods,
46
+ DrawdownPoint,
47
+ DrawdownResult,
48
+ };
49
+
50
+ // Risk-Adjusted Ratios
51
+ pub use ratios::{
52
+ calmar_ratio,
53
+ calculate_risk_adjusted_metrics,
54
+ information_ratio,
55
+ sharpe_ratio,
56
+ sortino_ratio,
57
+ treynor_ratio,
58
+ RiskAdjustedMetrics,
59
+ };
60
+
61
+ // Portfolio Risk
62
+ pub use portfolio::{
63
+ covariance,
64
+ covariance_matrix,
65
+ diversification_ratio,
66
+ portfolio_volatility,
67
+ risk_contribution,
68
+ };
69
+
70
+ // Leverage & Margin
71
+ pub use leverage::{
72
+ calculate_margin,
73
+ effective_leverage,
74
+ liquidation_price,
75
+ margin_requirement,
76
+ MarginResult,
77
+ PositionSide,
78
+ };
79
+
80
+ // Beta & Alpha
81
+ pub use beta::{
82
+ alpha,
83
+ beta,
84
+ calculate_beta_alpha,
85
+ BetaAlphaResult,
86
+ };
87
+
88
+ // ============================================================================
89
+ // Legacy Functions (for backward compatibility)
90
+ // ============================================================================
91
+
92
+ use serde::{Deserialize, Serialize};
93
+
94
+ /// Value at Risk (VaR) and Expected Shortfall (CVaR) result (legacy)
95
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
96
+ pub struct LegacyVaRResult {
97
+ /// Value at Risk at the given confidence level
98
+ pub var: f64,
99
+ /// Conditional VaR (Expected Shortfall)
100
+ pub cvar: f64,
101
+ /// Confidence level used
102
+ pub confidence_level: f64,
103
+ }
104
+
105
+ /// Drawdown analysis result (legacy)
106
+ #[derive(Debug, Clone, Serialize, Deserialize)]
107
+ pub struct LegacyDrawdownResult {
108
+ /// Maximum drawdown (as decimal, e.g., 0.25 = 25%)
109
+ pub max_drawdown: f64,
110
+ /// Maximum drawdown duration in periods
111
+ pub max_duration: usize,
112
+ /// Current drawdown
113
+ pub current_drawdown: f64,
114
+ /// Recovery factor (total return / max drawdown)
115
+ pub recovery_factor: f64,
116
+ }
117
+
118
+ /// Sharpe ratio result (legacy)
119
+ #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
120
+ pub struct SharpeResult {
121
+ /// Sharpe ratio
122
+ pub sharpe_ratio: f64,
123
+ /// Annualized Sharpe ratio
124
+ pub annualized_sharpe: f64,
125
+ /// Risk-free rate used
126
+ pub risk_free_rate: f64,
127
+ /// Average return
128
+ pub avg_return: f64,
129
+ /// Standard deviation of returns
130
+ pub std_dev: f64,
131
+ }
132
+
133
+ /// Calculate Value at Risk using historical simulation (legacy)
134
+ pub fn calculate_var_legacy(returns: &[f64], confidence_level: f64) -> LegacyVaRResult {
135
+ if returns.is_empty() {
136
+ return LegacyVaRResult {
137
+ var: 0.0,
138
+ cvar: 0.0,
139
+ confidence_level,
140
+ };
141
+ }
142
+
143
+ let mut sorted_returns = returns.to_vec();
144
+ sorted_returns.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
145
+
146
+ let index = ((1.0 - confidence_level) * returns.len() as f64).floor() as usize;
147
+ let index = index.min(sorted_returns.len() - 1);
148
+
149
+ let var = -sorted_returns[index];
150
+
151
+ // CVaR is the average of returns below VaR
152
+ let tail_returns: Vec<f64> = sorted_returns.iter().take(index + 1).cloned().collect();
153
+
154
+ let cvar = if tail_returns.is_empty() {
155
+ var
156
+ } else {
157
+ -tail_returns.iter().sum::<f64>() / tail_returns.len() as f64
158
+ };
159
+
160
+ LegacyVaRResult {
161
+ var,
162
+ cvar,
163
+ confidence_level,
164
+ }
165
+ }
166
+
167
+ /// Calculate maximum drawdown from equity curve (legacy)
168
+ pub fn calculate_drawdown_legacy(equity_curve: &[f64]) -> LegacyDrawdownResult {
169
+ if equity_curve.len() < 2 {
170
+ return LegacyDrawdownResult {
171
+ max_drawdown: 0.0,
172
+ max_duration: 0,
173
+ current_drawdown: 0.0,
174
+ recovery_factor: 0.0,
175
+ };
176
+ }
177
+
178
+ let mut max_drawdown = 0.0;
179
+ let mut max_duration = 0;
180
+ let mut peak = equity_curve[0];
181
+ let mut last_peak_idx = 0;
182
+
183
+ for (i, &equity) in equity_curve.iter().enumerate() {
184
+ if equity > peak {
185
+ peak = equity;
186
+ last_peak_idx = i;
187
+ } else {
188
+ let duration = i - last_peak_idx;
189
+ let drawdown = (peak - equity) / peak;
190
+ if drawdown > max_drawdown {
191
+ max_drawdown = drawdown;
192
+ max_duration = duration;
193
+ }
194
+ }
195
+ }
196
+
197
+ let current_drawdown = if peak > 0.0 {
198
+ (peak - equity_curve[equity_curve.len() - 1]) / peak
199
+ } else {
200
+ 0.0
201
+ };
202
+
203
+ let total_return = if equity_curve[0] > 0.0 {
204
+ (equity_curve[equity_curve.len() - 1] - equity_curve[0]) / equity_curve[0]
205
+ } else {
206
+ 0.0
207
+ };
208
+
209
+ let recovery_factor = if max_drawdown > 0.0 {
210
+ total_return / max_drawdown
211
+ } else {
212
+ f64::INFINITY
213
+ };
214
+
215
+ LegacyDrawdownResult {
216
+ max_drawdown,
217
+ max_duration,
218
+ current_drawdown,
219
+ recovery_factor,
220
+ }
221
+ }
222
+
223
+ /// Calculate Sharpe ratio (legacy)
224
+ pub fn calculate_sharpe_ratio(
225
+ returns: &[f64],
226
+ risk_free_rate: f64,
227
+ periods_per_year: usize,
228
+ ) -> SharpeResult {
229
+ if returns.is_empty() {
230
+ return SharpeResult {
231
+ sharpe_ratio: 0.0,
232
+ annualized_sharpe: 0.0,
233
+ risk_free_rate,
234
+ avg_return: 0.0,
235
+ std_dev: 0.0,
236
+ };
237
+ }
238
+
239
+ let n = returns.len() as f64;
240
+ let avg_return = returns.iter().sum::<f64>() / n;
241
+
242
+ let variance: f64 = returns
243
+ .iter()
244
+ .map(|r| (r - avg_return).powi(2))
245
+ .sum::<f64>()
246
+ / n;
247
+
248
+ let std_dev = variance.sqrt();
249
+
250
+ if std_dev == 0.0 {
251
+ return SharpeResult {
252
+ sharpe_ratio: 0.0,
253
+ annualized_sharpe: 0.0,
254
+ risk_free_rate,
255
+ avg_return,
256
+ std_dev,
257
+ };
258
+ }
259
+
260
+ // Periodic Sharpe
261
+ let excess_return = avg_return - risk_free_rate / periods_per_year as f64;
262
+ let sharpe_ratio = excess_return / std_dev;
263
+
264
+ // Annualized Sharpe
265
+ let annualized_sharpe = sharpe_ratio * (periods_per_year as f64).sqrt();
266
+
267
+ SharpeResult {
268
+ sharpe_ratio,
269
+ annualized_sharpe,
270
+ risk_free_rate,
271
+ avg_return,
272
+ std_dev,
273
+ }
274
+ }
275
+
276
+ /// Calculate Sortino ratio (only penalizes downside volatility) (legacy)
277
+ pub fn calculate_sortino_ratio(
278
+ returns: &[f64],
279
+ risk_free_rate: f64,
280
+ periods_per_year: usize,
281
+ ) -> f64 {
282
+ sortino_ratio(returns, risk_free_rate, periods_per_year)
283
+ }
284
+
285
+ /// Calculate beta and alpha against benchmark (legacy)
286
+ pub fn calculate_beta_alpha_legacy(asset_returns: &[f64], benchmark_returns: &[f64]) -> (f64, f64) {
287
+ if asset_returns.is_empty()
288
+ || benchmark_returns.is_empty()
289
+ || asset_returns.len() != benchmark_returns.len()
290
+ {
291
+ return (0.0, 0.0);
292
+ }
293
+
294
+ let n = asset_returns.len() as f64;
295
+ let avg_asset = crate::utils::mean(asset_returns);
296
+ let avg_bench = crate::utils::mean(benchmark_returns);
297
+
298
+ // Covariance
299
+ let cov: f64 = asset_returns
300
+ .iter()
301
+ .zip(benchmark_returns.iter())
302
+ .map(|(a, b)| (a - avg_asset) * (b - avg_bench))
303
+ .sum::<f64>()
304
+ / n;
305
+
306
+ // Benchmark variance
307
+ let var_bench: f64 = benchmark_returns
308
+ .iter()
309
+ .map(|r| (r - avg_bench).powi(2))
310
+ .sum::<f64>()
311
+ / n;
312
+
313
+ if var_bench == 0.0 {
314
+ return (0.0, 0.0);
315
+ }
316
+
317
+ let beta = cov / var_bench;
318
+ let alpha = avg_asset - beta * avg_bench;
319
+
320
+ (beta, alpha)
321
+ }
322
+
323
+ #[cfg(test)]
324
+ mod tests {
325
+ use super::*;
326
+
327
+ #[test]
328
+ fn test_var_calculation() {
329
+ let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
330
+ let result = calculate_var_legacy(&returns, 0.95);
331
+ assert!(result.var > 0.0);
332
+ assert!(result.cvar >= result.var);
333
+ }
334
+
335
+ #[test]
336
+ fn test_drawdown_calculation() {
337
+ let equity = vec![100.0, 110.0, 105.0, 95.0, 100.0, 90.0, 95.0, 110.0];
338
+ let result = calculate_drawdown_legacy(&equity);
339
+ assert!(result.max_drawdown > 0.0);
340
+ assert!(result.max_drawdown <= 1.0);
341
+ }
342
+
343
+ #[test]
344
+ fn test_sharpe_ratio() {
345
+ let returns = vec![0.01, 0.02, -0.01, 0.03, -0.02, 0.01, 0.02, 0.01, -0.01, 0.02];
346
+ let result = calculate_sharpe_ratio(&returns, 0.02, 252);
347
+ // Should be positive for these returns
348
+ assert!(result.sharpe_ratio.is_finite());
349
+ }
350
+
351
+ #[test]
352
+ fn test_position_sizing() {
353
+ let result = fixed_fractional_size(100000.0, 0.02, 50.0, 45.0);
354
+ assert_eq!(result.shares, 400);
355
+ assert_eq!(result.risk_amount, 2000.0);
356
+ }
357
+
358
+ #[test]
359
+ fn test_var_new() {
360
+ let returns = vec![-0.05, -0.02, 0.01, 0.03, 0.02, -0.04, 0.05, -0.01, 0.02, 0.01];
361
+ let result = calculate_var(&returns, VaRConfig::default());
362
+ assert!(result.var95 > 0.0);
363
+ assert!(result.var99 >= result.var95);
364
+ }
365
+
366
+ #[test]
367
+ fn test_drawdown_new() {
368
+ let equity = vec![100.0, 110.0, 105.0, 95.0, 100.0, 90.0, 95.0, 110.0];
369
+ let result = calculate_drawdown(&equity);
370
+ assert!(result.max_drawdown > 0.0);
371
+ assert!(result.max_drawdown <= 1.0);
372
+ }
373
+
374
+ #[test]
375
+ fn test_beta_alpha() {
376
+ let asset = vec![0.02, 0.03, -0.01, 0.04, -0.02];
377
+ let market = vec![0.01, 0.02, -0.005, 0.02, -0.01];
378
+ let result = calculate_beta_alpha(&asset, &market, 0.04, 252);
379
+ assert!(result.beta > 0.0);
380
+ assert!(result.correlation >= -1.0 && result.correlation <= 1.0);
381
+ }
382
+
383
+ #[test]
384
+ fn test_leverage() {
385
+ let liq_price = liquidation_price(100.0, 10.0, PositionSide::Long, 0.05);
386
+ assert!((liq_price - 90.5).abs() < 1e-10);
387
+ }
388
+ }
@@ -0,0 +1,287 @@
1
+ //! Portfolio Risk Functions
2
+ //!
3
+ //! Portfolio volatility, covariance matrices, diversification, and risk contribution.
4
+
5
+ use crate::utils::mean;
6
+
7
+ /// Calculate portfolio volatility
8
+ ///
9
+ /// Computes the standard deviation of portfolio returns using weights
10
+ /// and the covariance matrix.
11
+ ///
12
+ /// # Arguments
13
+ /// * `weights` - Array of position weights (should sum to 1.0)
14
+ /// * `cov_matrix` - Covariance matrix of asset returns
15
+ ///
16
+ /// # Returns
17
+ /// Portfolio volatility (standard deviation)
18
+ ///
19
+ /// # Example
20
+ /// ```
21
+ /// use quant_rust::risk::portfolio::portfolio_volatility;
22
+ /// let weights = vec![0.6, 0.4];
23
+ /// let cov_matrix = vec![vec![0.04, 0.01], vec![0.01, 0.09]];
24
+ /// let vol = portfolio_volatility(&weights, &cov_matrix);
25
+ /// assert!(vol > 0.0);
26
+ /// ```
27
+ pub fn portfolio_volatility(weights: &[f64], cov_matrix: &[Vec<f64>]) -> f64 {
28
+ let n = weights.len();
29
+ if n == 0 || cov_matrix.len() != n {
30
+ return 0.0;
31
+ }
32
+
33
+ let mut variance = 0.0;
34
+
35
+ for i in 0..n {
36
+ for j in 0..n {
37
+ if cov_matrix[i].len() > j {
38
+ variance += weights[i] * weights[j] * cov_matrix[i][j];
39
+ }
40
+ }
41
+ }
42
+
43
+ variance.sqrt()
44
+ }
45
+
46
+ /// Calculate covariance between two return series
47
+ ///
48
+ /// # Arguments
49
+ /// * `x` - First return series
50
+ /// * `y` - Second return series
51
+ ///
52
+ /// # Returns
53
+ /// Covariance (sample covariance using n-1)
54
+ ///
55
+ /// # Example
56
+ /// ```
57
+ /// use quant_rust::risk::portfolio::covariance;
58
+ /// let x = vec![0.01, 0.02, -0.01, 0.03];
59
+ /// let y = vec![0.02, 0.01, -0.02, 0.04];
60
+ /// let cov = covariance(&x, &y);
61
+ /// assert!(cov.is_finite());
62
+ /// ```
63
+ pub fn covariance(x: &[f64], y: &[f64]) -> f64 {
64
+ if x.len() != y.len() || x.len() < 2 {
65
+ return 0.0;
66
+ }
67
+
68
+ let n = x.len();
69
+ let mean_x = mean(x);
70
+ let mean_y = mean(y);
71
+
72
+ let mut sum = 0.0;
73
+ for i in 0..n {
74
+ sum += (x[i] - mean_x) * (y[i] - mean_y);
75
+ }
76
+
77
+ sum / (n - 1) as f64
78
+ }
79
+
80
+ /// Calculate covariance matrix from multiple return series
81
+ ///
82
+ /// # Arguments
83
+ /// * `returns_series` - Array of return series (one per asset)
84
+ ///
85
+ /// # Returns
86
+ /// Covariance matrix
87
+ ///
88
+ /// # Example
89
+ /// ```
90
+ /// use quant_rust::risk::portfolio::covariance_matrix;
91
+ /// let returns_series = vec![
92
+ /// vec![0.01, 0.02, -0.01, 0.03],
93
+ /// vec![0.02, 0.01, -0.02, 0.04],
94
+ /// ];
95
+ /// let cov_matrix = covariance_matrix(&returns_series);
96
+ /// assert_eq!(cov_matrix.len(), 2);
97
+ /// ```
98
+ pub fn covariance_matrix(returns_series: &[Vec<f64>]) -> Vec<Vec<f64>> {
99
+ let n = returns_series.len();
100
+ if n == 0 {
101
+ return vec![];
102
+ }
103
+
104
+ let mut matrix: Vec<Vec<f64>> = vec![vec![0.0; n]; n];
105
+
106
+ for i in 0..n {
107
+ for j in 0..n {
108
+ matrix[i][j] = covariance(&returns_series[i], &returns_series[j]);
109
+ }
110
+ }
111
+
112
+ matrix
113
+ }
114
+
115
+ /// Calculate diversification ratio
116
+ ///
117
+ /// Measures how much risk is reduced through diversification.
118
+ /// Ratio > 1 indicates diversification benefit.
119
+ ///
120
+ /// # Arguments
121
+ /// * `weights` - Position weights
122
+ /// * `volatilities` - Individual asset volatilities
123
+ /// * `corr_matrix` - Correlation matrix
124
+ ///
125
+ /// # Returns
126
+ /// Diversification ratio
127
+ ///
128
+ /// # Example
129
+ /// ```
130
+ /// use quant_rust::risk::portfolio::diversification_ratio;
131
+ /// let weights = vec![0.5, 0.5];
132
+ /// let volatilities = vec![0.2, 0.3];
133
+ /// let corr_matrix = vec![vec![1.0, 0.5], vec![0.5, 1.0]];
134
+ /// let ratio = diversification_ratio(&weights, &volatilities, &corr_matrix);
135
+ /// assert!(ratio > 1.0);
136
+ /// ```
137
+ pub fn diversification_ratio(
138
+ weights: &[f64],
139
+ volatilities: &[f64],
140
+ corr_matrix: &[Vec<f64>],
141
+ ) -> f64 {
142
+ let n = weights.len();
143
+ if n == 0 || volatilities.len() != n || corr_matrix.len() != n {
144
+ return 1.0;
145
+ }
146
+
147
+ // Weighted average volatility
148
+ let mut weighted_vol = 0.0;
149
+ for i in 0..n {
150
+ weighted_vol += weights[i] * volatilities[i];
151
+ }
152
+
153
+ // Portfolio volatility
154
+ let mut port_var = 0.0;
155
+ for i in 0..n {
156
+ for j in 0..n {
157
+ if corr_matrix[i].len() > j {
158
+ port_var += weights[i]
159
+ * weights[j]
160
+ * volatilities[i]
161
+ * volatilities[j]
162
+ * corr_matrix[i][j];
163
+ }
164
+ }
165
+ }
166
+ let port_vol = port_var.sqrt();
167
+
168
+ if port_vol > 0.0 {
169
+ weighted_vol / port_vol
170
+ } else {
171
+ 1.0
172
+ }
173
+ }
174
+
175
+ /// Calculate risk contribution of each asset
176
+ ///
177
+ /// Decomposes portfolio risk into contributions from each asset.
178
+ ///
179
+ /// # Arguments
180
+ /// * `weights` - Position weights
181
+ /// * `cov_matrix` - Covariance matrix
182
+ ///
183
+ /// # Returns
184
+ /// Vector of risk contributions (sums to portfolio volatility)
185
+ ///
186
+ /// # Example
187
+ /// ```
188
+ /// use quant_rust::risk::portfolio::risk_contribution;
189
+ /// let weights = vec![0.6, 0.4];
190
+ /// let cov_matrix = vec![vec![0.04, 0.01], vec![0.01, 0.09]];
191
+ /// let contributions = risk_contribution(&weights, &cov_matrix);
192
+ /// assert_eq!(contributions.len(), 2);
193
+ /// ```
194
+ pub fn risk_contribution(weights: &[f64], cov_matrix: &[Vec<f64>]) -> Vec<f64> {
195
+ let n = weights.len();
196
+ if n == 0 || cov_matrix.len() != n {
197
+ return vec![];
198
+ }
199
+
200
+ let port_vol = portfolio_volatility(weights, cov_matrix);
201
+ if port_vol == 0.0 {
202
+ return vec![0.0; n];
203
+ }
204
+
205
+ let mut contributions: Vec<f64> = Vec::with_capacity(n);
206
+
207
+ for i in 0..n {
208
+ let mut marginal_contrib = 0.0;
209
+ for j in 0..n {
210
+ if cov_matrix[i].len() > j {
211
+ marginal_contrib += weights[j] * cov_matrix[i][j];
212
+ }
213
+ }
214
+ contributions.push((weights[i] * marginal_contrib) / port_vol);
215
+ }
216
+
217
+ contributions
218
+ }
219
+
220
+ #[cfg(test)]
221
+ mod tests {
222
+ use super::*;
223
+
224
+ #[test]
225
+ fn test_portfolio_volatility() {
226
+ let weights = vec![0.6, 0.4];
227
+ let cov_matrix = vec![vec![0.04, 0.01], vec![0.01, 0.09]];
228
+ let vol = portfolio_volatility(&weights, &cov_matrix);
229
+ assert!(vol > 0.0);
230
+ }
231
+
232
+ #[test]
233
+ fn test_portfolio_volatility_empty() {
234
+ let vol = portfolio_volatility(&[], &[]);
235
+ assert_eq!(vol, 0.0);
236
+ }
237
+
238
+ #[test]
239
+ fn test_covariance() {
240
+ let x = vec![0.01, 0.02, -0.01, 0.03];
241
+ let y = vec![0.02, 0.01, -0.02, 0.04];
242
+ let cov = covariance(&x, &y);
243
+ assert!(cov.is_finite());
244
+ assert!(cov > 0.0); // Should be positive for these correlated returns
245
+ }
246
+
247
+ #[test]
248
+ fn test_covariance_mismatched() {
249
+ let x = vec![0.01, 0.02];
250
+ let y = vec![0.02];
251
+ let cov = covariance(&x, &y);
252
+ assert_eq!(cov, 0.0);
253
+ }
254
+
255
+ #[test]
256
+ fn test_covariance_matrix() {
257
+ let returns_series = vec![
258
+ vec![0.01, 0.02, -0.01, 0.03],
259
+ vec![0.02, 0.01, -0.02, 0.04],
260
+ ];
261
+ let cov_matrix = covariance_matrix(&returns_series);
262
+ assert_eq!(cov_matrix.len(), 2);
263
+ assert_eq!(cov_matrix[0].len(), 2);
264
+ }
265
+
266
+ #[test]
267
+ fn test_diversification_ratio() {
268
+ let weights = vec![0.5, 0.5];
269
+ let volatilities = vec![0.2, 0.3];
270
+ let corr_matrix = vec![vec![1.0, 0.5], vec![0.5, 1.0]];
271
+ let ratio = diversification_ratio(&weights, &volatilities, &corr_matrix);
272
+ // With less than perfect correlation, diversification ratio > 1
273
+ assert!(ratio > 1.0);
274
+ }
275
+
276
+ #[test]
277
+ fn test_risk_contribution() {
278
+ let weights = vec![0.6, 0.4];
279
+ let cov_matrix = vec![vec![0.04, 0.01], vec![0.01, 0.09]];
280
+ let contributions = risk_contribution(&weights, &cov_matrix);
281
+ assert_eq!(contributions.len(), 2);
282
+ // Sum should approximately equal portfolio volatility
283
+ let port_vol = portfolio_volatility(&weights, &cov_matrix);
284
+ let sum: f64 = contributions.iter().sum();
285
+ assert!((sum - port_vol).abs() < 1e-10);
286
+ }
287
+ }