quantwave_core/indicators/
channel_cycle.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::f64::consts::PI;
4
5#[derive(Debug, Clone)]
11pub struct ChannelCycle {
12 period: usize,
13 alpha: f64,
14 beta: f64,
15 price_window: Vec<f64>,
16 bp_history: [f64; 2],
17 detrended_prev: [f64; 2],
18 count: usize,
19}
20
21impl ChannelCycle {
22 pub fn new(period: usize) -> Self {
23 let delta = 0.1;
25 let beta = (2.0 * PI / period as f64).cos();
26 let gamma = 1.0 / (4.0 * PI * delta / period as f64).cos();
27 let alpha = gamma - (gamma * gamma - 1.0).sqrt();
28
29 Self {
30 period,
31 alpha,
32 beta,
33 price_window: Vec::with_capacity(period),
34 bp_history: [0.0; 2],
35 detrended_prev: [0.0; 2],
36 count: 0,
37 }
38 }
39}
40
41impl Default for ChannelCycle {
42 fn default() -> Self {
43 Self::new(20)
44 }
45}
46
47impl Next<f64> for ChannelCycle {
48 type Output = (f64, f64); fn next(&mut self, input: f64) -> Self::Output {
51 self.count += 1;
52 self.price_window.push(input);
53 if self.price_window.len() > self.period {
54 self.price_window.remove(0);
55 }
56
57 if self.price_window.len() < self.period {
58 return (0.0, 0.0);
59 }
60
61 let mut high = f64::MIN;
62 let mut low = f64::MAX;
63 for &p in &self.price_window {
64 if p > high { high = p; }
65 if p < low { low = p; }
66 }
67
68 let detrended = if high != low {
69 (input - low) / (high - low) - 0.5
70 } else {
71 0.0
72 };
73
74 let bp = 0.5 * (1.0 - self.alpha) * (detrended - self.detrended_prev[1])
76 + self.beta * (1.0 + self.alpha) * self.bp_history[0]
77 - self.alpha * self.bp_history[1];
78
79 let omega = 2.0 * PI / self.period as f64;
82 let leading = (bp - self.bp_history[0]) / omega;
83
84 self.bp_history[1] = self.bp_history[0];
86 self.bp_history[0] = bp;
87 self.detrended_prev[1] = self.detrended_prev[0];
88 self.detrended_prev[0] = detrended;
89
90 (bp, leading)
91 }
92}
93
94pub const CHANNEL_CYCLE_METADATA: IndicatorMetadata = IndicatorMetadata {
95 name: "ChannelCycle",
96 description: "Extracts cyclic components and a leading function using channel-normalized bandpass filtering.",
97 params: &[
98 ParamDef {
99 name: "period",
100 default: "20",
101 description: "Channel and Bandpass period",
102 },
103 ],
104 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/InferringTradingStrategies.pdf",
105 formula_latex: r#"
106\[
107Detrended = \frac{Price - Low}{High - Low} - 0.5
108\]
109\[
110BP = \text{Bandpass}(Detrended, Period)
111\]
112\[
113Leading = \frac{BP - BP_{t-1}}{2\pi/Period}
114\]
115"#,
116 gold_standard_file: "channel_cycle.json",
117 category: "Ehlers DSP",
118};
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::traits::Next;
124 use proptest::prelude::*;
125
126 #[test]
127 fn test_channel_cycle_basic() {
128 let mut cc = ChannelCycle::new(20);
129 for i in 0..100 {
130 let (s, l) = cc.next(100.0 + (i as f64 * 0.1).sin());
131 assert!(!s.is_nan());
132 assert!(!l.is_nan());
133 }
134 }
135
136 proptest! {
137 #[test]
138 fn test_channel_cycle_parity(
139 inputs in prop::collection::vec(1.0..100.0, 100..200),
140 ) {
141 let period = 20;
142 let mut cc = ChannelCycle::new(period);
143 let streaming_results: Vec<(f64, f64)> = inputs.iter().map(|&x| cc.next(x)).collect();
144
145 let mut batch_results = Vec::with_capacity(inputs.len());
147 let delta = 0.1;
148 let beta = (2.0 * PI / period as f64).cos();
149 let gamma = 1.0 / (4.0 * PI * delta / period as f64).cos();
150 let alpha = gamma - (gamma * gamma - 1.0).sqrt();
151 let omega = 2.0 * PI / period as f64;
152
153 let mut detrended_vals = Vec::new();
154 let mut bp_vals = vec![0.0; inputs.len() + 2];
155 let mut d_vals = vec![0.0; inputs.len() + 2];
156
157 for (i, &input) in inputs.iter().enumerate() {
158 let start = if i >= period - 1 { i + 1 - period } else { 0 };
159 let window = &inputs[start..i + 1];
160
161 if window.len() < period {
162 batch_results.push((0.0, 0.0));
163 detrended_vals.push(0.0);
164 continue;
165 }
166
167 let mut high = f64::MIN;
168 let mut low = f64::MAX;
169 for &p in window {
170 if p > high { high = p; }
171 if p < low { low = p; }
172 }
173
174 let detrended = if high != low {
175 (input - low) / (high - low) - 0.5
176 } else {
177 0.0
178 };
179 detrended_vals.push(detrended);
180
181 let idx = i + 2;
182 d_vals[idx] = detrended;
183 let bp = 0.5 * (1.0 - alpha) * (d_vals[idx] - d_vals[idx-2])
184 + beta * (1.0 + alpha) * bp_vals[idx-1]
185 - alpha * bp_vals[idx-2];
186 bp_vals[idx] = bp;
187
188 let leading = (bp - bp_vals[idx-1]) / omega;
189 batch_results.push((bp, leading));
190 }
191
192 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
193 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
194 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
195 }
196 }
197 }
198}