quantwave_core/indicators/
kinematic_kalman.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3
4#[derive(Debug, Clone)]
12pub struct KinematicKalmanFilter {
13 q_pos: f64,
14 q_vel: f64,
15 r: f64,
16 x: f64,
18 v: f64,
19 p00: f64,
21 p01: f64,
22 p11: f64,
23 initialized: bool,
24}
25
26impl KinematicKalmanFilter {
27 pub fn new(q_pos: f64, q_vel: f64, r: f64) -> Self {
28 Self {
29 q_pos,
30 q_vel,
31 r,
32 x: 0.0,
33 v: 0.0,
34 p00: 1.0,
35 p01: 0.0,
36 p11: 1.0,
37 initialized: false,
38 }
39 }
40}
41
42impl Next<f64> for KinematicKalmanFilter {
43 type Output = f64;
44
45 fn next(&mut self, input: f64) -> Self::Output {
46 if !self.initialized {
47 self.x = input;
48 self.v = 0.0;
49 self.initialized = true;
50 return self.x;
51 }
52
53 let x_pred = self.x + self.v;
58 let v_pred = self.v;
59
60 let p00_pred = self.p00 + 2.0 * self.p01 + self.p11 + self.q_pos;
63 let p01_pred = self.p01 + self.p11;
64 let p11_pred = self.p11 + self.q_vel;
65
66 let s = p00_pred + self.r;
70
71 let k0 = p00_pred / s;
73 let k1 = p01_pred / s;
74
75 let innovation = input - x_pred;
77 self.x = x_pred + k0 * innovation;
78 self.v = v_pred + k1 * innovation;
79
80 self.p00 = (1.0 - k0) * p00_pred;
85 self.p01 = (1.0 - k0) * p01_pred;
86 self.p11 = -k1 * p01_pred + p11_pred;
87
88 self.x
89 }
90}
91
92pub const KINEMATIC_KALMAN_METADATA: IndicatorMetadata = IndicatorMetadata {
93 name: "Kinematic Kalman Filter",
94 description: "A 2D Kalman filter tracking price and velocity to reduce lag in trends.",
95 usage: "Optimized for trend-following strategies where lag reduction is critical. q_pos controls price sensitivity, q_vel controls momentum sensitivity, and r controls overall smoothing.",
96 keywords: &["kalman", "adaptive", "kinematic", "momentum", "lag-reduction"],
97 ehlers_summary: "The Kinematic Kalman Filter extends the 1D model by incorporating a velocity state. This allows the filter to 'anticipate' the next price based on current momentum, providing a zero-lag-like response during strong trends while maintaining smoothness via its optimal error-correction logic.",
98 params: &[
99 ParamDef {
100 name: "q_pos",
101 default: "0.001",
102 description: "Process noise for position (price)",
103 },
104 ParamDef {
105 name: "q_vel",
106 default: "0.0001",
107 description: "Process noise for velocity (momentum)",
108 },
109 ParamDef {
110 name: "r",
111 default: "0.1",
112 description: "Measurement noise (smoothing strength)",
113 },
114 ],
115 formula_source: "https://www.cs.unc.edu/~welch/kalman/media/pdf/Kalman1960.pdf",
116 formula_latex: r#"
117\[
118\hat{x}_{k|k-1} = \Phi \hat{x}_{k-1|k-1}
119\]
120\[
121P_{k|k-1} = \Phi P_{k-1|k-1} \Phi^T + Q
122\]
123\[
124K_k = P_{k|k-1} H^T (H P_{k|k-1} H^T + R)^{-1}
125\]
126\[
127\hat{x}_{k|k} = \hat{x}_{k|k-1} + K_k (z_k - H \hat{x}_{k|k-1})
128\]
129\[
130P_{k|k} = (I - K_k H) P_{k|k-1}
131\]
132"#,
133 gold_standard_file: "kinematic_kalman.json",
134 category: "ML Features",
135};
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::traits::Next;
141 use proptest::prelude::*;
142
143 #[test]
144 fn test_kinematic_kalman_basic() {
145 let mut kf = KinematicKalmanFilter::new(0.001, 0.0001, 0.1);
146 let res = kf.next(100.0);
147 assert_eq!(res, 100.0); let res2 = kf.next(101.0);
151 assert!(res2 > 100.0 && res2 < 101.0);
152
153 let res3 = kf.next(102.0);
154 assert!(res3 > res2);
155 }
156
157 proptest! {
158 #[test]
159 fn test_kinematic_kalman_parity(
160 inputs in prop::collection::vec(10.0..200.0, 50..100),
161 ) {
162 let q_pos = 0.001;
163 let q_vel = 0.0001;
164 let r = 0.1;
165
166 let mut kf = KinematicKalmanFilter::new(q_pos, q_vel, r);
167 let streaming_results: Vec<f64> = inputs.iter().map(|&x| kf.next(x)).collect();
168
169 let mut batch_results = Vec::with_capacity(inputs.len());
171 let mut x = inputs[0];
172 let mut v = 0.0;
173 let mut p00 = 1.0;
174 let mut p01 = 0.0;
175 let mut p11 = 1.0;
176 batch_results.push(x);
177
178 for i in 1..inputs.len() {
179 let x_pred = x + v;
181 let v_pred = v;
182 let p00_pred = p00 + 2.0 * p01 + p11 + q_pos;
183 let p01_pred = p01 + p11;
184 let p11_pred = p11 + q_vel;
185
186 let s = p00_pred + r;
188 let k0 = p00_pred / s;
189 let k1 = p01_pred / s;
190
191 let innovation = inputs[i] - x_pred;
192 x = x_pred + k0 * innovation;
193 v = v_pred + k1 * innovation;
194
195 p00 = (1.0 - k0) * p00_pred;
196 p01 = (1.0 - k0) * p01_pred;
197 p11 = -k1 * p01_pred + p11_pred;
198
199 batch_results.push(x);
200 }
201
202 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
203 approx::assert_relative_eq!(s, b, epsilon = 1e-10);
204 }
205 }
206 }
207}