wickra_core/indicators/
ou_half_life.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
48pub struct OuHalfLife {
49 period: usize,
50 window: VecDeque<f64>,
51}
52
53impl OuHalfLife {
54 pub fn new(period: usize) -> Result<Self> {
60 if period < 3 {
61 return Err(Error::InvalidPeriod {
62 message: "OU half-life needs period >= 3",
63 });
64 }
65 Ok(Self {
66 period,
67 window: VecDeque::with_capacity(period),
68 })
69 }
70
71 pub const fn period(&self) -> usize {
73 self.period
74 }
75}
76
77impl Indicator for OuHalfLife {
78 type Input = (f64, f64);
79 type Output = f64;
80
81 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
82 let (a, b) = input;
83 if !a.is_finite() || !b.is_finite() {
84 return None;
85 }
86 if self.window.len() == self.period {
87 self.window.pop_front();
88 }
89 self.window.push_back(a - b);
90 if self.window.len() < self.period {
91 return None;
92 }
93 let spreads: Vec<f64> = self.window.iter().copied().collect();
95 let count = (spreads.len() - 1) as f64;
96 let mut sum_level = 0.0;
97 let mut sum_delta = 0.0;
98 let mut sum_ll = 0.0;
99 let mut sum_ld = 0.0;
100 for pair in spreads.windows(2) {
101 let level = pair[0];
102 let delta = pair[1] - pair[0];
103 sum_level += level;
104 sum_delta += delta;
105 sum_ll += level * level;
106 sum_ld += level * delta;
107 }
108 let mean_level = sum_level / count;
109 let mean_delta = sum_delta / count;
110 let var_level = sum_ll / count - mean_level * mean_level;
111 if var_level <= 0.0 {
112 return Some(0.0);
114 }
115 let cov = sum_ld / count - mean_level * mean_delta;
116 let lambda = cov / var_level;
117 if lambda >= 0.0 {
118 return Some(0.0);
120 }
121 Some(-std::f64::consts::LN_2 / lambda)
122 }
123
124 fn reset(&mut self) {
125 self.window.clear();
126 }
127
128 fn warmup_period(&self) -> usize {
129 self.period
130 }
131
132 fn is_ready(&self) -> bool {
133 self.window.len() == self.period
134 }
135
136 fn name(&self) -> &'static str {
137 "OuHalfLife"
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::traits::BatchExt;
145
146 #[test]
147 fn rejects_period_below_three() {
148 assert!(OuHalfLife::new(2).is_err());
149 assert!(OuHalfLife::new(3).is_ok());
150 }
151
152 #[test]
153 fn accessors_and_metadata() {
154 let hl = OuHalfLife::new(30).unwrap();
155 assert_eq!(hl.period(), 30);
156 assert_eq!(hl.warmup_period(), 30);
157 assert_eq!(hl.name(), "OuHalfLife");
158 assert!(!hl.is_ready());
159 }
160
161 #[test]
162 fn warmup_returns_none() {
163 let mut hl = OuHalfLife::new(4).unwrap();
164 assert_eq!(hl.update((1.0, 0.0)), None);
165 assert_eq!(hl.update((2.0, 0.0)), None);
166 assert_eq!(hl.update((3.0, 0.0)), None);
167 assert!(hl.update((4.0, 0.0)).is_some());
168 assert!(hl.is_ready());
169 }
170
171 #[test]
172 fn mean_reverting_spread_has_positive_half_life() {
173 let pairs: Vec<(f64, f64)> = (0..120)
175 .map(|t| {
176 let b = 100.0 + f64::from(t);
177 let a = b + 2.0 * (f64::from(t) * 0.9).sin();
178 (a, b)
179 })
180 .collect();
181 let last = OuHalfLife::new(40)
182 .unwrap()
183 .batch(&pairs)
184 .into_iter()
185 .flatten()
186 .last()
187 .unwrap();
188 assert!(last > 0.0 && last < 40.0, "half-life {last}");
189 }
190
191 #[test]
192 fn trending_spread_has_zero_half_life() {
193 let pairs: Vec<(f64, f64)> = (0..40)
195 .map(|t| (2.0 * f64::from(t), f64::from(t)))
196 .collect();
197 let last = OuHalfLife::new(20)
198 .unwrap()
199 .batch(&pairs)
200 .into_iter()
201 .flatten()
202 .last()
203 .unwrap();
204 assert_eq!(last, 0.0);
205 }
206
207 #[test]
208 fn flat_spread_returns_zero() {
209 let pairs: Vec<(f64, f64)> = (0..30)
211 .map(|t| (5.0 + f64::from(t), f64::from(t)))
212 .collect();
213 let last = OuHalfLife::new(10)
214 .unwrap()
215 .batch(&pairs)
216 .into_iter()
217 .flatten()
218 .last()
219 .unwrap();
220 assert_eq!(last, 0.0);
221 }
222
223 #[test]
224 fn reset_clears_state() {
225 let mut hl = OuHalfLife::new(5).unwrap();
226 for t in 0..10 {
227 hl.update((f64::from(t) + (f64::from(t) * 0.7).sin(), f64::from(t)));
228 }
229 assert!(hl.is_ready());
230 hl.reset();
231 assert!(!hl.is_ready());
232 assert_eq!(hl.update((1.0, 0.0)), None);
233 }
234
235 #[test]
236 fn batch_equals_streaming() {
237 let pairs: Vec<(f64, f64)> = (0..80)
238 .map(|t| {
239 let b = 50.0 + 0.5 * f64::from(t);
240 (b + (f64::from(t) * 0.6).sin(), b)
241 })
242 .collect();
243 let batch = OuHalfLife::new(25).unwrap().batch(&pairs);
244 let mut hl = OuHalfLife::new(25).unwrap();
245 let streamed: Vec<_> = pairs.iter().map(|p| hl.update(*p)).collect();
246 assert_eq!(batch, streamed);
247 }
248
249 #[test]
250 fn non_finite_input_returns_none() {
251 let mut hl = OuHalfLife::new(4).unwrap();
252 assert_eq!(hl.update((f64::NAN, 1.0)), None);
253 assert_eq!(hl.update((1.0, f64::INFINITY)), None);
254 assert_eq!(hl.update((1.0, 0.0)), None);
256 assert_eq!(hl.update((2.0, 0.0)), None);
257 assert_eq!(hl.update((3.0, 0.0)), None);
258 assert!(hl.update((4.0, 0.0)).is_some());
259 }
260}