quantwave_core/indicators/
cyber_cycle.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3
4#[derive(Debug, Clone)]
11pub struct CyberCycle {
12 alpha: f64,
13 x: [f64; 4], x_s: [f64; 3], cc: [f64; 3], trigger: f64,
17 t: usize,
18}
19
20impl CyberCycle {
21 pub fn new(length: usize) -> Self {
22 let alpha = 2.0 / ((length as f64) + 1.0);
23 Self {
24 alpha,
25 x: [0.0; 4],
26 x_s: [0.0; 3],
27 cc: [0.0; 3],
28 trigger: 0.0,
29 t: 0,
30 }
31 }
32}
33
34impl Next<f64> for CyberCycle {
35 type Output = (f64, f64); fn next(&mut self, input: f64) -> Self::Output {
38 self.x[3] = self.x[2];
39 self.x[2] = self.x[1];
40 self.x[1] = self.x[0];
41 self.x[0] = input;
42
43 let smooth = (self.x[0] + 2.0 * self.x[1] + 2.0 * self.x[2] + self.x[3]) / 6.0;
44
45 self.x_s[2] = self.x_s[1];
46 self.x_s[1] = self.x_s[0];
47 self.x_s[0] = smooth;
48
49 self.cc[2] = self.cc[1];
50 self.cc[1] = self.cc[0];
51
52 self.trigger = self.cc[1];
54
55 if self.t < 6 {
56 self.cc[0] = (self.x[0] - 2.0 * self.x[1] + self.x[2]) / 4.0;
57 } else {
58 let part1 =
59 (1.0 - 0.5 * self.alpha).powi(2) * (self.x_s[0] - 2.0 * self.x_s[1] + self.x_s[2]);
60 let part2 = 2.0 * (1.0 - self.alpha) * self.cc[1];
61 let part3 = (1.0 - self.alpha).powi(2) * self.cc[2];
62 self.cc[0] = part1 + part2 - part3;
63 }
64
65 self.t += 1;
66
67 (self.cc[0], self.trigger)
68 }
69}
70
71pub const CYBER_CYCLE_METADATA: IndicatorMetadata = IndicatorMetadata {
72 name: "Cyber Cycle",
73 description: "An oscillator introduced by John Ehlers that models the cyclical component of a time series using FIR smoothing.",
74 usage: "Use as a high-resolution short-term cycle oscillator to time entries and exits around cycle turns. Pair with a trend classifier to suppress signals in trending conditions.",
75 keywords: &["cycle", "oscillator", "ehlers", "dsp"],
76 ehlers_summary: "Ehlers introduces the Cyber Cycle in Cybernetic Analysis (2004) as a bandpass-like filter isolating the short-term cyclical component. The trigger line is the Cyber Cycle delayed by one bar, creating a clean crossover signal without derivative noise.",
77 params: &[ParamDef {
78 name: "length",
79 default: "14",
80 description: "Alpha smoothing length parameter",
81 }],
82 formula_source: "Cybernetic Analysis for Stocks and Futures, John Ehlers, 2004, Chapter 4",
83 formula_latex: r#"
84\[
85\alpha = \frac{2}{\text{Length} + 1}
86\]
87\[
88\text{Smooth} = \frac{X_t + 2X_{t-1} + 2X_{t-2} + X_{t-3}}{6}
89\]
90\[
91CC_t = \left(1 - \frac{\alpha}{2}\right)^2 (\text{Smooth}_t - 2\text{Smooth}_{t-1} + \text{Smooth}_{t-2}) + 2(1 - \alpha)CC_{t-1} - (1 - \alpha)^2 CC_{t-2}
92\]
93"#,
94 gold_standard_file: "cyber_cycle.json",
95 category: "Ehlers DSP",
96};
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use proptest::prelude::*;
102
103 fn cyber_cycle_batch(data: &[f64], length: usize) -> Vec<(f64, f64)> {
104 let mut cc = CyberCycle::new(length);
105 data.iter().map(|&x| cc.next(x)).collect()
106 }
107
108 proptest! {
109 #[test]
110 fn test_cyber_cycle_parity(input in prop::collection::vec(0.1..100.0, 1..100)) {
111 let length = 14;
112 let mut streaming_cc = CyberCycle::new(length);
113 let streaming_results: Vec<(f64, f64)> = input.iter().map(|&x| streaming_cc.next(x)).collect();
114 let batch_results = cyber_cycle_batch(&input, length);
115
116 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
117 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
118 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
119 }
120 }
121 }
122}