1use super::PriceBuilder;
9use super::Tick;
10use super::bar::Bar;
11use chrono::{DateTime, Utc};
12
13#[derive(Debug, Clone)]
18pub struct RangeBarConfig {
19 pub range_size: f64,
21 pub use_atr: bool,
23 pub atr_period: usize,
25 pub atr_multiplier: f64,
27}
28
29impl Default for RangeBarConfig {
30 fn default() -> Self {
31 Self {
32 range_size: 10.0,
33 use_atr: false,
34 atr_period: 14,
35 atr_multiplier: 1.0,
36 }
37 }
38}
39
40impl RangeBarConfig {
41 pub fn new(range_size: f64) -> Self {
43 Self {
44 range_size,
45 ..Default::default()
46 }
47 }
48
49 pub fn with_atr(mut self, period: usize, multiplier: f64) -> Self {
51 self.use_atr = true;
52 self.atr_period = period;
53 self.atr_multiplier = multiplier;
54 self
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct RangeBar {
61 pub open: f64,
63 pub high: f64,
65 pub low: f64,
67 pub close: f64,
69 pub volume: f64,
71 pub start_time: DateTime<Utc>,
73 pub end_time: DateTime<Utc>,
75 pub range_size: f64,
77 pub tick_cnt: usize,
79}
80
81impl RangeBar {
82 pub fn is_bullish(&self) -> bool {
84 self.close >= self.open
85 }
86
87 pub fn is_bearish(&self) -> bool {
89 self.close < self.open
90 }
91
92 pub fn range(&self) -> f64 {
94 self.high - self.low
95 }
96
97 pub fn body(&self) -> f64 {
99 (self.close - self.open).abs()
100 }
101
102 pub fn to_bar(&self) -> Bar {
104 Bar {
105 time: self.end_time,
106 open: self.open,
107 high: self.high,
108 low: self.low,
109 close: self.close,
110 volume: self.volume,
111 colors: None,
112 payload: None,
113 }
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum RangeBarSource {
120 Executions,
122 OhlcApproximation,
125}
126
127pub fn to_range_bars_from_ticks(ticks: &[Tick], config: &RangeBarConfig) -> Vec<RangeBar> {
133 let mut builder = RangeBarBuilder::new(config.range_size);
134 for tick in ticks {
135 builder.push_tick(tick);
136 }
137 builder.elements()
138}
139
140pub fn to_range_bars_from_ohlc(data: &[Bar], config: &RangeBarConfig) -> Vec<RangeBar> {
150 let range_size = if config.use_atr && data.len() >= config.atr_period {
152 calculate_atr(data, config.atr_period) * config.atr_multiplier
153 } else {
154 config.range_size
155 };
156 if data.is_empty() || !(range_size > 0.0 && range_size.is_finite()) {
157 return Vec::new();
158 }
159 let mut builder = RangeBarBuilder::new(range_size);
160 for bar in data[work_start(data, range_size)..]
161 .iter()
162 .filter(|bar| !bar.is_whitespace())
163 {
164 builder.push(bar);
165 }
166 builder.elements()
167}
168
169pub(crate) const MAX_BARS: usize = 5_000;
170
171#[derive(Clone)]
174pub(crate) struct RangeBarBuilder {
175 range_size: f64,
176 curr_bar: Option<RangeBar>,
178 bars: Vec<RangeBar>,
179}
180
181impl RangeBarBuilder {
182 pub(crate) fn new(range_size: f64) -> Self {
183 Self {
184 range_size,
185 curr_bar: None,
186 bars: Vec::new(),
187 }
188 }
189
190 pub(crate) fn push_tick(&mut self, tick: &Tick) {
195 let range_size = self.range_size;
196 let Some(mut curr_bar) = self.curr_bar.take() else {
197 self.curr_bar = Some(RangeBar {
198 open: tick.price,
199 high: tick.price,
200 low: tick.price,
201 close: tick.price,
202 volume: add_volume(0.0, tick.size),
203 start_time: tick.ts,
204 end_time: tick.ts,
205 range_size,
206 tick_cnt: 1,
207 });
208 return;
209 };
210 curr_bar.high = curr_bar.high.max(tick.price);
211 curr_bar.low = curr_bar.low.min(tick.price);
212 curr_bar.close = tick.price;
213 curr_bar.volume = add_volume(curr_bar.volume, tick.size);
214 curr_bar.end_time = tick.ts;
215 curr_bar.tick_cnt += 1;
216
217 const MAX_CLOSES: usize = 100;
218 let mut closes = 0;
219 while curr_bar.range() >= range_size && range_size > 0.0 && closes < MAX_CLOSES {
220 closes += 1;
221 if tick.price >= curr_bar.open {
222 curr_bar.close = curr_bar.low + range_size;
223 curr_bar.high = curr_bar.close;
224 } else {
225 curr_bar.close = curr_bar.high - range_size;
226 curr_bar.low = curr_bar.close;
227 }
228 self.bars.push(curr_bar.clone());
229 if self.bars.len() == 2 * MAX_BARS {
230 self.bars.drain(..MAX_BARS);
231 }
232
233 curr_bar = RangeBar {
235 open: curr_bar.close,
236 high: tick.price.max(curr_bar.close),
237 low: tick.price.min(curr_bar.close),
238 close: tick.price,
239 volume: 0.0,
240 start_time: tick.ts,
241 end_time: tick.ts,
242 range_size,
243 tick_cnt: usize::from(tick.price != curr_bar.close),
244 };
245 }
246 self.curr_bar = Some(curr_bar);
247 }
248}
249
250impl PriceBuilder for RangeBarBuilder {
251 type Element = RangeBar;
252
253 fn push(&mut self, bar: &Bar) {
254 let range_size = self.range_size;
255 let Some(mut curr_bar) = self.curr_bar.take() else {
256 self.curr_bar = Some(RangeBar {
257 open: bar.open,
258 high: bar.high,
259 low: bar.low,
260 close: bar.close,
261 volume: add_volume(0.0, bar.volume),
262 start_time: bar.time,
263 end_time: bar.time,
264 range_size,
265 tick_cnt: 1,
266 });
267 return;
268 };
269
270 let prices = [bar.open, bar.high, bar.low, bar.close];
272
273 for &price in &prices {
274 curr_bar.high = curr_bar.high.max(price);
275 curr_bar.low = curr_bar.low.min(price);
276
277 while curr_bar.range() >= range_size {
278 if curr_bar.high - curr_bar.open >= range_size {
280 if curr_bar.low + range_size == curr_bar.low {
282 break;
283 }
284 curr_bar.close = curr_bar.low + range_size;
286 curr_bar.high = curr_bar.close;
287 } else {
288 if curr_bar.high - range_size == curr_bar.high {
289 break;
290 }
291 curr_bar.close = curr_bar.high - range_size;
293 curr_bar.low = curr_bar.close;
294 }
295
296 curr_bar.end_time = bar.time;
297 self.bars.push(curr_bar.clone());
298
299 if self.bars.len() == 2 * MAX_BARS {
301 self.bars.drain(..MAX_BARS);
302 }
303
304 curr_bar = RangeBar {
306 open: curr_bar.close,
307 high: price.max(curr_bar.close),
308 low: price.min(curr_bar.close),
309 close: price,
310 volume: 0.0,
311 start_time: bar.time,
312 end_time: bar.time,
313 range_size,
314 tick_cnt: 0,
315 };
316 }
317 }
318
319 curr_bar.close = bar.close;
320 curr_bar.volume = add_volume(curr_bar.volume, bar.volume);
321 curr_bar.end_time = bar.time;
322 curr_bar.tick_cnt += 1;
323 self.curr_bar = Some(curr_bar);
324 }
325
326 fn elements(&self) -> Vec<RangeBar> {
327 let mut bars = self.bars.clone();
328 bars.extend(self.curr_bar.clone().filter(|b| b.tick_cnt > 0));
330 bars.drain(..bars.len().saturating_sub(MAX_BARS));
331 bars
332 }
333}
334
335fn add_volume(volume: f64, added: f64) -> f64 {
338 let sum = volume + added;
339 if sum.is_finite() { sum } else { volume }
340}
341
342pub(crate) fn work_start(bars: &[Bar], size: f64) -> usize {
346 let mut total = 0.0;
347 let mut priced = bars
348 .iter()
349 .enumerate()
350 .rev()
351 .filter(|(_, bar)| !bar.is_whitespace());
352 let Some((mut i, mut b)) = priced.next() else {
353 return 0;
354 };
355 for (previous_i, prev) in priced {
356 total += (((b.open - prev.close).abs()
357 + (b.high - b.open).abs()
358 + (b.high - b.low)
359 + (b.close - b.low).abs())
360 / size)
361 .max(0.0)
362 + 4.0;
363 if total + (prev.high - prev.low) / size > super::MAX_WORK {
364 return i;
365 }
366 (i, b) = (previous_i, prev);
367 }
368 0
369}
370
371pub(super) fn calculate_atr(data: &[Bar], period: usize) -> f64 {
373 if data.len() < 2 {
374 return 1.0;
375 }
376
377 let mut tr_values = Vec::new();
378
379 for i in 1..data.len() {
380 let high = data[i].high;
381 let low = data[i].low;
382 let prev_close = data[i - 1].close;
383
384 let tr = (high - low)
385 .max((high - prev_close).abs())
386 .max((low - prev_close).abs());
387 tr_values.push(tr);
388 }
389
390 if tr_values.len() < period {
391 return tr_values.iter().sum::<f64>() / tr_values.len() as f64;
392 }
393
394 let sum: f64 = tr_values.iter().rev().take(period).sum();
395 sum / period as f64
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn range_bars_from_ticks_follow_every_tick() {
404 type Ohlc = (f64, f64, f64, f64);
406 let cases: [(f64, &[f64], &[Ohlc]); 4] = [
407 (
408 10.0,
409 &[100.0, 150.0],
410 &[
411 (100.0, 110.0, 100.0, 110.0),
412 (110.0, 120.0, 110.0, 120.0),
413 (120.0, 130.0, 120.0, 130.0),
414 (130.0, 140.0, 130.0, 140.0),
415 (140.0, 150.0, 140.0, 150.0),
416 ],
417 ),
418 (
419 10.0,
420 &[100.0, 105.0, 112.0],
421 &[(100.0, 110.0, 100.0, 110.0), (110.0, 112.0, 110.0, 112.0)],
422 ),
423 (10.0, &[100.0, 110.0], &[(100.0, 110.0, 100.0, 110.0)]),
425 (0.0, &[100.0, 0.0], &[(100.0, 100.0, 0.0, 0.0)]),
427 ];
428 for (range, prices, expected) in cases {
429 let ticks: Vec<Tick> = prices
430 .iter()
431 .enumerate()
432 .map(|(i, &price)| {
433 Tick::new(
434 DateTime::from_timestamp(i as i64, 0).unwrap(),
435 price,
436 1.0,
437 None,
438 )
439 })
440 .collect();
441 let bars: Vec<_> = to_range_bars_from_ticks(&ticks, &RangeBarConfig::new(range))
442 .iter()
443 .map(|b| (b.open, b.high, b.low, b.close))
444 .collect();
445 assert_eq!(bars, expected, "{prices:?} range {range}");
446 }
447 }
448
449 #[test]
450 fn test_range_bars_from_ohlc() {
451 let first = DateTime::UNIX_EPOCH;
452 let next = first + chrono::Duration::minutes(1);
453 let data = [
454 Bar::new(first, 100.0, 100.0, 100.0, 100.0, 10.0),
455 Bar::new(next, 112.0, 112.0, 112.0, 112.0, 20.0),
456 ];
457 let bars = to_range_bars_from_ohlc(&data, &RangeBarConfig::new(5.0));
459 let actual: Vec<_> = bars
460 .iter()
461 .map(|b| {
462 (
463 b.open,
464 b.high,
465 b.low,
466 b.close,
467 b.volume,
468 b.start_time,
469 b.end_time,
470 )
471 })
472 .collect();
473 assert_eq!(
474 actual,
475 [
476 (100.0, 105.0, 100.0, 105.0, 10.0, first, next),
477 (105.0, 110.0, 105.0, 110.0, 0.0, next, next),
478 (110.0, 112.0, 110.0, 112.0, 20.0, next, next),
479 ]
480 );
481 }
482
483 #[test]
486 fn a_volume_that_would_overflow_is_left_out() {
487 let ts = Utc::now();
488 let tick = |volume| Tick::new(ts, 100.0, volume, None);
489 let bar = |volume| Bar::new(ts, 100.0, 100.0, 100.0, 100.0, volume);
490 let volumes = [f64::INFINITY, f64::MAX, f64::MAX];
491 let config = RangeBarConfig::new(5.0);
492 let from_ticks = to_range_bars_from_ticks(&volumes.map(tick), &config);
493 let from_bars = to_range_bars_from_ohlc(&volumes.map(bar), &config);
494 let volumes = |bars: Vec<RangeBar>| Vec::from_iter(bars.iter().map(|bar| bar.volume));
495 assert_eq!(volumes(from_ticks), [f64::MAX]);
496 assert_eq!(volumes(from_bars), [f64::MAX]);
497 }
498
499 #[test]
500 fn test_range_bar_properties() {
501 let bar = RangeBar {
502 open: 100.0,
503 high: 105.0,
504 low: 100.0,
505 close: 105.0,
506 volume: 1000.0,
507 start_time: Utc::now(),
508 end_time: Utc::now(),
509 range_size: 5.0,
510 tick_cnt: 10,
511 };
512
513 assert!(bar.is_bullish());
514 assert!(!bar.is_bearish());
515 assert!((bar.range() - 5.0).abs() < 0.01);
516 assert!((bar.body() - 5.0).abs() < 0.01);
517 }
518}