quantwave-core 0.1.3

A high-performance, Polars-native technical analysis library for Rust.
Documentation
use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
use std::collections::VecDeque;

/// Noise Elimination Technology (NET)
///
/// Based on John Ehlers' "Noise Elimination Technology" paper.
/// Uses Kendall correlation to strip out noise from an indicator in a nonlinear fashion
/// without introducing lag.
#[derive(Debug, Clone)]
pub struct NoiseElimination {
    length: usize,
    window: VecDeque<f64>,
    denom: f64,
}

impl NoiseElimination {
    pub fn new(length: usize) -> Self {
        let denom = 0.5 * (length as f64) * (length as f64 - 1.0);
        Self {
            length,
            window: VecDeque::with_capacity(length),
            denom,
        }
    }
}

impl Default for NoiseElimination {
    fn default() -> Self {
        Self::new(14)
    }
}

impl Next<f64> for NoiseElimination {
    type Output = f64;

    fn next(&mut self, input: f64) -> Self::Output {
        self.window.push_front(input);
        if self.window.len() > self.length {
            self.window.pop_back();
        }

        if self.window.len() < self.length {
            return 0.0;
        }

        let mut num = 0.0;
        // Ehlers' formula: Num = Num - Sign(X[count] - X[K])
        // where count = 2..N, K = 1..count-1 (1-indexed)
        // In my window [0..N-1], count-1 and K-1 are the indices.
        // i = count-1 (1..N-1), j = K-1 (0..i-1)
        for i in 1..self.length {
            for j in 0..i {
                let diff = self.window[i] - self.window[j];
                if diff > 0.0 {
                    num -= 1.0;
                } else if diff < 0.0 {
                    num += 1.0;
                }
            }
        }

        num / self.denom
    }
}

pub const NOISE_ELIMINATION_METADATA: IndicatorMetadata = IndicatorMetadata {
    name: "Noise Elimination Technology",
    description: "Nonlinear noise removal using Kendall correlation against a straight line.",
    params: &[ParamDef {
        name: "length",
        default: "14",
        description: "Correlation length",
    }],
    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/Noise%20Elimination%20Technology.pdf",
    formula_latex: r#"
\[
Num = \sum_{i=1}^{N-1} \sum_{j=0}^{i-1} -sgn(X_i - X_j)
\]
\[
Denom = \frac{N(N-1)}{2}
\]
\[
NET = \frac{Num}{Denom}
\]
"#,
    gold_standard_file: "noise_elimination.json",
    category: "Ehlers DSP",
};

#[cfg(test)]
mod tests {
    use super::*;
    use crate::traits::Next;
    use proptest::prelude::*;

    #[test]
    fn test_noise_elimination_basic() {
        let mut net = NoiseElimination::new(14);
        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];
        let mut last_net = 0.0;
        for input in inputs {
            last_net = net.next(input);
        }
        // Increasing input should result in positive NET (1.0)
        assert_eq!(last_net, 1.0);
    }

    proptest! {
        #[test]
        fn test_noise_elimination_parity(
            inputs in prop::collection::vec(-1.0..1.0, 20..100),
        ) {
            let length = 14;
            let mut net = NoiseElimination::new(length);
            let streaming_results: Vec<f64> = inputs.iter().map(|&x| net.next(x)).collect();

            // Batch implementation
            let mut batch_results = Vec::with_capacity(inputs.len());
            let denom = 0.5 * (length as f64) * (length as f64 - 1.0);

            for i in 0..inputs.len() {
                if i < length - 1 {
                    batch_results.push(0.0);
                    continue;
                }

                let mut num = 0.0;
                for ii in 1..length {
                    for jj in 0..ii {
                        let diff = inputs[i - ii] - inputs[i - jj];
                        if diff > 0.0 {
                            num -= 1.0;
                        } else if diff < 0.0 {
                            num += 1.0;
                        }
                    }
                }
                batch_results.push(num / denom);
            }

            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
            }
        }
    }
}