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 fn update(&mut self, candle: Candle) -> Vec<VolumeBar> {
100 if self.count == 0 {
101 self.open = candle.open;
102 self.high = candle.high;
103 self.low = candle.low;
104 } else {
105 self.high = self.high.max(candle.high);
106 self.low = self.low.min(candle.low);
107 }
108 self.close = candle.close;
109 self.accumulated += candle.volume;
110 self.count += 1;
111 if self.accumulated < self.volume_per_bar {
112 return Vec::new();
113 }
114 let bar = VolumeBar {
115 open: self.open,
116 high: self.high,
117 low: self.low,
118 close: self.close,
119 volume: self.accumulated,
120 };
121 self.count = 0;
122 self.accumulated = 0.0;
123 vec![bar]
124 }
125
126 fn reset(&mut self) {
127 self.count = 0;
128 self.accumulated = 0.0;
129 }
130
131 fn name(&self) -> &'static str {
132 "VolumeBars"
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use approx::assert_relative_eq;
140
141 fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
142 Candle::new(open, high, low, close, volume, 0).unwrap()
143 }
144
145 #[test]
146 fn rejects_invalid_threshold() {
147 assert!(matches!(
148 VolumeBars::new(0.0),
149 Err(Error::InvalidPeriod { .. })
150 ));
151 assert!(matches!(
152 VolumeBars::new(-100.0),
153 Err(Error::InvalidPeriod { .. })
154 ));
155 assert!(matches!(
156 VolumeBars::new(f64::INFINITY),
157 Err(Error::InvalidPeriod { .. })
158 ));
159 }
160
161 #[test]
162 fn accessors_and_metadata() {
163 let bars = VolumeBars::new(1000.0).unwrap();
164 assert_relative_eq!(bars.volume_per_bar(), 1000.0, epsilon = 1e-12);
165 assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
166 assert_eq!(bars.name(), "VolumeBars");
167 }
168
169 #[test]
170 fn closes_when_threshold_reached() {
171 let mut bars = VolumeBars::new(100.0).unwrap();
172 assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0)).is_empty());
173 let out = bars.update(candle(10.5, 10.5, 10.5, 10.5, 60.0));
174 assert_eq!(out.len(), 1);
175 assert_relative_eq!(out[0].volume, 120.0, epsilon = 1e-12);
176 }
177
178 #[test]
179 fn aggregates_ohlc() {
180 let mut bars = VolumeBars::new(100.0).unwrap();
181 bars.update(candle(10.0, 11.0, 9.0, 10.5, 50.0));
182 let out = bars.update(candle(10.5, 12.0, 10.0, 11.0, 60.0));
183 assert_relative_eq!(out[0].open, 10.0, epsilon = 1e-12);
184 assert_relative_eq!(out[0].high, 12.0, epsilon = 1e-12);
185 assert_relative_eq!(out[0].low, 9.0, epsilon = 1e-12);
186 assert_relative_eq!(out[0].close, 11.0, epsilon = 1e-12);
187 }
188
189 #[test]
190 fn below_threshold_emits_nothing() {
191 let mut bars = VolumeBars::new(100.0).unwrap();
192 bars.update(candle(10.0, 10.0, 10.0, 10.0, 30.0));
193 assert_relative_eq!(bars.accumulated(), 30.0, epsilon = 1e-12);
194 }
195
196 #[test]
197 fn reset_clears_state() {
198 let mut bars = VolumeBars::new(100.0).unwrap();
199 bars.update(candle(10.0, 10.0, 10.0, 10.0, 60.0));
200 bars.reset();
201 assert_relative_eq!(bars.accumulated(), 0.0, epsilon = 1e-12);
202 assert!(bars.update(candle(20.0, 20.0, 20.0, 20.0, 60.0)).is_empty());
203 }
204
205 #[test]
206 fn batch_concatenates_completed_bars() {
207 let mut bars = VolumeBars::new(100.0).unwrap();
208 let candles = [
209 candle(10.0, 10.0, 10.0, 10.0, 60.0),
210 candle(10.0, 10.0, 10.0, 10.0, 60.0),
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 ];
214 let out = bars.batch(&candles);
215 assert_eq!(out.len(), 2);
216 }
217}