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 Ok(Self {
72 ticks,
73 count: 0,
74 open: 0.0,
75 high: 0.0,
76 low: 0.0,
77 close: 0.0,
78 volume: 0.0,
79 })
80 }
81
82 pub const fn ticks(&self) -> usize {
84 self.ticks
85 }
86
87 pub const fn count(&self) -> usize {
89 self.count
90 }
91}
92
93impl BarBuilder for TickBars {
94 type Bar = TickBar;
95
96 fn update(&mut self, candle: Candle) -> Vec<TickBar> {
97 if self.count == 0 {
98 self.open = candle.open;
99 self.high = candle.high;
100 self.low = candle.low;
101 self.volume = 0.0;
102 } else {
103 self.high = self.high.max(candle.high);
104 self.low = self.low.min(candle.low);
105 }
106 self.close = candle.close;
107 self.volume += candle.volume;
108 self.count += 1;
109 if self.count < self.ticks {
110 return Vec::new();
111 }
112 self.count = 0;
113 vec![TickBar {
114 open: self.open,
115 high: self.high,
116 low: self.low,
117 close: self.close,
118 volume: self.volume,
119 }]
120 }
121
122 fn reset(&mut self) {
123 self.count = 0;
124 self.volume = 0.0;
125 }
126
127 fn name(&self) -> &'static str {
128 "TickBars"
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use approx::assert_relative_eq;
136
137 fn candle(open: f64, high: f64, low: f64, close: f64, volume: f64) -> Candle {
138 Candle::new(open, high, low, close, volume, 0).unwrap()
139 }
140
141 #[test]
142 fn rejects_zero_ticks() {
143 assert!(matches!(TickBars::new(0), Err(Error::PeriodZero)));
144 }
145
146 #[test]
147 fn accessors_and_metadata() {
148 let bars = TickBars::new(5).unwrap();
149 assert_eq!(bars.ticks(), 5);
150 assert_eq!(bars.count(), 0);
151 assert_eq!(bars.name(), "TickBars");
152 }
153
154 #[test]
155 fn emits_every_n_candles() {
156 let mut bars = TickBars::new(2).unwrap();
157 assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).is_empty());
158 assert_eq!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).len(), 1);
159 assert!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).is_empty());
160 assert_eq!(bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0)).len(), 1);
161 }
162
163 #[test]
164 fn aggregates_ohlcv() {
165 let mut bars = TickBars::new(3).unwrap();
166 bars.update(candle(10.0, 11.0, 9.0, 10.5, 100.0));
167 bars.update(candle(10.5, 12.0, 10.0, 11.0, 150.0));
168 let out = bars.update(candle(11.0, 11.5, 10.8, 11.2, 120.0));
169 assert_eq!(out.len(), 1);
170 assert_relative_eq!(out[0].open, 10.0, epsilon = 1e-12);
171 assert_relative_eq!(out[0].high, 12.0, epsilon = 1e-12);
172 assert_relative_eq!(out[0].low, 9.0, epsilon = 1e-12);
173 assert_relative_eq!(out[0].close, 11.2, epsilon = 1e-12);
174 assert_relative_eq!(out[0].volume, 370.0, epsilon = 1e-12);
175 }
176
177 #[test]
178 fn partial_group_emits_nothing() {
179 let mut bars = TickBars::new(4).unwrap();
180 bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
181 bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
182 assert_eq!(bars.count(), 2);
183 }
184
185 #[test]
186 fn reset_clears_state() {
187 let mut bars = TickBars::new(3).unwrap();
188 bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
189 bars.update(candle(10.0, 10.0, 10.0, 10.0, 1.0));
190 bars.reset();
191 assert_eq!(bars.count(), 0);
192 assert!(bars.update(candle(20.0, 20.0, 20.0, 20.0, 5.0)).is_empty());
194 assert_eq!(bars.count(), 1);
195 }
196
197 #[test]
198 fn batch_concatenates_completed_bars() {
199 let mut bars = TickBars::new(2).unwrap();
200 let candles = [
201 candle(10.0, 10.0, 10.0, 10.0, 1.0),
202 candle(10.0, 10.0, 10.0, 10.0, 1.0),
203 candle(10.0, 10.0, 10.0, 10.0, 1.0),
204 candle(10.0, 10.0, 10.0, 10.0, 1.0),
205 ];
206 let out = bars.batch(&candles);
207 assert_eq!(out.len(), 2);
208 }
209}