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