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