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