Skip to main content

quantwave_core/regimes/
analytics.rs

1//! Core Analytics and Diagnostics for Market Regimes
2//! 
3//! This module provides tools to analyze regime persistence, transitions, and stability.
4
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7use nalgebra::DMatrix;
8
9/// Statistics regarding the duration of a specific regime.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct DurationStats {
12    pub regime_id: u32,
13    pub mean_duration: f64,
14    pub median_duration: f64,
15    pub std_duration: f64,
16    pub max_duration: usize,
17    pub total_observations: usize,
18}
19
20/// Core analytical tools for analyzing regime sequences.
21pub struct RegimeAnalytics;
22
23impl RegimeAnalytics {
24    /// Constructs an empirical transition matrix from a sequence of regime states.
25    /// 
26    /// The matrix `T[i][j]` represents the probability of transitioning from state `i` to state `j`.
27    pub fn transition_matrix(states: &[u32], num_states: usize) -> Vec<Vec<f64>> {
28        let mut transitions = vec![vec![0usize; num_states]; num_states];
29        let mut row_totals = vec![0usize; num_states];
30
31        for pair in states.windows(2) {
32            let from = pair[0] as usize;
33            let to = pair[1] as usize;
34            if from < num_states && to < num_states {
35                transitions[from][to] += 1;
36                row_totals[from] += 1;
37            }
38        }
39
40        let mut matrix = vec![vec![0.0; num_states]; num_states];
41        for i in 0..num_states {
42            if row_totals[i] > 0 {
43                for j in 0..num_states {
44                    matrix[i][j] = transitions[i][j] as f64 / row_totals[i] as f64;
45                }
46            }
47        }
48        matrix
49    }
50
51    /// Forecasts the state probability distribution `n` steps ahead.
52    /// 
53    /// Uses matrix exponentiation (powering) of the transition matrix.
54    pub fn forecast_state(
55        transition_matrix: &[Vec<f64>],
56        current_state: u32,
57        steps: usize,
58    ) -> Vec<f64> {
59        let n = transition_matrix.len();
60        if n == 0 { return vec![]; }
61        
62        let mut mat_data = Vec::with_capacity(n * n);
63        for row in transition_matrix {
64            mat_data.extend_from_slice(row);
65        }
66        
67        // nalgebra DMatrix is column-major by default in some constructors, 
68        // but from_row_slice makes it clear.
69        let m = DMatrix::from_row_slice(n, n, &mat_data);
70        let m_n = m.pow(steps as u32);
71
72        let mut initial_dist = vec![0.0; n];
73        if (current_state as usize) < n {
74            initial_dist[current_state as usize] = 1.0;
75        } else {
76            return vec![0.0; n];
77        }
78
79        let v = nalgebra::DVector::from_vec(initial_dist);
80        // Distribution after n steps: v^T * M^n (if using row vectors)
81        // Or M^T * v (if using column vectors)
82        // nalgebra's pow and vector multiplication
83        let result = m_n.transpose() * v;
84        result.as_slice().to_vec()
85    }
86
87    /// Calculates duration statistics for each regime in the sequence.
88    pub fn duration_stats(states: &[u32], num_states: usize) -> Vec<DurationStats> {
89        let mut durations: BTreeMap<u32, Vec<usize>> = BTreeMap::new();
90        
91        if states.is_empty() { return vec![]; }
92
93        let mut current_regime = states[0];
94        let mut current_duration = 1;
95
96        for &state in &states[1..] {
97            if state == current_regime {
98                current_duration += 1;
99            } else {
100                durations.entry(current_regime).or_default().push(current_duration);
101                current_regime = state;
102                current_duration = 1;
103            }
104        }
105        durations.entry(current_regime).or_default().push(current_duration);
106
107        let mut results = Vec::new();
108        for i in 0..num_states as u32 {
109            if let Some(d_list) = durations.get(&i) {
110                let total_obs: usize = d_list.iter().sum();
111                let n = d_list.len() as f64;
112                let mean = total_obs as f64 / n;
113                
114                let mut sorted = d_list.clone();
115                sorted.sort_unstable();
116                let median = sorted[sorted.len() / 2] as f64;
117                let max_dur = *sorted.last().unwrap_or(&0);
118
119                let variance = d_list.iter()
120                    .map(|&d| (d as f64 - mean).powi(2))
121                    .sum::<f64>() / n;
122                let std = variance.sqrt();
123
124                results.push(DurationStats {
125                    regime_id: i,
126                    mean_duration: mean,
127                    median_duration: median,
128                    std_duration: std,
129                    max_duration: max_dur,
130                    total_observations: total_obs,
131                });
132            }
133        }
134        results
135    }
136
137    /// Calculates a stability score (0.0 to 1.0).
138    /// 
139    /// Higher scores indicate fewer regime switches relative to the total sequence length.
140    pub fn stability_score(states: &[u32]) -> f64 {
141        if states.len() < 2 { return 1.0; }
142        
143        let mut switches = 0;
144        for pair in states.windows(2) {
145            if pair[0] != pair[1] {
146                switches += 1;
147            }
148        }
149        
150        1.0 - (switches as f64 / (states.len() - 1) as f64)
151    }
152}