type Ohlcv5 = (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>);
type Ohlcv5AndLabels = (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<i64>);
pub fn aggregate_tick_bars(price: &[f64], size: &[f64], ticks_per_bar: usize) -> Ohlcv5 {
assert!(ticks_per_bar >= 1, "ticks_per_bar must be >= 1");
let n = price.len();
assert!(
n > 0 && size.len() == n,
"price and size must be non-empty and equal length"
);
let n_bars = n.div_ceil(ticks_per_bar);
let mut out_open = Vec::with_capacity(n_bars);
let mut out_high = Vec::with_capacity(n_bars);
let mut out_low = Vec::with_capacity(n_bars);
let mut out_close = Vec::with_capacity(n_bars);
let mut out_vol = Vec::with_capacity(n_bars);
let mut i = 0;
while i < n {
let end = (i + ticks_per_bar).min(n);
let bar_p = &price[i..end];
let bar_s = &size[i..end];
let bar_open = bar_p[0];
let bar_high = bar_p.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let bar_low = bar_p.iter().cloned().fold(f64::INFINITY, f64::min);
let bar_close = *bar_p.last().expect("slice cannot be empty");
let bar_vol: f64 = bar_s.iter().sum();
out_open.push(bar_open);
out_high.push(bar_high);
out_low.push(bar_low);
out_close.push(bar_close);
out_vol.push(bar_vol);
i = end;
}
(out_open, out_high, out_low, out_close, out_vol)
}
pub fn aggregate_volume_bars_ticks(price: &[f64], size: &[f64], volume_threshold: f64) -> Ohlcv5 {
assert!(volume_threshold > 0.0, "volume_threshold must be > 0");
let n = price.len();
assert!(
n > 0 && size.len() == n,
"price and size must be non-empty and equal length"
);
let mut out_open: Vec<f64> = Vec::new();
let mut out_high: Vec<f64> = Vec::new();
let mut out_low: Vec<f64> = Vec::new();
let mut out_close: Vec<f64> = Vec::new();
let mut out_vol: Vec<f64> = Vec::new();
let mut bar_open = price[0];
let mut bar_high = price[0];
let mut bar_low = price[0];
let mut bar_close = price[0];
let mut bar_vol = size[0];
for i in 1..n {
bar_high = bar_high.max(price[i]);
bar_low = bar_low.min(price[i]);
bar_close = price[i];
bar_vol += size[i];
if bar_vol >= volume_threshold {
out_open.push(bar_open);
out_high.push(bar_high);
out_low.push(bar_low);
out_close.push(bar_close);
out_vol.push(bar_vol);
if i + 1 < n {
bar_open = price[i + 1];
bar_high = price[i + 1];
bar_low = price[i + 1];
bar_close = price[i + 1];
bar_vol = size[i + 1];
} else {
bar_vol = 0.0;
}
}
}
if bar_vol > 0.0 {
out_open.push(bar_open);
out_high.push(bar_high);
out_low.push(bar_low);
out_close.push(bar_close);
out_vol.push(bar_vol);
}
(out_open, out_high, out_low, out_close, out_vol)
}
pub fn aggregate_time_bars(price: &[f64], size: &[f64], labels: &[i64]) -> Ohlcv5AndLabels {
let n = price.len();
assert!(
n > 0 && size.len() == n && labels.len() == n,
"price, size, and labels must be non-empty and equal length"
);
let mut out_open: Vec<f64> = Vec::new();
let mut out_high: Vec<f64> = Vec::new();
let mut out_low: Vec<f64> = Vec::new();
let mut out_close: Vec<f64> = Vec::new();
let mut out_vol: Vec<f64> = Vec::new();
let mut out_labels: Vec<i64> = Vec::new();
let mut cur_label = labels[0];
let mut bar_open = price[0];
let mut bar_high = price[0];
let mut bar_low = price[0];
let mut bar_close = price[0];
let mut bar_vol = size[0];
for i in 1..n {
if labels[i] != cur_label {
out_open.push(bar_open);
out_high.push(bar_high);
out_low.push(bar_low);
out_close.push(bar_close);
out_vol.push(bar_vol);
out_labels.push(cur_label);
cur_label = labels[i];
bar_open = price[i];
bar_high = price[i];
bar_low = price[i];
bar_close = price[i];
bar_vol = size[i];
} else {
bar_high = bar_high.max(price[i]);
bar_low = bar_low.min(price[i]);
bar_close = price[i];
bar_vol += size[i];
}
}
out_open.push(bar_open);
out_high.push(bar_high);
out_low.push(bar_low);
out_close.push(bar_close);
out_vol.push(bar_vol);
out_labels.push(cur_label);
(out_open, out_high, out_low, out_close, out_vol, out_labels)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tick_bars_exact_division() {
let price = [10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
let size = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let (o, h, l, c, v) = aggregate_tick_bars(&price, &size, 3);
assert_eq!(o.len(), 2);
assert!((o[0] - 10.0).abs() < 1e-10);
assert!((h[0] - 12.0).abs() < 1e-10);
assert!((l[0] - 10.0).abs() < 1e-10);
assert!((c[0] - 12.0).abs() < 1e-10);
assert!((v[0] - 6.0).abs() < 1e-10);
assert!((o[1] - 13.0).abs() < 1e-10);
assert!((h[1] - 15.0).abs() < 1e-10);
assert!((l[1] - 13.0).abs() < 1e-10);
assert!((c[1] - 15.0).abs() < 1e-10);
assert!((v[1] - 15.0).abs() < 1e-10);
}
#[test]
fn test_tick_bars_partial_last_bar() {
let price = [10.0, 11.0, 12.0, 13.0, 14.0];
let size = [1.0, 2.0, 3.0, 4.0, 5.0];
let (o, _h, _l, c, v) = aggregate_tick_bars(&price, &size, 3);
assert_eq!(o.len(), 2);
assert!((o[1] - 13.0).abs() < 1e-10);
assert!((c[1] - 14.0).abs() < 1e-10);
assert!((v[1] - 9.0).abs() < 1e-10);
}
#[test]
fn test_tick_bars_single_tick() {
let (o, h, l, c, v) = aggregate_tick_bars(&[42.0], &[100.0], 5);
assert_eq!(o.len(), 1);
assert!((o[0] - 42.0).abs() < 1e-10);
assert!((h[0] - 42.0).abs() < 1e-10);
assert!((l[0] - 42.0).abs() < 1e-10);
assert!((c[0] - 42.0).abs() < 1e-10);
assert!((v[0] - 100.0).abs() < 1e-10);
}
#[test]
#[should_panic(expected = "ticks_per_bar must be >= 1")]
fn test_tick_bars_zero_ticks() {
aggregate_tick_bars(&[1.0], &[1.0], 0);
}
#[test]
fn test_volume_bars_ticks_basic() {
let price = [10.0, 11.0, 12.0, 13.0, 14.0];
let size = [30.0, 40.0, 50.0, 20.0, 60.0];
let (o, h, l, c, v) = aggregate_volume_bars_ticks(&price, &size, 70.0);
assert!((o[0] - 10.0).abs() < 1e-10);
assert!((c[0] - 11.0).abs() < 1e-10);
assert!((v[0] - 70.0).abs() < 1e-10);
assert!((h[0] - 11.0).abs() < 1e-10);
assert!((l[0] - 10.0).abs() < 1e-10);
assert!(v.len() >= 2);
}
#[test]
fn test_volume_bars_ticks_single() {
let (o, _h, _l, _c, v) = aggregate_volume_bars_ticks(&[5.0], &[10.0], 100.0);
assert_eq!(o.len(), 1);
assert!((v[0] - 10.0).abs() < 1e-10);
}
#[test]
#[should_panic(expected = "volume_threshold must be > 0")]
fn test_volume_bars_ticks_zero_threshold() {
aggregate_volume_bars_ticks(&[1.0], &[1.0], 0.0);
}
#[test]
fn test_time_bars_basic() {
let price = [10.0, 11.0, 12.0, 13.0, 14.0];
let size = [1.0, 2.0, 3.0, 4.0, 5.0];
let labels: [i64; 5] = [0, 0, 1, 1, 1];
let (o, h, l, c, v, out_lbl) = aggregate_time_bars(&price, &size, &labels);
assert_eq!(o.len(), 2);
assert_eq!(out_lbl, vec![0, 1]);
assert!((o[0] - 10.0).abs() < 1e-10);
assert!((h[0] - 11.0).abs() < 1e-10);
assert!((l[0] - 10.0).abs() < 1e-10);
assert!((c[0] - 11.0).abs() < 1e-10);
assert!((v[0] - 3.0).abs() < 1e-10);
assert!((o[1] - 12.0).abs() < 1e-10);
assert!((h[1] - 14.0).abs() < 1e-10);
assert!((l[1] - 12.0).abs() < 1e-10);
assert!((c[1] - 14.0).abs() < 1e-10);
assert!((v[1] - 12.0).abs() < 1e-10);
}
#[test]
fn test_time_bars_all_same_label() {
let price = [5.0, 6.0, 4.0];
let size = [10.0, 20.0, 30.0];
let labels: [i64; 3] = [42, 42, 42];
let (o, h, l, c, v, out_lbl) = aggregate_time_bars(&price, &size, &labels);
assert_eq!(o.len(), 1);
assert_eq!(out_lbl, vec![42]);
assert!((o[0] - 5.0).abs() < 1e-10);
assert!((h[0] - 6.0).abs() < 1e-10);
assert!((l[0] - 4.0).abs() < 1e-10);
assert!((c[0] - 4.0).abs() < 1e-10);
assert!((v[0] - 60.0).abs() < 1e-10);
}
#[test]
fn test_time_bars_each_tick_own_label() {
let price = [10.0, 20.0, 30.0];
let size = [1.0, 2.0, 3.0];
let labels: [i64; 3] = [0, 1, 2];
let (o, _h, _l, _c, v, out_lbl) = aggregate_time_bars(&price, &size, &labels);
assert_eq!(o.len(), 3);
assert_eq!(out_lbl, vec![0, 1, 2]);
assert!((v[0] - 1.0).abs() < 1e-10);
assert!((v[1] - 2.0).abs() < 1e-10);
assert!((v[2] - 3.0).abs() < 1e-10);
}
#[test]
#[should_panic(expected = "price, size, and labels must be non-empty and equal length")]
fn test_time_bars_empty() {
aggregate_time_bars(&[], &[], &[]);
}
}