fin_primitives/risk/
correlation_matrix.rs1#[derive(Debug, Clone)]
10pub struct CorrelationMatrix {
11 pub matrix: Vec<Vec<f64>>,
13 pub n: usize,
15}
16
17impl CorrelationMatrix {
18 pub fn from_returns(returns: &[Vec<f64>]) -> Self {
23 let n = returns.len();
24 if n == 0 {
25 return Self { matrix: vec![], n: 0 };
26 }
27
28 let t = returns[0].len();
29
30 let means: Vec<f64> = returns
32 .iter()
33 .map(|r| r.iter().sum::<f64>() / t.max(1) as f64)
34 .collect();
35
36 let stds: Vec<f64> = returns
38 .iter()
39 .zip(means.iter())
40 .map(|(r, &m)| {
41 let var = r.iter().map(|&x| (x - m).powi(2)).sum::<f64>() / t.max(1) as f64;
42 var.sqrt()
43 })
44 .collect();
45
46 let mut matrix = vec![vec![0.0_f64; n]; n];
47 for i in 0..n {
48 matrix[i][i] = 1.0;
49 for j in (i + 1)..n {
50 if stds[i] < 1e-12 || stds[j] < 1e-12 {
51 matrix[i][j] = 0.0;
52 matrix[j][i] = 0.0;
53 continue;
54 }
55 let cov: f64 = returns[i]
56 .iter()
57 .zip(returns[j].iter())
58 .map(|(&xi, &xj)| (xi - means[i]) * (xj - means[j]))
59 .sum::<f64>()
60 / t.max(1) as f64;
61 let corr = cov / (stds[i] * stds[j]);
62 matrix[i][j] = corr;
63 matrix[j][i] = corr;
64 }
65 }
66
67 Self { matrix, n }
68 }
69
70 pub fn get(&self, i: usize, j: usize) -> f64 {
72 self.matrix[i][j]
73 }
74
75 pub fn is_positive_definite(&self) -> bool {
80 let n = self.n;
81 if n == 0 {
82 return false;
83 }
84 for k in 1..=n {
86 let det = leading_minor_det(&self.matrix, k);
88 if det <= 0.0 {
89 return false;
90 }
91 }
92 true
93 }
94
95 pub fn eigenvalues_approx(&self) -> Vec<f64> {
99 let n = self.n;
100 let max_k = 3.min(n);
101 let mut eigenvalues = Vec::with_capacity(max_k);
102 let mut a = self.matrix.clone();
104
105 for _ in 0..max_k {
106 let mut v = vec![1.0_f64; n];
108 let mut lambda = 0.0_f64;
109 for _ in 0..200 {
110 let av = mat_vec_mul(&a, &v);
111 let norm = vec_norm(&av);
112 if norm < 1e-12 {
113 break;
114 }
115 let new_v: Vec<f64> = av.iter().map(|&x| x / norm).collect();
116 let av2 = mat_vec_mul(&a, &new_v);
118 lambda = new_v.iter().zip(av2.iter()).map(|(&vi, &avi)| vi * avi).sum();
119 v = new_v;
120 }
121 eigenvalues.push(lambda);
122 for i in 0..n {
124 for j in 0..n {
125 a[i][j] -= lambda * v[i] * v[j];
126 }
127 }
128 }
129
130 eigenvalues
131 }
132
133 pub fn condition_number(&self) -> f64 {
135 let eigs = self.eigenvalues_approx();
136 if eigs.is_empty() {
137 return 1.0;
138 }
139 let max_eig = eigs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
140 let min_eig = eigs.iter().cloned().fold(f64::INFINITY, f64::min);
141 if min_eig.abs() < 1e-12 {
142 return f64::INFINITY;
143 }
144 max_eig.abs() / min_eig.abs()
145 }
146}
147
148fn leading_minor_det(matrix: &[Vec<f64>], k: usize) -> f64 {
150 let mut a: Vec<Vec<f64>> = (0..k).map(|i| matrix[i][..k].to_vec()).collect();
152 let mut det = 1.0_f64;
153 for col in 0..k {
154 let mut pivot_row = None;
156 for row in col..k {
157 if a[row][col].abs() > 1e-12 {
158 pivot_row = Some(row);
159 break;
160 }
161 }
162 let pr = match pivot_row {
163 Some(r) => r,
164 None => return 0.0,
165 };
166 if pr != col {
167 a.swap(col, pr);
168 det = -det;
169 }
170 det *= a[col][col];
171 let pivot = a[col][col];
172 for row in (col + 1)..k {
173 let factor = a[row][col] / pivot;
174 for c in col..k {
175 let sub = factor * a[col][c];
176 a[row][c] -= sub;
177 }
178 }
179 }
180 det
181}
182
183fn mat_vec_mul(a: &[Vec<f64>], v: &[f64]) -> Vec<f64> {
185 a.iter()
186 .map(|row| row.iter().zip(v.iter()).map(|(&aij, &vj)| aij * vj).sum())
187 .collect()
188}
189
190fn vec_norm(v: &[f64]) -> f64 {
192 v.iter().map(|&x| x * x).sum::<f64>().sqrt()
193}
194
195pub struct LedoitWolfShrinkage;
201
202impl LedoitWolfShrinkage {
203 pub fn shrink(
207 sample_corr: &CorrelationMatrix,
208 returns: &[Vec<f64>],
209 ) -> (CorrelationMatrix, f64) {
210 let alpha = Self::optimal_alpha(returns);
211 let target = Self::target_identity(sample_corr.n);
212 let blended = Self::blend(sample_corr, &target, alpha);
213 (blended, alpha)
214 }
215
216 pub fn optimal_alpha(returns: &[Vec<f64>]) -> f64 {
221 let p = returns.len();
222 if p < 2 {
223 return 0.0;
224 }
225 let t = returns[0].len();
226 if t < 2 {
227 return 0.0;
228 }
229
230 let sample = CorrelationMatrix::from_returns(returns);
232 let mut rho_sum = 0.0_f64;
233 let mut count = 0_usize;
234 for i in 0..p {
235 for j in (i + 1)..p {
236 rho_sum += sample.get(i, j).abs();
237 count += 1;
238 }
239 }
240 let rho_bar = if count > 0 { rho_sum / count as f64 } else { 0.0 };
241
242 let denom = (t as f64 - 1.0) * (1.0 - rho_bar);
243 if denom.abs() < 1e-12 {
244 return 0.5;
245 }
246
247 let alpha = ((1.0 - 2.0 / p as f64) * rho_bar) / denom;
248 alpha.clamp(0.0, 1.0)
249 }
250
251 pub fn target_identity(n: usize) -> CorrelationMatrix {
253 let mut matrix = vec![vec![0.0_f64; n]; n];
254 for i in 0..n {
255 matrix[i][i] = 1.0;
256 }
257 CorrelationMatrix { matrix, n }
258 }
259
260 pub fn blend(
262 sample: &CorrelationMatrix,
263 target: &CorrelationMatrix,
264 alpha: f64,
265 ) -> CorrelationMatrix {
266 let n = sample.n;
267 let alpha = alpha.clamp(0.0, 1.0);
268 let mut matrix = vec![vec![0.0_f64; n]; n];
269 for i in 0..n {
270 for j in 0..n {
271 matrix[i][j] =
272 (1.0 - alpha) * sample.matrix[i][j] + alpha * target.matrix[i][j];
273 }
274 }
275 CorrelationMatrix { matrix, n }
276 }
277}
278
279pub struct DccGarch;
285
286impl DccGarch {
287 pub fn rolling_correlation(series_a: &[f64], series_b: &[f64], window: usize) -> Vec<f64> {
291 let len = series_a.len().min(series_b.len());
292 if window == 0 || len < window {
293 return vec![];
294 }
295 let mut results = Vec::with_capacity(len - window + 1);
296 for start in 0..=(len - window) {
297 let a = &series_a[start..start + window];
298 let b = &series_b[start..start + window];
299 results.push(pearson_correlation(a, b));
300 }
301 results
302 }
303
304 pub fn ewma_correlation(series_a: &[f64], series_b: &[f64], lambda: f64) -> f64 {
308 let len = series_a.len().min(series_b.len());
309 if len == 0 {
310 return 0.0;
311 }
312
313 let mut mean_a = 0.0_f64;
315 let mut mean_b = 0.0_f64;
316 let mut weight_sum = 0.0_f64;
317
318 let mut w = 1.0_f64;
319 for k in (0..len).rev() {
320 mean_a += w * series_a[k];
321 mean_b += w * series_b[k];
322 weight_sum += w;
323 w *= lambda;
324 }
325 mean_a /= weight_sum;
326 mean_b /= weight_sum;
327
328 let mut cov = 0.0_f64;
330 let mut var_a = 0.0_f64;
331 let mut var_b = 0.0_f64;
332 w = 1.0_f64;
333 let mut ws = 0.0_f64;
334 for k in (0..len).rev() {
335 let da = series_a[k] - mean_a;
336 let db = series_b[k] - mean_b;
337 cov += w * da * db;
338 var_a += w * da * da;
339 var_b += w * db * db;
340 ws += w;
341 w *= lambda;
342 }
343 if ws > 0.0 {
344 cov /= ws;
345 var_a /= ws;
346 var_b /= ws;
347 }
348
349 let denom = (var_a * var_b).sqrt();
350 if denom < 1e-12 {
351 0.0
352 } else {
353 (cov / denom).clamp(-1.0, 1.0)
354 }
355 }
356
357 pub fn dcc_update(prev_corr: f64, a: f64, b: f64, epsilon_t: f64) -> f64 {
362 let rho_bar = prev_corr;
363 let q_t = (1.0 - a - b) * rho_bar + a * epsilon_t.powi(2) + b * prev_corr;
364 q_t.clamp(-1.0, 1.0)
365 }
366}
367
368fn pearson_correlation(a: &[f64], b: &[f64]) -> f64 {
370 let n = a.len();
371 if n == 0 {
372 return 0.0;
373 }
374 let mean_a = a.iter().sum::<f64>() / n as f64;
375 let mean_b = b.iter().sum::<f64>() / n as f64;
376 let mut cov = 0.0_f64;
377 let mut var_a = 0.0_f64;
378 let mut var_b = 0.0_f64;
379 for (&ai, &bi) in a.iter().zip(b.iter()) {
380 let da = ai - mean_a;
381 let db = bi - mean_b;
382 cov += da * db;
383 var_a += da * da;
384 var_b += db * db;
385 }
386 let denom = (var_a * var_b).sqrt();
387 if denom < 1e-12 {
388 0.0
389 } else {
390 (cov / denom).clamp(-1.0, 1.0)
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 fn sample_returns() -> Vec<Vec<f64>> {
399 vec![
400 vec![0.01, -0.02, 0.03, 0.01, -0.01],
401 vec![0.02, -0.01, 0.02, 0.00, -0.02],
402 ]
403 }
404
405 #[test]
406 fn correlation_diagonal_is_one() {
407 let cm = CorrelationMatrix::from_returns(&sample_returns());
408 assert!((cm.get(0, 0) - 1.0).abs() < 1e-9);
409 assert!((cm.get(1, 1) - 1.0).abs() < 1e-9);
410 }
411
412 #[test]
413 fn correlation_is_symmetric() {
414 let cm = CorrelationMatrix::from_returns(&sample_returns());
415 assert!((cm.get(0, 1) - cm.get(1, 0)).abs() < 1e-12);
416 }
417
418 #[test]
419 fn identity_is_positive_definite() {
420 let id = LedoitWolfShrinkage::target_identity(3);
421 assert!(id.is_positive_definite());
422 }
423
424 #[test]
425 fn shrink_alpha_in_range() {
426 let returns = sample_returns();
427 let alpha = LedoitWolfShrinkage::optimal_alpha(&returns);
428 assert!(alpha >= 0.0 && alpha <= 1.0);
429 }
430
431 #[test]
432 fn rolling_correlation_length() {
433 let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
434 let b = vec![5.0, 4.0, 3.0, 2.0, 1.0];
435 let rc = DccGarch::rolling_correlation(&a, &b, 3);
436 assert_eq!(rc.len(), 3);
437 }
438
439 #[test]
440 fn ewma_correlation_range() {
441 let a = vec![0.01, -0.02, 0.03, 0.01];
442 let b = vec![0.02, -0.01, 0.02, 0.00];
443 let c = DccGarch::ewma_correlation(&a, &b, 0.94);
444 assert!(c >= -1.0 && c <= 1.0);
445 }
446}