1use 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#[derive(Debug, Clone)]
20pub struct WindowBasedFeatures<F> {
21 pub small_window_features: WindowFeatures<F>,
23 pub medium_window_features: WindowFeatures<F>,
25 pub large_window_features: WindowFeatures<F>,
27 pub multi_scale_variance: Vec<F>,
29 pub multi_scale_trends: Vec<F>,
31 pub cross_window_correlations: CrossWindowFeatures<F>,
33 pub change_detection_features: ChangeDetectionFeatures<F>,
35 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#[derive(Debug, Clone)]
59pub struct WindowFeatures<F> {
60 pub window_size: usize,
62 pub rolling_means: Vec<F>,
64 pub rolling_stds: Vec<F>,
66 pub rolling_mins: Vec<F>,
68 pub rolling_maxs: Vec<F>,
70 pub rolling_medians: Vec<F>,
72 pub rolling_skewness: Vec<F>,
74 pub rolling_kurtosis: Vec<F>,
76 pub rolling_quantiles: Vec<(F, F)>,
78 pub rolling_ranges: Vec<F>,
80 pub rolling_cv: Vec<F>,
82 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#[derive(Debug, Clone)]
110pub struct WindowSummaryStats<F> {
111 pub mean_of_means: F,
113 pub std_of_means: F,
115 pub mean_of_stds: F,
117 pub std_of_stds: F,
119 pub max_range: F,
121 pub min_range: F,
123 pub mean_range: F,
125 pub trend_in_means: F,
127 pub trend_in_stds: F,
129 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#[derive(Debug, Clone)]
155pub struct CrossWindowFeatures<F> {
156 pub small_medium_correlation: F,
158 pub medium_large_correlation: F,
160 pub small_large_correlation: F,
162 pub scale_phase_differences: Vec<F>,
164 pub cross_scale_consistency: F,
166 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#[derive(Debug, Clone)]
188pub struct ChangeDetectionFeatures<F> {
189 pub mean_change_points: usize,
191 pub variance_change_points: usize,
193 pub cusum_mean_changes: Vec<F>,
195 pub cusum_variance_changes: Vec<F>,
197 pub max_cusum_mean: F,
199 pub max_cusum_variance: F,
201 pub stability_measure: F,
203 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#[derive(Debug, Clone)]
227pub struct RollingStatistics<F> {
228 pub ewma: Vec<F>,
230 pub ewmv: Vec<F>,
232 pub bollinger_bands: BollingerBandFeatures<F>,
234 pub macd_features: MACDFeatures<F>,
236 pub rsi_values: Vec<F>,
238 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#[derive(Debug, Clone)]
260pub struct BollingerBandFeatures<F> {
261 pub upper_band: Vec<F>,
263 pub lower_band: Vec<F>,
265 pub band_width: Vec<F>,
267 pub percent_above_upper: F,
269 pub percent_below_lower: F,
271 pub mean_band_width: F,
273 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#[derive(Debug, Clone)]
296pub struct MACDFeatures<F> {
297 pub macd_line: Vec<F>,
299 pub signal_line: Vec<F>,
301 pub histogram: Vec<F>,
303 pub bullish_crossovers: usize,
305 pub bearish_crossovers: usize,
307 pub mean_histogram: F,
309 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#[derive(Debug, Clone)]
332pub struct NormalizedRollingFeatures<F> {
333 pub normalized_means: Vec<F>,
335 pub normalized_stds: Vec<F>,
337 pub percentile_ranks: Vec<F>,
339 pub outlier_scores: Vec<F>,
341 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#[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 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 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 let multi_scale_variance =
404 calculate_multi_scale_variance(ts, &[small_size, medium_size, large_size])?;
405
406 let multi_scale_trends =
408 calculate_multi_scale_trends(ts, &[small_size, medium_size, large_size])?;
409
410 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 let change_features = if config.detect_changes {
419 calculate_change_detection_features(ts, &medium_features, config)?
420 } else {
421 ChangeDetectionFeatures::default()
422 };
423
424 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#[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 for i in 0..num_windows {
468 let window = _ts.slice(s![i..i + windowsize]);
469
470 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 let cv = if mean != F::zero() {
486 std / mean.abs()
487 } else {
488 F::zero()
489 };
490 rolling_cv.push(cv);
491
492 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 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 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#[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 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 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 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 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 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#[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#[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#[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 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 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 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 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 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#[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 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(); }
802 }
803
804 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(); }
825 }
826
827 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 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 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#[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 let ewma = calculate_ewma(ts, config.ewma_alpha)?;
875
876 let ewmv = calculate_ewmv(ts, &ewma, config.ewma_alpha)?;
878
879 let bollinger_bands = calculate_bollinger_bands(ts, config)?;
881
882 let macd_features = calculate_macd_features(ts, config)?;
884
885 let rsi_values = calculate_rsi(ts, config.rsi_period)?;
887
888 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#[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#[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#[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 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 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 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#[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 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 let macd_line: Vec<F> = fast_ema
1051 .iter()
1052 .zip(slow_ema.iter())
1053 .map(|(&fast, &slow)| fast - slow)
1054 .collect();
1055
1056 let macd_array = Array1::from_vec(macd_line.clone());
1058 let signal_line = calculate_ewma(&macd_array, signal_alpha)?;
1059
1060 let histogram: Vec<F> = macd_line
1062 .iter()
1063 .zip(signal_line.iter())
1064 .map(|(&macd, &signal)| macd - signal)
1065 .collect();
1066
1067 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 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#[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 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 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#[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 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 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 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}