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 self.window.len() == self.period {
84 self.window.pop_front();
85 }
86 self.window.push_back(a - b);
87 if self.window.len() < self.period {
88 return None;
89 }
90 let spreads: Vec<f64> = self.window.iter().copied().collect();
92 let count = (spreads.len() - 1) as f64;
93 let mut sum_level = 0.0;
94 let mut sum_delta = 0.0;
95 let mut sum_ll = 0.0;
96 let mut sum_ld = 0.0;
97 for pair in spreads.windows(2) {
98 let level = pair[0];
99 let delta = pair[1] - pair[0];
100 sum_level += level;
101 sum_delta += delta;
102 sum_ll += level * level;
103 sum_ld += level * delta;
104 }
105 let mean_level = sum_level / count;
106 let mean_delta = sum_delta / count;
107 let var_level = sum_ll / count - mean_level * mean_level;
108 if var_level <= 0.0 {
109 return Some(0.0);
111 }
112 let cov = sum_ld / count - mean_level * mean_delta;
113 let lambda = cov / var_level;
114 if lambda >= 0.0 {
115 return Some(0.0);
117 }
118 Some(-std::f64::consts::LN_2 / lambda)
119 }
120
121 fn reset(&mut self) {
122 self.window.clear();
123 }
124
125 fn warmup_period(&self) -> usize {
126 self.period
127 }
128
129 fn is_ready(&self) -> bool {
130 self.window.len() == self.period
131 }
132
133 fn name(&self) -> &'static str {
134 "OuHalfLife"
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::traits::BatchExt;
142
143 #[test]
144 fn rejects_period_below_three() {
145 assert!(OuHalfLife::new(2).is_err());
146 assert!(OuHalfLife::new(3).is_ok());
147 }
148
149 #[test]
150 fn accessors_and_metadata() {
151 let hl = OuHalfLife::new(30).unwrap();
152 assert_eq!(hl.period(), 30);
153 assert_eq!(hl.warmup_period(), 30);
154 assert_eq!(hl.name(), "OuHalfLife");
155 assert!(!hl.is_ready());
156 }
157
158 #[test]
159 fn warmup_returns_none() {
160 let mut hl = OuHalfLife::new(4).unwrap();
161 assert_eq!(hl.update((1.0, 0.0)), None);
162 assert_eq!(hl.update((2.0, 0.0)), None);
163 assert_eq!(hl.update((3.0, 0.0)), None);
164 assert!(hl.update((4.0, 0.0)).is_some());
165 assert!(hl.is_ready());
166 }
167
168 #[test]
169 fn mean_reverting_spread_has_positive_half_life() {
170 let pairs: Vec<(f64, f64)> = (0..120)
172 .map(|t| {
173 let b = 100.0 + f64::from(t);
174 let a = b + 2.0 * (f64::from(t) * 0.9).sin();
175 (a, b)
176 })
177 .collect();
178 let last = OuHalfLife::new(40)
179 .unwrap()
180 .batch(&pairs)
181 .into_iter()
182 .flatten()
183 .last()
184 .unwrap();
185 assert!(last > 0.0 && last < 40.0, "half-life {last}");
186 }
187
188 #[test]
189 fn trending_spread_has_zero_half_life() {
190 let pairs: Vec<(f64, f64)> = (0..40)
192 .map(|t| (2.0 * f64::from(t), f64::from(t)))
193 .collect();
194 let last = OuHalfLife::new(20)
195 .unwrap()
196 .batch(&pairs)
197 .into_iter()
198 .flatten()
199 .last()
200 .unwrap();
201 assert_eq!(last, 0.0);
202 }
203
204 #[test]
205 fn flat_spread_returns_zero() {
206 let pairs: Vec<(f64, f64)> = (0..30)
208 .map(|t| (5.0 + f64::from(t), f64::from(t)))
209 .collect();
210 let last = OuHalfLife::new(10)
211 .unwrap()
212 .batch(&pairs)
213 .into_iter()
214 .flatten()
215 .last()
216 .unwrap();
217 assert_eq!(last, 0.0);
218 }
219
220 #[test]
221 fn reset_clears_state() {
222 let mut hl = OuHalfLife::new(5).unwrap();
223 for t in 0..10 {
224 hl.update((f64::from(t) + (f64::from(t) * 0.7).sin(), f64::from(t)));
225 }
226 assert!(hl.is_ready());
227 hl.reset();
228 assert!(!hl.is_ready());
229 assert_eq!(hl.update((1.0, 0.0)), None);
230 }
231
232 #[test]
233 fn batch_equals_streaming() {
234 let pairs: Vec<(f64, f64)> = (0..80)
235 .map(|t| {
236 let b = 50.0 + 0.5 * f64::from(t);
237 (b + (f64::from(t) * 0.6).sin(), b)
238 })
239 .collect();
240 let batch = OuHalfLife::new(25).unwrap().batch(&pairs);
241 let mut hl = OuHalfLife::new(25).unwrap();
242 let streamed: Vec<_> = pairs.iter().map(|p| hl.update(*p)).collect();
243 assert_eq!(batch, streamed);
244 }
245}