use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
#[derive(Debug, Clone)]
pub struct KinematicKalmanFilter {
q_pos: f64,
q_vel: f64,
r: f64,
x: f64,
v: f64,
p00: f64,
p01: f64,
p11: f64,
initialized: bool,
}
impl KinematicKalmanFilter {
pub fn new(q_pos: f64, q_vel: f64, r: f64) -> Self {
Self {
q_pos,
q_vel,
r,
x: 0.0,
v: 0.0,
p00: 1.0,
p01: 0.0,
p11: 1.0,
initialized: false,
}
}
}
impl Next<f64> for KinematicKalmanFilter {
type Output = f64;
fn next(&mut self, input: f64) -> Self::Output {
if !self.initialized {
self.x = input;
self.v = 0.0;
self.initialized = true;
return self.x;
}
let x_pred = self.x + self.v;
let v_pred = self.v;
let p00_pred = self.p00 + 2.0 * self.p01 + self.p11 + self.q_pos;
let p01_pred = self.p01 + self.p11;
let p11_pred = self.p11 + self.q_vel;
let s = p00_pred + self.r;
let k0 = p00_pred / s;
let k1 = p01_pred / s;
let innovation = input - x_pred;
self.x = x_pred + k0 * innovation;
self.v = v_pred + k1 * innovation;
self.p00 = (1.0 - k0) * p00_pred;
self.p01 = (1.0 - k0) * p01_pred;
self.p11 = -k1 * p01_pred + p11_pred;
self.x
}
}
pub const KINEMATIC_KALMAN_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "Kinematic Kalman Filter",
description: "A 2D Kalman filter tracking price and velocity to reduce lag in trends.",
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.",
keywords: &["kalman", "adaptive", "kinematic", "momentum", "lag-reduction"],
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.",
params: &[
ParamDef {
name: "q_pos",
default: "0.001",
description: "Process noise for position (price)",
},
ParamDef {
name: "q_vel",
default: "0.0001",
description: "Process noise for velocity (momentum)",
},
ParamDef {
name: "r",
default: "0.1",
description: "Measurement noise (smoothing strength)",
},
],
formula_source: "https://www.cs.unc.edu/~welch/kalman/media/pdf/Kalman1960.pdf",
formula_latex: r#"
\[
\hat{x}_{k|k-1} = \Phi \hat{x}_{k-1|k-1}
\]
\[
P_{k|k-1} = \Phi P_{k-1|k-1} \Phi^T + Q
\]
\[
K_k = P_{k|k-1} H^T (H P_{k|k-1} H^T + R)^{-1}
\]
\[
\hat{x}_{k|k} = \hat{x}_{k|k-1} + K_k (z_k - H \hat{x}_{k|k-1})
\]
\[
P_{k|k} = (I - K_k H) P_{k|k-1}
\]
"#,
gold_standard_file: "kinematic_kalman.json",
category: "ML Features",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
use proptest::prelude::*;
#[test]
fn test_kinematic_kalman_basic() {
let mut kf = KinematicKalmanFilter::new(0.001, 0.0001, 0.1);
let res = kf.next(100.0);
assert_eq!(res, 100.0);
let res2 = kf.next(101.0);
assert!(res2 > 100.0 && res2 < 101.0);
let res3 = kf.next(102.0);
assert!(res3 > res2);
}
proptest! {
#[test]
fn test_kinematic_kalman_parity(
inputs in prop::collection::vec(10.0..200.0, 50..100),
) {
let q_pos = 0.001;
let q_vel = 0.0001;
let r = 0.1;
let mut kf = KinematicKalmanFilter::new(q_pos, q_vel, r);
let streaming_results: Vec<f64> = inputs.iter().map(|&x| kf.next(x)).collect();
let mut batch_results = Vec::with_capacity(inputs.len());
let mut x = inputs[0];
let mut v = 0.0;
let mut p00 = 1.0;
let mut p01 = 0.0;
let mut p11 = 1.0;
batch_results.push(x);
for i in 1..inputs.len() {
let x_pred = x + v;
let v_pred = v;
let p00_pred = p00 + 2.0 * p01 + p11 + q_pos;
let p01_pred = p01 + p11;
let p11_pred = p11 + q_vel;
let s = p00_pred + r;
let k0 = p00_pred / s;
let k1 = p01_pred / s;
let innovation = inputs[i] - x_pred;
x = x_pred + k0 * innovation;
v = v_pred + k1 * innovation;
p00 = (1.0 - k0) * p00_pred;
p01 = (1.0 - k0) * p01_pred;
p11 = -k1 * p01_pred + p11_pred;
batch_results.push(x);
}
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
approx::assert_relative_eq!(s, b, epsilon = 1e-10);
}
}
}
}