chore: prepare v1.1.0 release

Update version numbers across Rust, Python, and documentation files to 1.1.0. Enhance the .gitignore to include macOS dSYM files and plans directory. Introduce new dependencies in the Rust core library and update the README to reflect recent performance benchmarks and backtesting engine capabilities. Add new artifacts to the benchmarks manifest and improve documentation for the backtesting engine API.
This commit is contained in:
Pratik Bhadane
2026-03-30 12:45:52 +05:30
parent 2d776b6f90
commit 436954138f
174 changed files with 29297 additions and 10773 deletions
+346
View File
@@ -0,0 +1,346 @@
//! Tick / Trade Aggregation Pipeline — pure Rust, no PyO3.
//!
//! Aggregates raw tick/trade data into OHLCV bars:
//! - **tick bars** — fixed number of ticks per bar
//! - **volume bars** — fixed volume threshold per bar
//! - **time bars** — label-based grouping (labels from Python timestamps)
/// OHLCV 5-tuple return type alias.
type Ohlcv5 = (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>);
/// OHLCV 5-tuple plus labels return type alias.
type Ohlcv5AndLabels = (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<i64>);
// ---------------------------------------------------------------------------
// aggregate_tick_bars
// ---------------------------------------------------------------------------
/// Aggregate tick/trade data into tick bars (every N ticks become one bar).
///
/// Returns `(open, high, low, close, volume)` where volume = sum of sizes.
///
/// # Panics
/// Panics if `ticks_per_bar == 0`, arrays are empty, or lengths differ.
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)
}
// ---------------------------------------------------------------------------
// aggregate_volume_bars_ticks
// ---------------------------------------------------------------------------
/// Aggregate tick data into volume bars (fixed volume threshold).
///
/// Accumulates ticks until cumulative size >= `volume_threshold`, then emits
/// a bar. Any remaining partial bar is also emitted.
///
/// Returns `(open, high, low, close, volume)`.
///
/// # Panics
/// Panics if `volume_threshold <= 0`, arrays are empty, or lengths differ.
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;
}
}
}
// Push remaining partial bar
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)
}
// ---------------------------------------------------------------------------
// aggregate_time_bars
// ---------------------------------------------------------------------------
/// Aggregate tick data into time bars using pre-computed integer bucket labels.
///
/// Each tick is assigned a `label` (e.g. unix_ts // period_secs). Ticks with
/// the same label are accumulated into one bar. Labels must be non-decreasing.
///
/// Returns `(open, high, low, close, volume, unique_labels)`.
///
/// # Panics
/// Panics if arrays are empty or have unequal lengths.
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)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
// -- aggregate_tick_bars -------------------------------------------------
#[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);
// Bar 0: ticks 0..3
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);
// Bar 1: ticks 3..6
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);
// Partial bar: ticks 3..5
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);
}
// -- aggregate_volume_bars_ticks -----------------------------------------
#[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];
// threshold=70: bar0 = ticks 0+1 (vol=70), bar1 = tick2 (vol=50) + tick3 (vol=70),
// then tick4 as partial
let (o, h, l, c, v) = aggregate_volume_bars_ticks(&price, &size, 70.0);
// First bar: 30+40=70 >= 70
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);
}
// -- aggregate_time_bars -------------------------------------------------
#[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]);
// Group 0: ticks 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);
// Group 1: ticks 2,3,4
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(&[], &[], &[]);
}
}