Skip to main content

quantwave_core/indicators/
noise_elimination.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as 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    usage: "Use as a pre-filter to remove spike noise from price or intermediate indicator data without introducing lag. Particularly useful when raw tick or 1-minute data is used.",
71    keywords: &["filter", "noise", "ehlers", "dsp", "smoothing"],
72    ehlers_summary: "Ehlers Noise Elimination Technology (NET) is a nonlinear filter that removes isolated noise spikes while leaving genuine price moves intact. It works by comparing each bar to its neighbors and replacing outliers with interpolated values, achieving noise reduction without the lag of conventional smoothers.",
73    params: &[ParamDef {
74        name: "length",
75        default: "14",
76        description: "Correlation length",
77    }],
78    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Noise%20Elimination%20Technology.pdf",
79    formula_latex: r#"
80\[
81Num = \sum_{i=1}^{N-1} \sum_{j=0}^{i-1} -sgn(X_i - X_j)
82\]
83\[
84Denom = \frac{N(N-1)}{2}
85\]
86\[
87NET = \frac{Num}{Denom}
88\]
89"#,
90    gold_standard_file: "noise_elimination.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_noise_elimination_basic() {
102        let mut net = NoiseElimination::new(14);
103        let inputs = vec![
104            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,
105        ];
106        let mut last_net = 0.0;
107        for input in inputs {
108            last_net = net.next(input);
109        }
110        // Increasing input should result in positive NET (1.0)
111        assert_eq!(last_net, 1.0);
112    }
113
114    proptest! {
115        #[test]
116        fn test_noise_elimination_parity(
117            inputs in prop::collection::vec(-1.0..1.0, 20..100),
118        ) {
119            let length = 14;
120            let mut net = NoiseElimination::new(length);
121            let streaming_results: Vec<f64> = inputs.iter().map(|&x| net.next(x)).collect();
122
123            // Batch implementation
124            let mut batch_results = Vec::with_capacity(inputs.len());
125            let denom = 0.5 * (length as f64) * (length as f64 - 1.0);
126
127            for i in 0..inputs.len() {
128                if i < length - 1 {
129                    batch_results.push(0.0);
130                    continue;
131                }
132
133                let mut num = 0.0;
134                for ii in 1..length {
135                    for jj in 0..ii {
136                        let diff = inputs[i - ii] - inputs[i - jj];
137                        if diff > 0.0 {
138                            num -= 1.0;
139                        } else if diff < 0.0 {
140                            num += 1.0;
141                        }
142                    }
143                }
144                batch_results.push(num / denom);
145            }
146
147            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
148                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
149            }
150        }
151    }
152}