pub mod accumulation;
pub mod tier2;
pub mod tier3;
pub use accumulation::*;
pub use tier2::*;
pub use tier3::*;
use crate::interbar_types::TradeSnapshot;
use smallvec::SmallVec;
#[derive(Debug, Clone, Default)]
pub struct LookbackCache {
pub prices: SmallVec<[f64; 256]>,
pub volumes: SmallVec<[f64; 256]>,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub first_volume: f64,
pub total_volume: f64,
pub all_prices_finite: bool,
pub all_volumes_finite: bool,
}
#[cold]
#[inline(never)]
fn empty_lookback_cache() -> LookbackCache {
LookbackCache {
prices: SmallVec::new(),
volumes: SmallVec::new(),
open: 0.0,
high: 0.0,
low: 0.0,
close: 0.0,
first_volume: 0.0,
total_volume: 0.0,
all_prices_finite: true,
all_volumes_finite: true,
}
}
#[inline]
pub fn extract_lookback_cache(lookback: &[&TradeSnapshot]) -> LookbackCache {
if lookback.is_empty() {
return empty_lookback_cache();
}
let first_trade = &lookback[0];
let last_trade = &lookback[lookback.len() - 1];
let mut cache = LookbackCache {
prices: SmallVec::with_capacity(lookback.len()),
volumes: SmallVec::with_capacity(lookback.len()),
open: first_trade.price.to_f64(),
high: f64::MIN,
low: f64::MAX,
close: last_trade.price.to_f64(),
first_volume: first_trade.volume.to_f64(),
total_volume: 0.0,
all_prices_finite: true,
all_volumes_finite: true,
};
for trade in lookback {
let p = trade.price.to_f64();
let v = trade.volume.to_f64();
cache.prices.push(p);
cache.volumes.push(v);
cache.total_volume += v;
cache.all_prices_finite &= p.is_finite();
cache.all_volumes_finite &= v.is_finite();
cache.high = cache.high.max(p);
cache.low = cache.low.min(p);
}
cache
}
#[inline]
pub fn extract_lookback_cache_reuse(lookback: &[&TradeSnapshot], cache: &mut LookbackCache) {
cache.prices.clear();
cache.volumes.clear();
if lookback.is_empty() {
cache.open = 0.0;
cache.high = 0.0;
cache.low = 0.0;
cache.close = 0.0;
cache.first_volume = 0.0;
cache.total_volume = 0.0;
cache.all_prices_finite = true;
cache.all_volumes_finite = true;
return;
}
let first_trade = &lookback[0];
let last_trade = &lookback[lookback.len() - 1];
cache.open = first_trade.price.to_f64();
cache.high = f64::MIN;
cache.low = f64::MAX;
cache.close = last_trade.price.to_f64();
cache.first_volume = first_trade.volume.to_f64();
cache.total_volume = 0.0;
cache.all_prices_finite = true;
cache.all_volumes_finite = true;
cache.prices.reserve(lookback.len());
cache.volumes.reserve(lookback.len());
for trade in lookback {
let p = trade.price.to_f64();
let v = trade.volume.to_f64();
cache.prices.push(p);
cache.volumes.push(v);
cache.total_volume += v;
cache.all_prices_finite &= p.is_finite();
cache.all_volumes_finite &= v.is_finite();
cache.high = cache.high.max(p);
cache.low = cache.low.min(p);
}
}
pub struct EntropyCache {
cache: quick_cache::sync::Cache<u64, f64>,
hits: std::sync::Arc<std::sync::atomic::AtomicUsize>,
misses: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
impl EntropyCache {
pub fn new() -> Self {
Self {
cache: quick_cache::sync::Cache::new(128),
hits: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
misses: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
pub fn with_capacity(capacity: u64) -> Self {
Self {
cache: quick_cache::sync::Cache::new(capacity as usize),
hits: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
misses: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
fn price_hash(prices: &[f64]) -> u64 {
use foldhash::fast::FixedState;
use std::hash::{BuildHasher, Hash, Hasher};
let mut hasher = FixedState::default().build_hasher();
#[allow(unsafe_code)]
{
let price_bits: &[u64] =
unsafe { std::slice::from_raw_parts(prices.as_ptr().cast::<u64>(), prices.len()) };
price_bits.hash(&mut hasher);
}
hasher.finish()
}
pub fn get(&self, prices: &[f64]) -> Option<f64> {
if prices.is_empty() {
return None;
}
let hash = Self::price_hash(prices);
match self.cache.get(&hash) {
Some(entropy) => {
self.hits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Some(entropy)
}
None => {
self.misses
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
None
}
}
}
pub fn insert(&mut self, prices: &[f64], entropy: f64) {
if prices.is_empty() {
return;
}
let hash = Self::price_hash(prices);
self.cache.insert(hash, entropy);
}
pub fn metrics(&self) -> (usize, usize, f64) {
let hits = self.hits.load(std::sync::atomic::Ordering::Relaxed);
let misses = self.misses.load(std::sync::atomic::Ordering::Relaxed);
let total = hits + misses;
let hit_ratio = if total > 0 {
(hits as f64 / total as f64) * 100.0
} else {
0.0
};
(hits, misses, hit_ratio)
}
pub fn reset_metrics(&mut self) {
self.hits.store(0, std::sync::atomic::Ordering::Relaxed);
self.misses.store(0, std::sync::atomic::Ordering::Relaxed);
}
}
impl std::fmt::Debug for EntropyCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (hits, misses, hit_ratio) = self.metrics();
f.debug_struct("EntropyCache")
.field("cache_size", &"quick_cache(max_128)")
.field("hits", &hits)
.field("misses", &misses)
.field("hit_ratio_percent", &format!("{:.1}%", hit_ratio))
.finish()
}
}
impl Default for EntropyCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(any(feature = "simd-burstiness", feature = "simd-kyle-lambda"))]
pub mod simd {
use crate::interbar_types::TradeSnapshot;
use smallvec::SmallVec;
use wide::f64x4;
pub fn compute_burstiness_simd(lookback: &[&TradeSnapshot]) -> f64 {
if lookback.len() < 2 {
return 0.0;
}
let inter_arrivals = compute_inter_arrivals_simd(lookback);
let inv_n = 1.0 / inter_arrivals.len() as f64;
let mu = sum_f64_simd(&inter_arrivals) * inv_n;
let variance = variance_f64_simd(&inter_arrivals, mu, inv_n);
let sigma = variance.sqrt();
let denominator = sigma + mu;
let numerator = sigma - mu;
numerator / denominator.max(f64::EPSILON)
}
#[inline]
fn compute_inter_arrivals_simd(lookback: &[&TradeSnapshot]) -> SmallVec<[f64; 256]> {
let n = lookback.len();
if n < 2 {
return SmallVec::new();
}
let mut inter_arrivals: SmallVec<[f64; 256]> = smallvec::smallvec![0.0; n - 1];
let iter_count = (n - 1) / 4;
for i in 0..iter_count {
let idx = i * 4;
for j in 0..4 {
inter_arrivals[idx + j] =
(lookback[idx + j + 1].timestamp - lookback[idx + j].timestamp) as f64;
}
}
let remainder = (n - 1) % 4;
if remainder > 0 {
let idx = iter_count * 4;
for j in 0..remainder {
inter_arrivals[idx + j] =
(lookback[idx + j + 1].timestamp - lookback[idx + j].timestamp) as f64;
}
}
inter_arrivals
}
#[inline]
fn sum_f64_simd(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
let chunks = values.len() / 4;
let mut sum_vec = f64x4::splat(0.0);
for i in 0..chunks {
let idx = i * 4;
let chunk = f64x4::new([
values[idx],
values[idx + 1],
values[idx + 2],
values[idx + 3],
]);
sum_vec += chunk;
}
let simd_sum: [f64; 4] = sum_vec.into();
let mut total = simd_sum[0] + simd_sum[1] + simd_sum[2] + simd_sum[3];
let remainder = values.len() % 4;
for j in 0..remainder {
total += values[chunks * 4 + j];
}
total
}
#[inline]
fn variance_f64_simd(values: &[f64], mu: f64, inv_n: f64) -> f64 {
if values.is_empty() {
return 0.0;
}
let mu_vec = f64x4::splat(mu);
let chunks = values.len() / 4;
let mut sum_sq_vec = f64x4::splat(0.0);
for i in 0..chunks {
let idx = i * 4;
let chunk = f64x4::new([
values[idx],
values[idx + 1],
values[idx + 2],
values[idx + 3],
]);
let deviations = chunk - mu_vec;
let squared = deviations * deviations;
sum_sq_vec += squared;
}
let simd_sums: [f64; 4] = sum_sq_vec.into();
let mut sum_sq = simd_sums[0] + simd_sums[1] + simd_sums[2] + simd_sums[3];
let remainder = values.len() % 4;
for j in 0..remainder {
let v = values[chunks * 4 + j] - mu;
sum_sq += v * v;
}
sum_sq * inv_n
}
pub fn compute_kyle_lambda_simd(lookback: &[&TradeSnapshot]) -> f64 {
let n = lookback.len();
if n < 2 {
return 0.0;
}
let first_price = lookback[0].price.to_f64();
let last_price = lookback[n - 1].price.to_f64();
let (buy_vol, sell_vol) = if n > 500 {
accumulate_volumes_simd_wide(lookback, true)
} else {
accumulate_volumes_simd_wide(lookback, false)
};
let total_vol = buy_vol + sell_vol;
let first_price_abs = first_price.abs();
if buy_vol >= total_vol - f64::EPSILON {
return if first_price_abs > f64::EPSILON {
(last_price - first_price) / first_price
} else {
0.0
};
} else if sell_vol >= total_vol - f64::EPSILON {
return if first_price_abs > f64::EPSILON {
-((last_price - first_price) / first_price)
} else {
0.0
};
}
let normalized_imbalance = if total_vol > f64::EPSILON {
(buy_vol - sell_vol) / total_vol
} else {
0.0
};
let imbalance_abs = normalized_imbalance.abs();
if imbalance_abs <= f64::EPSILON {
return 0.0; }
let imbalance_valid = 1.0; let price_valid = if first_price_abs > f64::EPSILON {
1.0
} else {
0.0
};
let both_valid = imbalance_valid * price_valid;
let price_change = if first_price_abs > f64::EPSILON {
(last_price - first_price) / first_price
} else {
0.0
};
if both_valid > 0.0 {
price_change / normalized_imbalance
} else {
0.0
}
}
#[inline]
fn accumulate_volumes_simd_wide(lookback: &[&TradeSnapshot], subsample: bool) -> (f64, f64) {
let mut buy_vol = 0.0;
let mut sell_vol = 0.0;
if subsample {
for trade in lookback.iter().step_by(5) {
let vol = trade.volume.to_f64();
let is_buyer_mask = trade.is_buyer_maker as u32 as f64;
buy_vol += vol * (1.0 - is_buyer_mask);
sell_vol += vol * is_buyer_mask;
}
} else {
let n = lookback.len();
let pairs = n / 2;
for i in 0..pairs {
let idx = i * 2;
let t0 = lookback[idx];
let t1 = lookback[idx + 1];
let vol0 = t0.volume.to_f64();
let vol1 = t1.volume.to_f64();
let is_buyer_mask0 = t0.is_buyer_maker as u32 as f64;
let is_buyer_mask1 = t1.is_buyer_maker as u32 as f64;
buy_vol += vol0 * (1.0 - is_buyer_mask0);
sell_vol += vol0 * is_buyer_mask0;
buy_vol += vol1 * (1.0 - is_buyer_mask1);
sell_vol += vol1 * is_buyer_mask1;
}
if n % 2 == 1 {
let last_trade = lookback[n - 1];
let vol = last_trade.volume.to_f64();
let is_buyer_mask = last_trade.is_buyer_maker as u32 as f64;
buy_vol += vol * (1.0 - is_buyer_mask);
sell_vol += vol * is_buyer_mask;
}
}
(buy_vol, sell_vol)
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_snapshot(ts: i64, price: f64, volume: f64) -> TradeSnapshot {
TradeSnapshot {
timestamp: ts,
price: crate::FixedPoint((price * 1e8) as i64),
volume: crate::FixedPoint((volume * 1e8) as i64),
is_buyer_maker: false,
turnover: (price * volume * 1e8) as i128,
}
}
#[test]
fn test_burstiness_simd_edge_case_empty() {
let lookback: Vec<&TradeSnapshot> = vec![];
assert_eq!(compute_burstiness_simd(&lookback), 0.0);
}
#[test]
fn test_burstiness_simd_edge_case_single() {
let t0 = create_test_snapshot(0, 100.0, 1.0);
let lookback = vec![&t0];
assert_eq!(compute_burstiness_simd(&lookback), 0.0);
}
#[test]
fn test_burstiness_simd_regular_intervals() {
let t0 = create_test_snapshot(0, 100.0, 1.0);
let t1 = create_test_snapshot(1000, 100.0, 1.0);
let t2 = create_test_snapshot(2000, 100.0, 1.0);
let t3 = create_test_snapshot(3000, 100.0, 1.0);
let t4 = create_test_snapshot(4000, 100.0, 1.0);
let lookback = vec![&t0, &t1, &t2, &t3, &t4];
let b = compute_burstiness_simd(&lookback);
assert!((b - (-1.0)).abs() < 0.01);
}
#[test]
fn test_burstiness_simd_clustered_arrivals() {
let t0 = create_test_snapshot(0, 100.0, 1.0);
let t1 = create_test_snapshot(10, 100.0, 1.0);
let t2 = create_test_snapshot(20, 100.0, 1.0);
let t3 = create_test_snapshot(5000, 100.0, 1.0);
let t4 = create_test_snapshot(5010, 100.0, 1.0);
let t5 = create_test_snapshot(5020, 100.0, 1.0);
let lookback = vec![&t0, &t1, &t2, &t3, &t4, &t5];
let b = compute_burstiness_simd(&lookback);
assert!(b > 0.0);
assert!(b <= 1.0);
}
#[test]
fn test_burstiness_simd_bounds() {
let t0 = create_test_snapshot(0, 100.0, 1.0);
let t1 = create_test_snapshot(100, 100.0, 1.0);
let t2 = create_test_snapshot(200, 100.0, 1.0);
let t3 = create_test_snapshot(300, 100.0, 1.0);
let lookback = vec![&t0, &t1, &t2, &t3];
let b = compute_burstiness_simd(&lookback);
assert!(b >= -1.0 && b <= 1.0);
}
#[test]
fn test_simd_remainder_handling() {
let trades: Vec<_> = (0..7)
.map(|i| create_test_snapshot((i * 100) as i64, 100.0, 1.0))
.collect();
let trade_refs: Vec<_> = trades.iter().collect();
let b = compute_burstiness_simd(&trade_refs);
assert!(b >= -1.0 && b <= 1.0);
}
}
}