wickra_core/indicators/
expectancy.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
43pub struct Expectancy {
44 period: usize,
45 window: VecDeque<f64>,
46 sum: f64,
47 sum_abs_loss: f64,
48 loss_count: usize,
49}
50
51impl Expectancy {
52 pub fn new(period: usize) -> Result<Self> {
57 if period == 0 {
58 return Err(Error::PeriodZero);
59 }
60 if period > crate::error::MAX_PERIOD {
61 return Err(Error::InvalidPeriod {
62 message: crate::error::PERIOD_ABOVE_MAX,
63 });
64 }
65 Ok(Self {
66 period,
67 window: VecDeque::with_capacity(period),
68 sum: 0.0,
69 sum_abs_loss: 0.0,
70 loss_count: 0,
71 })
72 }
73
74 pub const fn period(&self) -> usize {
76 self.period
77 }
78}
79
80impl Indicator for Expectancy {
81 type Input = f64;
82 type Output = f64;
83
84 #[inline]
85 fn update(&mut self, ret: f64) -> Option<f64> {
86 if !ret.is_finite() {
87 return None;
88 }
89 if self.window.len() == self.period {
90 let old = self.window.pop_front().expect("window is non-empty");
91 self.sum -= old;
92 if old < 0.0 {
93 self.sum_abs_loss -= -old;
94 self.loss_count -= 1;
95 }
96 }
97 self.window.push_back(ret);
98 self.sum += ret;
99 if ret < 0.0 {
100 self.sum_abs_loss += -ret;
101 self.loss_count += 1;
102 }
103 if self.window.len() < self.period {
104 return None;
105 }
106 if self.loss_count == 0 {
107 return Some(0.0);
109 }
110 let mean = self.sum / self.period as f64;
111 let avg_loss = self.sum_abs_loss / self.loss_count as f64;
112 Some(mean / avg_loss)
113 }
114
115 fn reset(&mut self) {
116 self.window.clear();
117 self.sum = 0.0;
118 self.sum_abs_loss = 0.0;
119 self.loss_count = 0;
120 }
121
122 #[inline]
123 fn warmup_period(&self) -> usize {
124 self.period
125 }
126
127 #[inline]
128 fn is_ready(&self) -> bool {
129 self.window.len() == self.period
130 }
131
132 #[inline]
133 fn name(&self) -> &'static str {
134 "Expectancy"
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::traits::BatchExt;
142 use approx::assert_relative_eq;
143
144 #[test]
145 fn rejects_zero_period() {
146 assert!(matches!(Expectancy::new(0), Err(Error::PeriodZero)));
147 }
148
149 #[test]
150 fn accessors_and_metadata() {
151 let e = Expectancy::new(20).unwrap();
152 assert_eq!(e.period(), 20);
153 assert_eq!(e.warmup_period(), 20);
154 assert_eq!(e.name(), "Expectancy");
155 assert!(!e.is_ready());
156 }
157
158 #[test]
159 fn positive_edge() {
160 let mut e = Expectancy::new(4).unwrap();
162 let out = e.batch(&[2.0, -1.0, 2.0, -1.0]);
163 assert_relative_eq!(out[3].unwrap(), 0.5, epsilon = 1e-12);
164 }
165
166 #[test]
167 fn negative_edge() {
168 let mut e = Expectancy::new(4).unwrap();
170 let out = e.batch(&[1.0, -2.0, 1.0, -2.0]);
171 assert_relative_eq!(out[3].unwrap(), -0.25, epsilon = 1e-12);
172 }
173
174 #[test]
175 fn no_losses_returns_zero() {
176 let mut e = Expectancy::new(5).unwrap();
178 for v in e.batch(&[1.0, 2.0, 3.0, 1.0, 2.0]).into_iter().flatten() {
179 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
180 }
181 }
182
183 #[test]
184 fn flat_returns_are_not_losses() {
185 let mut e = Expectancy::new(4).unwrap();
188 let out = e.batch(&[2.0, 0.0, 2.0, 0.0]);
189 assert_relative_eq!(out[3].unwrap(), 0.0, epsilon = 1e-12);
190 }
191
192 #[test]
193 fn rolling_window_evicts_old_losses() {
194 let mut e = Expectancy::new(4).unwrap();
197 let out = e.batch(&[2.0, -1.0, 2.0, -1.0, 3.0, 3.0, 3.0, 3.0]);
198 assert_relative_eq!(out[3].unwrap(), 0.5, epsilon = 1e-12);
199 assert_relative_eq!(out[7].unwrap(), 0.0, epsilon = 1e-12);
200 }
201
202 #[test]
203 fn reset_clears_state() {
204 let mut e = Expectancy::new(5).unwrap();
205 e.batch(&[1.0, -1.0, 2.0, -2.0, 1.0]);
206 assert!(e.is_ready());
207 e.reset();
208 assert!(!e.is_ready());
209 assert_eq!(e.update(1.0), None);
210 }
211
212 #[test]
213 fn batch_equals_streaming() {
214 let rets: Vec<f64> = (0..60).map(|i| (f64::from(i) * 0.5).sin() * 2.0).collect();
215 let batch = Expectancy::new(14).unwrap().batch(&rets);
216 let mut b = Expectancy::new(14).unwrap();
217 let streamed: Vec<_> = rets.iter().map(|p| b.update(*p)).collect();
218 assert_eq!(batch, streamed);
219 }
220}