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