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::regimes::MarketRegime;
7use crate::regimes::volatility_clustering::VolatilityClusterer;
8use crate::traits::Next;
9use crate::utils::RingBuffer as VecDeque;
10use serde::{Deserialize, Serialize};
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 {
54            1.0
55        } else {
56            total_corr / pairs as f64
57        }
58    }
59
60    fn correlation(&self, i: usize, j: usize) -> f64 {
61        let x = &self.history[i];
62        let y = &self.history[j];
63        let n = x.len() as f64;
64
65        let mean_x = x.iter().sum::<f64>() / n;
66        let mean_y = y.iter().sum::<f64>() / n;
67
68        let mut cov = 0.0;
69        let mut var_x = 0.0;
70        let mut var_y = 0.0;
71
72        for k in 0..x.len() {
73            let dx = x[k] - mean_x;
74            let dy = y[k] - mean_y;
75            cov += dx * dy;
76            var_x += dx * dx;
77            var_y += dy * dy;
78        }
79
80        let den = (var_x * var_y).sqrt();
81        if den == 0.0 { 1.0 } else { cov / den }
82    }
83}
84
85impl Next<&[f64]> for MultiAssetClusterer {
86    type Output = MarketRegime;
87
88    fn next(&mut self, returns: &[f64]) -> Self::Output {
89        if returns.len() != self.n_assets {
90            return MarketRegime::Steady;
91        }
92
93        // Update history
94        for (i, &r) in returns.iter().enumerate() {
95            self.history[i].push_back(r);
96            if self.history[i].len() > self.window_size {
97                self.history[i].pop_front();
98            }
99        }
100
101        // Feature engineering
102        // 1. Mean absolute return (Magnitude of move)
103        let mean_abs_ret = returns.iter().map(|r| r.abs()).sum::<f64>() / self.n_assets as f64;
104
105        // 2. Dispersion (how much assets are moving in different directions)
106        let mean_ret = returns.iter().sum::<f64>() / self.n_assets as f64;
107        let dispersion =
108            returns.iter().map(|r| (r - mean_ret).powi(2)).sum::<f64>() / self.n_assets as f64;
109
110        // 3. Average Correlation
111        let avg_corr = self.calculate_average_correlation();
112
113        // Pass features to inner clusterer
114        // We use mean_abs_ret as the primary signal, dispersion and correlation as modifiers
115        // For VolatilityClusterer, we'll map these to high/low/close equivalents
116        self.inner.next((
117            mean_abs_ret,
118            mean_abs_ret * (1.0 - dispersion.sqrt()),
119            avg_corr,
120        ))
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_multi_asset_clusterer_basic() {
130        let mut clusterer = MultiAssetClusterer::new(2, 5, 2);
131
132        // Steady market
133        for _ in 0..10 {
134            clusterer.next(&[0.01, 0.01]);
135        }
136        let r1 = clusterer.next(&[0.01, 0.01]);
137
138        // Highly volatile/correlated
139        for _ in 0..10 {
140            clusterer.next(&[0.05, 0.05]);
141        }
142        let r2 = clusterer.next(&[0.05, 0.05]);
143
144        // Assert different regimes if enough data for clustering
145        // Since it's a dynamic clusterer, exact states depend on initialization
146        assert!(matches!(
147            r1,
148            MarketRegime::Steady | MarketRegime::Cluster(_)
149        ));
150        assert!(matches!(
151            r2,
152            MarketRegime::Steady | MarketRegime::Cluster(_)
153        ));
154    }
155}