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