scirs2_series/features/
window_based.rs

1//! Window-based aggregation features for time series analysis
2//!
3//! This module provides comprehensive sliding window analysis including multi-scale
4//! statistical features, cross-window correlations, change detection, rolling statistics,
5//! technical indicators, and normalized features for financial and statistical analysis.
6
7use scirs2_core::ndarray::{s, Array1};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use std::fmt::Debug;
10
11use super::config::WindowConfig;
12use super::utils::{calculate_pearson_correlation, linear_fit};
13use crate::error::Result;
14
15/// Window-based aggregation features for time series analysis
16///
17/// This struct contains comprehensive features computed over sliding windows
18/// of various sizes, enabling multi-scale statistical analysis.
19#[derive(Debug, Clone)]
20pub struct WindowBasedFeatures<F> {
21    /// Features from small windows (high temporal resolution)
22    pub small_window_features: WindowFeatures<F>,
23    /// Features from medium windows (balanced resolution)
24    pub medium_window_features: WindowFeatures<F>,
25    /// Features from large windows (low temporal resolution)
26    pub large_window_features: WindowFeatures<F>,
27    /// Multi-scale variance features
28    pub multi_scale_variance: Vec<F>,
29    /// Multi-scale trend features
30    pub multi_scale_trends: Vec<F>,
31    /// Cross-window correlation features
32    pub cross_window_correlations: CrossWindowFeatures<F>,
33    /// Window-based change detection features
34    pub change_detection_features: ChangeDetectionFeatures<F>,
35    /// Rolling aggregation statistics
36    pub rolling_statistics: RollingStatistics<F>,
37}
38
39impl<F> Default for WindowBasedFeatures<F>
40where
41    F: Float + FromPrimitive,
42{
43    fn default() -> Self {
44        Self {
45            small_window_features: WindowFeatures::default(),
46            medium_window_features: WindowFeatures::default(),
47            large_window_features: WindowFeatures::default(),
48            multi_scale_variance: Vec::new(),
49            multi_scale_trends: Vec::new(),
50            cross_window_correlations: CrossWindowFeatures::default(),
51            change_detection_features: ChangeDetectionFeatures::default(),
52            rolling_statistics: RollingStatistics::default(),
53        }
54    }
55}
56
57/// Features computed over a specific window size
58#[derive(Debug, Clone)]
59pub struct WindowFeatures<F> {
60    /// Window size used for computation
61    pub window_size: usize,
62    /// Rolling means across all windows
63    pub rolling_means: Vec<F>,
64    /// Rolling standard deviations
65    pub rolling_stds: Vec<F>,
66    /// Rolling minimums
67    pub rolling_mins: Vec<F>,
68    /// Rolling maximums
69    pub rolling_maxs: Vec<F>,
70    /// Rolling medians
71    pub rolling_medians: Vec<F>,
72    /// Rolling skewness values
73    pub rolling_skewness: Vec<F>,
74    /// Rolling kurtosis values
75    pub rolling_kurtosis: Vec<F>,
76    /// Rolling quantiles (25%, 75%)
77    pub rolling_quantiles: Vec<(F, F)>,
78    /// Rolling ranges (max - min)
79    pub rolling_ranges: Vec<F>,
80    /// Rolling coefficient of variation
81    pub rolling_cv: Vec<F>,
82    /// Summary statistics of rolling features
83    pub summary_stats: WindowSummaryStats<F>,
84}
85
86impl<F> Default for WindowFeatures<F>
87where
88    F: Float + FromPrimitive,
89{
90    fn default() -> Self {
91        Self {
92            window_size: 0,
93            rolling_means: Vec::new(),
94            rolling_stds: Vec::new(),
95            rolling_mins: Vec::new(),
96            rolling_maxs: Vec::new(),
97            rolling_medians: Vec::new(),
98            rolling_skewness: Vec::new(),
99            rolling_kurtosis: Vec::new(),
100            rolling_quantiles: Vec::new(),
101            rolling_ranges: Vec::new(),
102            rolling_cv: Vec::new(),
103            summary_stats: WindowSummaryStats::default(),
104        }
105    }
106}
107
108/// Summary statistics of rolling window features
109#[derive(Debug, Clone)]
110pub struct WindowSummaryStats<F> {
111    /// Mean of rolling means
112    pub mean_of_means: F,
113    /// Standard deviation of rolling means
114    pub std_of_means: F,
115    /// Mean of rolling standard deviations
116    pub mean_of_stds: F,
117    /// Standard deviation of rolling standard deviations
118    pub std_of_stds: F,
119    /// Maximum range observed
120    pub max_range: F,
121    /// Minimum range observed
122    pub min_range: F,
123    /// Mean range
124    pub mean_range: F,
125    /// Trend in rolling means (slope)
126    pub trend_in_means: F,
127    /// Trend in rolling standard deviations
128    pub trend_in_stds: F,
129    /// Variability index (CV of CVs)
130    pub variability_index: F,
131}
132
133impl<F> Default for WindowSummaryStats<F>
134where
135    F: Float + FromPrimitive,
136{
137    fn default() -> Self {
138        Self {
139            mean_of_means: F::zero(),
140            std_of_means: F::zero(),
141            mean_of_stds: F::zero(),
142            std_of_stds: F::zero(),
143            max_range: F::zero(),
144            min_range: F::zero(),
145            mean_range: F::zero(),
146            trend_in_means: F::zero(),
147            trend_in_stds: F::zero(),
148            variability_index: F::zero(),
149        }
150    }
151}
152
153/// Cross-window analysis features
154#[derive(Debug, Clone)]
155pub struct CrossWindowFeatures<F> {
156    /// Correlation between small and medium window means
157    pub small_medium_correlation: F,
158    /// Correlation between medium and large window means
159    pub medium_large_correlation: F,
160    /// Correlation between small and large window means
161    pub small_large_correlation: F,
162    /// Phase difference between different window scales
163    pub scale_phase_differences: Vec<F>,
164    /// Cross-scale consistency measure
165    pub cross_scale_consistency: F,
166    /// Multi-scale coherence
167    pub multi_scale_coherence: F,
168}
169
170impl<F> Default for CrossWindowFeatures<F>
171where
172    F: Float + FromPrimitive,
173{
174    fn default() -> Self {
175        Self {
176            small_medium_correlation: F::zero(),
177            medium_large_correlation: F::zero(),
178            small_large_correlation: F::zero(),
179            scale_phase_differences: Vec::new(),
180            cross_scale_consistency: F::zero(),
181            multi_scale_coherence: F::zero(),
182        }
183    }
184}
185
186/// Change detection features from window analysis
187#[derive(Debug, Clone)]
188pub struct ChangeDetectionFeatures<F> {
189    /// Number of significant mean changes detected
190    pub mean_change_points: usize,
191    /// Number of significant variance changes detected
192    pub variance_change_points: usize,
193    /// CUSUM (Cumulative Sum) statistics for mean changes
194    pub cusum_mean_changes: Vec<F>,
195    /// CUSUM statistics for variance changes
196    pub cusum_variance_changes: Vec<F>,
197    /// Maximum CUSUM value for mean
198    pub max_cusum_mean: F,
199    /// Maximum CUSUM value for variance
200    pub max_cusum_variance: F,
201    /// Window-based stability measure
202    pub stability_measure: F,
203    /// Relative change magnitude
204    pub relative_change_magnitude: F,
205}
206
207impl<F> Default for ChangeDetectionFeatures<F>
208where
209    F: Float + FromPrimitive,
210{
211    fn default() -> Self {
212        Self {
213            mean_change_points: 0,
214            variance_change_points: 0,
215            cusum_mean_changes: Vec::new(),
216            cusum_variance_changes: Vec::new(),
217            max_cusum_mean: F::zero(),
218            max_cusum_variance: F::zero(),
219            stability_measure: F::zero(),
220            relative_change_magnitude: F::zero(),
221        }
222    }
223}
224
225/// Rolling aggregation statistics
226#[derive(Debug, Clone)]
227pub struct RollingStatistics<F> {
228    /// Exponentially weighted moving average (EWMA)
229    pub ewma: Vec<F>,
230    /// Exponentially weighted moving variance
231    pub ewmv: Vec<F>,
232    /// Bollinger band features (upper, lower, width)
233    pub bollinger_bands: BollingerBandFeatures<F>,
234    /// Moving average convergence divergence (MACD) features
235    pub macd_features: MACDFeatures<F>,
236    /// Relative strength index (RSI) over windows
237    pub rsi_values: Vec<F>,
238    /// Z-score normalized rolling features
239    pub normalized_features: NormalizedRollingFeatures<F>,
240}
241
242impl<F> Default for RollingStatistics<F>
243where
244    F: Float + FromPrimitive,
245{
246    fn default() -> Self {
247        Self {
248            ewma: Vec::new(),
249            ewmv: Vec::new(),
250            bollinger_bands: BollingerBandFeatures::default(),
251            macd_features: MACDFeatures::default(),
252            rsi_values: Vec::new(),
253            normalized_features: NormalizedRollingFeatures::default(),
254        }
255    }
256}
257
258/// Bollinger band features
259#[derive(Debug, Clone)]
260pub struct BollingerBandFeatures<F> {
261    /// Upper Bollinger band values
262    pub upper_band: Vec<F>,
263    /// Lower Bollinger band values
264    pub lower_band: Vec<F>,
265    /// Band width (upper - lower)
266    pub band_width: Vec<F>,
267    /// Percentage above upper band
268    pub percent_above_upper: F,
269    /// Percentage below lower band
270    pub percent_below_lower: F,
271    /// Mean band width
272    pub mean_band_width: F,
273    /// Band squeeze periods (low width)
274    pub squeeze_periods: usize,
275}
276
277impl<F> Default for BollingerBandFeatures<F>
278where
279    F: Float + FromPrimitive,
280{
281    fn default() -> Self {
282        Self {
283            upper_band: Vec::new(),
284            lower_band: Vec::new(),
285            band_width: Vec::new(),
286            percent_above_upper: F::zero(),
287            percent_below_lower: F::zero(),
288            mean_band_width: F::zero(),
289            squeeze_periods: 0,
290        }
291    }
292}
293
294/// MACD (Moving Average Convergence Divergence) features
295#[derive(Debug, Clone)]
296pub struct MACDFeatures<F> {
297    /// MACD line (fast EMA - slow EMA)
298    pub macd_line: Vec<F>,
299    /// Signal line (EMA of MACD)
300    pub signal_line: Vec<F>,
301    /// MACD histogram (MACD - Signal)
302    pub histogram: Vec<F>,
303    /// Number of bullish crossovers
304    pub bullish_crossovers: usize,
305    /// Number of bearish crossovers
306    pub bearish_crossovers: usize,
307    /// Mean histogram value
308    pub mean_histogram: F,
309    /// MACD divergence measure
310    pub divergence_measure: F,
311}
312
313impl<F> Default for MACDFeatures<F>
314where
315    F: Float + FromPrimitive,
316{
317    fn default() -> Self {
318        Self {
319            macd_line: Vec::new(),
320            signal_line: Vec::new(),
321            histogram: Vec::new(),
322            bullish_crossovers: 0,
323            bearish_crossovers: 0,
324            mean_histogram: F::zero(),
325            divergence_measure: F::zero(),
326        }
327    }
328}
329
330/// Normalized rolling features
331#[derive(Debug, Clone)]
332pub struct NormalizedRollingFeatures<F> {
333    /// Z-score normalized rolling means
334    pub normalized_means: Vec<F>,
335    /// Z-score normalized rolling stds
336    pub normalized_stds: Vec<F>,
337    /// Percentile rank of rolling values
338    pub percentile_ranks: Vec<F>,
339    /// Outlier detection based on rolling statistics
340    pub outlier_scores: Vec<F>,
341    /// Number of rolling outliers detected
342    pub outlier_count: usize,
343}
344
345impl<F> Default for NormalizedRollingFeatures<F>
346where
347    F: Float + FromPrimitive,
348{
349    fn default() -> Self {
350        Self {
351            normalized_means: Vec::new(),
352            normalized_stds: Vec::new(),
353            percentile_ranks: Vec::new(),
354            outlier_scores: Vec::new(),
355            outlier_count: 0,
356        }
357    }
358}
359
360// =============================================================================
361// Main Calculation Function
362// =============================================================================
363
364/// Calculate comprehensive window-based features
365///
366/// This function performs extensive sliding window analysis including multi-scale
367/// statistical features, cross-window correlations, change detection, and technical
368/// indicators commonly used in financial analysis.
369///
370/// # Arguments
371///
372/// * `ts` - Input time series data
373/// * `config` - Window analysis configuration
374///
375/// # Returns
376///
377/// WindowBasedFeatures containing comprehensive windowed analysis
378#[allow(dead_code)]
379pub fn calculate_window_based_features<F>(
380    ts: &Array1<F>,
381    config: &WindowConfig,
382) -> Result<WindowBasedFeatures<F>>
383where
384    F: Float + FromPrimitive + Debug + Clone + scirs2_core::ndarray::ScalarOperand + std::iter::Sum,
385{
386    let n = ts.len();
387
388    // Validate window sizes
389    let small_size = config.small_window_size.max(3).min(n / 4);
390    let medium_size = config.medium_window_size.max(small_size + 1).min(n / 2);
391    let large_size = config.large_window_size.max(medium_size + 1).min(n - 1);
392
393    if n < small_size + 2 {
394        return Ok(WindowBasedFeatures::default());
395    }
396
397    // Calculate features for each window size
398    let small_features = calculate_window_features(ts, small_size)?;
399    let medium_features = calculate_window_features(ts, medium_size)?;
400    let large_features = calculate_window_features(ts, large_size)?;
401
402    // Multi-scale variance analysis
403    let multi_scale_variance =
404        calculate_multi_scale_variance(ts, &[small_size, medium_size, large_size])?;
405
406    // Multi-scale trend analysis
407    let multi_scale_trends =
408        calculate_multi_scale_trends(ts, &[small_size, medium_size, large_size])?;
409
410    // Cross-window correlations
411    let cross_correlations = if config.calculate_cross_correlations {
412        calculate_cross_window_correlations(&small_features, &medium_features, &large_features)?
413    } else {
414        CrossWindowFeatures::default()
415    };
416
417    // Change detection features
418    let change_features = if config.detect_changes {
419        calculate_change_detection_features(ts, &medium_features, config)?
420    } else {
421        ChangeDetectionFeatures::default()
422    };
423
424    // Rolling statistics
425    let rolling_stats = calculate_rolling_statistics(ts, config)?;
426
427    Ok(WindowBasedFeatures {
428        small_window_features: small_features,
429        medium_window_features: medium_features,
430        large_window_features: large_features,
431        multi_scale_variance,
432        multi_scale_trends,
433        cross_window_correlations: cross_correlations,
434        change_detection_features: change_features,
435        rolling_statistics: rolling_stats,
436    })
437}
438
439// =============================================================================
440// Window Feature Calculation
441// =============================================================================
442
443/// Calculate features for a specific window size
444#[allow(dead_code)]
445fn calculate_window_features<F>(_ts: &Array1<F>, windowsize: usize) -> Result<WindowFeatures<F>>
446where
447    F: Float + FromPrimitive + Debug + Clone,
448{
449    let n = _ts.len();
450    if n < windowsize {
451        return Ok(WindowFeatures::default());
452    }
453
454    let num_windows = n - windowsize + 1;
455    let mut rolling_means = Vec::with_capacity(num_windows);
456    let mut rolling_stds = Vec::with_capacity(num_windows);
457    let mut rolling_mins = Vec::with_capacity(num_windows);
458    let mut rolling_maxs = Vec::with_capacity(num_windows);
459    let mut rolling_medians = Vec::with_capacity(num_windows);
460    let mut rolling_skewness = Vec::with_capacity(num_windows);
461    let mut rolling_kurtosis = Vec::with_capacity(num_windows);
462    let mut rolling_quantiles = Vec::with_capacity(num_windows);
463    let mut rolling_ranges = Vec::with_capacity(num_windows);
464    let mut rolling_cv = Vec::with_capacity(num_windows);
465
466    // Calculate rolling statistics
467    for i in 0..num_windows {
468        let window = _ts.slice(s![i..i + windowsize]);
469
470        // Basic statistics
471        let mean = window.sum() / F::from(windowsize).unwrap();
472        rolling_means.push(mean);
473
474        let variance = window.mapv(|x| (x - mean).powi(2)).sum() / F::from(windowsize).unwrap();
475        let std = variance.sqrt();
476        rolling_stds.push(std);
477
478        let min = window.iter().fold(F::infinity(), |a, &b| a.min(b));
479        let max = window.iter().fold(F::neg_infinity(), |a, &b| a.max(b));
480        rolling_mins.push(min);
481        rolling_maxs.push(max);
482        rolling_ranges.push(max - min);
483
484        // Coefficient of variation
485        let cv = if mean != F::zero() {
486            std / mean.abs()
487        } else {
488            F::zero()
489        };
490        rolling_cv.push(cv);
491
492        // Median and quantiles
493        let mut sorted_window: Vec<F> = window.iter().cloned().collect();
494        sorted_window.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
495
496        let median_idx = windowsize / 2;
497        let median = if windowsize.is_multiple_of(2) {
498            (sorted_window[median_idx - 1] + sorted_window[median_idx]) / F::from(2.0).unwrap()
499        } else {
500            sorted_window[median_idx]
501        };
502        rolling_medians.push(median);
503
504        let q1_idx = windowsize / 4;
505        let q3_idx = 3 * windowsize / 4;
506        let q1 = sorted_window[q1_idx];
507        let q3 = sorted_window[q3_idx.min(windowsize - 1)];
508        rolling_quantiles.push((q1, q3));
509
510        // Higher-order moments (skewness and kurtosis)
511        if std != F::zero() {
512            let sum_cube = window.mapv(|x| ((x - mean) / std).powi(3)).sum();
513            let sum_quad = window.mapv(|x| ((x - mean) / std).powi(4)).sum();
514
515            let skewness = sum_cube / F::from(windowsize).unwrap();
516            let kurtosis = sum_quad / F::from(windowsize).unwrap() - F::from(3.0).unwrap();
517
518            rolling_skewness.push(skewness);
519            rolling_kurtosis.push(kurtosis);
520        } else {
521            rolling_skewness.push(F::zero());
522            rolling_kurtosis.push(F::zero());
523        }
524    }
525
526    // Calculate summary statistics
527    let summary_stats = calculate_window_summary_stats(
528        &rolling_means,
529        &rolling_stds,
530        &rolling_ranges,
531        &rolling_cv,
532    )?;
533
534    Ok(WindowFeatures {
535        window_size: windowsize,
536        rolling_means,
537        rolling_stds,
538        rolling_mins,
539        rolling_maxs,
540        rolling_medians,
541        rolling_skewness,
542        rolling_kurtosis,
543        rolling_quantiles,
544        rolling_ranges,
545        rolling_cv,
546        summary_stats,
547    })
548}
549
550/// Calculate summary statistics for window features
551#[allow(dead_code)]
552fn calculate_window_summary_stats<F>(
553    rolling_means: &[F],
554    rolling_stds: &[F],
555    rolling_ranges: &[F],
556    rolling_cv: &[F],
557) -> Result<WindowSummaryStats<F>>
558where
559    F: Float + FromPrimitive + Debug,
560{
561    let n = rolling_means.len();
562    if n == 0 {
563        return Ok(WindowSummaryStats::default());
564    }
565
566    let n_f = F::from(n).unwrap();
567
568    // Mean and std of rolling _means
569    let mean_of_means = rolling_means.iter().fold(F::zero(), |acc, &x| acc + x) / n_f;
570    let std_of_means = if n > 1 {
571        let variance = rolling_means.iter().fold(F::zero(), |acc, &x| {
572            acc + (x - mean_of_means) * (x - mean_of_means)
573        }) / F::from(n - 1).unwrap();
574        variance.sqrt()
575    } else {
576        F::zero()
577    };
578
579    // Mean and std of rolling _stds
580    let mean_of_stds = rolling_stds.iter().fold(F::zero(), |acc, &x| acc + x) / n_f;
581    let std_of_stds = if n > 1 {
582        let variance = rolling_stds.iter().fold(F::zero(), |acc, &x| {
583            acc + (x - mean_of_stds) * (x - mean_of_stds)
584        }) / F::from(n - 1).unwrap();
585        variance.sqrt()
586    } else {
587        F::zero()
588    };
589
590    // Range statistics
591    let max_range = rolling_ranges
592        .iter()
593        .fold(F::neg_infinity(), |a, &b| a.max(b));
594    let min_range = rolling_ranges.iter().fold(F::infinity(), |a, &b| a.min(b));
595    let mean_range = rolling_ranges.iter().fold(F::zero(), |acc, &x| acc + x) / n_f;
596
597    // Trend calculations (linear regression slope)
598    let indices: Vec<F> = (0..n).map(|i| F::from(i).unwrap()).collect();
599    let (trend_in_means_, _) = linear_fit(&indices, rolling_means);
600    let (trend_in_stds_, _) = linear_fit(&indices, rolling_stds);
601
602    // Variability index (CV of CVs)
603    let mean_cv = rolling_cv.iter().fold(F::zero(), |acc, &x| acc + x) / n_f;
604    let variability_index = if mean_cv != F::zero() && n > 1 {
605        let cv_variance = rolling_cv
606            .iter()
607            .fold(F::zero(), |acc, &x| acc + (x - mean_cv) * (x - mean_cv))
608            / F::from(n - 1).unwrap();
609        cv_variance.sqrt() / mean_cv
610    } else {
611        F::zero()
612    };
613
614    Ok(WindowSummaryStats {
615        mean_of_means,
616        std_of_means,
617        mean_of_stds,
618        std_of_stds,
619        max_range,
620        min_range,
621        mean_range,
622        trend_in_means: trend_in_means_,
623        trend_in_stds: trend_in_stds_,
624        variability_index,
625    })
626}
627
628// =============================================================================
629// Multi-Scale Analysis
630// =============================================================================
631
632/// Calculate multi-scale variance features
633#[allow(dead_code)]
634fn calculate_multi_scale_variance<F>(_ts: &Array1<F>, windowsizes: &[usize]) -> Result<Vec<F>>
635where
636    F: Float + FromPrimitive + Debug,
637{
638    let mut variances = Vec::with_capacity(windowsizes.len());
639
640    for &window_size in windowsizes {
641        let mut scale_variances = Vec::new();
642        let num_windows = _ts.len().saturating_sub(window_size).saturating_add(1);
643
644        for i in 0..num_windows {
645            let window = _ts.slice(s![i..i + window_size]);
646            let mean = window.sum() / F::from(window_size).unwrap();
647            let variance =
648                window.mapv(|x| (x - mean) * (x - mean)).sum() / F::from(window_size).unwrap();
649            scale_variances.push(variance);
650        }
651
652        let mean_variance = if !scale_variances.is_empty() {
653            scale_variances.iter().fold(F::zero(), |acc, &x| acc + x)
654                / F::from(scale_variances.len()).unwrap()
655        } else {
656            F::zero()
657        };
658
659        variances.push(mean_variance);
660    }
661
662    Ok(variances)
663}
664
665/// Calculate multi-scale trend features
666#[allow(dead_code)]
667fn calculate_multi_scale_trends<F>(_ts: &Array1<F>, windowsizes: &[usize]) -> Result<Vec<F>>
668where
669    F: Float + FromPrimitive + Debug,
670{
671    let mut trends = Vec::with_capacity(windowsizes.len());
672
673    for &window_size in windowsizes {
674        let mut scale_trends = Vec::new();
675        let num_windows = _ts.len().saturating_sub(window_size).saturating_add(1);
676
677        for i in 0..num_windows {
678            let window = _ts.slice(s![i..i + window_size]);
679            let indices: Vec<F> = (0..window_size).map(|j| F::from(j).unwrap()).collect();
680            let values: Vec<F> = window.iter().cloned().collect();
681            let (slope_, _) = linear_fit(&indices, &values);
682            scale_trends.push(slope_);
683        }
684
685        let mean_trend = if !scale_trends.is_empty() {
686            scale_trends.iter().fold(F::zero(), |acc, &x| acc + x)
687                / F::from(scale_trends.len()).unwrap()
688        } else {
689            F::zero()
690        };
691
692        trends.push(mean_trend);
693    }
694
695    Ok(trends)
696}
697
698// =============================================================================
699// Cross-Window Analysis
700// =============================================================================
701
702/// Calculate cross-window correlations
703#[allow(dead_code)]
704fn calculate_cross_window_correlations<F>(
705    small: &WindowFeatures<F>,
706    medium: &WindowFeatures<F>,
707    large: &WindowFeatures<F>,
708) -> Result<CrossWindowFeatures<F>>
709where
710    F: Float + FromPrimitive + Debug + Clone + std::iter::Sum + scirs2_core::ndarray::ScalarOperand,
711{
712    // Align arrays by taking common length
713    let min_len = small
714        .rolling_means
715        .len()
716        .min(medium.rolling_means.len())
717        .min(large.rolling_means.len());
718
719    if min_len < 2 {
720        return Ok(CrossWindowFeatures::default());
721    }
722
723    // Calculate correlations between different window sizes
724    let small_array = Array1::from_vec(small.rolling_means[..min_len].to_vec());
725    let medium_array = Array1::from_vec(medium.rolling_means[..min_len].to_vec());
726    let large_array = Array1::from_vec(large.rolling_means[..min_len].to_vec());
727
728    let small_medium_correlation = calculate_pearson_correlation(&small_array, &medium_array)?;
729    let medium_large_correlation = calculate_pearson_correlation(&medium_array, &large_array)?;
730    let small_large_correlation = calculate_pearson_correlation(&small_array, &large_array)?;
731
732    // Calculate phase differences (simplified as mean differences)
733    let mut scale_phase_differences = Vec::new();
734    for i in 0..min_len {
735        let small_val = small.rolling_means[i];
736        let medium_val = medium.rolling_means[i];
737        let large_val = large.rolling_means[i];
738
739        let phase_diff_sm = (small_val - medium_val).abs();
740        let phase_diff_ml = (medium_val - large_val).abs();
741        scale_phase_differences.push(phase_diff_sm);
742        scale_phase_differences.push(phase_diff_ml);
743    }
744
745    // Cross-scale consistency
746    let correlations = [
747        small_medium_correlation,
748        medium_large_correlation,
749        small_large_correlation,
750    ];
751    let mean_correlation =
752        correlations.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(3.0).unwrap();
753    let cross_scale_consistency = mean_correlation;
754
755    // Multi-scale coherence (average of absolute correlations)
756    let multi_scale_coherence =
757        correlations.iter().fold(F::zero(), |acc, &x| acc + x.abs()) / F::from(3.0).unwrap();
758
759    Ok(CrossWindowFeatures {
760        small_medium_correlation,
761        medium_large_correlation,
762        small_large_correlation,
763        scale_phase_differences,
764        cross_scale_consistency,
765        multi_scale_coherence,
766    })
767}
768
769// =============================================================================
770// Change Detection
771// =============================================================================
772
773/// Calculate change detection features
774#[allow(dead_code)]
775fn calculate_change_detection_features<F>(
776    ts: &Array1<F>,
777    window_features: &WindowFeatures<F>,
778    config: &WindowConfig,
779) -> Result<ChangeDetectionFeatures<F>>
780where
781    F: Float + FromPrimitive + Debug,
782{
783    let n = ts.len();
784    if n < 10 {
785        return Ok(ChangeDetectionFeatures::default());
786    }
787
788    // CUSUM for mean changes
789    let target_mean = ts.sum() / F::from(n).unwrap();
790    let mut cusum_mean = F::zero();
791    let mut cusum_mean_changes = Vec::new();
792    let mut mean_change_points = 0;
793
794    for &value in ts.iter() {
795        cusum_mean = cusum_mean + (value - target_mean);
796        cusum_mean_changes.push(cusum_mean);
797
798        if cusum_mean.abs() > F::from(config.change_detection_threshold).unwrap() {
799            mean_change_points += 1;
800            cusum_mean = F::zero(); // Reset after detection
801        }
802    }
803
804    // CUSUM for variance changes
805    let rolling_stds = &window_features.rolling_stds;
806    let target_std = if !rolling_stds.is_empty() {
807        rolling_stds.iter().fold(F::zero(), |acc, &x| acc + x)
808            / F::from(rolling_stds.len()).unwrap()
809    } else {
810        F::zero()
811    };
812
813    let mut cusum_variance = F::zero();
814    let mut cusum_variance_changes = Vec::new();
815    let mut variance_change_points = 0;
816
817    for &std_val in rolling_stds.iter() {
818        cusum_variance = cusum_variance + (std_val - target_std);
819        cusum_variance_changes.push(cusum_variance);
820
821        if cusum_variance.abs() > F::from(config.change_detection_threshold).unwrap() {
822            variance_change_points += 1;
823            cusum_variance = F::zero(); // Reset after detection
824        }
825    }
826
827    // Maximum CUSUM values
828    let max_cusum_mean = cusum_mean_changes
829        .iter()
830        .fold(F::neg_infinity(), |a, &b| a.max(b.abs()));
831    let max_cusum_variance = cusum_variance_changes
832        .iter()
833        .fold(F::neg_infinity(), |a, &b| a.max(b.abs()));
834
835    // Stability measure
836    let total_changes = mean_change_points + variance_change_points;
837    let stability_measure = F::one() - F::from(total_changes).unwrap() / F::from(n).unwrap();
838
839    // Relative change magnitude
840    let data_range = ts.iter().fold(F::neg_infinity(), |a, &b| a.max(b))
841        - ts.iter().fold(F::infinity(), |a, &b| a.min(b));
842    let relative_change_magnitude = if data_range > F::zero() {
843        max_cusum_mean / data_range
844    } else {
845        F::zero()
846    };
847
848    Ok(ChangeDetectionFeatures {
849        mean_change_points,
850        variance_change_points,
851        cusum_mean_changes,
852        cusum_variance_changes,
853        max_cusum_mean,
854        max_cusum_variance,
855        stability_measure,
856        relative_change_magnitude,
857    })
858}
859
860// =============================================================================
861// Rolling Statistics and Technical Indicators
862// =============================================================================
863
864/// Calculate rolling statistics including technical indicators
865#[allow(dead_code)]
866fn calculate_rolling_statistics<F>(
867    ts: &Array1<F>,
868    config: &WindowConfig,
869) -> Result<RollingStatistics<F>>
870where
871    F: Float + FromPrimitive + Debug + Clone,
872{
873    // Calculate EWMA
874    let ewma = calculate_ewma(ts, config.ewma_alpha)?;
875
876    // Calculate EWMV
877    let ewmv = calculate_ewmv(ts, &ewma, config.ewma_alpha)?;
878
879    // Calculate Bollinger Bands
880    let bollinger_bands = calculate_bollinger_bands(ts, config)?;
881
882    // Calculate MACD
883    let macd_features = calculate_macd_features(ts, config)?;
884
885    // Calculate RSI
886    let rsi_values = calculate_rsi(ts, config.rsi_period)?;
887
888    // Calculate normalized features
889    let normalized_features = calculate_normalized_features(ts, config)?;
890
891    Ok(RollingStatistics {
892        ewma,
893        ewmv,
894        bollinger_bands,
895        macd_features,
896        rsi_values,
897        normalized_features,
898    })
899}
900
901/// Calculate Exponentially Weighted Moving Average
902#[allow(dead_code)]
903fn calculate_ewma<F>(ts: &Array1<F>, alpha: f64) -> Result<Vec<F>>
904where
905    F: Float + FromPrimitive + Clone,
906{
907    let alpha_f = F::from(alpha).unwrap();
908    let one_minus_alpha = F::one() - alpha_f;
909    let mut ewma = Vec::with_capacity(ts.len());
910
911    if ts.is_empty() {
912        return Ok(ewma);
913    }
914
915    ewma.push(ts[0]);
916
917    for i in 1..ts.len() {
918        let new_val = alpha_f * ts[i] + one_minus_alpha * ewma[i - 1];
919        ewma.push(new_val);
920    }
921
922    Ok(ewma)
923}
924
925/// Calculate Exponentially Weighted Moving Variance
926#[allow(dead_code)]
927fn calculate_ewmv<F>(ts: &Array1<F>, ewma: &[F], alpha: f64) -> Result<Vec<F>>
928where
929    F: Float + FromPrimitive + Clone,
930{
931    let alpha_f = F::from(alpha).unwrap();
932    let one_minus_alpha = F::one() - alpha_f;
933    let mut ewmv = Vec::with_capacity(ts.len());
934
935    if ts.is_empty() || ewma.is_empty() {
936        return Ok(ewmv);
937    }
938
939    ewmv.push(F::zero());
940
941    for i in 1..ts.len() {
942        let diff = ts[i] - ewma[i];
943        let new_var = alpha_f * diff * diff + one_minus_alpha * ewmv[i - 1];
944        ewmv.push(new_var);
945    }
946
947    Ok(ewmv)
948}
949
950/// Calculate Bollinger Bands
951#[allow(dead_code)]
952fn calculate_bollinger_bands<F>(
953    ts: &Array1<F>,
954    config: &WindowConfig,
955) -> Result<BollingerBandFeatures<F>>
956where
957    F: Float + FromPrimitive + Debug + Clone,
958{
959    let window_size = config.bollinger_window;
960    let multiplier = F::from(config.bollinger_multiplier).unwrap();
961    let n = ts.len();
962
963    if n < window_size {
964        return Ok(BollingerBandFeatures::default());
965    }
966
967    let mut upper_band = Vec::new();
968    let mut lower_band = Vec::new();
969    let mut band_width = Vec::new();
970
971    // Calculate rolling mean and std for Bollinger Bands
972    for i in 0..=(n - window_size) {
973        let window = ts.slice(s![i..i + window_size]);
974        let mean = window.sum() / F::from(window_size).unwrap();
975        let variance =
976            window.mapv(|x| (x - mean) * (x - mean)).sum() / F::from(window_size).unwrap();
977        let std = variance.sqrt();
978
979        let upper = mean + multiplier * std;
980        let lower = mean - multiplier * std;
981        let width = upper - lower;
982
983        upper_band.push(upper);
984        lower_band.push(lower);
985        band_width.push(width);
986    }
987
988    // Calculate additional Bollinger Band features
989    let mut above_upper = 0;
990    let mut below_lower = 0;
991
992    for (i, &value) in ts.iter().enumerate() {
993        if i < upper_band.len() {
994            if value > upper_band[i] {
995                above_upper += 1;
996            }
997            if value < lower_band[i] {
998                below_lower += 1;
999            }
1000        }
1001    }
1002
1003    let percent_above_upper = F::from(above_upper).unwrap() / F::from(upper_band.len()).unwrap();
1004    let percent_below_lower = F::from(below_lower).unwrap() / F::from(lower_band.len()).unwrap();
1005
1006    let mean_band_width = if !band_width.is_empty() {
1007        band_width.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(band_width.len()).unwrap()
1008    } else {
1009        F::zero()
1010    };
1011
1012    // Count squeeze periods (when band width is unusually low)
1013    let min_width = band_width.iter().fold(F::infinity(), |a, &b| a.min(b));
1014    let squeeze_threshold = min_width * F::from(1.2).unwrap();
1015    let squeeze_periods = band_width
1016        .iter()
1017        .filter(|&&w| w <= squeeze_threshold)
1018        .count();
1019
1020    Ok(BollingerBandFeatures {
1021        upper_band,
1022        lower_band,
1023        band_width,
1024        percent_above_upper,
1025        percent_below_lower,
1026        mean_band_width,
1027        squeeze_periods,
1028    })
1029}
1030
1031/// Calculate MACD features
1032#[allow(dead_code)]
1033fn calculate_macd_features<F>(ts: &Array1<F>, config: &WindowConfig) -> Result<MACDFeatures<F>>
1034where
1035    F: Float + FromPrimitive + Debug + Clone,
1036{
1037    let fast_period = config.macd_fast_period;
1038    let slow_period = config.macd_slow_period;
1039    let signal_period = config.macd_signal_period;
1040
1041    // Calculate EMAs
1042    let fast_alpha = 2.0 / (fast_period as f64 + 1.0);
1043    let slow_alpha = 2.0 / (slow_period as f64 + 1.0);
1044    let signal_alpha = 2.0 / (signal_period as f64 + 1.0);
1045
1046    let fast_ema = calculate_ewma(ts, fast_alpha)?;
1047    let slow_ema = calculate_ewma(ts, slow_alpha)?;
1048
1049    // Calculate MACD line
1050    let macd_line: Vec<F> = fast_ema
1051        .iter()
1052        .zip(slow_ema.iter())
1053        .map(|(&fast, &slow)| fast - slow)
1054        .collect();
1055
1056    // Calculate signal line (EMA of MACD)
1057    let macd_array = Array1::from_vec(macd_line.clone());
1058    let signal_line = calculate_ewma(&macd_array, signal_alpha)?;
1059
1060    // Calculate histogram
1061    let histogram: Vec<F> = macd_line
1062        .iter()
1063        .zip(signal_line.iter())
1064        .map(|(&macd, &signal)| macd - signal)
1065        .collect();
1066
1067    // Count crossovers
1068    let mut bullish_crossovers = 0;
1069    let mut bearish_crossovers = 0;
1070
1071    for i in 1..histogram.len() {
1072        if histogram[i - 1] <= F::zero() && histogram[i] > F::zero() {
1073            bullish_crossovers += 1;
1074        } else if histogram[i - 1] >= F::zero() && histogram[i] < F::zero() {
1075            bearish_crossovers += 1;
1076        }
1077    }
1078
1079    let mean_histogram = if !histogram.is_empty() {
1080        histogram.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(histogram.len()).unwrap()
1081    } else {
1082        F::zero()
1083    };
1084
1085    // Calculate divergence measure (simplified)
1086    let divergence_measure = histogram.iter().fold(F::zero(), |acc, &x| acc + x.abs())
1087        / F::from(histogram.len().max(1)).unwrap();
1088
1089    Ok(MACDFeatures {
1090        macd_line,
1091        signal_line,
1092        histogram,
1093        bullish_crossovers,
1094        bearish_crossovers,
1095        mean_histogram,
1096        divergence_measure,
1097    })
1098}
1099
1100/// Calculate Relative Strength Index (RSI)
1101#[allow(dead_code)]
1102fn calculate_rsi<F>(ts: &Array1<F>, period: usize) -> Result<Vec<F>>
1103where
1104    F: Float + FromPrimitive + Debug,
1105{
1106    let n = ts.len();
1107    if n < period + 1 {
1108        return Ok(Vec::new());
1109    }
1110
1111    let mut rsi_values = Vec::new();
1112
1113    // Calculate price changes
1114    let mut gains = Vec::new();
1115    let mut losses = Vec::new();
1116
1117    for i in 1..n {
1118        let change = ts[i] - ts[i - 1];
1119        if change > F::zero() {
1120            gains.push(change);
1121            losses.push(F::zero());
1122        } else {
1123            gains.push(F::zero());
1124            losses.push(-change);
1125        }
1126    }
1127
1128    // Calculate RSI for each window
1129    for i in 0..=(gains.len() - period) {
1130        let avg_gain = gains[i..i + period]
1131            .iter()
1132            .fold(F::zero(), |acc, &x| acc + x)
1133            / F::from(period).unwrap();
1134        let avg_loss = losses[i..i + period]
1135            .iter()
1136            .fold(F::zero(), |acc, &x| acc + x)
1137            / F::from(period).unwrap();
1138
1139        let rs = if avg_loss != F::zero() {
1140            avg_gain / avg_loss
1141        } else {
1142            F::from(100.0).unwrap()
1143        };
1144
1145        let rsi = F::from(100.0).unwrap() - (F::from(100.0).unwrap() / (F::one() + rs));
1146        rsi_values.push(rsi);
1147    }
1148
1149    Ok(rsi_values)
1150}
1151
1152/// Calculate normalized features
1153#[allow(dead_code)]
1154fn calculate_normalized_features<F>(
1155    ts: &Array1<F>,
1156    config: &WindowConfig,
1157) -> Result<NormalizedRollingFeatures<F>>
1158where
1159    F: Float + FromPrimitive + Debug + Clone,
1160{
1161    let window_size = config.normalization_window;
1162    let n = ts.len();
1163
1164    if n < window_size {
1165        return Ok(NormalizedRollingFeatures::default());
1166    }
1167
1168    let mut normalized_means = Vec::new();
1169    let mut normalized_stds = Vec::new();
1170    let mut percentile_ranks = Vec::new();
1171    let mut outlier_scores = Vec::new();
1172    let mut outlier_count = 0;
1173
1174    for i in 0..=(n - window_size) {
1175        let window = ts.slice(s![i..i + window_size]);
1176        let mean = window.sum() / F::from(window_size).unwrap();
1177        let variance =
1178            window.mapv(|x| (x - mean) * (x - mean)).sum() / F::from(window_size).unwrap();
1179        let std = variance.sqrt();
1180
1181        // Z-score normalization
1182        let current_value = ts[i + window_size - 1];
1183        let z_score_mean = if std != F::zero() {
1184            (current_value - mean) / std
1185        } else {
1186            F::zero()
1187        };
1188        normalized_means.push(z_score_mean);
1189
1190        let z_score_std = if mean != F::zero() {
1191            std / mean.abs()
1192        } else {
1193            F::zero()
1194        };
1195        normalized_stds.push(z_score_std);
1196
1197        // Percentile rank
1198        let rank = window.iter().filter(|&&x| x <= current_value).count();
1199        let percentile = F::from(rank).unwrap() / F::from(window_size).unwrap();
1200        percentile_ranks.push(percentile);
1201
1202        // Outlier detection (values beyond 2 standard deviations)
1203        let outlier_score = z_score_mean.abs();
1204        outlier_scores.push(outlier_score);
1205
1206        if outlier_score > F::from(2.0).unwrap() {
1207            outlier_count += 1;
1208        }
1209    }
1210
1211    Ok(NormalizedRollingFeatures {
1212        normalized_means,
1213        normalized_stds,
1214        percentile_ranks,
1215        outlier_scores,
1216        outlier_count,
1217    })
1218}