Skip to main content

quantwave_core/indicators/
triangle.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Triangle Windowed FIR Filter
6///
7/// Based on John Ehlers' "Windowing" (S&C 2021).
8/// A finite impulse response (FIR) filter using a triangle-shaped window for smoothing.
9#[derive(Debug, Clone)]
10pub struct TriangleFilter {
11    length: usize,
12    window: VecDeque<f64>,
13    coefficients: Vec<f64>,
14    coef_sum: f64,
15}
16
17impl TriangleFilter {
18    pub fn new(length: usize) -> Self {
19        let mut coefficients = Vec::with_capacity(length);
20        let mut coef_sum = 0.0;
21        for count in 1..=length {
22            let coef = if (count as f64) < (length as f64 / 2.0) {
23                count as f64
24            } else if (count as f64) == (length as f64 / 2.0) {
25                length as f64 / 2.0
26            } else {
27                length as f64 + 1.0 - count as f64
28            };
29            coefficients.push(coef);
30            coef_sum += coef;
31        }
32
33        Self {
34            length,
35            window: VecDeque::with_capacity(length),
36            coefficients,
37            coef_sum,
38        }
39    }
40}
41
42impl Default for TriangleFilter {
43    fn default() -> Self {
44        Self::new(20)
45    }
46}
47
48impl Next<f64> for TriangleFilter {
49    type Output = f64;
50
51    fn next(&mut self, input: f64) -> Self::Output {
52        self.window.push_front(input);
53        if self.window.len() > self.length {
54            self.window.pop_back();
55        }
56
57        if self.window.len() < self.length {
58            return input;
59        }
60
61        let mut filt = 0.0;
62        for (i, &val) in self.window.iter().enumerate() {
63            filt += self.coefficients[i] * val;
64        }
65
66        if self.coef_sum.abs() > 1e-10 {
67            filt / self.coef_sum
68        } else {
69            input
70        }
71    }
72}
73
74pub const TRIANGLE_FILTER_METADATA: IndicatorMetadata = IndicatorMetadata {
75    name: "TriangleFilter",
76    description: "Triangle windowed FIR filter.",
77    params: &[
78        ParamDef {
79            name: "length",
80            default: "20",
81            description: "Filter length",
82        },
83    ],
84    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’ TIPS - SEPTEMBER 2021.html",
85    formula_latex: r#"
86\[
87Coef(n) = \begin{cases} n & n < L/2 \\ L/2 & n = L/2 \\ L + 1 - n & n > L/2 \end{cases}
88\]
89\[
90Filt = \frac{\sum_{n=1}^L Coef(n) \cdot Price_{t-n+1}}{\sum Coef(n)}
91\]
92"#,
93    gold_standard_file: "triangle_filter.json",
94    category: "Ehlers DSP",
95};
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::traits::Next;
101    use proptest::prelude::*;
102
103    #[test]
104    fn test_triangle_basic() {
105        let mut tri = TriangleFilter::new(20);
106        for _ in 0..50 {
107            let val = tri.next(100.0);
108            approx::assert_relative_eq!(val, 100.0, epsilon = 1e-10);
109        }
110    }
111
112    proptest! {
113        #[test]
114        fn test_triangle_parity(
115            inputs in prop::collection::vec(1.0..100.0, 50..100),
116        ) {
117            let length = 20;
118            let mut tri = TriangleFilter::new(length);
119            let streaming_results: Vec<f64> = inputs.iter().map(|&x| tri.next(x)).collect();
120
121            // Batch implementation
122            let mut batch_results = Vec::with_capacity(inputs.len());
123            let mut coeffs = Vec::new();
124            let mut c_sum = 0.0;
125            for count in 1..=length {
126                let coef = if (count as f64) < (length as f64 / 2.0) {
127                    count as f64
128                } else if (count as f64) == (length as f64 / 2.0) {
129                    length as f64 / 2.0
130                } else {
131                    length as f64 + 1.0 - count as f64
132                };
133                coeffs.push(coef);
134                c_sum += coef;
135            }
136
137            for i in 0..inputs.len() {
138                if i < length - 1 {
139                    batch_results.push(inputs[i]);
140                    continue;
141                }
142                let mut f = 0.0;
143                for j in 0..length {
144                    f += coeffs[j] * inputs[i - j];
145                }
146                batch_results.push(f / c_sum);
147            }
148
149            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
150                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
151            }
152        }
153    }
154}