Skip to main content

quantwave_core/indicators/
swiss_army_knife.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3
4/// Swiss Army Knife Mode
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub enum SwissArmyKnifeMode {
7    EMA,
8    SMA,
9    Gauss,
10    Butterworth,
11    Smooth,
12    HighPass,
13    TwoPoleHighPass,
14    BandPass,
15    BandStop,
16}
17
18/// Swiss Army Knife Indicator
19///
20/// Based on John Ehlers' "Swiss Army Knife Indicator".
21/// A versatile general-purpose filter that can be configured as various
22/// low-pass, high-pass, band-pass, and band-stop filters.
23#[derive(Debug, Clone)]
24pub struct SwissArmyKnife {
25    mode: SwissArmyKnifeMode,
26    period: usize,
27    delta: f64,
28    c0: f64,
29    c1: f64,
30    b0: f64,
31    b1: f64,
32    b2: f64,
33    a1: f64,
34    a2: f64,
35    x: [f64; 3],         // x[t], x[t-1], x[t-2]
36    f: [f64; 2],         // f[t-1], f[t-2]
37    history_x: Vec<f64>, // For SMA mode only (c1 * Price[N])
38    count: usize,
39}
40
41impl SwissArmyKnife {
42    pub fn new(mode: SwissArmyKnifeMode, period: usize, delta: f64) -> Self {
43        let mut sak = Self {
44            mode,
45            period,
46            delta,
47            c0: 1.0,
48            c1: 0.0,
49            b0: 1.0,
50            b1: 0.0,
51            b2: 0.0,
52            a1: 0.0,
53            a2: 0.0,
54            x: [0.0; 3],
55            f: [0.0; 2],
56            history_x: Vec::new(),
57            count: 0,
58        };
59        sak.initialize();
60        sak
61    }
62
63    fn initialize(&mut self) {
64        let period_f = self.period as f64;
65        let angle = 2.0 * std::f64::consts::PI / period_f;
66
67        match self.mode {
68            SwissArmyKnifeMode::EMA => {
69                let alpha = (angle.cos() + angle.sin() - 1.0) / angle.cos();
70                self.b0 = alpha;
71                self.a1 = 1.0 - alpha;
72            }
73            SwissArmyKnifeMode::SMA => {
74                self.c1 = 1.0 / period_f;
75                self.b0 = 1.0 / period_f;
76                self.a1 = 1.0;
77            }
78            SwissArmyKnifeMode::Gauss => {
79                let beta = 2.415 * (1.0 - angle.cos());
80                let alpha = -beta + (beta * beta + 2.0 * beta).sqrt();
81                self.c0 = alpha * alpha;
82                self.a1 = 2.0 * (1.0 - alpha);
83                self.a2 = -(1.0 - alpha) * (1.0 - alpha);
84            }
85            SwissArmyKnifeMode::Butterworth => {
86                let beta = 2.415 * (1.0 - angle.cos());
87                let alpha = -beta + (beta * beta + 2.0 * beta).sqrt();
88                self.c0 = alpha * alpha / 4.0;
89                self.b1 = 2.0;
90                self.b2 = 1.0;
91                self.a1 = 2.0 * (1.0 - alpha);
92                self.a2 = -(1.0 - alpha) * (1.0 - alpha);
93            }
94            SwissArmyKnifeMode::Smooth => {
95                self.c0 = 0.25;
96                self.b1 = 2.0;
97                self.b2 = 1.0;
98            }
99            SwissArmyKnifeMode::HighPass => {
100                let alpha = (angle.cos() + angle.sin() - 1.0) / angle.cos();
101                self.c0 = 1.0 - alpha / 2.0;
102                self.b1 = -1.0;
103                self.a1 = 1.0 - alpha;
104            }
105            SwissArmyKnifeMode::TwoPoleHighPass => {
106                let beta = 2.415 * (1.0 - angle.cos());
107                let alpha = -beta + (beta * beta + 2.0 * beta).sqrt();
108                self.c0 = (1.0 - alpha / 2.0) * (1.0 - alpha / 2.0);
109                self.b1 = -2.0;
110                self.b2 = 1.0;
111                self.a1 = 2.0 * (1.0 - alpha);
112                self.a2 = -(1.0 - alpha) * (1.0 - alpha);
113            }
114            SwissArmyKnifeMode::BandPass => {
115                let beta = angle.cos();
116                let gamma = 1.0 / (4.0 * std::f64::consts::PI * self.delta / period_f).cos();
117                let alpha = gamma - (gamma * gamma - 1.0).sqrt();
118                self.c0 = (1.0 - alpha) / 2.0;
119                self.b2 = -1.0;
120                self.a1 = beta * (1.0 + alpha);
121                self.a2 = -alpha;
122            }
123            SwissArmyKnifeMode::BandStop => {
124                let beta = angle.cos();
125                let gamma = 1.0 / (4.0 * std::f64::consts::PI * self.delta / period_f).cos();
126                let alpha = gamma - (gamma * gamma - 1.0).sqrt();
127                self.c0 = (1.0 + alpha) / 2.0;
128                self.b1 = -2.0 * beta;
129                self.b2 = 1.0;
130                self.a1 = beta * (1.0 + alpha);
131                self.a2 = -alpha;
132            }
133        }
134    }
135}
136
137impl Next<f64> for SwissArmyKnife {
138    type Output = f64;
139
140    fn next(&mut self, input: f64) -> Self::Output {
141        self.count += 1;
142        self.x[2] = self.x[1];
143        self.x[1] = self.x[0];
144        self.x[0] = input;
145
146        if self.mode == SwissArmyKnifeMode::SMA {
147            self.history_x.push(input);
148        }
149
150        let filt = if self.count <= self.period {
151            match self.mode {
152                SwissArmyKnifeMode::HighPass | SwissArmyKnifeMode::TwoPoleHighPass => 0.0,
153                _ => input,
154            }
155        } else {
156            let x_n = if self.mode == SwissArmyKnifeMode::SMA {
157                self.history_x[self.count - 1 - self.period]
158            } else {
159                0.0
160            };
161
162            self.c0 * (self.b0 * self.x[0] + self.b1 * self.x[1] + self.b2 * self.x[2])
163                + self.a1 * self.f[0]
164                + self.a2 * self.f[1]
165                - self.c1 * x_n
166        };
167
168        self.f[1] = self.f[0];
169        self.f[0] = filt;
170
171        filt
172    }
173}
174
175pub const SWISS_ARMY_KNIFE_METADATA: IndicatorMetadata = IndicatorMetadata {
176    name: "Swiss Army Knife Indicator",
177    description: "A versatile indicator that can be configured as EMA, SMA, Gaussian, Butterworth, High Pass, Band Pass, or Band Stop filter.",
178    usage: "Use as a single configurable filter that can emulate SMA, EMA, Gaussian, Butterworth, or bandpass responses by switching mode flags. Ideal for prototyping different filter designs without code changes.",
179    keywords: &["filter", "ehlers", "dsp", "multi-purpose", "smoothing"],
180    ehlers_summary: "Ehlers presents the Swiss Army Knife Indicator in Cycle Analytics for Traders as a unified filter framework. A set of boolean flags selects the operating mode, making it possible to compare multiple filter responses on the same data by simply toggling flags rather than reimplementing each filter.",
181    params: &[
182        ParamDef {
183            name: "mode",
184            default: "BandPass",
185            description: "Filter mode (EMA, SMA, Gauss, Butter, Smooth, HP, 2PHP, BP, BS)",
186        },
187        ParamDef {
188            name: "period",
189            default: "20",
190            description: "Filter period",
191        },
192        ParamDef {
193            name: "delta",
194            default: "0.1",
195            description: "Bandwidth parameter for BP and BS modes",
196        },
197    ],
198    formula_source: "https://github.com/lavs9/quantwave/blob/main/references/Ehlers%20Papers/SwissArmyKnifeIndicator.pdf",
199    formula_latex: r#"
200\[
201Filt = c_0(b_0 x_t + b_1 x_{t-1} + b_2 x_{t-2}) + a_1 Filt_{t-1} + a_2 Filt_{t-2} - c_1 x_{t-N}
202\]
203"#,
204    gold_standard_file: "swiss_army_knife.json",
205    category: "Ehlers DSP",
206};
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::traits::Next;
212    use proptest::prelude::*;
213
214    #[test]
215    fn test_sak_ema_basic() {
216        let mut sak = SwissArmyKnife::new(SwissArmyKnifeMode::EMA, 20, 0.1);
217        let inputs = vec![10.0, 11.0, 12.0, 11.0, 10.0];
218        for input in inputs {
219            let val = sak.next(input);
220            assert!(!val.is_nan());
221        }
222    }
223
224    proptest! {
225        #[test]
226        fn test_sak_parity(
227            inputs in prop::collection::vec(1.0..100.0, 30..100),
228        ) {
229            let period = 20;
230            let delta = 0.1;
231            let mode = SwissArmyKnifeMode::Gauss;
232            let mut sak = SwissArmyKnife::new(mode, period, delta);
233
234            let streaming_results: Vec<f64> = inputs.iter().map(|&x| sak.next(x)).collect();
235
236            // Batch implementation
237            let mut batch_results = Vec::with_capacity(inputs.len());
238            let mut x = [0.0; 3];
239            let mut f = [0.0; 2];
240
241            // Re-calculate coefficients for verification
242            let angle = 2.0 * std::f64::consts::PI / (period as f64);
243            let beta = 2.415 * (1.0 - angle.cos());
244            let alpha = -beta + (beta * beta + 2.0 * beta).sqrt();
245            let c0 = alpha * alpha;
246            let a1 = 2.0 * (1.0 - alpha);
247            let a2 = -(1.0 - alpha) * (1.0 - alpha);
248
249            for (i, &input) in inputs.iter().enumerate() {
250                x[2] = x[1];
251                x[1] = x[0];
252                x[0] = input;
253
254                let filt = if i + 1 <= period {
255                    input
256                } else {
257                    c0 * x[0] + a1 * f[0] + a2 * f[1]
258                };
259
260                f[1] = f[0];
261                f[0] = filt;
262                batch_results.push(filt);
263            }
264
265            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
266                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
267            }
268        }
269    }
270}