quantwave_core/indicators/
correlation_cycle.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4use std::f64::consts::PI;
5
6#[derive(Debug, Clone)]
12pub struct CorrelationCycle {
13 period: usize,
14 price_window: VecDeque<f64>,
15 cosine_wave: Vec<f64>,
16 sine_wave: Vec<f64>,
17 prev_angle: f64,
18 count: usize,
19}
20
21impl CorrelationCycle {
22 pub fn new(period: usize) -> Self {
23 let mut cosine_wave = Vec::with_capacity(period);
24 let mut sine_wave = Vec::with_capacity(period);
25 for n in 0..period {
26 let angle = 2.0 * PI * n as f64 / period as f64;
27 cosine_wave.push(angle.cos());
28 sine_wave.push(-angle.sin());
29 }
30
31 Self {
32 period,
33 price_window: VecDeque::with_capacity(period),
34 cosine_wave,
35 sine_wave,
36 prev_angle: 0.0,
37 count: 0,
38 }
39 }
40
41 fn pearson_correlation(n: usize, x: &VecDeque<f64>, y: &Vec<f64>) -> f64 {
42 let mut sx = 0.0;
43 let mut sy = 0.0;
44 let mut sxx = 0.0;
45 let mut syy = 0.0;
46 let mut sxy = 0.0;
47
48 for i in 0..n {
49 let xi = x[i];
50 let yi = y[i];
51 sx += xi;
52 sy += yi;
53 sxx += xi * xi;
54 syy += yi * yi;
55 sxy += xi * yi;
56 }
57
58 let nf = n as f64;
59 let num = nf * sxy - sx * sy;
60 let den = ((nf * sxx - sx * sx) * (nf * syy - sy * sy)).sqrt();
61
62 if den > 0.0 {
63 num / den
64 } else {
65 0.0
66 }
67 }
68}
69
70impl Default for CorrelationCycle {
71 fn default() -> Self {
72 Self::new(20)
73 }
74}
75
76impl Next<f64> for CorrelationCycle {
77 type Output = (f64, f64, f64); fn next(&mut self, input: f64) -> Self::Output {
80 self.count += 1;
81 self.price_window.push_front(input);
82 if self.price_window.len() > self.period {
83 self.price_window.pop_back();
84 }
85
86 if self.price_window.len() < self.period {
87 return (0.0, 0.0, 0.0);
88 }
89
90 let real = Self::pearson_correlation(self.period, &self.price_window, &self.cosine_wave);
91 let imag = Self::pearson_correlation(self.period, &self.price_window, &self.sine_wave);
92
93 let mut angle = if imag != 0.0 {
96 (real / imag).atan().to_degrees() + 90.0
97 } else {
98 90.0
99 };
100
101 if imag > 0.0 {
102 angle -= 180.0;
103 }
104
105 if self.count > self.period + 1 {
108 if self.prev_angle - angle < 270.0 && angle < self.prev_angle {
109 angle = self.prev_angle;
110 }
111 }
112
113 self.prev_angle = angle;
114
115 (real, imag, angle)
116 }
117}
118
119pub const CORRELATION_CYCLE_METADATA: IndicatorMetadata = IndicatorMetadata {
120 name: "CorrelationCycle",
121 description: "Determines cycle phase angle by correlating price with orthogonal sinusoids.",
122 params: &[
123 ParamDef {
124 name: "period",
125 default: "20",
126 description: "Correlation wavelength",
127 },
128 ],
129 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/CORRELATION%20AS%20A%20CYCLE%20INDICATOR.pdf",
130 formula_latex: r#"
131\[
132R = \text{Corr}(Price, \cos(2\pi n/P)), I = \text{Corr}(Price, -\sin(2\pi n/P))
133\]
134\[
135\text{Angle} = 90 + \arctan(R/I) \text{ (with quadrant resolution)}
136\]
137"#,
138 gold_standard_file: "correlation_cycle.json",
139 category: "Ehlers DSP",
140};
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::traits::Next;
146 use proptest::prelude::*;
147
148 #[test]
149 fn test_correlation_cycle_basic() {
150 let mut cc = CorrelationCycle::new(20);
151 for i in 0..100 {
152 let (r, im, a) = cc.next((2.0 * PI * i as f64 / 20.0).sin());
153 assert!(!r.is_nan());
154 assert!(!im.is_nan());
155 assert!(!a.is_nan());
156 }
157 }
158
159 proptest! {
160 #[test]
161 fn test_correlation_cycle_parity(
162 inputs in prop::collection::vec(1.0..100.0, 100..150),
163 ) {
164 let period = 20;
165 let mut cc = CorrelationCycle::new(period);
166 let streaming_results: Vec<(f64, f64, f64)> = inputs.iter().map(|&x| cc.next(x)).collect();
167
168 let mut batch_results = Vec::with_capacity(inputs.len());
170 let mut cos_w = Vec::new();
171 let mut sin_w = Vec::new();
172 for n in 0..period {
173 let ang = 2.0 * PI * n as f64 / period as f64;
174 cos_w.push(ang.cos());
175 sin_w.push(-ang.sin());
176 }
177
178 let mut prev_a = 0.0;
179 for i in 0..inputs.len() {
180 if i < period - 1 {
181 batch_results.push((0.0, 0.0, 0.0));
182 continue;
183 }
184
185 let mut sx = 0.0;
186 let mut sy_c = 0.0;
187 let mut sy_s = 0.0;
188 let mut sxx = 0.0;
189 let mut syy_c = 0.0;
190 let mut syy_s = 0.0;
191 let mut sxy_c = 0.0;
192 let mut sxy_s = 0.0;
193
194 for j in 0..period {
195 let xi = inputs[i - j];
196 let yc = cos_w[j];
197 let ys = sin_w[j];
198 sx += xi;
199 sy_c += yc;
200 sy_s += ys;
201 sxx += xi * xi;
202 syy_c += yc * yc;
203 syy_s += ys * ys;
204 sxy_c += xi * yc;
205 sxy_s += xi * ys;
206 }
207
208 let nf = period as f64;
209 let den_c = ((nf * sxx - sx * sx) * (nf * syy_c - sy_c * sy_c)).sqrt();
210 let real = if den_c > 0.0 { (nf * sxy_c - sx * sy_c) / den_c } else { 0.0 };
211
212 let den_s = ((nf * sxx - sx * sx) * (nf * syy_s - sy_s * sy_s)).sqrt();
213 let imag = if den_s > 0.0 { (nf * sxy_s - sx * sy_s) / den_s } else { 0.0 };
214
215 let mut angle = if imag != 0.0 {
216 (real / imag).atan().to_degrees() + 90.0
217 } else {
218 90.0
219 };
220 if imag > 0.0 { angle -= 180.0; }
221
222 if i > period {
223 if prev_a - angle < 270.0 && angle < prev_a {
224 angle = prev_a;
225 }
226 }
227
228 prev_a = angle;
229 batch_results.push((real, imag, angle));
230 }
231
232 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
233 approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-10);
234 approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-10);
235 approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-10);
236 }
237 }
238 }
239}