@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.
- package/README.md +161 -0
- package/bun-ffi.d.ts +54 -0
- package/dist/index.js +576 -0
- package/dist/src/index.d.ts +324 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +403 -0
- package/dist/types/index.d.ts.map +1 -0
- package/native/README.md +62 -0
- package/native/darwin-arm64/libquant_rust.dylib +0 -0
- package/package.json +70 -0
- package/scripts/postinstall.cjs +85 -0
- package/src/ffi.rs +496 -0
- package/src/index.ts +1073 -0
- package/src/indicators/ma.rs +222 -0
- package/src/indicators/mod.rs +18 -0
- package/src/indicators/momentum.rs +353 -0
- package/src/indicators/sr.rs +195 -0
- package/src/indicators/trend.rs +351 -0
- package/src/indicators/volatility.rs +270 -0
- package/src/indicators/volume.rs +213 -0
- package/src/lib.rs +130 -0
- package/src/patterns/breakout.rs +431 -0
- package/src/patterns/chart.rs +772 -0
- package/src/patterns/mod.rs +394 -0
- package/src/patterns/sr.rs +423 -0
- package/src/prediction/amm.rs +338 -0
- package/src/prediction/arbitrage.rs +230 -0
- package/src/prediction/calibration.rs +317 -0
- package/src/prediction/kelly.rs +232 -0
- package/src/prediction/lmsr.rs +194 -0
- package/src/prediction/mod.rs +59 -0
- package/src/prediction/odds.rs +229 -0
- package/src/prediction/pnl.rs +254 -0
- package/src/prediction/risk.rs +228 -0
- package/src/risk/beta.rs +257 -0
- package/src/risk/drawdown.rs +256 -0
- package/src/risk/leverage.rs +201 -0
- package/src/risk/mod.rs +388 -0
- package/src/risk/portfolio.rs +287 -0
- package/src/risk/ratios.rs +290 -0
- package/src/risk/sizing.rs +194 -0
- package/src/risk/var.rs +222 -0
- package/src/stats/cdf.rs +257 -0
- package/src/stats/correlation.rs +225 -0
- package/src/stats/distribution.rs +194 -0
- package/src/stats/hypothesis.rs +177 -0
- package/src/stats/matrix.rs +346 -0
- package/src/stats/mod.rs +257 -0
- package/src/stats/regression.rs +239 -0
- package/src/stats/rolling.rs +193 -0
- package/src/stats/timeseries.rs +263 -0
- package/src/types.rs +224 -0
- package/src/utils/mod.rs +215 -0
- package/src/utils/normalize.rs +192 -0
- package/src/utils/price.rs +167 -0
- package/src/utils/quantiles.rs +177 -0
- package/src/utils/returns.rs +158 -0
- package/src/utils/rolling.rs +97 -0
- package/src/utils/stats.rs +154 -0
- package/types/index.ts +513 -0
package/src/risk/mod.rs
ADDED
|
@@ -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
|
+
}
|