wickra_core/indicators/
equivolume.rs1use crate::error::{Error, Result};
4use crate::indicators::sma::Sma;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct EquivolumeOutput {
11 pub height: f64,
13 pub width: f64,
15}
16
17#[derive(Debug, Clone)]
51pub struct Equivolume {
52 period: usize,
53 vol_sma: Sma,
54 last: Option<EquivolumeOutput>,
55}
56
57impl Equivolume {
58 pub fn new(period: usize) -> Result<Self> {
64 if period == 0 {
65 return Err(Error::PeriodZero);
66 }
67 if period > crate::error::MAX_PERIOD {
68 return Err(Error::InvalidPeriod {
69 message: crate::error::PERIOD_ABOVE_MAX,
70 });
71 }
72 Ok(Self {
73 period,
74 vol_sma: Sma::new(period)?,
75 last: None,
76 })
77 }
78
79 pub const fn period(&self) -> usize {
81 self.period
82 }
83
84 pub const fn value(&self) -> Option<EquivolumeOutput> {
86 self.last
87 }
88}
89
90impl Indicator for Equivolume {
91 type Input = Candle;
92 type Output = EquivolumeOutput;
93
94 #[inline]
95 fn update(&mut self, candle: Candle) -> Option<EquivolumeOutput> {
96 let avg_vol = self.vol_sma.update(candle.volume)?;
97 let height = candle.high - candle.low;
98 let width = if avg_vol > 0.0 {
99 candle.volume / avg_vol
100 } else {
101 0.0
102 };
103 let out = EquivolumeOutput { height, width };
104 self.last = Some(out);
105 Some(out)
106 }
107
108 fn reset(&mut self) {
109 self.vol_sma.reset();
110 self.last = None;
111 }
112
113 #[inline]
114 fn warmup_period(&self) -> usize {
115 self.period
116 }
117
118 #[inline]
119 fn is_ready(&self) -> bool {
120 self.last.is_some()
121 }
122
123 #[inline]
124 fn name(&self) -> &'static str {
125 "Equivolume"
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::traits::BatchExt;
133 use approx::assert_relative_eq;
134
135 fn c(high: f64, low: f64, volume: f64) -> Candle {
136 Candle::new_unchecked(low, high, low, f64::midpoint(high, low), volume, 0)
137 }
138
139 #[test]
140 fn rejects_zero_period() {
141 assert!(matches!(Equivolume::new(0), Err(Error::PeriodZero)));
142 }
143
144 #[test]
145 fn accessors_and_metadata() {
146 let e = Equivolume::new(14).unwrap();
147 assert_eq!(e.period(), 14);
148 assert_eq!(e.warmup_period(), 14);
149 assert_eq!(e.name(), "Equivolume");
150 assert!(!e.is_ready());
151 assert_eq!(e.value(), None);
152 }
153
154 #[test]
155 fn first_emission_at_warmup_period() {
156 let mut e = Equivolume::new(3).unwrap();
157 let candles: Vec<Candle> = (0..6).map(|_| c(102.0, 98.0, 1_000.0)).collect();
158 let out = e.batch(&candles);
159 for v in out.iter().take(2) {
160 assert!(v.is_none());
161 }
162 assert!(out[2].is_some());
163 }
164
165 #[test]
166 fn height_is_range() {
167 let mut e = Equivolume::new(2).unwrap();
168 let out = e
169 .batch(&[c(105.0, 100.0, 1_000.0), c(105.0, 100.0, 1_000.0)])
170 .into_iter()
171 .flatten()
172 .last()
173 .unwrap();
174 assert_relative_eq!(out.height, 5.0, epsilon = 1e-9);
175 }
176
177 #[test]
178 fn average_volume_width_is_one() {
179 let mut e = Equivolume::new(3).unwrap();
180 let out = e
181 .batch(&[c(102.0, 98.0, 1_000.0); 6])
182 .into_iter()
183 .flatten()
184 .last()
185 .unwrap();
186 assert_relative_eq!(out.width, 1.0, epsilon = 1e-9);
187 }
188
189 #[test]
190 fn heavy_bar_is_wide() {
191 let mut e = Equivolume::new(3).unwrap();
192 let candles = [
193 c(102.0, 98.0, 1_000.0),
194 c(102.0, 98.0, 1_000.0),
195 c(102.0, 98.0, 4_000.0),
196 ];
197 let out = e.batch(&candles).into_iter().flatten().last().unwrap();
198 assert!(
199 out.width > 1.0,
200 "a heavy bar should be wider than average, got {}",
201 out.width
202 );
203 }
204
205 #[test]
206 fn reset_clears_state() {
207 let mut e = Equivolume::new(3).unwrap();
208 e.batch(&[c(102.0, 98.0, 1_000.0); 6]);
209 assert!(e.is_ready());
210 e.reset();
211 assert!(!e.is_ready());
212 assert_eq!(e.value(), None);
213 assert_eq!(e.update(c(102.0, 98.0, 1_000.0)), None);
214 }
215
216 #[test]
217 fn zero_volume_gives_zero_width() {
218 let mut e = Equivolume::new(2).unwrap();
219 let out = e
220 .batch(&[c(11.0, 9.0, 0.0), c(12.0, 10.0, 0.0), c(13.0, 11.0, 0.0)])
221 .into_iter()
222 .flatten()
223 .last()
224 .unwrap();
225 assert_eq!(out.width, 0.0);
226 }
227
228 #[test]
229 fn batch_equals_streaming() {
230 let candles: Vec<Candle> = (0..80)
231 .map(|i| {
232 c(
233 110.0 + (f64::from(i) * 0.25).sin() * 5.0,
234 90.0,
235 1_000.0 + f64::from(i),
236 )
237 })
238 .collect();
239 let batch = Equivolume::new(14).unwrap().batch(&candles);
240 let mut b = Equivolume::new(14).unwrap();
241 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
242 assert_eq!(batch, streamed);
243 }
244}