Skip to main content

quantwave_core/indicators/
correlation_trend.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Correlation Trend Indicator
6///
7/// Based on John Ehlers' "Correlation as a Trend Indicator" (2020).
8/// It calculates the Pearson correlation coefficient between price and a downward-sloping linear ramp
9/// (representing the time axis going backwards).
10/// A value near +1 indicates a strong uptrend, and -1 indicates a strong downtrend.
11#[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    params: &[ParamDef {
77        name: "length",
78        default: "20",
79        description: "Correlation window length",
80    }],
81    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/CORRELATION%20AS%20A%20TREND%20INDICATOR.pdf",
82    formula_latex: r#"
83\[
84X_i = Price_{t-i}, Y_i = -i
85\]
86\[
87R = \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)}}
88\]
89"#,
90    gold_standard_file: "correlation_trend.json",
91    category: "Ehlers DSP",
92};
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::traits::Next;
98    use proptest::prelude::*;
99
100    #[test]
101    fn test_correlation_trend_basic() {
102        let mut ct = CorrelationTrend::new(20);
103        for i in 0..30 {
104            let res = ct.next(i as f64);
105            if i >= 19 {
106                // For a perfectly straight line, correlation should be 1.0 (or -1.0 depending on Y direction)
107                // Here Y = -i, so as i increases, Y decreases.
108                // Price = i, so as i increases, Price increases.
109                // This is anti-correlation? No, wait.
110                // count=0: Close[0]=20, Y=0
111                // count=1: Close[1]=19, Y=-1
112                // Price decreases as Y decreases. So positive correlation.
113                assert!(res > 0.99);
114            }
115        }
116    }
117
118    proptest! {
119        #[test]
120        fn test_correlation_trend_parity(
121            inputs in prop::collection::vec(1.0..100.0, 50..100),
122        ) {
123            let length = 20;
124            let mut ct = CorrelationTrend::new(length);
125            let streaming_results: Vec<f64> = inputs.iter().map(|&x| ct.next(x)).collect();
126
127            // Batch implementation
128            let mut batch_results = Vec::with_capacity(inputs.len());
129            let l_f = length as f64;
130            let mut sy = 0.0;
131            let mut syy = 0.0;
132            for i in 0..length {
133                let y = -(i as f64);
134                sy += y;
135                syy += y * y;
136            }
137
138            for i in 0..inputs.len() {
139                if i < length - 1 {
140                    batch_results.push(0.0);
141                    continue;
142                }
143
144                let mut sx = 0.0;
145                let mut sxx = 0.0;
146                let mut sxy = 0.0;
147                for j in 0..length {
148                    let x = inputs[i-j];
149                    let y = -(j as f64);
150                    sx += x;
151                    sxx += x * x;
152                    sxy += x * y;
153                }
154
155                let div1 = l_f * sxx - sx * sx;
156                let div2 = l_f * syy - sy * sy;
157                let res = if div1 > 0.0 && div2 > 0.0 {
158                    (l_f * sxy - sx * sy) / (div1 * div2).sqrt()
159                } else {
160                    0.0
161                };
162                batch_results.push(res);
163            }
164
165            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
166                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
167            }
168        }
169    }
170}