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: &["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); // Initialized to first value
148        
149        // Trend up: price goes to 101, 102, 103
150        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            // Reference implementation (batch)
170            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                // Predict
180                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                // Update
187                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}