quantwave_core/indicators/
simple_predictor.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::indicators::high_pass::HighPass;
4use crate::indicators::super_smoother::SuperSmoother;
5
6#[derive(Debug, Clone)]
11pub struct SimplePredictor {
12 hp: HighPass,
13 ss: SuperSmoother,
14 q: f64,
15 signal_history: [f64; 2],
16 count: usize,
17}
18
19impl SimplePredictor {
20 pub fn new(hp_len: usize, lp_len: usize, q: f64) -> Self {
21 Self {
22 hp: HighPass::new(hp_len),
23 ss: SuperSmoother::new(lp_len),
24 q,
25 signal_history: [0.0; 2],
26 count: 0,
27 }
28 }
29}
30
31impl Default for SimplePredictor {
32 fn default() -> Self {
33 Self::new(15, 30, 0.35)
34 }
35}
36
37impl Next<f64> for SimplePredictor {
38 type Output = f64;
39
40 fn next(&mut self, input: f64) -> Self::Output {
41 self.count += 1;
42 let signal = self.ss.next(self.hp.next(input));
43
44 let c1 = 1.8 * self.q;
45 let c2 = -self.q * self.q;
46 let sum = 1.0 - c1 - c2;
47
48 let res = if self.count < 3 {
49 signal
50 } else {
51 (signal - c1 * self.signal_history[0] - c2 * self.signal_history[1]) / sum
58 };
59
60 self.signal_history[1] = self.signal_history[0];
61 self.signal_history[0] = signal;
62
63 res
64 }
65}
66
67pub const SIMPLE_PREDICTOR_METADATA: IndicatorMetadata = IndicatorMetadata {
68 name: "SimplePredictor",
69 description: "A fixed-coefficient 2-pole linear predictive filter.",
70 usage: "Use as a lightweight one-bar-ahead price predictor for cycle-mode markets. Its low computational cost makes it suitable for real-time streaming at high frequency.",
71 keywords: &["prediction", "cycle", "ehlers", "dsp"],
72 ehlers_summary: "Ehlers derives a Simple Predictor that extrapolates price one bar forward using only the current and prior bars weighted by the dominant cycle coefficient. Despite its simplicity it provides useful one-bar forecasts in cycling markets, demonstrating the predictive value of cycle measurement.",
73 params: &[
74 ParamDef {
75 name: "hp_len",
76 default: "15",
77 description: "HighPass filter length",
78 },
79 ParamDef {
80 name: "lp_len",
81 default: "30",
82 description: "LowPass (SuperSmoother) length",
83 },
84 ParamDef {
85 name: "q",
86 default: "0.35",
87 description: "Damping/Predictor coefficient",
88 },
89 ],
90 formula_source: "https://github.com/lavs9/quantwave/blob/main/references/traderstipsreference/TRADERS’%20TIPS%20-%20JANUARY%202025.html",
91 formula_latex: r#"
92\[
93Predict = \frac{Signal - 1.8Q \cdot Signal_{t-1} + Q^2 \cdot Signal_{t-2}}{1 - 1.8Q + Q^2}
94\]
95"#,
96 gold_standard_file: "simple_predictor.json",
97 category: "Ehlers DSP",
98};
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::traits::Next;
104 use proptest::prelude::*;
105
106 #[test]
107 fn test_simple_predictor_basic() {
108 let mut sp = SimplePredictor::new(15, 30, 0.35);
109 for i in 0..50 {
110 let val = sp.next(100.0 + i as f64);
111 assert!(!val.is_nan());
112 }
113 }
114
115 proptest! {
116 #[test]
117 fn test_simple_predictor_parity(
118 inputs in prop::collection::vec(1.0..100.0, 50..100),
119 ) {
120 let hp_len = 15;
121 let lp_len = 30;
122 let q = 0.35;
123 let mut sp = SimplePredictor::new(hp_len, lp_len, q);
124 let streaming_results: Vec<f64> = inputs.iter().map(|&x| sp.next(x)).collect();
125
126 let mut batch_results = Vec::with_capacity(inputs.len());
128 let mut hp = HighPass::new(hp_len);
129 let mut ss = SuperSmoother::new(lp_len);
130 let signal_vals: Vec<f64> = inputs.iter().map(|&x| ss.next(hp.next(x))).collect();
131
132 let c1 = 1.8 * q;
133 let c2 = -q * q;
134 let sum = 1.0 - c1 - c2;
135
136 for (i, &signal) in signal_vals.iter().enumerate() {
137 let bar = i + 1;
138 let res = if bar < 3 {
139 signal
140 } else {
141 (signal - c1 * signal_vals[i-1] - c2 * signal_vals[i-2]) / sum
142 };
143 batch_results.push(res);
144 }
145
146 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
147 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
148 }
149 }
150 }
151}