#[derive(Debug, Clone)]
pub struct CorrelationMatrix {
pub matrix: Vec<Vec<f64>>,
pub n: usize,
}
impl CorrelationMatrix {
pub fn from_returns(returns: &[Vec<f64>]) -> Self {
let n = returns.len();
if n == 0 {
return Self { matrix: vec![], n: 0 };
}
let t = returns[0].len();
let means: Vec<f64> = returns
.iter()
.map(|r| r.iter().sum::<f64>() / t.max(1) as f64)
.collect();
let stds: Vec<f64> = returns
.iter()
.zip(means.iter())
.map(|(r, &m)| {
let var = r.iter().map(|&x| (x - m).powi(2)).sum::<f64>() / t.max(1) as f64;
var.sqrt()
})
.collect();
let mut matrix = vec![vec![0.0_f64; n]; n];
for i in 0..n {
matrix[i][i] = 1.0;
for j in (i + 1)..n {
if stds[i] < 1e-12 || stds[j] < 1e-12 {
matrix[i][j] = 0.0;
matrix[j][i] = 0.0;
continue;
}
let cov: f64 = returns[i]
.iter()
.zip(returns[j].iter())
.map(|(&xi, &xj)| (xi - means[i]) * (xj - means[j]))
.sum::<f64>()
/ t.max(1) as f64;
let corr = cov / (stds[i] * stds[j]);
matrix[i][j] = corr;
matrix[j][i] = corr;
}
}
Self { matrix, n }
}
pub fn get(&self, i: usize, j: usize) -> f64 {
self.matrix[i][j]
}
pub fn is_positive_definite(&self) -> bool {
let n = self.n;
if n == 0 {
return false;
}
for k in 1..=n {
let det = leading_minor_det(&self.matrix, k);
if det <= 0.0 {
return false;
}
}
true
}
pub fn eigenvalues_approx(&self) -> Vec<f64> {
let n = self.n;
let max_k = 3.min(n);
let mut eigenvalues = Vec::with_capacity(max_k);
let mut a = self.matrix.clone();
for _ in 0..max_k {
let mut v = vec![1.0_f64; n];
let mut lambda = 0.0_f64;
for _ in 0..200 {
let av = mat_vec_mul(&a, &v);
let norm = vec_norm(&av);
if norm < 1e-12 {
break;
}
let new_v: Vec<f64> = av.iter().map(|&x| x / norm).collect();
let av2 = mat_vec_mul(&a, &new_v);
lambda = new_v.iter().zip(av2.iter()).map(|(&vi, &avi)| vi * avi).sum();
v = new_v;
}
eigenvalues.push(lambda);
for i in 0..n {
for j in 0..n {
a[i][j] -= lambda * v[i] * v[j];
}
}
}
eigenvalues
}
pub fn condition_number(&self) -> f64 {
let eigs = self.eigenvalues_approx();
if eigs.is_empty() {
return 1.0;
}
let max_eig = eigs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min_eig = eigs.iter().cloned().fold(f64::INFINITY, f64::min);
if min_eig.abs() < 1e-12 {
return f64::INFINITY;
}
max_eig.abs() / min_eig.abs()
}
}
fn leading_minor_det(matrix: &[Vec<f64>], k: usize) -> f64 {
let mut a: Vec<Vec<f64>> = (0..k).map(|i| matrix[i][..k].to_vec()).collect();
let mut det = 1.0_f64;
for col in 0..k {
let mut pivot_row = None;
for row in col..k {
if a[row][col].abs() > 1e-12 {
pivot_row = Some(row);
break;
}
}
let pr = match pivot_row {
Some(r) => r,
None => return 0.0,
};
if pr != col {
a.swap(col, pr);
det = -det;
}
det *= a[col][col];
let pivot = a[col][col];
for row in (col + 1)..k {
let factor = a[row][col] / pivot;
for c in col..k {
let sub = factor * a[col][c];
a[row][c] -= sub;
}
}
}
det
}
fn mat_vec_mul(a: &[Vec<f64>], v: &[f64]) -> Vec<f64> {
a.iter()
.map(|row| row.iter().zip(v.iter()).map(|(&aij, &vj)| aij * vj).sum())
.collect()
}
fn vec_norm(v: &[f64]) -> f64 {
v.iter().map(|&x| x * x).sum::<f64>().sqrt()
}
pub struct LedoitWolfShrinkage;
impl LedoitWolfShrinkage {
pub fn shrink(
sample_corr: &CorrelationMatrix,
returns: &[Vec<f64>],
) -> (CorrelationMatrix, f64) {
let alpha = Self::optimal_alpha(returns);
let target = Self::target_identity(sample_corr.n);
let blended = Self::blend(sample_corr, &target, alpha);
(blended, alpha)
}
pub fn optimal_alpha(returns: &[Vec<f64>]) -> f64 {
let p = returns.len();
if p < 2 {
return 0.0;
}
let t = returns[0].len();
if t < 2 {
return 0.0;
}
let sample = CorrelationMatrix::from_returns(returns);
let mut rho_sum = 0.0_f64;
let mut count = 0_usize;
for i in 0..p {
for j in (i + 1)..p {
rho_sum += sample.get(i, j).abs();
count += 1;
}
}
let rho_bar = if count > 0 { rho_sum / count as f64 } else { 0.0 };
let denom = (t as f64 - 1.0) * (1.0 - rho_bar);
if denom.abs() < 1e-12 {
return 0.5;
}
let alpha = ((1.0 - 2.0 / p as f64) * rho_bar) / denom;
alpha.clamp(0.0, 1.0)
}
pub fn target_identity(n: usize) -> CorrelationMatrix {
let mut matrix = vec![vec![0.0_f64; n]; n];
for i in 0..n {
matrix[i][i] = 1.0;
}
CorrelationMatrix { matrix, n }
}
pub fn blend(
sample: &CorrelationMatrix,
target: &CorrelationMatrix,
alpha: f64,
) -> CorrelationMatrix {
let n = sample.n;
let alpha = alpha.clamp(0.0, 1.0);
let mut matrix = vec![vec![0.0_f64; n]; n];
for i in 0..n {
for j in 0..n {
matrix[i][j] =
(1.0 - alpha) * sample.matrix[i][j] + alpha * target.matrix[i][j];
}
}
CorrelationMatrix { matrix, n }
}
}
pub struct DccGarch;
impl DccGarch {
pub fn rolling_correlation(series_a: &[f64], series_b: &[f64], window: usize) -> Vec<f64> {
let len = series_a.len().min(series_b.len());
if window == 0 || len < window {
return vec![];
}
let mut results = Vec::with_capacity(len - window + 1);
for start in 0..=(len - window) {
let a = &series_a[start..start + window];
let b = &series_b[start..start + window];
results.push(pearson_correlation(a, b));
}
results
}
pub fn ewma_correlation(series_a: &[f64], series_b: &[f64], lambda: f64) -> f64 {
let len = series_a.len().min(series_b.len());
if len == 0 {
return 0.0;
}
let mut mean_a = 0.0_f64;
let mut mean_b = 0.0_f64;
let mut weight_sum = 0.0_f64;
let mut w = 1.0_f64;
for k in (0..len).rev() {
mean_a += w * series_a[k];
mean_b += w * series_b[k];
weight_sum += w;
w *= lambda;
}
mean_a /= weight_sum;
mean_b /= weight_sum;
let mut cov = 0.0_f64;
let mut var_a = 0.0_f64;
let mut var_b = 0.0_f64;
w = 1.0_f64;
let mut ws = 0.0_f64;
for k in (0..len).rev() {
let da = series_a[k] - mean_a;
let db = series_b[k] - mean_b;
cov += w * da * db;
var_a += w * da * da;
var_b += w * db * db;
ws += w;
w *= lambda;
}
if ws > 0.0 {
cov /= ws;
var_a /= ws;
var_b /= ws;
}
let denom = (var_a * var_b).sqrt();
if denom < 1e-12 {
0.0
} else {
(cov / denom).clamp(-1.0, 1.0)
}
}
pub fn dcc_update(prev_corr: f64, a: f64, b: f64, epsilon_t: f64) -> f64 {
let rho_bar = prev_corr;
let q_t = (1.0 - a - b) * rho_bar + a * epsilon_t.powi(2) + b * prev_corr;
q_t.clamp(-1.0, 1.0)
}
}
fn pearson_correlation(a: &[f64], b: &[f64]) -> f64 {
let n = a.len();
if n == 0 {
return 0.0;
}
let mean_a = a.iter().sum::<f64>() / n as f64;
let mean_b = b.iter().sum::<f64>() / n as f64;
let mut cov = 0.0_f64;
let mut var_a = 0.0_f64;
let mut var_b = 0.0_f64;
for (&ai, &bi) in a.iter().zip(b.iter()) {
let da = ai - mean_a;
let db = bi - mean_b;
cov += da * db;
var_a += da * da;
var_b += db * db;
}
let denom = (var_a * var_b).sqrt();
if denom < 1e-12 {
0.0
} else {
(cov / denom).clamp(-1.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_returns() -> Vec<Vec<f64>> {
vec![
vec![0.01, -0.02, 0.03, 0.01, -0.01],
vec![0.02, -0.01, 0.02, 0.00, -0.02],
]
}
#[test]
fn correlation_diagonal_is_one() {
let cm = CorrelationMatrix::from_returns(&sample_returns());
assert!((cm.get(0, 0) - 1.0).abs() < 1e-9);
assert!((cm.get(1, 1) - 1.0).abs() < 1e-9);
}
#[test]
fn correlation_is_symmetric() {
let cm = CorrelationMatrix::from_returns(&sample_returns());
assert!((cm.get(0, 1) - cm.get(1, 0)).abs() < 1e-12);
}
#[test]
fn identity_is_positive_definite() {
let id = LedoitWolfShrinkage::target_identity(3);
assert!(id.is_positive_definite());
}
#[test]
fn shrink_alpha_in_range() {
let returns = sample_returns();
let alpha = LedoitWolfShrinkage::optimal_alpha(&returns);
assert!(alpha >= 0.0 && alpha <= 1.0);
}
#[test]
fn rolling_correlation_length() {
let a = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let b = vec![5.0, 4.0, 3.0, 2.0, 1.0];
let rc = DccGarch::rolling_correlation(&a, &b, 3);
assert_eq!(rc.len(), 3);
}
#[test]
fn ewma_correlation_range() {
let a = vec![0.01, -0.02, 0.03, 0.01];
let b = vec![0.02, -0.01, 0.02, 0.00];
let c = DccGarch::ewma_correlation(&a, &b, 0.94);
assert!(c >= -1.0 && c <= 1.0);
}
}