quantwave_core/indicators/
triangle.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5#[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 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}