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    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![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];
104        let mut last_net = 0.0;
105        for input in inputs {
106            last_net = net.next(input);
107        }
108        // Increasing input should result in positive NET (1.0)
109        assert_eq!(last_net, 1.0);
110    }
111
112    proptest! {
113        #[test]
114        fn test_noise_elimination_parity(
115            inputs in prop::collection::vec(-1.0..1.0, 20..100),
116        ) {
117            let length = 14;
118            let mut net = NoiseElimination::new(length);
119            let streaming_results: Vec<f64> = inputs.iter().map(|&x| net.next(x)).collect();
120
121            // Batch implementation
122            let mut batch_results = Vec::with_capacity(inputs.len());
123            let denom = 0.5 * (length as f64) * (length as f64 - 1.0);
124
125            for i in 0..inputs.len() {
126                if i < length - 1 {
127                    batch_results.push(0.0);
128                    continue;
129                }
130
131                let mut num = 0.0;
132                for ii in 1..length {
133                    for jj in 0..ii {
134                        let diff = inputs[i - ii] - inputs[i - jj];
135                        if diff > 0.0 {
136                            num -= 1.0;
137                        } else if diff < 0.0 {
138                            num += 1.0;
139                        }
140                    }
141                }
142                batch_results.push(num / denom);
143            }
144
145            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
146                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
147            }
148        }
149    }
150}