1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::BarBuilder;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct TickBar {
10 pub open: f64,
12 pub high: f64,
14 pub low: f64,
16 pub close: f64,
18 pub volume: f64,
20}
21
22#[derive(Debug, Clone)]
51pub struct TickBars {
52 ticks: usize,
53 count: usize,
54 open: f64,
55 high: f64,
56 low: f64,
57 close: f64,
58 volume: f64,
59}
60
61impl TickBars {
62 pub fn new(ticks: usize) -> Result<Self> {
68 if ticks == 0 {
69 return Err(Error::PeriodZero);
70 }
71 if ticks > crate::error::MAX_PERIOD {
72 return Err(Error::InvalidPeriod {
73 message: crate::error::PERIOD_ABOVE_MAX,
74 });
75 }
76 Ok(Self {
77 ticks,
78 count: 0,
79 open: 0.0,
80 high: 0.0,
81 low: 0.0,
82 close: 0.0,
83 volume: 0.0,
84 })
85 }
86
87 pub const fn ticks(&self) -> usize {
89 self.ticks
90 }
91
92 pub const fn count(&self) -> usize {
94 self.count
95 }
96}
97
98impl BarBuilder for TickBars {
99 type Bar = TickBar;
100
101 #[inline]
102 fn update(&mut self, candle: Candle) -> Vec<TickBar> {
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.count += 1;
115 if self.count < self.ticks {
116 return Vec::new();
117 }
118 self.count = 0;
119 vec![TickBar {
120 open: self.open,
121 high: self.high,
122 low: self.low,
123 close: self.close,
124 volume: self.volume,
125 }]
126 }
127
128 fn reset(&mut self) {
129 self.count = 0;
130 self.volume = 0.0;
131 }
132
133 #[inline]
134 fn name(&self) -> &'static str {
135 "TickBars"
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use approx::assert_relative_eq;
143
144 fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
145 Candle::new(open, high, low, close, volume, 0).unwrap()
146 }
147
148 #[test]
149 fn rejects_zero_ticks() {
150 assert!(matches!(TickBars::new(0), Err(Error::PeriodZero)));
151 }
152
153 #[test]
154 fn accessors_and_metadata() {
155 let bars = TickBars::new(5).unwrap();
156 assert_eq!(bars.ticks(), 5);
157 assert_eq!(bars.count(), 0);
158 assert_eq!(bars.name(), "TickBars");
159 }
160
161 #[test]
162 fn emits_every_n_candles() {
163 let mut bars = TickBars::new(2).unwrap();
164 assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).is_empty());
165 assert_eq!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).len(), 1);
166 assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).is_empty());
167 assert_eq!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).len(), 1);
168 }
169
170 #[test]
171 fn aggregates_ohlcv() {
172 let mut bars = TickBars::new(3).unwrap();
173 bars.update(candle(10.0, 11.0, 9.0, 10.5, 100.0));
174 bars.update(candle(10.5, 12.0, 10.0, 11.0, 150.0));
175 let out = bars.update(candle(11.0, 11.5, 10.8, 11.2, 120.0));
176 assert_eq!(out.len(), 1);
177 assert_relative_eq!(out[0].open, 10.0, epsilon = 1e-12);
178 assert_relative_eq!(out[0].high, 12.0, epsilon = 1e-12);
179 assert_relative_eq!(out[0].low, 9.0, epsilon = 1e-12);
180 assert_relative_eq!(out[0].close, 11.2, epsilon = 1e-12);
181 assert_relative_eq!(out[0].volume, 370.0, epsilon = 1e-12);
182 }
183
184 #[test]
185 fn partial_group_emits_nothing() {
186 let mut bars = TickBars::new(4).unwrap();
187 bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
188 bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
189 assert_eq!(bars.count(), 2);
190 }
191
192 #[test]
193 fn reset_clears_state() {
194 let mut bars = TickBars::new(3).unwrap();
195 bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
196 bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
197 bars.reset();
198 assert_eq!(bars.count(), 0);
199 assert!(bars.update(candle(20.0, 20.0, 20.0, 20.0, 5.0)).is_empty());
201 assert_eq!(bars.count(), 1);
202 }
203
204 #[test]
205 fn batch_concatenates_completed_bars() {
206 let mut bars = TickBars::new(2).unwrap();
207 let candles = [
208 candle(10.0, 10.0, 10.0, 10.0, 1.0),
209 candle(10.0, 10.0, 10.0, 10.0, 1.0),
210 candle(10.0, 10.0, 10.0, 10.0, 1.0),
211 candle(10.0, 10.0, 10.0, 10.0, 1.0),
212 ];
213 let out = bars.batch(&candles);
214 assert_eq!(out.len(), 2);
215 }
216}