quantwave_core/indicators/
correlation_trend.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
12pub struct CorrelationTrend {
13 length: usize,
14 window: VecDeque<f64>,
15 sy: f64,
16 syy: f64,
17}
18
19impl CorrelationTrend {
20 pub fn new(length: usize) -> Self {
21 let mut sy = 0.0;
22 let mut syy = 0.0;
23 for i in 0..length {
24 let y = -(i as f64);
25 sy += y;
26 syy += y * y;
27 }
28
29 Self {
30 length,
31 window: VecDeque::with_capacity(length),
32 sy,
33 syy,
34 }
35 }
36}
37
38impl Next<f64> for CorrelationTrend {
39 type Output = f64;
40
41 fn next(&mut self, input: f64) -> Self::Output {
42 self.window.push_front(input);
43 if self.window.len() > self.length {
44 self.window.pop_back();
45 }
46
47 if self.window.len() < self.length {
48 return 0.0;
49 }
50
51 let mut sx = 0.0;
52 let mut sxx = 0.0;
53 let mut sxy = 0.0;
54 for (i, &x) in self.window.iter().enumerate() {
55 let y = -(i as f64);
56 sx += x;
57 sxx += x * x;
58 sxy += x * y;
59 }
60
61 let l_f = self.length as f64;
62 let div1 = l_f * sxx - sx * sx;
63 let div2 = l_f * self.syy - self.sy * self.sy;
64
65 if div1 > 0.0 && div2 > 0.0 {
66 (l_f * sxy - sx * self.sy) / (div1 * div2).sqrt()
67 } else {
68 0.0
69 }
70 }
71}
72
73pub const CORRELATION_TREND_METADATA: IndicatorMetadata = IndicatorMetadata {
74 name: "Correlation Trend",
75 description: "Calculates the Pearson correlation between price and a linear time ramp to identify trends.",
76 usage: "Use to confirm whether price is trending or cycling before applying directional strategies. High correlation indicates a strong trend; low correlation indicates a cycling market.",
77 keywords: &["trend", "correlation", "ehlers", "statistics"],
78 ehlers_summary: "In 'Correlation As A Trend Indicator' (2020), Ehlers uses the Pearson correlation coefficient between price and a linear ramp to identify trend strength. A coefficient near +1.0 indicates a consistent uptrend, while -1.0 indicates a consistent downtrend. Unlike standard moving averages, this approach is independent of price amplitude and focuses purely on the linearity of the move.",
79 params: &[ParamDef {
80 name: "length",
81 default: "20",
82 description: "Correlation window length",
83 }],
84 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/CORRELATION%20AS%20A%20TREND%20INDICATOR.pdf",
85 formula_latex: r#"
86\[
87X_i = Price_{t-i}, Y_i = -i
88\]
89\[
90R = \frac{n \sum X_i Y_i - \sum X_i \sum Y_i}{\sqrt{(n \sum X_i^2 - (\sum X_i)^2)(n \sum Y_i^2 - (\sum Y_i)^2)}}
91\]
92"#,
93 gold_standard_file: "correlation_trend.json",
94 category: "Ehlers DSP",
95};
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::traits::Next;
101 use proptest::prelude::*;
102
103 #[test]
104 fn test_correlation_trend_basic() {
105 let mut ct = CorrelationTrend::new(20);
106 for i in 0..30 {
107 let res = ct.next(i as f64);
108 if i >= 19 {
109 assert!(res > 0.99);
117 }
118 }
119 }
120
121 proptest! {
122 #[test]
123 fn test_correlation_trend_parity(
124 inputs in prop::collection::vec(1.0..100.0, 50..100),
125 ) {
126 let length = 20;
127 let mut ct = CorrelationTrend::new(length);
128 let streaming_results: Vec<f64> = inputs.iter().map(|&x| ct.next(x)).collect();
129
130 let mut batch_results = Vec::with_capacity(inputs.len());
132 let l_f = length as f64;
133 let mut sy = 0.0;
134 let mut syy = 0.0;
135 for i in 0..length {
136 let y = -(i as f64);
137 sy += y;
138 syy += y * y;
139 }
140
141 for i in 0..inputs.len() {
142 if i < length - 1 {
143 batch_results.push(0.0);
144 continue;
145 }
146
147 let mut sx = 0.0;
148 let mut sxx = 0.0;
149 let mut sxy = 0.0;
150 for j in 0..length {
151 let x = inputs[i-j];
152 let y = -(j as f64);
153 sx += x;
154 sxx += x * x;
155 sxy += x * y;
156 }
157
158 let div1 = l_f * sxx - sx * sx;
159 let div2 = l_f * syy - sy * sy;
160 let res = if div1 > 0.0 && div2 > 0.0 {
161 (l_f * sxy - sx * sy) / (div1 * div2).sqrt()
162 } else {
163 0.0
164 };
165 batch_results.push(res);
166 }
167
168 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
169 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
170 }
171 }
172 }
173}