wickra_core/indicators/
kalman_hedge_ratio.rs1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct KalmanHedgeRatioOutput {
9 pub hedge_ratio: f64,
11 pub intercept: f64,
13 pub spread: f64,
16}
17
18#[derive(Debug, Clone)]
60pub struct KalmanHedgeRatio {
61 delta: f64,
62 transition_var: f64,
63 observation_var: f64,
64 beta: f64,
65 alpha: f64,
66 cov: [[f64; 2]; 2],
68 count: usize,
69}
70
71impl KalmanHedgeRatio {
72 pub fn new(delta: f64, observation_var: f64) -> Result<Self> {
81 if !delta.is_finite() || delta <= 0.0 || delta >= 1.0 {
82 return Err(Error::InvalidParameter {
83 message: "kalman hedge ratio needs delta in (0, 1)",
84 });
85 }
86 if !observation_var.is_finite() || observation_var <= 0.0 {
87 return Err(Error::InvalidParameter {
88 message: "kalman hedge ratio needs observation_var > 0",
89 });
90 }
91 Ok(Self {
92 delta,
93 transition_var: delta / (1.0 - delta),
94 observation_var,
95 beta: 0.0,
96 alpha: 0.0,
97 cov: [[0.0; 2]; 2],
98 count: 0,
99 })
100 }
101
102 pub const fn delta(&self) -> f64 {
104 self.delta
105 }
106
107 pub const fn observation_var(&self) -> f64 {
109 self.observation_var
110 }
111}
112
113impl Indicator for KalmanHedgeRatio {
114 type Input = (f64, f64);
115 type Output = KalmanHedgeRatioOutput;
116
117 #[inline]
118 fn update(&mut self, input: (f64, f64)) -> Option<KalmanHedgeRatioOutput> {
119 let (a, b) = input;
120 if !a.is_finite() || !b.is_finite() {
121 return None;
122 }
123 let mut cov_pred = self.cov;
126 if self.count > 0 {
127 cov_pred[0][0] += self.transition_var;
128 cov_pred[1][1] += self.transition_var;
129 }
130 let predicted = self.beta * b + self.alpha;
132 let innovation = a - predicted;
133 let fr0 = b * cov_pred[0][0] + cov_pred[1][0];
135 let fr1 = b * cov_pred[0][1] + cov_pred[1][1];
136 let innovation_var = fr0 * b + fr1 + self.observation_var;
138 let rft0 = cov_pred[0][0] * b + cov_pred[0][1];
140 let rft1 = cov_pred[1][0] * b + cov_pred[1][1];
141 let gain0 = rft0 / innovation_var;
142 let gain1 = rft1 / innovation_var;
143 self.beta += gain0 * innovation;
145 self.alpha += gain1 * innovation;
146 self.cov[0][0] = cov_pred[0][0] - gain0 * fr0;
148 self.cov[0][1] = cov_pred[0][1] - gain0 * fr1;
149 self.cov[1][0] = cov_pred[1][0] - gain1 * fr0;
150 self.cov[1][1] = cov_pred[1][1] - gain1 * fr1;
151 self.count += 1;
152 Some(KalmanHedgeRatioOutput {
153 hedge_ratio: self.beta,
154 intercept: self.alpha,
155 spread: innovation,
156 })
157 }
158
159 fn reset(&mut self) {
160 self.beta = 0.0;
161 self.alpha = 0.0;
162 self.cov = [[0.0; 2]; 2];
163 self.count = 0;
164 }
165
166 #[inline]
167 fn warmup_period(&self) -> usize {
168 1
169 }
170
171 #[inline]
172 fn is_ready(&self) -> bool {
173 self.count >= 1
174 }
175
176 #[inline]
177 fn name(&self) -> &'static str {
178 "KalmanHedgeRatio"
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::traits::BatchExt;
186
187 #[test]
188 fn rejects_bad_parameters() {
189 assert!(KalmanHedgeRatio::new(0.0, 1.0).is_err());
190 assert!(KalmanHedgeRatio::new(1.0, 1.0).is_err());
191 assert!(KalmanHedgeRatio::new(-0.1, 1.0).is_err());
192 assert!(KalmanHedgeRatio::new(f64::NAN, 1.0).is_err());
193 assert!(KalmanHedgeRatio::new(0.001, 0.0).is_err());
194 assert!(KalmanHedgeRatio::new(0.001, -1.0).is_err());
195 assert!(KalmanHedgeRatio::new(0.001, f64::INFINITY).is_err());
196 assert!(KalmanHedgeRatio::new(0.001, 0.001).is_ok());
197 }
198
199 #[test]
200 fn accessors_and_metadata() {
201 let k = KalmanHedgeRatio::new(0.001, 0.01).unwrap();
202 assert_eq!(k.delta(), 0.001);
203 assert_eq!(k.observation_var(), 0.01);
204 assert_eq!(k.warmup_period(), 1);
205 assert_eq!(k.name(), "KalmanHedgeRatio");
206 assert!(!k.is_ready());
207 }
208
209 #[test]
210 fn emits_from_first_update() {
211 let mut k = KalmanHedgeRatio::new(0.001, 0.001).unwrap();
212 let first = k.update((10.0, 5.0)).unwrap();
213 assert_eq!(first.hedge_ratio, 0.0);
215 assert_eq!(first.intercept, 0.0);
216 assert_eq!(first.spread, 10.0);
217 assert!(k.is_ready());
218 }
219
220 #[test]
221 fn converges_to_static_relationship() {
222 let pairs: Vec<(f64, f64)> = (0..500)
225 .map(|t| {
226 let b = 100.0 + (f64::from(t) * 0.5).sin() * 95.0;
227 (2.0 * b + 5.0, b)
228 })
229 .collect();
230 let out = KalmanHedgeRatio::new(1e-2, 1e-3)
231 .unwrap()
232 .batch(&pairs)
233 .into_iter()
234 .flatten()
235 .last()
236 .unwrap();
237 assert!(
238 (out.hedge_ratio - 2.0).abs() < 0.05,
239 "beta {}",
240 out.hedge_ratio
241 );
242 assert!((out.intercept - 5.0).abs() < 1.0, "alpha {}", out.intercept);
243 assert!(out.spread.abs() < 0.05, "spread {}", out.spread);
244 }
245
246 #[test]
247 fn tracks_a_changing_hedge_ratio() {
248 let mut pairs: Vec<(f64, f64)> = (0..300)
251 .map(|t| {
252 let b = 100.0 + (f64::from(t) * 0.5).sin() * 95.0;
253 (2.0 * b + 5.0, b)
254 })
255 .collect();
256 pairs.extend((0..300).map(|t| {
257 let b = 100.0 + (f64::from(t) * 0.5).cos() * 95.0;
258 (3.0 * b + 5.0, b)
259 }));
260 let out = KalmanHedgeRatio::new(1e-2, 1e-3)
261 .unwrap()
262 .batch(&pairs)
263 .into_iter()
264 .flatten()
265 .last()
266 .unwrap();
267 assert!(out.hedge_ratio > 2.5, "beta {}", out.hedge_ratio);
268 }
269
270 #[test]
271 fn reset_clears_state() {
272 let mut k = KalmanHedgeRatio::new(0.001, 0.001).unwrap();
273 for t in 0..50 {
274 let b = 100.0 + f64::from(t);
275 k.update((2.0 * b, b));
276 }
277 assert!(k.is_ready());
278 k.reset();
279 assert!(!k.is_ready());
280 let first = k.update((10.0, 5.0)).unwrap();
281 assert_eq!(first.hedge_ratio, 0.0);
282 }
283
284 #[test]
285 fn batch_equals_streaming() {
286 let pairs: Vec<(f64, f64)> = (0..120)
287 .map(|t| {
288 let b = 30.0 + 0.7 * f64::from(t);
289 (1.8 * b + 2.0 + (f64::from(t) * 0.4).sin(), b)
290 })
291 .collect();
292 let batch = KalmanHedgeRatio::new(1e-3, 1e-2).unwrap().batch(&pairs);
293 let mut k = KalmanHedgeRatio::new(1e-3, 1e-2).unwrap();
294 let streamed: Vec<_> = pairs.iter().map(|p| k.update(*p)).collect();
295 assert_eq!(batch, streamed);
296 }
297}