scirs2_series/features/
turning_points.rs

1//! Turning points and trend analysis features for time series
2//!
3//! This module provides comprehensive turning point detection and trend analysis
4//! including local extrema detection, directional change analysis, momentum features,
5//! trend reversals, pattern detection, and multi-scale analysis.
6
7use scirs2_core::ndarray::{s, Array1};
8use scirs2_core::numeric::{Float, FromPrimitive};
9use std::fmt::Debug;
10
11use super::config::TurningPointsConfig;
12use super::utils::detect_turning_points;
13use crate::error::{Result, TimeSeriesError};
14
15/// Comprehensive turning points analysis features
16#[derive(Debug, Clone)]
17pub struct TurningPointsFeatures<F> {
18    // Basic turning point counts
19    /// Total number of turning points in the series
20    pub total_turning_points: usize,
21    /// Number of local minima (valleys)
22    pub local_minima_count: usize,
23    /// Number of local maxima (peaks)
24    pub local_maxima_count: usize,
25    /// Ratio of peaks to valleys
26    pub peak_valley_ratio: F,
27    /// Average distance between consecutive turning points
28    pub average_turning_point_distance: F,
29
30    // Directional change analysis
31    /// Number of upward directional changes
32    pub upward_changes: usize,
33    /// Number of downward directional changes  
34    pub downward_changes: usize,
35    /// Ratio of upward to downward changes
36    pub directional_change_ratio: F,
37    /// Average magnitude of upward changes
38    pub average_upward_magnitude: F,
39    /// Average magnitude of downward changes
40    pub average_downward_magnitude: F,
41    /// Standard deviation of directional change magnitudes
42    pub directional_change_std: F,
43
44    // Momentum and persistence features
45    /// Longest consecutive upward sequence length
46    pub longest_upward_sequence: usize,
47    /// Longest consecutive downward sequence length
48    pub longest_downward_sequence: usize,
49    /// Average length of upward sequences
50    pub average_upward_sequence_length: F,
51    /// Average length of downward sequences
52    pub average_downward_sequence_length: F,
53    /// Momentum persistence ratio (long sequences / total sequences)
54    pub momentum_persistence_ratio: F,
55
56    // Local extrema characteristics
57    /// Average amplitude of local maxima
58    pub average_peak_amplitude: F,
59    /// Average amplitude of local minima
60    pub average_valley_amplitude: F,
61    /// Standard deviation of peak amplitudes
62    pub peak_amplitude_std: F,
63    /// Standard deviation of valley amplitudes
64    pub valley_amplitude_std: F,
65    /// Peak-to-valley amplitude ratio
66    pub peak_valley_amplitude_ratio: F,
67    /// Asymmetry in peak and valley distributions
68    pub extrema_asymmetry: F,
69
70    // Trend reversal features
71    /// Number of major trend reversals (large directional changes)
72    pub major_trend_reversals: usize,
73    /// Number of minor trend reversals (small directional changes)
74    pub minor_trend_reversals: usize,
75    /// Average magnitude of major reversals
76    pub average_major_reversal_magnitude: F,
77    /// Average magnitude of minor reversals
78    pub average_minor_reversal_magnitude: F,
79    /// Trend reversal frequency (reversals per unit time)
80    pub trend_reversal_frequency: F,
81    /// Reversal strength index (cumulative reversal magnitude)
82    pub reversal_strength_index: F,
83
84    // Temporal pattern features
85    /// Regularity of turning point intervals (coefficient of variation)
86    pub turning_point_regularity: F,
87    /// Clustering tendency of turning points
88    pub turning_point_clustering: F,
89    /// Periodicity strength of turning points
90    pub turning_point_periodicity: F,
91    /// Auto-correlation of turning point intervals
92    pub turning_point_autocorrelation: F,
93
94    // Volatility and stability measures
95    /// Volatility around turning points (average local variance)
96    pub turning_point_volatility: F,
97    /// Stability index (inverse of turning point frequency)
98    pub stability_index: F,
99    /// Noise-to-signal ratio around turning points
100    pub noise_signal_ratio: F,
101    /// Trend consistency measure
102    pub trend_consistency: F,
103
104    // Advanced pattern features
105    /// Number of double peaks (M patterns)
106    pub double_peak_count: usize,
107    /// Number of double bottoms (W patterns)
108    pub double_bottom_count: usize,
109    /// Head and shoulders pattern count
110    pub head_shoulders_count: usize,
111    /// Triangular pattern count (converging peaks/valleys)
112    pub triangular_pattern_count: usize,
113
114    // Relative position features
115    /// Proportion of turning points in upper half of range
116    pub upper_half_turning_points: F,
117    /// Proportion of turning points in lower half of range
118    pub lower_half_turning_points: F,
119    /// Skewness of turning point vertical positions
120    pub turning_point_position_skewness: F,
121    /// Kurtosis of turning point vertical positions
122    pub turning_point_position_kurtosis: F,
123
124    // Multi-scale turning point features
125    /// Turning points at different smoothing scales
126    pub multiscale_turning_points: Vec<usize>,
127    /// Scale-dependent turning point ratio
128    pub scale_turning_point_ratio: F,
129    /// Cross-scale turning point consistency
130    pub cross_scale_consistency: F,
131    /// Hierarchical turning point structure
132    pub hierarchical_structure_index: F,
133}
134
135impl<F> Default for TurningPointsFeatures<F>
136where
137    F: Float + FromPrimitive,
138{
139    fn default() -> Self {
140        Self {
141            // Basic turning point counts
142            total_turning_points: 0,
143            local_minima_count: 0,
144            local_maxima_count: 0,
145            peak_valley_ratio: F::one(),
146            average_turning_point_distance: F::zero(),
147
148            // Directional change analysis
149            upward_changes: 0,
150            downward_changes: 0,
151            directional_change_ratio: F::one(),
152            average_upward_magnitude: F::zero(),
153            average_downward_magnitude: F::zero(),
154            directional_change_std: F::zero(),
155
156            // Momentum and persistence features
157            longest_upward_sequence: 0,
158            longest_downward_sequence: 0,
159            average_upward_sequence_length: F::zero(),
160            average_downward_sequence_length: F::zero(),
161            momentum_persistence_ratio: F::zero(),
162
163            // Local extrema characteristics
164            average_peak_amplitude: F::zero(),
165            average_valley_amplitude: F::zero(),
166            peak_amplitude_std: F::zero(),
167            valley_amplitude_std: F::zero(),
168            peak_valley_amplitude_ratio: F::one(),
169            extrema_asymmetry: F::zero(),
170
171            // Trend reversal features
172            major_trend_reversals: 0,
173            minor_trend_reversals: 0,
174            average_major_reversal_magnitude: F::zero(),
175            average_minor_reversal_magnitude: F::zero(),
176            trend_reversal_frequency: F::zero(),
177            reversal_strength_index: F::zero(),
178
179            // Temporal pattern features
180            turning_point_regularity: F::zero(),
181            turning_point_clustering: F::zero(),
182            turning_point_periodicity: F::zero(),
183            turning_point_autocorrelation: F::zero(),
184
185            // Volatility and stability measures
186            turning_point_volatility: F::zero(),
187            stability_index: F::zero(),
188            noise_signal_ratio: F::zero(),
189            trend_consistency: F::zero(),
190
191            // Advanced pattern features
192            double_peak_count: 0,
193            double_bottom_count: 0,
194            head_shoulders_count: 0,
195            triangular_pattern_count: 0,
196
197            // Relative position features
198            upper_half_turning_points: F::from(0.5).unwrap(),
199            lower_half_turning_points: F::from(0.5).unwrap(),
200            turning_point_position_skewness: F::zero(),
201            turning_point_position_kurtosis: F::zero(),
202
203            // Multi-scale turning point features
204            multiscale_turning_points: Vec::new(),
205            scale_turning_point_ratio: F::zero(),
206            cross_scale_consistency: F::zero(),
207            hierarchical_structure_index: F::zero(),
208        }
209    }
210}
211
212// =============================================================================
213// Helper Structures for Intermediate Calculations
214// =============================================================================
215
216/// Helper struct for directional change statistics
217#[derive(Debug, Clone)]
218struct DirectionalChangeStats<F> {
219    directional_change_ratio: F,
220    average_upward_magnitude: F,
221    average_downward_magnitude: F,
222    directional_change_std: F,
223}
224
225/// Helper struct for momentum and persistence features
226#[derive(Debug, Clone)]
227struct MomentumFeatures<F> {
228    longest_upward_sequence: usize,
229    longest_downward_sequence: usize,
230    average_upward_sequence_length: F,
231    average_downward_sequence_length: F,
232    momentum_persistence_ratio: F,
233}
234
235/// Helper struct for local extrema characteristics
236#[derive(Debug, Clone)]
237struct ExtremaFeatures<F> {
238    average_peak_amplitude: F,
239    average_valley_amplitude: F,
240    peak_amplitude_std: F,
241    valley_amplitude_std: F,
242    peak_valley_amplitude_ratio: F,
243    extrema_asymmetry: F,
244}
245
246/// Helper struct for trend reversal features
247#[derive(Debug, Clone)]
248struct TrendReversalFeatures<F> {
249    major_trend_reversals: usize,
250    minor_trend_reversals: usize,
251    average_major_reversal_magnitude: F,
252    average_minor_reversal_magnitude: F,
253    trend_reversal_frequency: F,
254    reversal_strength_index: F,
255}
256
257/// Helper struct for temporal pattern features of turning points
258#[derive(Debug, Clone)]
259struct TurningPointTemporalFeatures<F> {
260    turning_point_regularity: F,
261    turning_point_clustering: F,
262    turning_point_periodicity: F,
263    turning_point_autocorrelation: F,
264}
265
266impl<F> Default for TurningPointTemporalFeatures<F>
267where
268    F: Float + FromPrimitive,
269{
270    fn default() -> Self {
271        Self {
272            turning_point_regularity: F::zero(),
273            turning_point_clustering: F::zero(),
274            turning_point_periodicity: F::zero(),
275            turning_point_autocorrelation: F::zero(),
276        }
277    }
278}
279
280/// Helper struct for stability and volatility features
281#[derive(Debug, Clone)]
282struct StabilityFeatures<F> {
283    turning_point_volatility: F,
284    stability_index: F,
285    noise_signal_ratio: F,
286    trend_consistency: F,
287}
288
289/// Helper struct for advanced pattern features
290#[derive(Debug, Clone, Default)]
291struct AdvancedPatternFeatures {
292    double_peak_count: usize,
293    double_bottom_count: usize,
294    head_shoulders_count: usize,
295    triangular_pattern_count: usize,
296}
297
298/// Helper struct for position features
299#[derive(Debug, Clone)]
300struct PositionFeatures<F> {
301    upper_half_turning_points: F,
302    lower_half_turning_points: F,
303    turning_point_position_skewness: F,
304    turning_point_position_kurtosis: F,
305}
306
307/// Helper struct for multi-scale features
308#[derive(Debug, Clone)]
309struct MultiscaleTurningPointFeatures<F> {
310    multiscale_turning_points: Vec<usize>,
311    scale_turning_point_ratio: F,
312    cross_scale_consistency: F,
313    hierarchical_structure_index: F,
314}
315
316impl<F> Default for MultiscaleTurningPointFeatures<F>
317where
318    F: Float + FromPrimitive,
319{
320    fn default() -> Self {
321        Self {
322            multiscale_turning_points: Vec::new(),
323            scale_turning_point_ratio: F::zero(),
324            cross_scale_consistency: F::zero(),
325            hierarchical_structure_index: F::zero(),
326        }
327    }
328}
329
330// =============================================================================
331// Main Calculation Function
332// =============================================================================
333
334/// Calculate comprehensive turning points features
335///
336/// This function performs extensive turning point analysis including basic counts,
337/// directional changes, momentum patterns, extrema characteristics, trend reversals,
338/// temporal patterns, stability measures, advanced pattern detection, position analysis,
339/// and multi-scale analysis.
340///
341/// # Arguments
342///
343/// * `ts` - Input time series data
344/// * `config` - Turning points analysis configuration
345///
346/// # Returns
347///
348/// Comprehensive turning points features structure
349#[allow(dead_code)]
350pub fn calculate_turning_points_features<F>(
351    ts: &Array1<F>,
352    config: &TurningPointsConfig,
353) -> Result<TurningPointsFeatures<F>>
354where
355    F: Float + FromPrimitive + Debug + Clone + std::iter::Sum + scirs2_core::ndarray::ScalarOperand,
356    for<'a> F: std::iter::Sum<&'a F>,
357{
358    let n = ts.len();
359    if n < config.extrema_window_size * 2 {
360        return Err(TimeSeriesError::InsufficientData {
361            message: "Insufficient data for turning points analysis".to_string(),
362            required: config.extrema_window_size * 2,
363            actual: n,
364        });
365    }
366
367    // Detect basic turning points and local extrema
368    let (turning_points, local_maxima, local_minima) = detect_turning_points(ts, config)?;
369
370    // Calculate basic counts and ratios
371    let total_turning_points = turning_points.len();
372    let local_maxima_count = local_maxima.len();
373    let local_minima_count = local_minima.len();
374    let peak_valley_ratio = if local_minima_count > 0 {
375        F::from(local_maxima_count).unwrap() / F::from(local_minima_count).unwrap()
376    } else {
377        F::zero()
378    };
379
380    // Calculate average distance between turning points
381    let average_turning_point_distance = if total_turning_points > 1 {
382        let total_distance: usize = turning_points.windows(2).map(|w| w[1] - w[0]).sum();
383        F::from(total_distance).unwrap() / F::from(total_turning_points - 1).unwrap()
384    } else {
385        F::zero()
386    };
387
388    // Analyze directional changes
389    let (upward_changes, downward_changes, directional_stats) =
390        analyze_directional_changes(ts, &turning_points, config)?;
391
392    // Analyze momentum and persistence
393    let momentum_features = analyze_momentum_persistence(ts, config)?;
394
395    // Characterize local extrema
396    let extrema_features = characterize_local_extrema(ts, &local_maxima, &local_minima)?;
397
398    // Detect trend reversals
399    let reversal_features = detect_trend_reversals(ts, &turning_points, config)?;
400
401    // Analyze temporal patterns
402    let temporal_features = if config.calculate_temporal_patterns {
403        analyze_temporal_patterns(&turning_points, config)?
404    } else {
405        TurningPointTemporalFeatures::default()
406    };
407
408    // Calculate volatility and stability measures
409    let stability_features = calculate_stability_measures(ts, &turning_points)?;
410
411    // Detect advanced patterns
412    let pattern_features = if config.detect_advanced_patterns {
413        detect_advanced_patterns(ts, &local_maxima, &local_minima, config)?
414    } else {
415        AdvancedPatternFeatures::default()
416    };
417
418    // Analyze relative positions
419    let position_features = analyze_turning_point_positions(ts, &turning_points)?;
420
421    // Multi-scale analysis
422    let multiscale_features = if config.multiscale_analysis {
423        analyze_multiscale_turning_points(ts, config)?
424    } else {
425        MultiscaleTurningPointFeatures::default()
426    };
427
428    Ok(TurningPointsFeatures {
429        // Basic turning point counts
430        total_turning_points,
431        local_minima_count,
432        local_maxima_count,
433        peak_valley_ratio,
434        average_turning_point_distance,
435
436        // Directional change analysis
437        upward_changes,
438        downward_changes,
439        directional_change_ratio: directional_stats.directional_change_ratio,
440        average_upward_magnitude: directional_stats.average_upward_magnitude,
441        average_downward_magnitude: directional_stats.average_downward_magnitude,
442        directional_change_std: directional_stats.directional_change_std,
443
444        // Momentum and persistence features
445        longest_upward_sequence: momentum_features.longest_upward_sequence,
446        longest_downward_sequence: momentum_features.longest_downward_sequence,
447        average_upward_sequence_length: momentum_features.average_upward_sequence_length,
448        average_downward_sequence_length: momentum_features.average_downward_sequence_length,
449        momentum_persistence_ratio: momentum_features.momentum_persistence_ratio,
450
451        // Local extrema characteristics
452        average_peak_amplitude: extrema_features.average_peak_amplitude,
453        average_valley_amplitude: extrema_features.average_valley_amplitude,
454        peak_amplitude_std: extrema_features.peak_amplitude_std,
455        valley_amplitude_std: extrema_features.valley_amplitude_std,
456        peak_valley_amplitude_ratio: extrema_features.peak_valley_amplitude_ratio,
457        extrema_asymmetry: extrema_features.extrema_asymmetry,
458
459        // Trend reversal features
460        major_trend_reversals: reversal_features.major_trend_reversals,
461        minor_trend_reversals: reversal_features.minor_trend_reversals,
462        average_major_reversal_magnitude: reversal_features.average_major_reversal_magnitude,
463        average_minor_reversal_magnitude: reversal_features.average_minor_reversal_magnitude,
464        trend_reversal_frequency: reversal_features.trend_reversal_frequency,
465        reversal_strength_index: reversal_features.reversal_strength_index,
466
467        // Temporal pattern features
468        turning_point_regularity: temporal_features.turning_point_regularity,
469        turning_point_clustering: temporal_features.turning_point_clustering,
470        turning_point_periodicity: temporal_features.turning_point_periodicity,
471        turning_point_autocorrelation: temporal_features.turning_point_autocorrelation,
472
473        // Volatility and stability measures
474        turning_point_volatility: stability_features.turning_point_volatility,
475        stability_index: stability_features.stability_index,
476        noise_signal_ratio: stability_features.noise_signal_ratio,
477        trend_consistency: stability_features.trend_consistency,
478
479        // Advanced pattern features
480        double_peak_count: pattern_features.double_peak_count,
481        double_bottom_count: pattern_features.double_bottom_count,
482        head_shoulders_count: pattern_features.head_shoulders_count,
483        triangular_pattern_count: pattern_features.triangular_pattern_count,
484
485        // Relative position features
486        upper_half_turning_points: position_features.upper_half_turning_points,
487        lower_half_turning_points: position_features.lower_half_turning_points,
488        turning_point_position_skewness: position_features.turning_point_position_skewness,
489        turning_point_position_kurtosis: position_features.turning_point_position_kurtosis,
490
491        // Multi-scale turning point features
492        multiscale_turning_points: multiscale_features.multiscale_turning_points,
493        scale_turning_point_ratio: multiscale_features.scale_turning_point_ratio,
494        cross_scale_consistency: multiscale_features.cross_scale_consistency,
495        hierarchical_structure_index: multiscale_features.hierarchical_structure_index,
496    })
497}
498
499// =============================================================================
500// Analysis Functions
501// =============================================================================
502
503/// Analyze directional changes in the time series
504#[allow(dead_code)]
505fn analyze_directional_changes<F>(
506    ts: &Array1<F>,
507    turning_points: &[usize],
508    _config: &TurningPointsConfig,
509) -> Result<(usize, usize, DirectionalChangeStats<F>)>
510where
511    F: Float + FromPrimitive + Debug + Clone,
512{
513    let mut upward_changes = 0;
514    let mut downward_changes = 0;
515    let mut upward_magnitudes = Vec::new();
516    let mut downward_magnitudes = Vec::new();
517
518    // Analyze changes between consecutive turning _points
519    for window in turning_points.windows(2) {
520        let start_idx = window[0];
521        let end_idx = window[1];
522
523        if start_idx < ts.len() && end_idx < ts.len() {
524            let change = ts[end_idx] - ts[start_idx];
525            let magnitude = change.abs();
526
527            if change > F::zero() {
528                upward_changes += 1;
529                upward_magnitudes.push(magnitude);
530            } else if change < F::zero() {
531                downward_changes += 1;
532                downward_magnitudes.push(magnitude);
533            }
534        }
535    }
536
537    // Calculate directional statistics
538    let directional_change_ratio = if downward_changes > 0 {
539        F::from(upward_changes).unwrap() / F::from(downward_changes).unwrap()
540    } else {
541        F::from(upward_changes).unwrap()
542    };
543
544    let average_upward_magnitude = if !upward_magnitudes.is_empty() {
545        upward_magnitudes.iter().fold(F::zero(), |acc, &x| acc + x)
546            / F::from(upward_magnitudes.len()).unwrap()
547    } else {
548        F::zero()
549    };
550
551    let average_downward_magnitude = if !downward_magnitudes.is_empty() {
552        downward_magnitudes
553            .iter()
554            .fold(F::zero(), |acc, &x| acc + x)
555            / F::from(downward_magnitudes.len()).unwrap()
556    } else {
557        F::zero()
558    };
559
560    // Calculate standard deviation of all directional changes
561    let all_magnitudes: Vec<F> = upward_magnitudes
562        .into_iter()
563        .chain(downward_magnitudes)
564        .collect();
565
566    let directional_change_std = if all_magnitudes.len() > 1 {
567        let mean = all_magnitudes.iter().fold(F::zero(), |acc, &x| acc + x)
568            / F::from(all_magnitudes.len()).unwrap();
569        let variance = all_magnitudes
570            .iter()
571            .fold(F::zero(), |acc, &x| acc + (x - mean) * (x - mean))
572            / F::from(all_magnitudes.len() - 1).unwrap();
573        variance.sqrt()
574    } else {
575        F::zero()
576    };
577
578    let stats = DirectionalChangeStats {
579        directional_change_ratio,
580        average_upward_magnitude,
581        average_downward_magnitude,
582        directional_change_std,
583    };
584
585    Ok((upward_changes, downward_changes, stats))
586}
587
588/// Analyze momentum and persistence patterns
589#[allow(dead_code)]
590fn analyze_momentum_persistence<F>(
591    ts: &Array1<F>,
592    config: &TurningPointsConfig,
593) -> Result<MomentumFeatures<F>>
594where
595    F: Float + FromPrimitive + Debug,
596{
597    let n = ts.len();
598    let mut current_up_sequence = 0;
599    let mut current_down_sequence = 0;
600    let mut longest_upward_sequence = 0;
601    let mut longest_downward_sequence = 0;
602    let mut upward_sequences = Vec::new();
603    let mut downward_sequences = Vec::new();
604
605    // Analyze consecutive directional movements
606    for i in 1..n {
607        let change = ts[i] - ts[i - 1];
608
609        if change > F::zero() {
610            // Upward movement
611            current_up_sequence += 1;
612            if current_down_sequence >= config.min_sequence_length {
613                downward_sequences.push(current_down_sequence);
614            }
615            current_down_sequence = 0;
616        } else if change < F::zero() {
617            // Downward movement
618            current_down_sequence += 1;
619            if current_up_sequence >= config.min_sequence_length {
620                upward_sequences.push(current_up_sequence);
621            }
622            current_up_sequence = 0;
623        }
624
625        longest_upward_sequence = longest_upward_sequence.max(current_up_sequence);
626        longest_downward_sequence = longest_downward_sequence.max(current_down_sequence);
627    }
628
629    // Handle final sequences
630    if current_up_sequence >= config.min_sequence_length {
631        upward_sequences.push(current_up_sequence);
632    }
633    if current_down_sequence >= config.min_sequence_length {
634        downward_sequences.push(current_down_sequence);
635    }
636
637    // Calculate average sequence lengths
638    let average_upward_sequence_length = if !upward_sequences.is_empty() {
639        F::from(upward_sequences.iter().sum::<usize>()).unwrap()
640            / F::from(upward_sequences.len()).unwrap()
641    } else {
642        F::zero()
643    };
644
645    let average_downward_sequence_length = if !downward_sequences.is_empty() {
646        F::from(downward_sequences.iter().sum::<usize>()).unwrap()
647            / F::from(downward_sequences.len()).unwrap()
648    } else {
649        F::zero()
650    };
651
652    // Calculate momentum persistence ratio
653    let long_sequences = upward_sequences
654        .iter()
655        .filter(|&&len| len >= config.min_sequence_length * 2)
656        .count()
657        + downward_sequences
658            .iter()
659            .filter(|&&len| len >= config.min_sequence_length * 2)
660            .count();
661    let total_sequences = upward_sequences.len() + downward_sequences.len();
662
663    let momentum_persistence_ratio = if total_sequences > 0 {
664        F::from(long_sequences).unwrap() / F::from(total_sequences).unwrap()
665    } else {
666        F::zero()
667    };
668
669    Ok(MomentumFeatures {
670        longest_upward_sequence,
671        longest_downward_sequence,
672        average_upward_sequence_length,
673        average_downward_sequence_length,
674        momentum_persistence_ratio,
675    })
676}
677
678/// Characterize local extrema (peaks and valleys)
679#[allow(dead_code)]
680fn characterize_local_extrema<F>(
681    ts: &Array1<F>,
682    local_maxima: &[usize],
683    local_minima: &[usize],
684) -> Result<ExtremaFeatures<F>>
685where
686    F: Float + FromPrimitive + Debug,
687{
688    // Calculate peak amplitudes
689    let peak_amplitudes: Vec<F> = local_maxima
690        .iter()
691        .filter_map(|&idx| if idx < ts.len() { Some(ts[idx]) } else { None })
692        .collect();
693
694    // Calculate valley amplitudes
695    let valley_amplitudes: Vec<F> = local_minima
696        .iter()
697        .filter_map(|&idx| if idx < ts.len() { Some(ts[idx]) } else { None })
698        .collect();
699
700    // Average peak amplitude
701    let average_peak_amplitude = if !peak_amplitudes.is_empty() {
702        peak_amplitudes.iter().fold(F::zero(), |acc, &x| acc + x)
703            / F::from(peak_amplitudes.len()).unwrap()
704    } else {
705        F::zero()
706    };
707
708    // Average valley amplitude
709    let average_valley_amplitude = if !valley_amplitudes.is_empty() {
710        valley_amplitudes.iter().fold(F::zero(), |acc, &x| acc + x)
711            / F::from(valley_amplitudes.len()).unwrap()
712    } else {
713        F::zero()
714    };
715
716    // Peak amplitude standard deviation
717    let peak_amplitude_std = if peak_amplitudes.len() > 1 {
718        let variance = peak_amplitudes.iter().fold(F::zero(), |acc, &x| {
719            acc + (x - average_peak_amplitude) * (x - average_peak_amplitude)
720        }) / F::from(peak_amplitudes.len() - 1).unwrap();
721        variance.sqrt()
722    } else {
723        F::zero()
724    };
725
726    // Valley amplitude standard deviation
727    let valley_amplitude_std = if valley_amplitudes.len() > 1 {
728        let variance = valley_amplitudes.iter().fold(F::zero(), |acc, &x| {
729            acc + (x - average_valley_amplitude) * (x - average_valley_amplitude)
730        }) / F::from(valley_amplitudes.len() - 1).unwrap();
731        variance.sqrt()
732    } else {
733        F::zero()
734    };
735
736    // Peak-to-valley amplitude ratio
737    let peak_valley_amplitude_ratio = if average_valley_amplitude != F::zero() {
738        average_peak_amplitude / average_valley_amplitude
739    } else {
740        F::one()
741    };
742
743    // Extrema asymmetry (skewness of combined peak and valley distributions)
744    let all_extrema: Vec<F> = peak_amplitudes
745        .into_iter()
746        .chain(valley_amplitudes)
747        .collect();
748
749    let extrema_asymmetry = if all_extrema.len() > 2 {
750        let mean = all_extrema.iter().fold(F::zero(), |acc, &x| acc + x)
751            / F::from(all_extrema.len()).unwrap();
752        let variance = all_extrema
753            .iter()
754            .fold(F::zero(), |acc, &x| acc + (x - mean) * (x - mean))
755            / F::from(all_extrema.len()).unwrap();
756
757        if variance > F::zero() {
758            let std_dev = variance.sqrt();
759            let skewness = all_extrema.iter().fold(F::zero(), |acc, &x| {
760                let normalized = (x - mean) / std_dev;
761                acc + normalized * normalized * normalized
762            }) / F::from(all_extrema.len()).unwrap();
763            skewness
764        } else {
765            F::zero()
766        }
767    } else {
768        F::zero()
769    };
770
771    Ok(ExtremaFeatures {
772        average_peak_amplitude,
773        average_valley_amplitude,
774        peak_amplitude_std,
775        valley_amplitude_std,
776        peak_valley_amplitude_ratio,
777        extrema_asymmetry,
778    })
779}
780
781/// Detect trend reversals
782#[allow(dead_code)]
783fn detect_trend_reversals<F>(
784    ts: &Array1<F>,
785    turning_points: &[usize],
786    config: &TurningPointsConfig,
787) -> Result<TrendReversalFeatures<F>>
788where
789    F: Float + FromPrimitive + Debug,
790{
791    let major_threshold = F::from(config.major_reversal_threshold).unwrap();
792    let mut major_reversals = Vec::new();
793    let mut minor_reversals = Vec::new();
794
795    // Calculate data range for relative thresholds
796    let min_val = ts.iter().fold(F::infinity(), |a, &b| a.min(b));
797    let max_val = ts.iter().fold(F::neg_infinity(), |a, &b| a.max(b));
798    let range = max_val - min_val;
799    let major_abs_threshold = major_threshold * range;
800
801    // Analyze changes between turning _points
802    for window in turning_points.windows(2) {
803        let start_idx = window[0];
804        let end_idx = window[1];
805
806        if start_idx < ts.len() && end_idx < ts.len() {
807            let change_magnitude = (ts[end_idx] - ts[start_idx]).abs();
808
809            if change_magnitude >= major_abs_threshold {
810                major_reversals.push(change_magnitude);
811            } else {
812                minor_reversals.push(change_magnitude);
813            }
814        }
815    }
816
817    let major_trend_reversals = major_reversals.len();
818    let minor_trend_reversals = minor_reversals.len();
819
820    let average_major_reversal_magnitude = if !major_reversals.is_empty() {
821        major_reversals.iter().fold(F::zero(), |acc, &x| acc + x)
822            / F::from(major_reversals.len()).unwrap()
823    } else {
824        F::zero()
825    };
826
827    let average_minor_reversal_magnitude = if !minor_reversals.is_empty() {
828        minor_reversals.iter().fold(F::zero(), |acc, &x| acc + x)
829            / F::from(minor_reversals.len()).unwrap()
830    } else {
831        F::zero()
832    };
833
834    let trend_reversal_frequency = F::from(major_trend_reversals + minor_trend_reversals).unwrap()
835        / F::from(ts.len()).unwrap();
836
837    let reversal_strength_index = major_reversals.iter().fold(F::zero(), |acc, &x| acc + x)
838        + minor_reversals.iter().fold(F::zero(), |acc, &x| acc + x);
839
840    Ok(TrendReversalFeatures {
841        major_trend_reversals,
842        minor_trend_reversals,
843        average_major_reversal_magnitude,
844        average_minor_reversal_magnitude,
845        trend_reversal_frequency,
846        reversal_strength_index,
847    })
848}
849
850/// Analyze temporal patterns in turning points
851#[allow(dead_code)]
852fn analyze_temporal_patterns<F>(
853    turning_points: &[usize],
854    config: &TurningPointsConfig,
855) -> Result<TurningPointTemporalFeatures<F>>
856where
857    F: Float + FromPrimitive + Debug + Clone + std::iter::Sum + scirs2_core::ndarray::ScalarOperand,
858{
859    if turning_points.len() < 3 {
860        return Ok(TurningPointTemporalFeatures::default());
861    }
862
863    // Calculate intervals between turning _points
864    let intervals: Vec<F> = turning_points
865        .windows(2)
866        .map(|w| F::from(w[1] - w[0]).unwrap())
867        .collect();
868
869    // Turning point regularity (coefficient of variation of intervals)
870    let mean_interval =
871        intervals.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(intervals.len()).unwrap();
872
873    let interval_variance = if intervals.len() > 1 {
874        intervals.iter().fold(F::zero(), |acc, &x| {
875            acc + (x - mean_interval) * (x - mean_interval)
876        }) / F::from(intervals.len() - 1).unwrap()
877    } else {
878        F::zero()
879    };
880
881    let turning_point_regularity = if mean_interval > F::zero() {
882        interval_variance.sqrt() / mean_interval
883    } else {
884        F::zero()
885    };
886
887    // Turning point clustering (analyze distribution of intervals)
888    let turning_point_clustering = calculate_clustering_coefficient(&intervals)?;
889
890    // Turning point periodicity (simplified autocorrelation)
891    let turning_point_periodicity = calculate_periodicity_strength(&intervals)?;
892
893    // Turning point autocorrelation
894    let max_lag = config.max_autocorr_lag.min(intervals.len() / 2);
895    let turning_point_autocorrelation = if max_lag > 0 {
896        calculate_autocorrelation_at_lag(&intervals, 1)?
897    } else {
898        F::zero()
899    };
900
901    Ok(TurningPointTemporalFeatures {
902        turning_point_regularity,
903        turning_point_clustering,
904        turning_point_periodicity,
905        turning_point_autocorrelation,
906    })
907}
908
909/// Calculate stability and volatility measures
910#[allow(dead_code)]
911fn calculate_stability_measures<F>(
912    ts: &Array1<F>,
913    turning_points: &[usize],
914) -> Result<StabilityFeatures<F>>
915where
916    F: Float + FromPrimitive + Debug,
917{
918    let n = ts.len();
919
920    // Turning point volatility (average local variance around turning points)
921    let mut local_variances = Vec::new();
922    let window_size = 5; // Local window around turning _points
923
924    for &tp_idx in turning_points {
925        let start = tp_idx.saturating_sub(window_size / 2);
926        let end = (tp_idx + window_size / 2 + 1).min(n);
927
928        if end > start + 1 {
929            let local_slice = ts.slice(s![start..end]);
930            let local_mean = local_slice.sum() / F::from(local_slice.len()).unwrap();
931            let local_variance = local_slice
932                .mapv(|x| (x - local_mean) * (x - local_mean))
933                .sum()
934                / F::from(local_slice.len()).unwrap();
935            local_variances.push(local_variance);
936        }
937    }
938
939    let turning_point_volatility = if !local_variances.is_empty() {
940        local_variances.iter().fold(F::zero(), |acc, &x| acc + x)
941            / F::from(local_variances.len()).unwrap()
942    } else {
943        F::zero()
944    };
945
946    // Stability index (inverse of turning point frequency)
947    let turning_point_frequency = F::from(turning_points.len()).unwrap() / F::from(n).unwrap();
948    let stability_index = if turning_point_frequency > F::zero() {
949        F::one() / turning_point_frequency
950    } else {
951        F::from(n).unwrap()
952    };
953
954    // Noise-to-signal ratio
955    let signal_mean = ts.sum() / F::from(n).unwrap();
956    let signal_variance =
957        ts.mapv(|x| (x - signal_mean) * (x - signal_mean)).sum() / F::from(n).unwrap();
958    let noise_signal_ratio = if signal_variance > F::zero() {
959        turning_point_volatility / signal_variance
960    } else {
961        F::zero()
962    };
963
964    // Trend consistency (measure of directional persistence)
965    let mut directional_changes = 0;
966    for i in 1..n {
967        if i >= 2 {
968            let prev_change = ts[i - 1] - ts[i - 2];
969            let curr_change = ts[i] - ts[i - 1];
970            if (prev_change > F::zero()) != (curr_change > F::zero()) {
971                directional_changes += 1;
972            }
973        }
974    }
975
976    let trend_consistency =
977        F::one() - F::from(directional_changes).unwrap() / F::from(n - 2).unwrap();
978
979    Ok(StabilityFeatures {
980        turning_point_volatility,
981        stability_index,
982        noise_signal_ratio,
983        trend_consistency,
984    })
985}
986
987/// Detect advanced patterns (double peaks, head-shoulders, etc.)
988#[allow(dead_code)]
989fn detect_advanced_patterns<F>(
990    ts: &Array1<F>,
991    local_maxima: &[usize],
992    local_minima: &[usize],
993    _config: &TurningPointsConfig,
994) -> Result<AdvancedPatternFeatures>
995where
996    F: Float + FromPrimitive + Debug + PartialOrd,
997{
998    // Detect double peaks (M patterns)
999    let double_peak_count = detect_double_peaks(ts, local_maxima)?;
1000
1001    // Detect double bottoms (W patterns)
1002    let double_bottom_count = detect_double_bottoms(ts, local_minima)?;
1003
1004    // Detect head and shoulders patterns
1005    let head_shoulders_count = detect_head_and_shoulders(ts, local_maxima, local_minima)?;
1006
1007    // Detect triangular patterns
1008    let triangular_pattern_count = detect_triangular_patterns(ts, local_maxima, local_minima)?;
1009
1010    Ok(AdvancedPatternFeatures {
1011        double_peak_count,
1012        double_bottom_count,
1013        head_shoulders_count,
1014        triangular_pattern_count,
1015    })
1016}
1017
1018/// Analyze relative positions of turning points
1019#[allow(dead_code)]
1020fn analyze_turning_point_positions<F>(
1021    ts: &Array1<F>,
1022    turning_points: &[usize],
1023) -> Result<PositionFeatures<F>>
1024where
1025    F: Float + FromPrimitive + Debug,
1026{
1027    if turning_points.is_empty() {
1028        return Ok(PositionFeatures {
1029            upper_half_turning_points: F::from(0.5).unwrap(),
1030            lower_half_turning_points: F::from(0.5).unwrap(),
1031            turning_point_position_skewness: F::zero(),
1032            turning_point_position_kurtosis: F::zero(),
1033        });
1034    }
1035
1036    // Get data range
1037    let min_val = ts.iter().fold(F::infinity(), |a, &b| a.min(b));
1038    let max_val = ts.iter().fold(F::neg_infinity(), |a, &b| a.max(b));
1039    let range = max_val - min_val;
1040    let midpoint = min_val + range / F::from(2.0).unwrap();
1041
1042    // Analyze turning point positions
1043    let tp_values: Vec<F> = turning_points
1044        .iter()
1045        .filter_map(|&idx| if idx < ts.len() { Some(ts[idx]) } else { None })
1046        .collect();
1047
1048    let upper_half_count = tp_values.iter().filter(|&&x| x > midpoint).count();
1049    let total_count = tp_values.len();
1050
1051    let upper_half_turning_points =
1052        F::from(upper_half_count).unwrap() / F::from(total_count).unwrap();
1053    let lower_half_turning_points = F::one() - upper_half_turning_points;
1054
1055    // Calculate skewness and kurtosis of turning point positions
1056    let mean_position =
1057        tp_values.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(total_count).unwrap();
1058
1059    let variance = tp_values.iter().fold(F::zero(), |acc, &x| {
1060        acc + (x - mean_position) * (x - mean_position)
1061    }) / F::from(total_count).unwrap();
1062
1063    let (turning_point_position_skewness, turning_point_position_kurtosis) = if variance > F::zero()
1064    {
1065        let std_dev = variance.sqrt();
1066
1067        let skewness = tp_values.iter().fold(F::zero(), |acc, &x| {
1068            let normalized = (x - mean_position) / std_dev;
1069            acc + normalized * normalized * normalized
1070        }) / F::from(total_count).unwrap();
1071
1072        let kurtosis = tp_values.iter().fold(F::zero(), |acc, &x| {
1073            let normalized = (x - mean_position) / std_dev;
1074            let normalized_sq = normalized * normalized;
1075            acc + normalized_sq * normalized_sq
1076        }) / F::from(total_count).unwrap()
1077            - F::from(3.0).unwrap();
1078
1079        (skewness, kurtosis)
1080    } else {
1081        (F::zero(), F::zero())
1082    };
1083
1084    Ok(PositionFeatures {
1085        upper_half_turning_points,
1086        lower_half_turning_points,
1087        turning_point_position_skewness,
1088        turning_point_position_kurtosis,
1089    })
1090}
1091
1092/// Analyze multi-scale turning points
1093#[allow(dead_code)]
1094fn analyze_multiscale_turning_points<F>(
1095    ts: &Array1<F>,
1096    config: &TurningPointsConfig,
1097) -> Result<MultiscaleTurningPointFeatures<F>>
1098where
1099    F: Float + FromPrimitive + Debug + Clone + std::iter::Sum + scirs2_core::ndarray::ScalarOperand,
1100{
1101    let mut multiscale_turning_points = Vec::new();
1102    let mut scale_consistencies = Vec::new();
1103
1104    // Analyze turning points at different smoothing scales
1105    for &window_size in &config.smoothing_windows {
1106        // Apply simple moving average smoothing
1107        let smoothed = apply_moving_average(ts, window_size)?;
1108
1109        // Create smoothed config
1110        let smoothed_config = TurningPointsConfig {
1111            min_turning_point_threshold: config.min_turning_point_threshold,
1112            extrema_window_size: config.extrema_window_size,
1113            major_reversal_threshold: config.major_reversal_threshold,
1114            detect_advanced_patterns: false,
1115            smoothing_windows: vec![],
1116            calculate_temporal_patterns: false,
1117            max_autocorr_lag: 0,
1118            analyze_clustering: false,
1119            min_sequence_length: config.min_sequence_length,
1120            multiscale_analysis: false,
1121        };
1122
1123        // Detect turning points at this scale
1124        let (tp__, _, _) = detect_turning_points(&smoothed, &smoothed_config)?;
1125        multiscale_turning_points.push(tp__.len());
1126
1127        // Calculate scale consistency (similarity with original scale)
1128        if !multiscale_turning_points.is_empty() {
1129            let original_count = multiscale_turning_points[0] as f64;
1130            let current_count = tp__.len() as f64;
1131            let consistency =
1132                1.0 - (original_count - current_count).abs() / original_count.max(current_count);
1133            scale_consistencies.push(F::from(consistency).unwrap());
1134        }
1135    }
1136
1137    // Calculate scale turning point ratio
1138    let scale_turning_point_ratio = if multiscale_turning_points.len() > 1 {
1139        let first_scale = F::from(multiscale_turning_points[0]).unwrap();
1140        let last_scale = F::from(*multiscale_turning_points.last().unwrap()).unwrap();
1141        if last_scale > F::zero() {
1142            first_scale / last_scale
1143        } else {
1144            F::one()
1145        }
1146    } else {
1147        F::one()
1148    };
1149
1150    // Calculate cross-scale consistency
1151    let cross_scale_consistency = if !scale_consistencies.is_empty() {
1152        scale_consistencies
1153            .iter()
1154            .fold(F::zero(), |acc, &x| acc + x)
1155            / F::from(scale_consistencies.len()).unwrap()
1156    } else {
1157        F::zero()
1158    };
1159
1160    // Calculate hierarchical structure index
1161    let hierarchical_structure_index = if multiscale_turning_points.len() > 2 {
1162        let mut structure_measure = F::zero();
1163        for i in 1..multiscale_turning_points.len() {
1164            let ratio = F::from(multiscale_turning_points[i - 1]).unwrap()
1165                / F::from(multiscale_turning_points[i])
1166                    .unwrap()
1167                    .max(F::from(1.0).unwrap());
1168            structure_measure = structure_measure + ratio;
1169        }
1170        structure_measure / F::from(multiscale_turning_points.len() - 1).unwrap()
1171    } else {
1172        F::one()
1173    };
1174
1175    Ok(MultiscaleTurningPointFeatures {
1176        multiscale_turning_points,
1177        scale_turning_point_ratio,
1178        cross_scale_consistency,
1179        hierarchical_structure_index,
1180    })
1181}
1182
1183// =============================================================================
1184// Helper Functions
1185// =============================================================================
1186
1187/// Apply simple moving average smoothing
1188#[allow(dead_code)]
1189fn apply_moving_average<F>(_ts: &Array1<F>, windowsize: usize) -> Result<Array1<F>>
1190where
1191    F: Float + FromPrimitive + Clone,
1192{
1193    let n = _ts.len();
1194    if windowsize >= n {
1195        return Ok(_ts.clone());
1196    }
1197
1198    let mut smoothed = Array1::zeros(n);
1199    let half_window = windowsize / 2;
1200
1201    for i in 0..n {
1202        let start = i.saturating_sub(half_window);
1203        let end = (i + half_window + 1).min(n);
1204
1205        let window_sum = _ts.slice(s![start..end]).sum();
1206        let window_len = F::from(end - start).unwrap();
1207        smoothed[i] = window_sum / window_len;
1208    }
1209
1210    Ok(smoothed)
1211}
1212
1213/// Calculate clustering coefficient for intervals
1214#[allow(dead_code)]
1215fn calculate_clustering_coefficient<F>(intervals: &[F]) -> Result<F>
1216where
1217    F: Float + FromPrimitive,
1218{
1219    if intervals.len() < 3 {
1220        return Ok(F::zero());
1221    }
1222
1223    // Simple clustering measure: variance of interval ratios
1224    let mut ratios = Vec::new();
1225    for i in 1..intervals.len() {
1226        if intervals[i] > F::zero() && intervals[i - 1] > F::zero() {
1227            ratios.push(intervals[i] / intervals[i - 1]);
1228        }
1229    }
1230
1231    if ratios.len() < 2 {
1232        return Ok(F::zero());
1233    }
1234
1235    let mean_ratio =
1236        ratios.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(ratios.len()).unwrap();
1237    let variance = ratios.iter().fold(F::zero(), |acc, &x| {
1238        acc + (x - mean_ratio) * (x - mean_ratio)
1239    }) / F::from(ratios.len()).unwrap();
1240
1241    // Clustering is inverse of variance (higher variance = less clustering)
1242    Ok(F::one() / (F::one() + variance))
1243}
1244
1245/// Calculate periodicity strength
1246#[allow(dead_code)]
1247fn calculate_periodicity_strength<F>(intervals: &[F]) -> Result<F>
1248where
1249    F: Float + FromPrimitive,
1250{
1251    if intervals.len() < 4 {
1252        return Ok(F::zero());
1253    }
1254
1255    // Simple periodicity measure: autocorrelation at lag 1
1256    calculate_autocorrelation_at_lag(intervals, 1)
1257}
1258
1259/// Calculate autocorrelation at specific lag
1260#[allow(dead_code)]
1261fn calculate_autocorrelation_at_lag<F>(data: &[F], lag: usize) -> Result<F>
1262where
1263    F: Float + FromPrimitive,
1264{
1265    if data.len() <= lag {
1266        return Ok(F::zero());
1267    }
1268
1269    let n = data.len() - lag;
1270    if n < 2 {
1271        return Ok(F::zero());
1272    }
1273
1274    let mean = data.iter().fold(F::zero(), |acc, &x| acc + x) / F::from(data.len()).unwrap();
1275
1276    let mut numerator = F::zero();
1277    let mut denominator = F::zero();
1278
1279    for i in 0..n {
1280        let x_centered = data[i] - mean;
1281        let y_centered = data[i + lag] - mean;
1282        numerator = numerator + x_centered * y_centered;
1283        denominator = denominator + x_centered * x_centered;
1284    }
1285
1286    if denominator > F::zero() {
1287        Ok(numerator / denominator)
1288    } else {
1289        Ok(F::zero())
1290    }
1291}
1292
1293// Pattern detection functions (simplified implementations)
1294#[allow(dead_code)]
1295fn detect_double_peaks<F>(_ts: &Array1<F>, localmaxima: &[usize]) -> Result<usize>
1296where
1297    F: Float + FromPrimitive + PartialOrd,
1298{
1299    // Simplified: count consecutive peak pairs
1300    let mut count = 0;
1301    for window in localmaxima.windows(3) {
1302        let spacing1 = window[1] - window[0];
1303        let spacing2 = window[2] - window[1];
1304
1305        // Simple heuristic: double peaks have similar spacing
1306        if spacing1 > 0 && spacing2 > 0 {
1307            let ratio = spacing1 as f64 / spacing2 as f64;
1308            if (0.5..=2.0).contains(&ratio) {
1309                count += 1;
1310            }
1311        }
1312    }
1313    Ok(count)
1314}
1315
1316#[allow(dead_code)]
1317fn detect_double_bottoms<F>(_ts: &Array1<F>, localminima: &[usize]) -> Result<usize>
1318where
1319    F: Float + FromPrimitive + PartialOrd,
1320{
1321    // Simplified: count consecutive valley pairs
1322    let mut count = 0;
1323    for window in localminima.windows(3) {
1324        let spacing1 = window[1] - window[0];
1325        let spacing2 = window[2] - window[1];
1326
1327        // Simple heuristic: double bottoms have similar spacing
1328        if spacing1 > 0 && spacing2 > 0 {
1329            let ratio = spacing1 as f64 / spacing2 as f64;
1330            if (0.5..=2.0).contains(&ratio) {
1331                count += 1;
1332            }
1333        }
1334    }
1335    Ok(count)
1336}
1337
1338#[allow(dead_code)]
1339fn detect_head_and_shoulders<F>(
1340    _ts: &Array1<F>,
1341    local_maxima: &[usize],
1342    _local_minima: &[usize],
1343) -> Result<usize>
1344where
1345    F: Float + FromPrimitive + PartialOrd,
1346{
1347    // Simplified: count groups of 3 peaks where middle is highest
1348    let mut count = 0;
1349    if local_maxima.len() >= 3 {
1350        for window in local_maxima.windows(3) {
1351            // Simple spacing check for head-and-shoulders pattern
1352            let spacing1 = window[1] - window[0];
1353            let spacing2 = window[2] - window[1];
1354
1355            if spacing1 > 0 && spacing2 > 0 && spacing1 <= spacing2 * 2 && spacing2 <= spacing1 * 2
1356            {
1357                count += 1;
1358            }
1359        }
1360    }
1361    Ok(count)
1362}
1363
1364#[allow(dead_code)]
1365fn detect_triangular_patterns<F>(
1366    _ts: &Array1<F>,
1367    local_maxima: &[usize],
1368    local_minima: &[usize],
1369) -> Result<usize>
1370where
1371    F: Float + FromPrimitive + PartialOrd,
1372{
1373    // Simplified: count patterns where extrema converge
1374    let min_pattern_length = 4;
1375    let mut count = 0;
1376
1377    if local_maxima.len() >= 2 && local_minima.len() >= 2 {
1378        // Check for converging pattern in peaks and valleys
1379        for i in 0..(local_maxima.len().saturating_sub(min_pattern_length)) {
1380            let peak_range_start = local_maxima[i + 1] - local_maxima[i];
1381            let peak_range_end = if i + 3 < local_maxima.len() {
1382                local_maxima[i + 3] - local_maxima[i + 2]
1383            } else {
1384                continue;
1385            };
1386
1387            // Simple convergence check
1388            if peak_range_end > 0 && peak_range_start > peak_range_end {
1389                count += 1;
1390            }
1391        }
1392    }
1393
1394    Ok(count)
1395}