Skip to main content

quantwave_core/indicators/
noise_elimination.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Noise Elimination Technology (NET)
6///
7/// Based on John Ehlers' "Noise Elimination Technology" paper.
8/// Uses Kendall correlation to strip out noise from an indicator in a nonlinear fashion
9/// without introducing lag.
10#[derive(Debug, Clone)]
11pub struct NoiseElimination {
12    length: usize,
13    window: VecDeque<f64>,
14    denom: f64,
15}
16
17impl NoiseElimination {
18    pub fn new(length: usize) -> Self {
19        let denom = 0.5 * (length as f64) * (length as f64 - 1.0);
20        Self {
21            length,
22            window: VecDeque::with_capacity(length),
23            denom,
24        }
25    }
26}
27
28impl Default for NoiseElimination {
29    fn default() -> Self {
30        Self::new(14)
31    }
32}
33
34impl Next<f64> for NoiseElimination {
35    type Output = f64;
36
37    fn next(&mut self, input: f64) -> Self::Output {
38        self.window.push_front(input);
39        if self.window.len() > self.length {
40            self.window.pop_back();
41        }
42
43        if self.window.len() < self.length {
44            return 0.0;
45        }
46
47        let mut num = 0.0;
48        // Ehlers' formula: Num = Num - Sign(X[count] - X[K])
49        // where count = 2..N, K = 1..count-1 (1-indexed)
50        // In my window [0..N-1], count-1 and K-1 are the indices.
51        // i = count-1 (1..N-1), j = K-1 (0..i-1)
52        for i in 1..self.length {
53            for j in 0..i {
54                let diff = self.window[i] - self.window[j];
55                if diff > 0.0 {
56                    num -= 1.0;
57                } else if diff < 0.0 {
58                    num += 1.0;
59                }
60            }
61        }
62
63        num / self.denom
64    }
65}
66
67pub const NOISE_ELIMINATION_METADATA: IndicatorMetadata = IndicatorMetadata {
68    name: "Noise Elimination Technology",
69    description: "Nonlinear noise removal using Kendall correlation against a straight line.",
70    params: &[ParamDef {
71        name: "length",
72        default: "14",
73        description: "Correlation length",
74    }],
75    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Noise%20Elimination%20Technology.pdf",
76    formula_latex: r#"
77\[
78Num = \sum_{i=1}^{N-1} \sum_{j=0}^{i-1} -sgn(X_i - X_j)
79\]
80\[
81Denom = \frac{N(N-1)}{2}
82\]
83\[
84NET = \frac{Num}{Denom}
85\]
86"#,
87    gold_standard_file: "noise_elimination.json",
88    category: "Ehlers DSP",
89};
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::traits::Next;
95    use proptest::prelude::*;
96
97    #[test]
98    fn test_noise_elimination_basic() {
99        let mut net = NoiseElimination::new(14);
100        let inputs = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0];
101        let mut last_net = 0.0;
102        for input in inputs {
103            last_net = net.next(input);
104        }
105        // Increasing input should result in positive NET (1.0)
106        assert_eq!(last_net, 1.0);
107    }
108
109    proptest! {
110        #[test]
111        fn test_noise_elimination_parity(
112            inputs in prop::collection::vec(-1.0..1.0, 20..100),
113        ) {
114            let length = 14;
115            let mut net = NoiseElimination::new(length);
116            let streaming_results: Vec<f64> = inputs.iter().map(|&x| net.next(x)).collect();
117
118            // Batch implementation
119            let mut batch_results = Vec::with_capacity(inputs.len());
120            let denom = 0.5 * (length as f64) * (length as f64 - 1.0);
121
122            for i in 0..inputs.len() {
123                if i < length - 1 {
124                    batch_results.push(0.0);
125                    continue;
126                }
127
128                let mut num = 0.0;
129                for ii in 1..length {
130                    for jj in 0..ii {
131                        let diff = inputs[i - ii] - inputs[i - jj];
132                        if diff > 0.0 {
133                            num -= 1.0;
134                        } else if diff < 0.0 {
135                            num += 1.0;
136                        }
137                    }
138                }
139                batch_results.push(num / denom);
140            }
141
142            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
143                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
144            }
145        }
146    }
147}