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 nalgebra::DMatrix;
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
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 {
61            return vec![];
62        }
63
64        let mut mat_data = Vec::with_capacity(n * n);
65        for row in transition_matrix {
66            mat_data.extend_from_slice(row);
67        }
68
69        // nalgebra DMatrix is column-major by default in some constructors,
70        // but from_row_slice makes it clear.
71        let m = DMatrix::from_row_slice(n, n, &mat_data);
72        let m_n = m.pow(steps as u32);
73
74        let mut initial_dist = vec![0.0; n];
75        if (current_state as usize) < n {
76            initial_dist[current_state as usize] = 1.0;
77        } else {
78            return vec![0.0; n];
79        }
80
81        let v = nalgebra::DVector::from_vec(initial_dist);
82        // Distribution after n steps: v^T * M^n (if using row vectors)
83        // Or M^T * v (if using column vectors)
84        // nalgebra's pow and vector multiplication
85        let result = m_n.transpose() * v;
86        result.as_slice().to_vec()
87    }
88
89    /// Calculates duration statistics for each regime in the sequence.
90    pub fn duration_stats(states: &[u32], num_states: usize) -> Vec<DurationStats> {
91        let mut durations: BTreeMap<u32, Vec<usize>> = BTreeMap::new();
92
93        if states.is_empty() {
94            return vec![];
95        }
96
97        let mut current_regime = states[0];
98        let mut current_duration = 1;
99
100        for &state in &states[1..] {
101            if state == current_regime {
102                current_duration += 1;
103            } else {
104                durations
105                    .entry(current_regime)
106                    .or_default()
107                    .push(current_duration);
108                current_regime = state;
109                current_duration = 1;
110            }
111        }
112        durations
113            .entry(current_regime)
114            .or_default()
115            .push(current_duration);
116
117        let mut results = Vec::new();
118        for i in 0..num_states as u32 {
119            if let Some(d_list) = durations.get(&i) {
120                let total_obs: usize = d_list.iter().sum();
121                let n = d_list.len() as f64;
122                let mean = total_obs as f64 / n;
123
124                let mut sorted = d_list.clone();
125                sorted.sort_unstable();
126                let median = sorted[sorted.len() / 2] as f64;
127                let max_dur = *sorted.last().unwrap_or(&0);
128
129                let variance = d_list
130                    .iter()
131                    .map(|&d| (d as f64 - mean).powi(2))
132                    .sum::<f64>()
133                    / n;
134                let std = variance.sqrt();
135
136                results.push(DurationStats {
137                    regime_id: i,
138                    mean_duration: mean,
139                    median_duration: median,
140                    std_duration: std,
141                    max_duration: max_dur,
142                    total_observations: total_obs,
143                });
144            }
145        }
146        results
147    }
148
149    /// Calculates a stability score (0.0 to 1.0).
150    ///
151    /// Higher scores indicate fewer regime switches relative to the total sequence length.
152    pub fn stability_score(states: &[u32]) -> f64 {
153        if states.len() < 2 {
154            return 1.0;
155        }
156
157        let mut switches = 0;
158        for pair in states.windows(2) {
159            if pair[0] != pair[1] {
160                switches += 1;
161            }
162        }
163
164        1.0 - (switches as f64 / (states.len() - 1) as f64)
165    }
166}