Skip to main content

quantwave_core/regimes/
multi_asset.rs

1//! Multi-Asset Regime Detection
2//! 
3//! Identifies joint market regimes across multiple assets by clustering 
4//! based on returns and rolling correlation structures.
5
6use crate::traits::Next;
7use crate::regimes::MarketRegime;
8use crate::regimes::volatility_clustering::VolatilityClusterer;
9use serde::{Deserialize, Serialize};
10use std::collections::VecDeque;
11
12/// A clusterer for identifying regimes across multiple assets.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct MultiAssetClusterer {
15    n_assets: usize,
16    window_size: usize,
17    /// We use a VolatilityClusterer on a combined feature vector
18    inner: VolatilityClusterer,
19    history: Vec<VecDeque<f64>>,
20}
21
22impl MultiAssetClusterer {
23    pub fn new(n_assets: usize, window_size: usize, k: usize) -> Self {
24        // Feature vector size: 
25        // 1. Mean absolute return (1)
26        // 2. Dispersion (1)
27        // 3. Average correlation (1)
28        // Total features: 3
29        Self {
30            n_assets,
31            window_size,
32            inner: VolatilityClusterer::new(14, window_size, k),
33            history: vec![VecDeque::with_capacity(window_size); n_assets],
34        }
35    }
36
37    fn calculate_average_correlation(&self) -> f64 {
38        if self.history[0].len() < self.window_size {
39            return 1.0;
40        }
41
42        let mut total_corr = 0.0;
43        let mut pairs = 0;
44
45        for i in 0..self.n_assets {
46            for j in (i + 1)..self.n_assets {
47                let corr = self.correlation(i, j);
48                total_corr += corr;
49                pairs += 1;
50            }
51        }
52
53        if pairs == 0 { 1.0 } else { total_corr / pairs as f64 }
54    }
55
56    fn correlation(&self, i: usize, j: usize) -> f64 {
57        let x = &self.history[i];
58        let y = &self.history[j];
59        let n = x.len() as f64;
60
61        let mean_x = x.iter().sum::<f64>() / n;
62        let mean_y = y.iter().sum::<f64>() / n;
63
64        let mut cov = 0.0;
65        let mut var_x = 0.0;
66        let mut var_y = 0.0;
67
68        for k in 0..x.len() {
69            let dx = x[k] - mean_x;
70            let dy = y[k] - mean_y;
71            cov += dx * dy;
72            var_x += dx * dx;
73            var_y += dy * dy;
74        }
75
76        let den = (var_x * var_y).sqrt();
77        if den == 0.0 { 1.0 } else { cov / den }
78    }
79}
80
81impl Next<&[f64]> for MultiAssetClusterer {
82    type Output = MarketRegime;
83
84    fn next(&mut self, returns: &[f64]) -> Self::Output {
85        if returns.len() != self.n_assets {
86            return MarketRegime::Steady;
87        }
88
89        // Update history
90        for (i, &r) in returns.iter().enumerate() {
91            self.history[i].push_back(r);
92            if self.history[i].len() > self.window_size {
93                self.history[i].pop_front();
94            }
95        }
96
97        // Feature engineering
98        // 1. Mean absolute return (Magnitude of move)
99        let mean_abs_ret = returns.iter().map(|r| r.abs()).sum::<f64>() / self.n_assets as f64;
100        
101        // 2. Dispersion (how much assets are moving in different directions)
102        let mean_ret = returns.iter().sum::<f64>() / self.n_assets as f64;
103        let dispersion = returns.iter().map(|r| (r - mean_ret).powi(2)).sum::<f64>() / self.n_assets as f64;
104
105        // 3. Average Correlation
106        let avg_corr = self.calculate_average_correlation();
107
108        // Pass features to inner clusterer
109        // We use mean_abs_ret as the primary signal, dispersion and correlation as modifiers
110        // For VolatilityClusterer, we'll map these to high/low/close equivalents
111        self.inner.next((mean_abs_ret, mean_abs_ret * (1.0 - dispersion.sqrt()), avg_corr))
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_multi_asset_clusterer_basic() {
121        let mut clusterer = MultiAssetClusterer::new(2, 5, 2);
122        
123        // Steady market
124        for _ in 0..10 {
125            clusterer.next(&[0.01, 0.01]);
126        }
127        let r1 = clusterer.next(&[0.01, 0.01]);
128        
129        // Highly volatile/correlated
130        for _ in 0..10 {
131            clusterer.next(&[0.05, 0.05]);
132        }
133        let r2 = clusterer.next(&[0.05, 0.05]);
134        
135        // Assert different regimes if enough data for clustering
136        // Since it's a dynamic clusterer, exact states depend on initialization
137        assert!(matches!(r1, MarketRegime::Steady | MarketRegime::Cluster(_)));
138        assert!(matches!(r2, MarketRegime::Steady | MarketRegime::Cluster(_)));
139    }
140}