Skip to main content

quantwave_core/indicators/
kinematic_kalman.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3
4/// Kinematic Kalman Filter (2D)
5///
6/// A second-order Kalman filter that tracks both price (position) and momentum (velocity).
7/// By modeling the price as a moving object with velocity, this filter significantly
8/// reduces lag during trending markets compared to 1D filters.
9///
10/// It implements the recursive state-space equations from R.E. Kalman's 1960 paper.
11#[derive(Debug, Clone)]
12pub struct KinematicKalmanFilter {
13    q_pos: f64,
14    q_vel: f64,
15    r: f64,
16    // State: [position, velocity]
17    x: f64,
18    v: f64,
19    // Covariance matrix P (symmetric: p01 == p10)
20    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        // 1. Prediction (Time Update)
54        // Transition matrix F = [[1, 1], [0, 1]]
55        // x_pred = x + v
56        // v_pred = v
57        let x_pred = self.x + self.v;
58        let v_pred = self.v;
59
60        // P_pred = F * P * F^T + Q
61        // Q = [[q_pos, 0], [0, q_vel]]
62        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        // 2. Correction (Measurement Update)
67        // Measurement matrix H = [1, 0]
68        // Innovation S = H * P_pred * H^T + R
69        let s = p00_pred + self.r;
70
71        // Kalman Gain K = P_pred * H^T * inv(S)
72        let k0 = p00_pred / s;
73        let k1 = p01_pred / s;
74
75        // Update State: X = X_pred + K * (input - H * X_pred)
76        let innovation = input - x_pred;
77        self.x = x_pred + k0 * innovation;
78        self.v = v_pred + k1 * innovation;
79
80        // Update Covariance: P = (I - K * H) * P_pred
81        // p00 = (1 - k0) * p00_pred
82        // p01 = (1 - k0) * p01_pred
83        // p11 = -k1 * p01_pred + p11_pred
84        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: &[
97        "kalman",
98        "adaptive",
99        "kinematic",
100        "momentum",
101        "lag-reduction",
102    ],
103    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.",
104    params: &[
105        ParamDef {
106            name: "q_pos",
107            default: "0.001",
108            description: "Process noise for position (price)",
109        },
110        ParamDef {
111            name: "q_vel",
112            default: "0.0001",
113            description: "Process noise for velocity (momentum)",
114        },
115        ParamDef {
116            name: "r",
117            default: "0.1",
118            description: "Measurement noise (smoothing strength)",
119        },
120    ],
121    formula_source: "https://www.cs.unc.edu/~welch/kalman/media/pdf/Kalman1960.pdf",
122    formula_latex: r#"
123\[
124\hat{x}_{k|k-1} = \Phi \hat{x}_{k-1|k-1}
125\]
126\[
127P_{k|k-1} = \Phi P_{k-1|k-1} \Phi^T + Q
128\]
129\[
130K_k = P_{k|k-1} H^T (H P_{k|k-1} H^T + R)^{-1}
131\]
132\[
133\hat{x}_{k|k} = \hat{x}_{k|k-1} + K_k (z_k - H \hat{x}_{k|k-1})
134\]
135\[
136P_{k|k} = (I - K_k H) P_{k|k-1}
137\]
138"#,
139    gold_standard_file: "kinematic_kalman.json",
140    category: "ML Features",
141};
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::traits::Next;
147    use proptest::prelude::*;
148
149    #[test]
150    fn test_kinematic_kalman_basic() {
151        let mut kf = KinematicKalmanFilter::new(0.001, 0.0001, 0.1);
152        let res = kf.next(100.0);
153        assert_eq!(res, 100.0); // Initialized to first value
154
155        // Trend up: price goes to 101, 102, 103
156        let res2 = kf.next(101.0);
157        assert!(res2 > 100.0 && res2 < 101.0);
158
159        let res3 = kf.next(102.0);
160        assert!(res3 > res2);
161    }
162
163    proptest! {
164        #[test]
165        fn test_kinematic_kalman_parity(
166            inputs in prop::collection::vec(10.0..200.0, 50..100),
167        ) {
168            let q_pos = 0.001;
169            let q_vel = 0.0001;
170            let r = 0.1;
171
172            let mut kf = KinematicKalmanFilter::new(q_pos, q_vel, r);
173            let streaming_results: Vec<f64> = inputs.iter().map(|&x| kf.next(x)).collect();
174
175            // Reference implementation (batch)
176            let mut batch_results = Vec::with_capacity(inputs.len());
177            let mut x = inputs[0];
178            let mut v = 0.0;
179            let mut p00 = 1.0;
180            let mut p01 = 0.0;
181            let mut p11 = 1.0;
182            batch_results.push(x);
183
184            for i in 1..inputs.len() {
185                // Predict
186                let x_pred = x + v;
187                let v_pred = v;
188                let p00_pred = p00 + 2.0 * p01 + p11 + q_pos;
189                let p01_pred = p01 + p11;
190                let p11_pred = p11 + q_vel;
191
192                // Update
193                let s = p00_pred + r;
194                let k0 = p00_pred / s;
195                let k1 = p01_pred / s;
196
197                let innovation = inputs[i] - x_pred;
198                x = x_pred + k0 * innovation;
199                v = v_pred + k1 * innovation;
200
201                p00 = (1.0 - k0) * p00_pred;
202                p01 = (1.0 - k0) * p01_pred;
203                p11 = -k1 * p01_pred + p11_pred;
204
205                batch_results.push(x);
206            }
207
208            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
209                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
210            }
211        }
212    }
213}