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