Skip to main content

quantwave_core/features/
cyber_cycle.rs

1//! Cyber Cycle feature extractor wrapper.
2//!
3//! Wraps the existing `CyberCycle` to expose both the cycle value and trigger line
4//! plus a few derived features useful for ML (momentum, zero-cross signal, etc.).
5//!
6//! Primary source: quantwave-core/src/indicators/cyber_cycle.rs:35
7//! (returns (CyberCycle, Trigger) per Ehlers "Cybernetic Analysis for Stocks and Futures")
8
9use crate::indicators::cyber_cycle::CyberCycle;
10use crate::traits::Next;
11
12/// Rich multi-dimensional output for Cyber Cycle features.
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct CyberCycleFeatures {
15    pub cycle: f64,
16    pub trigger: f64,
17    /// Simple momentum of the cycle (cycle - previous cycle)
18    pub cycle_momentum: f64,
19    /// Sign of (cycle - trigger) as a basic oscillator signal
20    pub trigger_signal: f64,
21}
22
23#[derive(Debug, Clone)]
24pub struct CyberCycleFeatureExtractor {
25    inner: CyberCycle,
26    prev_cycle: f64,
27}
28
29impl CyberCycleFeatureExtractor {
30    pub fn new(length: usize) -> Self {
31        Self {
32            inner: CyberCycle::new(length),
33            prev_cycle: 0.0,
34        }
35    }
36}
37
38impl Next<f64> for CyberCycleFeatureExtractor {
39    type Output = CyberCycleFeatures;
40
41    fn next(&mut self, input: f64) -> Self::Output {
42        let (cycle, trigger) = self.inner.next(input);
43
44        let momentum = if self.prev_cycle == 0.0 { 0.0 } else { cycle - self.prev_cycle };
45        let trigger_signal = (cycle - trigger).signum();
46
47        self.prev_cycle = cycle;
48
49        CyberCycleFeatures {
50            cycle,
51            trigger,
52            cycle_momentum: momentum,
53            trigger_signal,
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use approx::assert_relative_eq;
62
63    #[test]
64    fn test_cyber_cycle_features_basic() {
65        let mut extractor = CyberCycleFeatureExtractor::new(14);
66
67        // Feed some oscillatory data
68        for i in 0..50 {
69            let val = 100.0 + 5.0 * (i as f64 * 0.3).sin();
70            let f = extractor.next(val);
71            if !f.cycle.is_nan() {
72                assert!(f.cycle.abs() < 20.0); // sanity
73            }
74        }
75    }
76}