wickra_core/indicators/
evwma.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
45pub struct Evwma {
46 period: usize,
47 window: VecDeque<(f64, f64)>,
49 sum_v: f64,
50 current: Option<f64>,
51}
52
53impl Evwma {
54 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_v: 0.0,
64 current: None,
65 })
66 }
67
68 pub const fn period(&self) -> usize {
70 self.period
71 }
72
73 pub const fn value(&self) -> Option<f64> {
75 self.current
76 }
77}
78
79impl Indicator for Evwma {
80 type Input = Candle;
81 type Output = f64;
82
83 fn update(&mut self, candle: Candle) -> Option<f64> {
84 let close = candle.close;
85 let volume = candle.volume;
86 if self.window.len() == self.period {
87 let (_, old_v) = self.window.pop_front().expect("window is non-empty");
88 self.sum_v -= old_v;
89 }
90 self.window.push_back((close, volume));
91 self.sum_v += volume;
92 if self.window.len() < self.period {
93 return None;
94 }
95 if self.sum_v <= 0.0 {
98 if self.current.is_none() {
99 self.current = Some(close);
100 }
101 return self.current;
102 }
103 let prev = self.current.unwrap_or(close);
104 let next = ((self.sum_v - volume) * prev + volume * close) / self.sum_v;
105 self.current = Some(next);
106 Some(next)
107 }
108
109 fn reset(&mut self) {
110 self.window.clear();
111 self.sum_v = 0.0;
112 self.current = None;
113 }
114
115 fn warmup_period(&self) -> usize {
116 self.period
117 }
118
119 fn is_ready(&self) -> bool {
120 self.current.is_some()
121 }
122
123 fn name(&self) -> &'static str {
124 "EVWMA"
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::traits::BatchExt;
132 use approx::assert_relative_eq;
133
134 fn candle(close: f64, volume: f64, ts: i64) -> Candle {
135 Candle::new(close, close, close, close, volume, ts).unwrap()
136 }
137
138 #[test]
139 fn rejects_zero_period() {
140 assert!(matches!(Evwma::new(0), Err(Error::PeriodZero)));
141 }
142
143 #[test]
144 fn accessors_and_metadata() {
145 let mut e = Evwma::new(5).unwrap();
146 assert_eq!(e.period(), 5);
147 assert_eq!(e.warmup_period(), 5);
148 assert_eq!(e.name(), "EVWMA");
149 assert_eq!(e.value(), None);
150 for i in 0..5 {
151 e.update(candle(10.0, 1.0, i));
152 }
153 assert!(e.value().is_some());
154 }
155
156 #[test]
157 fn constant_series_yields_the_constant() {
158 let mut e = Evwma::new(5).unwrap();
162 let candles: Vec<Candle> = (0..30).map(|i| candle(42.0, 3.0, i)).collect();
163 let out = e.batch(&candles);
164 for v in out.iter().skip(4).flatten() {
165 assert_relative_eq!(*v, 42.0, epsilon = 1e-12);
166 }
167 }
168
169 #[test]
170 fn reference_value_period_2() {
171 let mut e = Evwma::new(2).unwrap();
178 assert_eq!(e.update(candle(10.0, 1.0, 0)), None);
179 assert_relative_eq!(
180 e.update(candle(20.0, 3.0, 1)).unwrap(),
181 20.0,
182 epsilon = 1e-12
183 );
184 assert_relative_eq!(
185 e.update(candle(30.0, 1.0, 2)).unwrap(),
186 22.5,
187 epsilon = 1e-12
188 );
189 }
190
191 #[test]
192 fn warmup_emits_first_value_at_period() {
193 let mut e = Evwma::new(4).unwrap();
194 for i in 0..3 {
195 assert_eq!(e.update(candle(10.0, 1.0, i)), None);
196 }
197 assert!(e.update(candle(10.0, 1.0, 3)).is_some());
198 }
199
200 #[test]
201 fn zero_volume_window_holds_value() {
202 let mut e = Evwma::new(3).unwrap();
205 e.update(candle(10.0, 0.0, 0));
206 e.update(candle(15.0, 0.0, 1));
207 let v = e.update(candle(20.0, 0.0, 2)).unwrap();
208 assert_relative_eq!(v, 20.0, epsilon = 1e-12);
209 let v2 = e.update(candle(50.0, 0.0, 3)).unwrap();
211 assert_relative_eq!(v2, 20.0, epsilon = 1e-12);
212 }
213
214 #[test]
215 fn batch_equals_streaming() {
216 let candles: Vec<Candle> = (0..60_i64)
217 .map(|i| {
218 let c = 100.0 + (i as f64 * 0.3).sin() * 8.0;
219 candle(c, 1.0 + (i % 7) as f64, i)
220 })
221 .collect();
222 let batch = Evwma::new(10).unwrap().batch(&candles);
223 let mut b = Evwma::new(10).unwrap();
224 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
225 assert_eq!(batch, streamed);
226 }
227
228 #[test]
229 fn reset_clears_state() {
230 let mut e = Evwma::new(3).unwrap();
231 let candles: Vec<Candle> = (0..10).map(|i| candle(10.0 + i as f64, 2.0, i)).collect();
232 e.batch(&candles);
233 assert!(e.is_ready());
234 e.reset();
235 assert!(!e.is_ready());
236 assert_eq!(e.update(candle(10.0, 1.0, 0)), None);
237 }
238}