1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::BarBuilder;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct VolumeBar {
10 pub open: f64,
12 pub high: f64,
14 pub low: f64,
16 pub close: f64,
18 pub volume: f64,
21}
22
23#[derive(Debug, Clone)]
52pub struct VolumeBars {
53 volume_per_bar: f64,
54 count: usize,
55 open: f64,
56 high: f64,
57 low: f64,
58 close: f64,
59 accumulated: f64,
60}
61
62impl VolumeBars {
63 pub fn new(volume_per_bar: f64) -> Result<Self> {
69 if !volume_per_bar.is_finite() || volume_per_bar <= 0.0 {
70 return Err(Error::InvalidPeriod {
71 message: "volume_per_bar must be finite and positive",
72 });
73 }
74 Ok(Self {
75 volume_per_bar,
76 count: 0,
77 open: 0.0,
78 high: 0.0,
79 low: 0.0,
80 close: 0.0,
81 accumulated: 0.0,
82 })
83 }
84
85 pub const fn volume_per_bar(&self) -> f64 {
87 self.volume_per_bar
88 }
89
90 pub const fn accumulated(&self) -> f64 {
92 self.accumulated
93 }
94}
95
96impl BarBuilder for VolumeBars {
97 type Bar = VolumeBar;
98
99 #[inline]
100 fn update(&mut self, candle: Candle) -> Vec<VolumeBar> {
101 if self.count == 0 {
102 self.open = candle.open;
103 self.high = candle.high;
104 self.low = candle.low;
105 } else {
106 self.high = self.high.max(candle.high);
107 self.low = self.low.min(candle.low);
108 }
109 self.close = candle.close;
110 self.accumulated += candle.volume;
111 self.count += 1;
112 if self.accumulated < self.volume_per_bar {
113 return Vec::new();
114 }
115 let bar = VolumeBar {
116 open: self.open,
117 high: self.high,
118 low: self.low,
119 close: self.close,
120 volume: self.accumulated,
121 };
122 self.count = 0;
123 self.accumulated = 0.0;
124 vec![bar]
125 }
126
127 fn reset(&mut self) {
128 self.count = 0;
129 self.accumulated = 0.0;
130 }
131
132 #[inline]
133 fn name(&self) -> &'static str {
134 "VolumeBars"
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use approx::assert_relative_eq;
142
143 fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
144 Candle::new(open, high, low, close, volume, 0).unwrap()
145 }
146
147 #[test]
148 fn rejects_invalid_threshold() {
149 assert!(matches!(
150 VolumeBars::new(0.0),
151 Err(Error::InvalidPeriod { .. })
152 ));
153 assert!(matches!(
154 VolumeBars::new(-100.0),
155 Err(Error::InvalidPeriod { .. })
156 ));
157 assert!(matches!(
158 VolumeBars::new(f64::INFINITY),
159 Err(Error::InvalidPeriod { .. })
160 ));
161 }
162
163 #[test]
164 fn accessors_and_metadata() {
165 let bars = VolumeBars::new(1000.0).unwrap();
166 assert_relative_eq!(bars.volume_per_bar(), 1000.0, epsilon = 1e-12);
167 assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
168 assert_eq!(bars.name(), "VolumeBars");
169 }
170
171 #[test]
172 fn closes_when_threshold_reached() {
173 let mut bars = VolumeBars::new(100.0).unwrap();
174 assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0)).is_empty());
175 let out = bars.update(candle(10.5, 10.5, 10.5, 10.5, 60.0));
176 assert_eq!(out.len(), 1);
177 assert_relative_eq!(out[0].volume, 120.0, epsilon = 1e-12);
178 }
179
180 #[test]
181 fn aggregates_ohlc() {
182 let mut bars = VolumeBars::new(100.0).unwrap();
183 bars.update(candle(10.0, 11.0, 9.0, 10.5, 50.0));
184 let out = bars.update(candle(10.5, 12.0, 10.0, 11.0, 60.0));
185 assert_relative_eq!(out[0].open, 10.0, epsilon = 1e-12);
186 assert_relative_eq!(out[0].high, 12.0, epsilon = 1e-12);
187 assert_relative_eq!(out[0].low, 9.0, epsilon = 1e-12);
188 assert_relative_eq!(out[0].close, 11.0, epsilon = 1e-12);
189 }
190
191 #[test]
192 fn below_threshold_emits_nothing() {
193 let mut bars = VolumeBars::new(100.0).unwrap();
194 bars.update(candle(10.0, 10.0, 10.0, 10.0, 30.0));
195 assert_relative_eq!(bars.accumulated(), 30.0, epsilon = 1e-12);
196 }
197
198 #[test]
199 fn reset_clears_state() {
200 let mut bars = VolumeBars::new(100.0).unwrap();
201 bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0));
202 bars.reset();
203 assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
204 assert!(bars.update(candle(20.0, 20.0, 20.0, 20.0, 60.0)).is_empty());
205 }
206
207 #[test]
208 fn batch_concatenates_completed_bars() {
209 let mut bars = VolumeBars::new(100.0).unwrap();
210 let candles = [
211 candle(10.0, 10.0, 10.0, 10.0, 60.0),
212 candle(10.0, 10.0, 10.0, 10.0, 60.0),
213 candle(10.0, 10.0, 10.0, 10.0, 60.0),
214 candle(10.0, 10.0, 10.0, 10.0, 60.0),
215 ];
216 let out = bars.batch(&candles);
217 assert_eq!(out.len(), 2);
218 }
219}