use libm;
use crate::constants::entropy::{ORDINAL_M, ORDINAL_M_FACTORIAL, ORDINAL_TAU};
use crate::interbar_math::tier3::ordinal_pattern_index_m3;
pub const BAR_CLOSE_LOOKBACK_COUNT: usize = 200;
pub fn compute_bar_petrosian_fd(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 2 {
return 0.0;
}
if !closes.iter().all(|x| x.is_finite()) {
return 0.0;
}
let mut prev_neg = (closes[1] - closes[0]).is_sign_negative();
let mut n_delta: usize = 0;
for i in 1..(n - 1) {
let cur_neg = (closes[i + 1] - closes[i]).is_sign_negative();
if cur_neg != prev_neg {
n_delta += 1;
}
prev_neg = cur_neg;
}
let n_f = n as f64;
let log_n = libm::log10(n_f);
let pfd = log_n / (log_n + libm::log10(n_f / (n_f + 0.4 * (n_delta as f64))));
if pfd.is_finite() { pfd } else { 0.0 }
}
pub fn compute_bar_katz_fd(closes: &[f64]) -> f64 {
let n_closes = closes.len();
if n_closes < 2 {
return 0.0;
}
if !closes.iter().all(|x| x.is_finite()) {
return 0.0;
}
let n = n_closes - 1;
let mut l = 0.0_f64;
for i in 0..n {
l += (closes[i + 1] - closes[i]).abs();
}
let a = l / n as f64;
let x0 = closes[0];
let mut d = 0.0_f64;
for &x in closes {
let dist = (x - x0).abs();
if dist > d {
d = dist;
}
}
let kfd = libm::log10(l / a) / libm::log10(d / a);
if kfd.is_finite() { kfd } else { 0.0 }
}
const DISP_C: usize = 6;
const DISP_M: usize = 2;
const DISP_D: usize = 1;
const DISPERSION_HISTOGRAM_SIZE: usize = DISP_C * DISP_C + DISP_C + 1;
const _: () = assert!(
DISP_M == 2 && DISP_D == 1,
"dispersion key `z[i] + c*z[i+d]` and DISPERSION_HISTOGRAM_SIZE = c^2+c+1 are \
hardcoded for m=2/d=1; generalize the key encoding AND the histogram size \
before changing DISP_M/DISP_D"
);
#[inline]
fn ncdf_standard(z: f64) -> f64 {
0.5 * (1.0 + libm::erf(z / std::f64::consts::SQRT_2))
}
#[inline]
fn dispersion_class(z: f64) -> usize {
let bin = (ncdf_standard(z) * DISP_C as f64) as usize; (bin + 1).min(DISP_C)
}
pub fn compute_bar_dispersion_entropy(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 2 {
return f64::NAN;
}
if !closes.iter().all(|x| x.is_finite()) {
return f64::NAN;
}
let n_f = n as f64;
let mean = closes.iter().sum::<f64>() / n_f;
let var = closes
.iter()
.map(|&x| {
let d = x - mean;
d * d
})
.sum::<f64>()
/ n_f;
let sigma = var.sqrt();
if sigma == 0.0 {
return f64::NAN; }
let classes: Vec<usize> = closes
.iter()
.map(|&x| dispersion_class((x - mean) / sigma))
.collect();
let l = n - (DISP_M - 1) * DISP_D; if l <= 1 {
return f64::NAN;
}
let mut counts = [0u32; DISPERSION_HISTOGRAM_SIZE];
for i in 0..l {
let key = classes[i] + DISP_C * classes[i + DISP_D];
debug_assert!(
key < DISPERSION_HISTOGRAM_SIZE,
"dispersion ordinal key {key} exceeds histogram size {DISPERSION_HISTOGRAM_SIZE}"
);
counts[key] += 1;
}
let l_f = l as f64;
let mut entropy = 0.0_f64;
for &cnt in &counts {
if cnt > 0 {
let p = cnt as f64 / l_f;
entropy -= p * libm::log(p);
}
}
entropy / libm::log(DISP_C.pow(DISP_M as u32) as f64)
}
const CECP_HALF: usize = BAR_CLOSE_LOOKBACK_COUNT / 2;
const _: () = assert!(
BAR_CLOSE_LOOKBACK_COUNT.is_multiple_of(2),
"CECP 50/50 split (n // 2) requires an even BAR_CLOSE_LOOKBACK_COUNT"
);
const _: () = assert!(
ORDINAL_M == 3 && ORDINAL_TAU == 1,
"compute_bar_cecp_velocity hardcodes the (x[i], x[i+1], x[i+2]) τ=1 triplet \
and the [_; ORDINAL_M_FACTORIAL] histogram; generalize the embedding before \
changing ORDINAL_M/ORDINAL_TAU"
);
#[allow(clippy::many_single_char_names)]
#[inline]
fn cecp_hc(seg: &[f64]) -> (f64, f64) {
let l = seg.len() - (ORDINAL_M - 1) * ORDINAL_TAU; debug_assert!(l >= 1, "cecp_hc needs at least one ordinal triplet");
let mut counts = [0u32; ORDINAL_M_FACTORIAL];
for i in 0..l {
let cls = ordinal_pattern_index_m3(seg[i], seg[i + 1], seg[i + 2]);
counts[cls] += 1;
}
let l_f = l as f64;
let n = ORDINAL_M_FACTORIAL as f64;
let u = 1.0 / n;
let mut s_p = 0.0_f64; let mut s_ppu = 0.0_f64; let mut occurring = 0usize;
for &cnt in &counts {
if cnt > 0 {
occurring += 1;
let p = cnt as f64 / l_f;
s_p -= p * libm::log(p);
let ppu = 0.5 * (p + u);
s_ppu -= ppu * libm::log(ppu);
}
}
let n_not = n - occurring as f64; let half_u = 0.5 * u;
let s_of_p_plus_u_over_2 = s_ppu - half_u * libm::log(half_u) * n_not;
let s_of_p_over_2 = 0.5 * s_p;
let s_of_u_over_2 = 0.5 * libm::log(n);
let js_div = s_of_p_plus_u_over_2 - s_of_p_over_2 - s_of_u_over_2;
let js_div_max =
-0.5 * (((n + 1.0) / n) * libm::log(n + 1.0) + libm::log(n) - 2.0 * libm::log(2.0 * n));
let h = s_p / libm::log(n);
let c = h * js_div / js_div_max;
(h, c)
}
pub fn compute_bar_cecp_velocity(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 2 * (ORDINAL_M + 2) {
return f64::NAN;
}
if !closes.iter().all(|x| x.is_finite()) {
return f64::NAN;
}
let mut lo = closes[0];
let mut hi = closes[0];
for &x in closes {
if x < lo {
lo = x;
}
if x > hi {
hi = x;
}
}
if hi - lo == 0.0 {
return f64::NAN;
}
let half = n / 2;
debug_assert!(
n != BAR_CLOSE_LOOKBACK_COUNT || (half == CECP_HALF && n - half == CECP_HALF),
"at the production lookback both CECP halves must equal CECP_HALF"
);
let (h1, c1) = cecp_hc(&closes[..half]);
let (h2, c2) = cecp_hc(&closes[half..]);
let vel = libm::hypot(h2 - h1, c2 - c1);
if vel.is_finite() { vel } else { f64::NAN }
}
const RECURR_EPSILON: f64 = 0.0;
const _: () = assert!(
RECURR_EPSILON == 0.0,
"categorical recurrence rate is defined at epsilon = 0 (exact equality)"
);
pub fn compute_bar_categorical_recurrence_rate(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 2 || !closes.iter().all(|x| x.is_finite()) {
return f64::NAN;
}
let mut sorted: Vec<f64> = closes.to_vec();
sorted.sort_unstable_by(f64::total_cmp);
let mut tie_sum: u64 = 0;
let mut run: u64 = 1;
for w in sorted.windows(2) {
if w[1].to_bits() == w[0].to_bits() {
run += 1;
} else {
tie_sum += run * (run - 1);
run = 1;
}
}
tie_sum += run * (run - 1);
let denom = (n as u64) * (n as u64 - 1);
tie_sum as f64 / denom as f64
}
const SIGNFLUX_STATES: usize = 3;
const SIGNFLUX_LAG: usize = 1;
const _: () = assert!(
SIGNFLUX_STATES == 3 && SIGNFLUX_LAG == 1,
"sign-Markov flux is defined on the 3-state sign alphabet at lag 1"
);
pub fn compute_bar_sign_markov_flux(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 2 || !closes.iter().all(|&c| c.is_finite() && c > 0.0) {
return f64::NAN;
}
if n - 1 < 4 {
return f64::NAN;
}
let logs: Vec<f64> = closes.iter().map(|&c| libm::log(c)).collect();
let sign_index = |t: usize| -> usize {
let d = logs[t + 1] - logs[t];
if d > 0.0 {
2 } else if d < 0.0 {
0 } else {
1 }
};
let mut counts = [[0u64; SIGNFLUX_STATES]; SIGNFLUX_STATES];
let mut total: u64 = 0;
let mut prev = sign_index(0);
for t in SIGNFLUX_LAG..n - 1 {
let cur = sign_index(t);
counts[prev][cur] += 1;
total += 1;
prev = cur;
}
if total == 0 {
return f64::NAN;
}
let num = counts[0][1].abs_diff(counts[1][0])
+ counts[0][2].abs_diff(counts[2][0])
+ counts[1][2].abs_diff(counts[2][1]);
num as f64 / total as f64
}
const RRBICOV_LAG: usize = 1;
const _: () = assert!(
RRBICOV_LAG == 1,
"Ramsey-Rothman bicovariance is defined at lag 1"
);
pub fn compute_bar_ramsey_rothman_bicov_lag1(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 2 || !closes.iter().all(|&c| c.is_finite() && c > 0.0) {
return f64::NAN;
}
let m = n - 1; if m < 3 {
return f64::NAN;
}
let logret: Vec<f64> = (0..m)
.map(|t| libm::log(closes[t + 1]) - libm::log(closes[t]))
.collect();
let mean = logret.iter().sum::<f64>() / m as f64;
let y: Vec<f64> = logret.iter().map(|&r| r - mean).collect();
let var = y.iter().map(|&v| v * v).sum::<f64>() / m as f64;
let sigma = libm::sqrt(var);
if !sigma.is_finite() || sigma == 0.0 {
return f64::NAN;
}
let pairs = (m - RRBICOV_LAG) as f64;
let mut s1 = 0.0_f64; let mut s2 = 0.0_f64; for t in RRBICOV_LAG..m {
let yt = y[t];
let ytm1 = y[t - 1];
s1 += yt * yt * ytm1;
s2 += yt * ytm1 * ytm1;
}
let gamma = s1 / pairs - s2 / pairs;
let result = gamma / (sigma * sigma * sigma);
if result.is_finite() { result } else { f64::NAN }
}
pub fn compute_bar_ehlers_increment_asymmetry(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 3 || !closes.iter().all(|&c| c.is_finite()) {
return f64::NAN;
}
let mut s2 = 0.0_f64; let mut s3 = 0.0_f64; for t in 1..n {
let d = closes[t] - closes[t - 1];
s2 += d * d;
s3 += d * d * d;
}
if !s2.is_finite() || s2 == 0.0 {
return f64::NAN; }
let e = s3 / libm::pow(s2, 1.5);
if e.is_finite() { e } else { f64::NAN }
}
pub fn compute_bar_cox_stuart_trend_z(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 4 || !closes.iter().all(|&c| c.is_finite()) {
return f64::NAN;
}
let m = n / 2;
let offset = n - m;
let mut s_plus: u64 = 0;
for i in 0..m {
if closes[offset + i] > closes[i] {
s_plus += 1;
}
}
let mf = m as f64;
let z = (s_plus as f64 - mf / 2.0) / libm::sqrt(mf / 4.0);
if z.is_finite() { z } else { f64::NAN }
}
pub fn compute_bar_groeneveld_meeden_b3_skewness(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 2 || !closes.iter().all(|&c| c.is_finite() && c > 0.0) {
return f64::NAN;
}
let m = n - 1; if m < 2 {
return f64::NAN;
}
let x: Vec<f64> = (0..m)
.map(|t| libm::log(closes[t + 1]) - libm::log(closes[t]))
.collect();
let mut sorted = x.clone();
sorted.sort_unstable_by(f64::total_cmp);
let median = if m % 2 == 1 {
sorted[m / 2]
} else {
f64::midpoint(sorted[m / 2 - 1], sorted[m / 2])
};
let mean = x.iter().sum::<f64>() / m as f64;
let mad = x.iter().map(|&v| (v - median).abs()).sum::<f64>() / m as f64;
if !mad.is_finite() || mad == 0.0 {
return f64::NAN; }
let b3 = (mean - median) / mad;
if b3.is_finite() { b3 } else { f64::NAN }
}
pub fn compute_bar_l_kurtosis_tau4(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 5 || !closes.iter().all(|&c| c.is_finite() && c > 0.0) {
return f64::NAN;
}
let m = n - 1; let mut x: Vec<f64> = (0..m)
.map(|t| libm::log(closes[t + 1]) - libm::log(closes[t]))
.collect();
x.sort_unstable_by(f64::total_cmp); let nf = m as f64;
let (mut b0, mut b1, mut b2, mut b3) = (0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64);
for (idx, &xi) in x.iter().enumerate() {
let i = (idx + 1) as f64; let w1 = (i - 1.0) / (nf - 1.0);
let w2 = (i - 1.0) * (i - 2.0) / ((nf - 1.0) * (nf - 2.0));
let w3 = (i - 1.0) * (i - 2.0) * (i - 3.0) / ((nf - 1.0) * (nf - 2.0) * (nf - 3.0));
b0 += xi;
b1 += w1 * xi;
b2 += w2 * xi;
b3 += w3 * xi;
}
b0 /= nf;
b1 /= nf;
b2 /= nf;
b3 /= nf;
let l2 = 2.0 * b1 - b0;
if !l2.is_finite() || l2 == 0.0 {
return f64::NAN; }
let l4 = 20.0 * b3 - 30.0 * b2 + 12.0 * b1 - b0;
let tau4 = l4 / l2;
if tau4.is_finite() { tau4 } else { f64::NAN }
}
#[allow(clippy::float_cmp)]
fn bar_close_average_ranks(x: &[f64]) -> Vec<f64> {
let n = x.len();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| x[a].total_cmp(&x[b]));
let mut ranks = vec![0.0_f64; n];
let mut i = 0;
while i < n {
let mut j = i + 1;
while j < n && x[order[j]] == x[order[i]] {
j += 1;
}
let avg = ((i + 1 + j) as f64) / 2.0;
for &idx in &order[i..j] {
ranks[idx] = avg;
}
i = j;
}
ranks
}
#[allow(clippy::many_single_char_names)]
pub fn compute_bar_bartels_rank_vn_ratio(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 5 || !closes.iter().all(|&c| c.is_finite() && c > 0.0) {
return f64::NAN;
}
let m = n - 1; let x: Vec<f64> = (0..m).map(|t| closes[t + 1] / closes[t]).collect();
let r = bar_close_average_ranks(&x);
let mf = m as f64;
let rbar = f64::midpoint(mf, 1.0);
let mut denom = 0.0_f64;
for &ri in &r {
let d = ri - rbar;
denom += d * d;
}
if !denom.is_finite() || denom == 0.0 {
return f64::NAN; }
let mut num = 0.0_f64;
for w in r.windows(2) {
let d = w[1] - w[0];
num += d * d;
}
let rvn = num / denom;
let sigma2 = 4.0 * (mf - 2.0) * (5.0 * mf * mf - 2.0 * mf - 9.0)
/ (5.0 * mf * (mf + 1.0) * (mf - 1.0) * (mf - 1.0));
if !sigma2.is_finite() || sigma2 <= 0.0 {
return f64::NAN;
}
let z = (rvn - 2.0) / libm::sqrt(sigma2);
if z.is_finite() { z } else { f64::NAN }
}
fn bar_close_ordinal_ranks(x: &[f64]) -> Vec<usize> {
let n = x.len();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| x[a].total_cmp(&x[b]).then(a.cmp(&b)));
let mut ranks = vec![0usize; n];
for (pos, &idx) in order.iter().enumerate() {
ranks[idx] = pos;
}
ranks
}
pub fn compute_bar_hoeffding_phi_squared_midreturn_duration(
closes: &[f64],
durations_us: &[f64],
) -> f64 {
let n = closes.len();
if n < 5 || durations_us.len() != n {
return f64::NAN;
}
if !closes.iter().all(|&c| c.is_finite() && c > 0.0)
|| !durations_us.iter().all(|&d| d.is_finite())
{
return f64::NAN;
}
let m = n - 1; let x: Vec<f64> = (0..m).map(|t| closes[t + 1] / closes[t]).collect();
let rk = bar_close_ordinal_ranks(&x);
let sk = bar_close_ordinal_ranks(&durations_us[1..]);
let mut grid = vec![0u32; m * m];
for k in 0..m {
grid[rk[k] * m + sk[k]] = 1;
}
for i in 1..m {
for j in 0..m {
grid[i * m + j] += grid[(i - 1) * m + j];
}
}
for i in 0..m {
for j in 1..m {
grid[i * m + j] += grid[i * m + j - 1];
}
}
let mf = m as f64;
let mut sum = 0.0_f64;
for i in 0..m {
let gi = (i + 1) as f64 / mf;
for j in 0..m {
let cemp = f64::from(grid[i * m + j]) / mf;
let d = cemp - gi * ((j + 1) as f64 / mf);
sum += d * d;
}
}
let phi2 = 90.0 * (sum / (mf * mf));
if phi2.is_finite() { phi2 } else { f64::NAN }
}
pub fn compute_bar_hvg_forward_visibility_horizon_mean(closes: &[f64]) -> f64 {
let n = closes.len();
debug_assert!(n < (1 << 32), "horizon sum may wrap u64 for n >= 2^32");
if n < 2 || !closes.iter().all(|&c| c.is_finite()) {
return f64::NAN;
}
let mut sum: u64 = 0;
let mut count: u64 = 0;
let mut stack: Vec<usize> = Vec::with_capacity(n);
for (j, &cj) in closes.iter().enumerate() {
while let Some(&t) = stack.last() {
if cj >= closes[t] {
stack.pop();
sum += (j - t) as u64;
count += 1;
} else {
break;
}
}
stack.push(j);
}
if count == 0 {
return f64::NAN;
}
let v = (sum as f64) / (count as f64);
if v.is_finite() { v } else { f64::NAN }
}
fn hvg_adjacency_leftscan(x: &[f64]) -> (Vec<Vec<usize>>, Vec<u64>) {
let n = x.len();
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut bits = vec![0u64; (n * n).div_ceil(64)];
let connect = |adj: &mut Vec<Vec<usize>>, bits: &mut Vec<u64>, i: usize, j: usize| {
adj[i].push(j);
adj[j].push(i);
let k = i * n + j;
bits[k / 64] |= 1u64 << (k % 64);
};
for j in 1..n {
connect(&mut adj, &mut bits, j - 1, j);
let mut m = x[j - 1];
let mut i = j as isize - 2;
while i >= 0 && m < x[j] {
let iu = i as usize;
if m < x[iu] {
connect(&mut adj, &mut bits, iu, j);
}
if x[iu] > m {
m = x[iu];
}
i -= 1;
}
}
(adj, bits)
}
#[allow(clippy::many_single_char_names)]
#[inline]
fn hvg_has_edge(bits: &[u64], n: usize, a: usize, b: usize) -> bool {
let (i, j) = if a < b { (a, b) } else { (b, a) };
let k = i * n + j;
bits[k / 64] & (1u64 << (k % 64)) != 0
}
pub fn compute_bar_vg_time_directed_clustering_meangap(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 5 || !closes.iter().all(|&c| c.is_finite()) {
return f64::NAN;
}
let (adj, bits) = hvg_adjacency_leftscan(closes);
let mut past_sum = 0.0_f64;
let mut past_cnt = 0u32;
let mut fut_sum = 0.0_f64;
let mut fut_cnt = 0u32;
let mut past: Vec<usize> = Vec::with_capacity(8);
let mut fut: Vec<usize> = Vec::with_capacity(8);
for (v, nbrs) in adj.iter().enumerate() {
past.clear();
fut.clear();
for &u in nbrs {
if u < v {
past.push(u);
} else {
fut.push(u);
}
}
for (set, sum, cnt) in [
(&past, &mut past_sum, &mut past_cnt),
(&fut, &mut fut_sum, &mut fut_cnt),
] {
let k = set.len();
if k >= 2 {
let mut links = 0u32;
for a in 0..k {
for b in a + 1..k {
if hvg_has_edge(&bits, n, set[a], set[b]) {
links += 1;
}
}
}
*sum += 2.0 * f64::from(links) / ((k * (k - 1)) as f64);
*cnt += 1;
}
}
}
if past_cnt == 0 || fut_cnt == 0 {
return f64::NAN;
}
let val = past_sum / f64::from(past_cnt) - fut_sum / f64::from(fut_cnt);
if val.is_finite() { val } else { f64::NAN }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dispersion_entropy_three_distinct_classes_is_ln2_over_ln36() {
let v = compute_bar_dispersion_entropy(&[1.0, 2.0, 3.0]);
let expected = 2f64.ln() / 36f64.ln();
assert!((v - expected).abs() < 1e-12, "got {v}, want {expected}");
}
#[test]
fn dispersion_entropy_flat_window_is_nan() {
assert!(compute_bar_dispersion_entropy(&[42.0; 50]).is_nan());
}
#[test]
fn dispersion_entropy_degenerate_inputs_are_nan() {
assert!(compute_bar_dispersion_entropy(&[1.0]).is_nan());
assert!(compute_bar_dispersion_entropy(&[1.0, f64::NAN, 3.0]).is_nan());
}
#[test]
fn dispersion_entropy_bounded_on_varied_window() {
let w: Vec<f64> = (0..BAR_CLOSE_LOOKBACK_COUNT)
.map(|i| 100.0 + (i as f64 * 0.7).sin() * 3.0 + i as f64 * 0.01)
.collect();
let v = compute_bar_dispersion_entropy(&w);
assert!(
v.is_finite() && (0.0..=1.0).contains(&v),
"out of [0,1]: {v}"
);
}
#[test]
fn ncdf_standard_is_centered_and_symmetric() {
assert_eq!(ncdf_standard(0.0), 0.5, "Φ(0) must be exactly 0.5");
for &z in &[0.1, 0.5, 1.0, 2.0, 3.0, 7.5] {
let s = ncdf_standard(z) + ncdf_standard(-z);
assert!(
(s - 1.0).abs() < 1e-12,
"Φ(z)+Φ(-z) must be 1, got {s} at z={z}"
);
}
let mut prev = ncdf_standard(-8.0);
for k in -79..=80 {
let y = ncdf_standard(k as f64 * 0.1);
assert!(
y >= prev - 1e-15 && (0.0..=1.0).contains(&y),
"Φ non-monotone/out-of-range at k={k}: {y}"
);
prev = y;
}
assert!(
ncdf_standard(40.0) >= 1.0 - 1e-12,
"right tail saturates to 1"
);
assert!(ncdf_standard(-40.0) <= 1e-12, "left tail saturates to 0");
}
#[test]
fn dispersion_class_bounded_and_monotone() {
for &z in &[
f64::NAN,
f64::NEG_INFINITY,
f64::MIN,
-1e12,
-40.0,
-1.0,
0.0,
1.0,
40.0,
1e12,
f64::MAX,
f64::INFINITY,
] {
let c = dispersion_class(z);
assert!(
(1..=DISP_C).contains(&c),
"class {c} out of [1,{DISP_C}] at z={z}"
);
}
assert_eq!(
dispersion_class(f64::NAN),
1,
"NaN z → class 1 (no OOB key)"
);
assert_eq!(dispersion_class(f64::INFINITY), DISP_C, "+∞ z → class c");
assert_eq!(dispersion_class(f64::NEG_INFINITY), 1, "-∞ z → class 1");
let mut prev = 0usize;
let mut z = -6.0_f64;
while z <= 6.0 {
let c = dispersion_class(z);
assert!((1..=DISP_C).contains(&c), "class {c} out of range at z={z}");
assert!(
c >= prev,
"class must be non-decreasing in z (got {c} after {prev})"
);
prev = c;
z += 0.05;
}
assert_eq!(dispersion_class(-40.0), 1, "deep left tail → class 1");
assert_eq!(dispersion_class(40.0), DISP_C, "deep right tail → class c");
}
#[test]
fn dispersion_entropy_mean_overflow_is_finite_never_inf() {
let w = [f64::MAX; BAR_CLOSE_LOOKBACK_COUNT];
let v = compute_bar_dispersion_entropy(&w);
assert!(!v.is_infinite(), "must never be ±Inf, got {v}");
assert!(
v == 0.0 || v.is_nan(),
"expected degenerate 0.0 or NaN, got {v}"
);
}
#[test]
fn dispersion_entropy_variance_overflow_is_finite() {
let w: Vec<f64> = (0..BAR_CLOSE_LOOKBACK_COUNT)
.map(|i| if i % 2 == 0 { 1e200 } else { -1e200 })
.collect();
let v = compute_bar_dispersion_entropy(&w);
assert!(
!v.is_infinite() && (v.is_nan() || (0.0..=1.0).contains(&v)),
"must be NaN-or-[0,1], never ±Inf, got {v}"
);
}
#[test]
fn cecp_velocity_flat_window_is_nan() {
assert!(compute_bar_cecp_velocity(&[42.0; BAR_CLOSE_LOOKBACK_COUNT]).is_nan());
}
#[test]
fn cecp_velocity_degenerate_inputs_are_nan() {
assert!(
compute_bar_cecp_velocity(&[1.0, 2.0, 3.0]).is_nan(),
"len < 10"
);
let mut w = vec![1.0_f64; BAR_CLOSE_LOOKBACK_COUNT];
w[100] = f64::NAN;
assert!(compute_bar_cecp_velocity(&w).is_nan(), "non-finite input");
}
#[test]
fn cecp_velocity_bounded_on_varied_window() {
let w: Vec<f64> = (0..BAR_CLOSE_LOOKBACK_COUNT)
.map(|i| 100.0 + (i as f64 * 0.7).sin() * 3.0 + i as f64 * 0.01)
.collect();
let v = compute_bar_cecp_velocity(&w);
assert!(
v.is_finite() && (0.0..=std::f64::consts::SQRT_2).contains(&v),
"out of [0, √2]: {v}"
);
}
#[test]
fn cecp_velocity_identical_halves_is_zero() {
let mut w = Vec::with_capacity(BAR_CLOSE_LOOKBACK_COUNT);
let half: Vec<f64> = (0..CECP_HALF)
.map(|i| 100.0 + (i as f64 * 0.5).sin())
.collect();
w.extend_from_slice(&half);
w.extend_from_slice(&half);
assert_eq!(w.len(), BAR_CLOSE_LOOKBACK_COUNT);
let v = compute_bar_cecp_velocity(&w);
assert_eq!(v, 0.0, "identical halves → zero displacement, got {v}");
}
#[test]
fn cecp_hc_coordinates_in_unit_square() {
let constant: Vec<f64> = vec![42.0; CECP_HALF];
let trend: Vec<f64> = (0..CECP_HALF).map(|i| i as f64).collect();
let wave: Vec<f64> = (0..CECP_HALF)
.map(|i| 100.0 + (i as f64 * 0.7).sin() * 3.0 + i as f64 * 0.01)
.collect();
let noisy: Vec<f64> = (0..CECP_HALF)
.map(|i| ((i * 2_654_435_761usize) % 1000) as f64) .collect();
for seg in [&constant, &trend, &wave, &noisy] {
let (h, c) = cecp_hc(seg);
assert!(
h.is_finite() && (0.0..=1.0).contains(&h),
"H out of [0,1]: {h}"
);
assert!(
c.is_finite() && (0.0..=1.0).contains(&c),
"C out of [0,1]: {c}"
);
}
assert_eq!(cecp_hc(&constant), (0.0, 0.0));
}
#[test]
fn cecp_ordinal_classing_matches_argsort_over_all_triplets() {
#[rustfmt::skip]
const ARGSORT_REF: [[u8; 3]; 27] = [
[0,1,2],[0,1,2],[0,1,2],[0,2,1],[0,1,2],[0,1,2],[0,2,1],[0,2,1],[0,1,2],
[1,2,0],[1,0,2],[1,0,2],[2,0,1],[0,1,2],[0,1,2],[2,0,1],[0,2,1],[0,1,2],
[1,2,0],[1,2,0],[1,0,2],[2,1,0],[1,2,0],[1,0,2],[2,0,1],[2,0,1],[0,1,2],
];
let mut triplets = Vec::with_capacity(27);
for a in 0..3u8 {
for b in 0..3u8 {
for c in 0..3u8 {
triplets.push((a, b, c));
}
}
}
assert_eq!(triplets.len(), ARGSORT_REF.len());
for i in 0..triplets.len() {
for j in 0..triplets.len() {
let (ai, bi, ci) = triplets[i];
let (aj, bj, cj) = triplets[j];
let rust_same = ordinal_pattern_index_m3(ai as f64, bi as f64, ci as f64)
== ordinal_pattern_index_m3(aj as f64, bj as f64, cj as f64);
let argsort_same = ARGSORT_REF[i] == ARGSORT_REF[j];
assert_eq!(
rust_same, argsort_same,
"classing partition mismatch: {:?} vs {:?} (rust_same={rust_same}, argsort_same={argsort_same})",
triplets[i], triplets[j]
);
}
}
}
fn rr_eq(got: f64, num: u64, den: u64) -> bool {
got.to_bits() == (num as f64 / den as f64).to_bits()
}
#[test]
fn recurr_all_distinct_is_zero() {
let w: Vec<f64> = (1..=BAR_CLOSE_LOOKBACK_COUNT).map(|i| i as f64).collect();
assert!(rr_eq(compute_bar_categorical_recurrence_rate(&w), 0, 39800));
}
#[test]
fn recurr_all_equal_is_one() {
let w = [42.0_f64; BAR_CLOSE_LOOKBACK_COUNT];
assert!(rr_eq(
compute_bar_categorical_recurrence_rate(&w),
39800,
39800
));
}
#[test]
fn recurr_hand_three_tie() {
let w = [1.0, 1.0, 1.0, 2.0, 3.0];
assert!(rr_eq(compute_bar_categorical_recurrence_rate(&w), 6, 20));
}
#[test]
fn recurr_two_pairs() {
let w = [1.0, 1.0, 2.0, 2.0];
assert!(rr_eq(compute_bar_categorical_recurrence_rate(&w), 4, 12));
}
#[test]
fn recurr_mixed_multiplicities_in_unit_range() {
let w = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0];
let got = compute_bar_categorical_recurrence_rate(&w);
assert!((0.0..=1.0).contains(&got));
assert!(rr_eq(got, 8, 42));
}
#[test]
fn recurr_nan_on_short_or_empty() {
assert!(compute_bar_categorical_recurrence_rate(&[]).is_nan());
assert!(compute_bar_categorical_recurrence_rate(&[5.0]).is_nan());
}
#[test]
fn recurr_nan_on_nonfinite() {
assert!(compute_bar_categorical_recurrence_rate(&[1.0, f64::NAN, 2.0]).is_nan());
assert!(compute_bar_categorical_recurrence_rate(&[1.0, f64::INFINITY, 2.0]).is_nan());
assert!(compute_bar_categorical_recurrence_rate(&[f64::NEG_INFINITY, 1.0]).is_nan());
}
#[test]
fn recurr_never_inf_on_degenerate() {
for w in [vec![0.0_f64; 200], vec![-3.5_f64; 200], vec![7.0_f64; 2]] {
assert!(!compute_bar_categorical_recurrence_rate(&w).is_infinite());
}
}
#[test]
fn recurr_tie_preserving_under_constant_rescale() {
let base = [1.0, 1.0, 2.0, 3.0, 3.0, 3.0];
for factor in [2.0_f64, 0.5, 100.0] {
let scaled: Vec<f64> = base.iter().map(|&x| x * factor).collect();
assert_eq!(
compute_bar_categorical_recurrence_rate(&base).to_bits(),
compute_bar_categorical_recurrence_rate(&scaled).to_bits(),
"RR not tie-preserving under ×{factor}"
);
}
}
#[test]
fn signflux_zero_on_monotone_series() {
let w = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
assert_eq!(
compute_bar_sign_markov_flux(&w).to_bits(),
0.0_f64.to_bits()
);
}
#[test]
fn signflux_one_on_perfect_three_cycle() {
let w = [1.0, 2.0, 2.0, 1.0, 2.0, 2.0, 1.0, 2.0, 2.0, 1.0];
assert_eq!(
compute_bar_sign_markov_flux(&w).to_bits(),
1.0_f64.to_bits()
);
}
#[test]
fn signflux_in_unit_range_on_mixed_window() {
let w = [1.0, 2.0, 1.5, 1.5, 3.0, 2.0, 2.5, 2.5, 1.0, 4.0];
let got = compute_bar_sign_markov_flux(&w);
assert!((0.0..=1.0).contains(&got), "flux {got} out of [0,1]");
}
#[test]
fn signflux_nan_on_short_or_undefined() {
assert!(compute_bar_sign_markov_flux(&[1.0, 2.0, 3.0]).is_nan());
assert!(compute_bar_sign_markov_flux(&[]).is_nan());
assert!(compute_bar_sign_markov_flux(&[1.0, 2.0, 0.0, 3.0, 4.0]).is_nan());
assert!(compute_bar_sign_markov_flux(&[1.0, -2.0, 3.0, 4.0, 5.0]).is_nan());
assert!(compute_bar_sign_markov_flux(&[1.0, f64::NAN, 3.0, 4.0, 5.0]).is_nan());
assert!(compute_bar_sign_markov_flux(&[1.0, f64::INFINITY, 3.0, 4.0, 5.0]).is_nan());
}
#[test]
fn signflux_never_inf_on_degenerate() {
let flat = vec![5.0_f64; 200];
let v = compute_bar_sign_markov_flux(&flat);
assert!(!v.is_infinite());
assert_eq!(v.to_bits(), 0.0_f64.to_bits());
for w in [vec![7.0_f64; 2], vec![1.0, 2.0, 3.0, 4.0]] {
assert!(!compute_bar_sign_markov_flux(&w).is_infinite());
}
}
#[test]
fn signflux_invariant_under_positive_rescale() {
let base = [1.0, 2.0, 2.0, 1.0, 3.0, 2.5, 2.5, 4.0];
for factor in [2.0_f64, 0.5, 1000.0] {
let scaled: Vec<f64> = base.iter().map(|&x| x * factor).collect();
assert_eq!(
compute_bar_sign_markov_flux(&base).to_bits(),
compute_bar_sign_markov_flux(&scaled).to_bits(),
"sign-flux not invariant under ×{factor}"
);
}
}
fn closes_from_logret(logret: &[f64]) -> Vec<f64> {
let mut c = Vec::with_capacity(logret.len() + 1);
c.push(100.0_f64);
for &r in logret {
let prev = *c.last().unwrap();
c.push(prev * libm::exp(r));
}
c
}
#[test]
fn rrbicov_time_reversal_antisymmetry() {
let logret = [
0.10, 0.04, -0.18, 0.06, -0.02, 0.09, -0.15, 0.03, 0.07, -0.05, 0.12, -0.08,
];
let fwd = closes_from_logret(&logret);
let mut rev_lr = logret;
rev_lr.reverse();
let rev = closes_from_logret(&rev_lr);
let v_fwd = compute_bar_ramsey_rothman_bicov_lag1(&fwd);
let v_rev = compute_bar_ramsey_rothman_bicov_lag1(&rev);
assert!(v_fwd.is_finite() && v_rev.is_finite());
assert!(
v_fwd.abs() > 1e-6,
"asymmetric series must be non-trivially irreversible (got {v_fwd})"
);
assert!(
(v_fwd + v_rev).abs() < 1e-12,
"time-reversal antisymmetry breach: {v_fwd} + {v_rev} != 0"
);
}
#[test]
fn rrbicov_nan_on_short_or_undefined() {
assert!(compute_bar_ramsey_rothman_bicov_lag1(&[1.0, 2.0, 3.0]).is_nan());
assert!(compute_bar_ramsey_rothman_bicov_lag1(&[]).is_nan());
assert!(compute_bar_ramsey_rothman_bicov_lag1(&[1.0, 2.0, 0.0, 3.0, 4.0]).is_nan());
assert!(compute_bar_ramsey_rothman_bicov_lag1(&[1.0, -2.0, 3.0, 4.0, 5.0]).is_nan());
assert!(compute_bar_ramsey_rothman_bicov_lag1(&[1.0, f64::NAN, 3.0, 4.0, 5.0]).is_nan());
assert!(compute_bar_ramsey_rothman_bicov_lag1(&[5.0; 200]).is_nan());
}
#[test]
fn rrbicov_never_inf_on_degenerate() {
for w in [
vec![5.0_f64; 200],
vec![7.0_f64; 4],
vec![1.0, 2.0, 3.0],
vec![1.0, 1.0, 1.0, 1.0, 2.0],
] {
assert!(!compute_bar_ramsey_rothman_bicov_lag1(&w).is_infinite());
}
}
fn closes_from_increments(incr: &[f64]) -> Vec<f64> {
let mut c = Vec::with_capacity(incr.len() + 1);
c.push(100.0_f64);
for &d in incr {
let prev = *c.last().unwrap();
c.push(prev + d);
}
c
}
#[test]
fn ehlers_zero_on_symmetric_increments() {
let closes = closes_from_increments(&[1.0, -1.0, 2.0, -2.0, 3.0, -3.0]);
assert_eq!(
compute_bar_ehlers_increment_asymmetry(&closes).to_bits(),
0.0_f64.to_bits()
);
}
#[test]
fn ehlers_odd_under_increment_negation() {
let incr = [0.7, -0.2, 0.5, 0.9, -0.4, 0.3, -0.8, 0.6];
let fwd = closes_from_increments(&incr);
let neg_incr: Vec<f64> = incr.iter().map(|&d| -d).collect();
let neg = closes_from_increments(&neg_incr);
let e = compute_bar_ehlers_increment_asymmetry(&fwd);
let e_neg = compute_bar_ehlers_increment_asymmetry(&neg);
assert!(
e.is_finite() && e.abs() > 1e-6,
"need a non-trivially skewed window"
);
assert_eq!(e_neg.to_bits(), (-e).to_bits(), "E not odd under negation");
}
#[test]
fn ehlers_nan_on_short_or_flat() {
assert!(compute_bar_ehlers_increment_asymmetry(&[1.0, 2.0]).is_nan());
assert!(compute_bar_ehlers_increment_asymmetry(&[]).is_nan());
assert!(compute_bar_ehlers_increment_asymmetry(&[5.0; 200]).is_nan());
assert!(compute_bar_ehlers_increment_asymmetry(&[1.0, f64::NAN, 3.0]).is_nan());
assert!(compute_bar_ehlers_increment_asymmetry(&[1.0, 2.0, f64::INFINITY]).is_nan());
}
#[test]
fn ehlers_never_inf_on_degenerate() {
for w in [
vec![5.0_f64; 200],
vec![1.0, 2.0, 3.0],
vec![1.0, 1.0, 1.0, 1.0, 5.0],
] {
assert!(!compute_bar_ehlers_increment_asymmetry(&w).is_infinite());
}
}
#[test]
fn coxstuart_zero_on_trendless_pairing() {
let w = [1.0, 2.0, 3.0, 1.0]; assert_eq!(
compute_bar_cox_stuart_trend_z(&w).to_bits(),
0.0_f64.to_bits()
);
}
#[test]
fn coxstuart_max_positive_on_monotone_up() {
let w: Vec<f64> = (1..=BAR_CLOSE_LOOKBACK_COUNT).map(|i| i as f64).collect();
assert_eq!(
compute_bar_cox_stuart_trend_z(&w).to_bits(),
10.0_f64.to_bits()
);
}
#[test]
fn coxstuart_max_negative_on_monotone_down() {
let w: Vec<f64> = (1..=BAR_CLOSE_LOOKBACK_COUNT)
.rev()
.map(|i| i as f64)
.collect();
assert_eq!(
compute_bar_cox_stuart_trend_z(&w).to_bits(),
(-10.0_f64).to_bits()
);
}
#[test]
fn coxstuart_sign_tracks_trend_direction() {
let up = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let down = [6.0, 5.0, 4.0, 3.0, 2.0, 1.0];
assert!(compute_bar_cox_stuart_trend_z(&up) > 0.0);
assert!(compute_bar_cox_stuart_trend_z(&down) < 0.0);
}
#[test]
fn coxstuart_invariant_under_positive_rescale() {
let base = [1.0, 5.0, 2.0, 9.0, 3.0, 1.0, 4.0, 8.0, 2.5, 7.0];
for factor in [2.0_f64, 0.5, 1000.0, 0.001] {
let scaled: Vec<f64> = base.iter().map(|&x| x * factor).collect();
assert_eq!(
compute_bar_cox_stuart_trend_z(&base).to_bits(),
compute_bar_cox_stuart_trend_z(&scaled).to_bits(),
"cox-stuart z not invariant under ×{factor}"
);
}
}
#[test]
fn coxstuart_nan_on_short_or_nonfinite() {
assert!(compute_bar_cox_stuart_trend_z(&[1.0, 2.0, 3.0]).is_nan()); assert!(compute_bar_cox_stuart_trend_z(&[]).is_nan());
assert!(compute_bar_cox_stuart_trend_z(&[1.0, f64::NAN, 3.0, 4.0]).is_nan());
assert!(compute_bar_cox_stuart_trend_z(&[1.0, 2.0, f64::INFINITY, 4.0]).is_nan());
assert!(compute_bar_cox_stuart_trend_z(&[5.0; 200]).is_finite());
}
#[test]
fn coxstuart_never_inf_on_degenerate() {
for w in [
vec![5.0_f64; 200],
vec![1.0, 2.0, 3.0, 4.0],
vec![1.0, 2.0, 3.0],
] {
assert!(!compute_bar_cox_stuart_trend_z(&w).is_infinite());
}
}
#[test]
fn gmskew_zero_on_symmetric_logrets() {
let logret = [-0.02, -0.01, 0.01, 0.02]; let closes = closes_from_logret(&logret);
let b3 = compute_bar_groeneveld_meeden_b3_skewness(&closes);
assert!(
b3.is_finite() && b3.abs() <= 1e-9,
"symmetric b3 should be ~0, got {b3}"
);
}
#[test]
fn gmskew_antisymmetric_under_logret_negation() {
let logret = [
0.03, -0.01, 0.05, 0.12, -0.02, 0.04, -0.08, 0.01, 0.06, -0.03,
];
let fwd = closes_from_logret(&logret);
let neg: Vec<f64> = logret.iter().map(|&r| -r).collect();
let rev = closes_from_logret(&neg);
let b = compute_bar_groeneveld_meeden_b3_skewness(&fwd);
let bn = compute_bar_groeneveld_meeden_b3_skewness(&rev);
assert!(
b.is_finite() && b.abs() > 1e-6,
"need a non-trivially skewed window"
);
assert!(
(b + bn).abs() <= 1e-9,
"b3 not antisymmetric: {b} + {bn} != 0"
);
}
#[test]
fn gmskew_in_unit_range_on_mixed_window() {
let logret = [
0.03, -0.01, 0.05, 0.12, -0.02, 0.04, -0.08, 0.01, 0.06, -0.03, 0.09, -0.04, 0.02,
-0.07, 0.11,
];
let closes = closes_from_logret(&logret);
let b3 = compute_bar_groeneveld_meeden_b3_skewness(&closes);
assert!(
b3.is_finite() && (-1.0..=1.0).contains(&b3),
"b3 {b3} out of [-1,1]"
);
}
#[test]
fn gmskew_nan_on_short_or_undefined() {
assert!(compute_bar_groeneveld_meeden_b3_skewness(&[1.0, 2.0]).is_nan());
assert!(compute_bar_groeneveld_meeden_b3_skewness(&[]).is_nan());
assert!(compute_bar_groeneveld_meeden_b3_skewness(&[1.0, 0.0, 2.0, 3.0]).is_nan());
assert!(compute_bar_groeneveld_meeden_b3_skewness(&[1.0, -2.0, 3.0]).is_nan());
assert!(compute_bar_groeneveld_meeden_b3_skewness(&[1.0, f64::NAN, 3.0]).is_nan());
assert!(compute_bar_groeneveld_meeden_b3_skewness(&[5.0; 200]).is_nan());
}
#[test]
fn gmskew_never_inf_on_degenerate() {
for w in [
vec![5.0_f64; 200],
vec![1.0, 2.0, 3.0],
vec![1.0, 1.0, 1.0, 1.0, 2.0],
] {
assert!(!compute_bar_groeneveld_meeden_b3_skewness(&w).is_infinite());
}
}
#[test]
fn gmskew_even_n_median_averaging_path() {
let closes = closes_from_logret(&[0.10, 0.01, 0.02, 0.03]);
assert_eq!(closes.len(), 5, "5 closes → 4 logrets (even median path)");
let b3 = compute_bar_groeneveld_meeden_b3_skewness(&closes);
assert!(b3.is_finite() && (-1.0..=1.0).contains(&b3));
assert!(
(b3 - 0.6).abs() <= 1e-9,
"even-n median-averaged b3 should be ~0.6, got {b3}"
);
}
fn normalish_logrets(count: usize, seed: u64) -> Vec<f64> {
let mut state = seed;
let mut next_u = || -> f64 {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(state >> 11) as f64 / (1u64 << 53) as f64
};
(0..count)
.map(|_| {
let s: f64 = (0..12).map(|_| next_u()).sum();
0.01 * (s - 6.0) })
.collect()
}
#[test]
fn lkurt_near_normal_tau4_approx_0p1226() {
let lr = normalish_logrets(200, 0x9E3779B97F4A7C15);
let closes = closes_from_logret(&lr);
assert_eq!(closes.len(), 201);
let t4 = compute_bar_l_kurtosis_tau4(&closes);
assert!(t4.is_finite(), "near-normal tau4 must be finite, got {t4}");
assert!(
(0.05..=0.20).contains(&t4),
"near-normal tau4 should sit near the Gaussian 0.1226, got {t4}"
);
}
#[test]
fn lkurt_nan_on_short_or_undefined() {
assert!(compute_bar_l_kurtosis_tau4(&[1.0, 2.0, 3.0, 4.0]).is_nan());
assert!(compute_bar_l_kurtosis_tau4(&[]).is_nan());
assert!(compute_bar_l_kurtosis_tau4(&[1.0, 0.0, 2.0, 3.0, 4.0]).is_nan());
assert!(compute_bar_l_kurtosis_tau4(&[1.0, -2.0, 3.0, 4.0, 5.0]).is_nan());
assert!(compute_bar_l_kurtosis_tau4(&[1.0, f64::NAN, 3.0, 4.0, 5.0]).is_nan());
assert!(compute_bar_l_kurtosis_tau4(&[5.0; 200]).is_nan());
}
#[test]
fn lkurt_never_inf_on_degenerate() {
let cases: Vec<Vec<f64>> = vec![
vec![1.0, 1.0, 1.0, 1.0, 1.000_000_001],
vec![1e-12, 1.0, 1e12, 1.0, 1e-12, 1.0],
vec![1.0; 5],
];
for w in cases {
assert!(!compute_bar_l_kurtosis_tau4(&w).is_infinite());
}
}
#[test]
fn lkurt_finite_in_sample_range_on_mixed_window() {
let lr = normalish_logrets(120, 0x123456789ABCDEF0);
let closes = closes_from_logret(&lr);
let t4 = compute_bar_l_kurtosis_tau4(&closes);
assert!(t4.is_finite(), "tau4 must be finite, got {t4}");
assert!(
(-0.30..=1.0).contains(&t4),
"tau4 out of valid L-kurtosis band: {t4}"
);
}
#[test]
fn bartels_average_ranks_match_scipy_convention() {
let r = bar_close_average_ranks(&[3.0, 1.0, 1.0, 5.0]);
assert_eq!(r, vec![3.0, 1.5, 1.5, 4.0]);
let r2 = bar_close_average_ranks(&[10.0, 20.0, 30.0]);
assert_eq!(r2, vec![1.0, 2.0, 3.0]);
let r3 = bar_close_average_ranks(&[7.0, 7.0, 7.0, 7.0]);
assert_eq!(r3, vec![2.5, 2.5, 2.5, 2.5]);
}
#[test]
fn bartels_hand_pinned_small_window() {
let closes = [100.0_f64, 101.0, 103.0, 106.0, 102.0];
let z = compute_bar_bartels_rank_vn_ratio(&closes);
assert!(
(z - 0.267_261_241_912_424_4).abs() <= 1e-12,
"hand-pinned bartels z mismatch: got {z}"
);
}
#[test]
fn bartels_strong_negative_z_on_monotone_series() {
let lr: Vec<f64> = (0..150).map(|i| 0.001 + 0.0001 * i as f64).collect();
let closes = closes_from_logret(&lr);
let z = compute_bar_bartels_rank_vn_ratio(&closes);
assert!(
z.is_finite() && z < -3.0,
"monotone series should give strong negative z, got {z}"
);
}
#[test]
fn bartels_scale_invariant_rank_feature() {
let base = closes_from_logret(&normalish_logrets(80, 0xA5A5A5A55A5A5A5A));
let z0 = compute_bar_bartels_rank_vn_ratio(&base);
assert!(z0.is_finite());
for factor in [0.5_f64, 2.0, 13.0, 1e-4, 9999.0] {
let scaled: Vec<f64> = base.iter().map(|&c| c * factor).collect();
let z = compute_bar_bartels_rank_vn_ratio(&scaled);
assert!(
(z0 - z).abs() <= 1e-9,
"bartels not rank/scale-invariant under ×{factor}: {z0} vs {z}"
);
}
}
#[test]
fn bartels_random_series_is_finite_and_sane() {
let lr = normalish_logrets(199, 0x0123456789ABCDEF);
let closes = closes_from_logret(&lr);
let z = compute_bar_bartels_rank_vn_ratio(&closes);
assert!(
z.is_finite() && z.abs() < 8.0,
"random-series bartels z out of sane band: {z}"
);
}
#[test]
fn bartels_nan_on_short_or_undefined() {
assert!(compute_bar_bartels_rank_vn_ratio(&[1.0, 2.0, 3.0, 4.0]).is_nan());
assert!(compute_bar_bartels_rank_vn_ratio(&[]).is_nan());
assert!(compute_bar_bartels_rank_vn_ratio(&[1.0, 0.0, 2.0, 3.0, 4.0]).is_nan());
assert!(compute_bar_bartels_rank_vn_ratio(&[1.0, -2.0, 3.0, 4.0, 5.0]).is_nan());
assert!(compute_bar_bartels_rank_vn_ratio(&[1.0, f64::NAN, 3.0, 4.0, 5.0]).is_nan());
assert!(compute_bar_bartels_rank_vn_ratio(&[5.0; 200]).is_nan());
}
#[test]
fn bartels_never_inf_on_degenerate() {
let cases: Vec<Vec<f64>> = vec![
vec![1.0, 1.0, 1.0, 1.0, 1.000_000_001],
vec![1e-9, 1.0, 1e9, 1.0, 1e-9, 1.0],
vec![1.0; 5],
];
for w in cases {
assert!(!compute_bar_bartels_rank_vn_ratio(&w).is_infinite());
}
}
fn hoeffding_phi2_brute(closes: &[f64], durations_us: &[f64]) -> f64 {
let n = closes.len();
assert!(n >= 5 && durations_us.len() == n);
let m = n - 1;
let x: Vec<f64> = (0..m).map(|t| closes[t + 1] / closes[t]).collect();
let rk = bar_close_ordinal_ranks(&x);
let sk = bar_close_ordinal_ranks(&durations_us[1..]);
let mf = m as f64;
let mut sum = 0.0_f64;
for i in 1..=m {
for j in 1..=m {
let mut cnt = 0usize;
for k in 0..m {
if rk[k] < i && sk[k] < j {
cnt += 1;
}
}
let d = (cnt as f64) / mf - ((i as f64) / mf) * ((j as f64) / mf);
sum += d * d;
}
}
90.0 * (sum / (mf * mf))
}
fn hoeffding_lcg_fixture(n: usize) -> (Vec<f64>, Vec<f64>) {
let mut state: u64 = 0x00c0_ffee_d00d_5eed;
let mut next = || {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(state >> 33) as f64 / f64::from(1u32 << 31)
};
let mut closes = Vec::with_capacity(n);
let mut durs = Vec::with_capacity(n);
let mut c = 40_000.0_f64;
for _ in 0..n {
c *= 1.0 + (next() - 0.5) * 0.004;
closes.push((c * 1e8).round() / 1e8); durs.push((1.0 + (next() * 3_000_000.0)).floor()); }
(closes, durs)
}
#[test]
fn hoeffding_kernel_matches_brute_force_independent_leg() {
let (closes, durs) = hoeffding_lcg_fixture(61);
let fast = compute_bar_hoeffding_phi_squared_midreturn_duration(&closes, &durs);
let brute = hoeffding_phi2_brute(&closes, &durs);
assert!(
(fast - brute).abs() <= 1e-12,
"cumsum-grid {fast} vs direct-count {brute} diverge"
);
}
#[test]
fn hoeffding_monotone_dependence_near_one() {
let n = 200usize;
let mut closes = Vec::with_capacity(n);
let mut c = 1000.0_f64;
for t in 0..n {
c *= 1.0 + 0.0001 * (t as f64 + 1.0);
closes.push(c);
}
let up: Vec<f64> = (0..n).map(|t| 1_000.0 + 1_000.0 * t as f64).collect();
let down: Vec<f64> = (0..n).map(|t| 1_000_000.0 - 1_000.0 * t as f64).collect();
let co = compute_bar_hoeffding_phi_squared_midreturn_duration(&closes, &up);
let counter = compute_bar_hoeffding_phi_squared_midreturn_duration(&closes, &down);
assert!(co > 0.9, "co-monotone Φ² should approach 1, got {co}");
assert!(
counter > 0.9,
"counter-monotone Φ² should approach 1, got {counter}"
);
assert!(
co <= 1.001 && counter <= 1.001,
"grid Φ² exceeded 1 + O(1/m): {co} / {counter}"
);
}
#[test]
fn hoeffding_bounded_and_deterministic() {
let (closes, durs) = hoeffding_lcg_fixture(200);
let a = compute_bar_hoeffding_phi_squared_midreturn_duration(&closes, &durs);
let b = compute_bar_hoeffding_phi_squared_midreturn_duration(&closes, &durs);
assert!((0.0..=1.0).contains(&a), "Φ² out of [0,1]: {a}");
assert_eq!(a.to_bits(), b.to_bits(), "non-deterministic Φ²");
}
#[test]
fn hoeffding_scale_and_monotone_transform_invariant() {
let (closes, durs) = hoeffding_lcg_fixture(200);
let base = compute_bar_hoeffding_phi_squared_midreturn_duration(&closes, &durs);
let scaled: Vec<f64> = closes.iter().map(|&c| c * 4.0).collect();
let stretched: Vec<f64> = durs.iter().map(|&d| d * 7.0 + 3.0).collect();
let v_scaled = compute_bar_hoeffding_phi_squared_midreturn_duration(&scaled, &durs);
let v_stretched = compute_bar_hoeffding_phi_squared_midreturn_duration(&closes, &stretched);
assert_eq!(
base.to_bits(),
v_scaled.to_bits(),
"closes×4 changed Φ² bits"
);
assert_eq!(
base.to_bits(),
v_stretched.to_bits(),
"monotone duration transform changed Φ² bits"
);
}
#[test]
fn hoeffding_nan_policy() {
let f = compute_bar_hoeffding_phi_squared_midreturn_duration;
assert!(f(&[1.0, 2.0, 3.0, 4.0], &[1.0, 2.0, 3.0, 4.0]).is_nan()); assert!(f(&[1.0, 2.0, 3.0, 4.0, 5.0], &[1.0, 2.0, 3.0, 4.0]).is_nan()); assert!(f(&[1.0, 0.0, 3.0, 4.0, 5.0], &[1.0; 5]).is_nan()); assert!(f(&[1.0, -2.0, 3.0, 4.0, 5.0], &[1.0; 5]).is_nan()); assert!(f(&[1.0, f64::NAN, 3.0, 4.0, 5.0], &[1.0; 5]).is_nan()); assert!(f(&[1.0, 2.0, 3.0, 4.0, 5.0], &[1.0, 2.0, f64::NAN, 4.0, 5.0]).is_nan()); let tied = f(&[1.0, 2.0, 1.5, 3.0, 2.5, 4.0], &[7.0; 6]);
assert!(
tied.is_finite() && (0.0..=1.0).contains(&tied),
"all-tied durations must be DEFINED"
);
}
#[test]
fn hoeffding_never_inf_on_degenerate() {
let cases: Vec<(Vec<f64>, Vec<f64>)> = vec![
(vec![1.0, 1.0, 1.0, 1.0, 1.000_000_001], vec![1.0; 5]),
(
vec![1e-9, 1.0, 1e9, 1.0, 1e-9, 1.0],
vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0],
),
(vec![1.0; 5], vec![5.0, 4.0, 3.0, 2.0, 1.0]),
];
for (c, d) in cases {
assert!(!compute_bar_hoeffding_phi_squared_midreturn_duration(&c, &d).is_infinite());
}
}
#[test]
fn hoeffding_ordinal_ranks_stable_ties() {
assert_eq!(
bar_close_ordinal_ranks(&[2.0, 1.0, 2.0, 0.5]),
vec![2, 1, 3, 0]
);
assert_eq!(bar_close_ordinal_ranks(&[7.0; 4]), vec![0, 1, 2, 3]);
}
fn hvghorizon_naive(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 2 || !closes.iter().all(|&c| c.is_finite()) {
return f64::NAN;
}
let mut sum: u64 = 0;
let mut count: u64 = 0;
for t in 0..n - 1 {
for k in 1..n - t {
if closes[t + k] >= closes[t] {
sum += k as u64;
count += 1;
break;
}
}
}
if count == 0 {
f64::NAN
} else {
(sum as f64) / (count as f64)
}
}
fn hvghorizon_lcg_closes(n: usize) -> Vec<f64> {
let mut state: u64 = 0x5eed_cafe_f00d_d00d;
let mut next = || {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(state >> 33) as f64 / f64::from(1u32 << 31)
};
let mut closes = Vec::with_capacity(n);
let mut c = 40_000.0_f64;
for _ in 0..n {
c *= 1.0 + (next() - 0.5) * 0.004;
closes.push((c * 1e8).round() / 1e8); }
closes
}
#[test]
fn hvghorizon_kernel_matches_naive_brute_force_leg() {
for n in [5usize, 23, 61, 200] {
let closes = hvghorizon_lcg_closes(n);
let fast = compute_bar_hvg_forward_visibility_horizon_mean(&closes);
let brute = hvghorizon_naive(&closes);
assert_eq!(
fast.to_bits(),
brute.to_bits(),
"stack {fast} vs naive {brute} diverge at n={n}"
);
}
}
#[test]
fn hvghorizon_closed_form_anchors() {
let inc: Vec<f64> = (1..=200).map(|i| i as f64).collect();
assert_eq!(compute_bar_hvg_forward_visibility_horizon_mean(&inc), 1.0);
assert_eq!(
compute_bar_hvg_forward_visibility_horizon_mean(&[7.0; 200]),
1.0
);
let dec: Vec<f64> = (1..=200).rev().map(|i| i as f64).collect();
assert!(compute_bar_hvg_forward_visibility_horizon_mean(&dec).is_nan());
let v = compute_bar_hvg_forward_visibility_horizon_mean(&[3.0, 1.0, 2.0, 3.0]);
assert_eq!(v, 5.0 / 3.0, "hand-pinned [3,1,2,3] must give exactly 5/3");
}
#[test]
fn hvghorizon_positive_scale_invariant_bits() {
let closes = hvghorizon_lcg_closes(200);
let base = compute_bar_hvg_forward_visibility_horizon_mean(&closes);
for &k in &[2.0_f64, 0.5, 4.0, 1000.0] {
let scaled: Vec<f64> = closes.iter().map(|&c| c * k).collect();
let got = compute_bar_hvg_forward_visibility_horizon_mean(&scaled);
assert_eq!(base.to_bits(), got.to_bits(), "scale x{k} changed the bits");
}
}
#[test]
fn hvghorizon_deterministic_and_bounded() {
let closes = hvghorizon_lcg_closes(200);
let a = compute_bar_hvg_forward_visibility_horizon_mean(&closes);
let b = compute_bar_hvg_forward_visibility_horizon_mean(&closes);
assert_eq!(a.to_bits(), b.to_bits(), "non-deterministic value");
assert!(
(1.0..=199.0).contains(&a),
"mean horizon out of [1, n-1]: {a}"
);
}
#[test]
fn hvghorizon_nan_policy_and_never_inf() {
let f = compute_bar_hvg_forward_visibility_horizon_mean;
assert!(f(&[]).is_nan());
assert!(f(&[42.0]).is_nan()); assert!(f(&[1.0, f64::NAN, 3.0]).is_nan());
assert!(f(&[1.0, f64::INFINITY, 3.0]).is_nan());
assert!(f(&[3.0, 2.0, 1.0]).is_nan()); let cases: Vec<Vec<f64>> = vec![
vec![0.0, 0.0, 0.0, 0.0],
vec![-3.0, -1.0, -2.0, -0.5],
vec![1e-12, 1e12, 1e-12, 1e12],
];
for w in cases {
let v = f(&w);
assert!(!v.is_infinite(), "±Inf on {w:?}");
}
}
fn vgclust_naive(closes: &[f64]) -> f64 {
let n = closes.len();
if n < 5 || !closes.iter().all(|&c| c.is_finite()) {
return f64::NAN;
}
let mut edge = vec![false; n * n];
for i in 0..n - 1 {
for j in i + 1..n {
let lo = closes[i].min(closes[j]);
if (i + 1..j).all(|k| closes[k] < lo) {
edge[i * n + j] = true;
}
}
}
let has = |a: usize, b: usize| {
let (i, j) = if a < b { (a, b) } else { (b, a) };
edge[i * n + j]
};
let (mut ps, mut pc, mut fs, mut fc) = (0.0_f64, 0u32, 0.0_f64, 0u32);
for v in 0..n {
let past: Vec<usize> = (0..v).filter(|&u| has(u, v)).collect();
let fut: Vec<usize> = (v + 1..n).filter(|&u| has(v, u)).collect();
for (set, sum, cnt) in [(&past, &mut ps, &mut pc), (&fut, &mut fs, &mut fc)] {
let k = set.len();
if k >= 2 {
let mut links = 0u32;
for a in 0..k {
for b in a + 1..k {
if has(set[a], set[b]) {
links += 1;
}
}
}
*sum += 2.0 * f64::from(links) / ((k * (k - 1)) as f64);
*cnt += 1;
}
}
}
if pc == 0 || fc == 0 {
return f64::NAN;
}
ps / f64::from(pc) - fs / f64::from(fc)
}
fn vgclust_lcg_closes(n: usize) -> Vec<f64> {
let mut state: u64 = 0xdead_beef_cafe_1234;
let mut next = || {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(state >> 33) as f64 / f64::from(1u32 << 31)
};
let mut closes = Vec::with_capacity(n);
let mut c = 40_000.0_f64;
for _ in 0..n {
c *= 1.0 + (next() - 0.5) * 0.004;
closes.push((c * 1e8).round() / 1e8);
}
closes
}
#[test]
fn vgclust_kernel_matches_naive_brute_force_leg() {
for n in [5usize, 23, 61, 200] {
let closes = vgclust_lcg_closes(n);
let fast = compute_bar_vg_time_directed_clustering_meangap(&closes);
let brute = vgclust_naive(&closes);
assert_eq!(
fast.to_bits(),
brute.to_bits(),
"left-scan {fast} vs naive {brute} diverge at n={n}"
);
}
let ties: Vec<Vec<f64>> = vec![
vec![2.0, 1.0, 1.0, 2.0, 1.5],
vec![3.0, 3.0, 1.0, 3.0, 2.0],
vec![5.0, 5.0, 5.0, 5.0, 5.0],
vec![1.0, 2.0, 2.0, 1.0, 2.0, 2.0, 1.0],
vec![2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0],
];
for w in ties {
let fast = compute_bar_vg_time_directed_clustering_meangap(&w);
let brute = vgclust_naive(&w);
assert!(
fast.to_bits() == brute.to_bits() || (fast.is_nan() && brute.is_nan()),
"tie case {w:?}: left-scan {fast} vs naive {brute}"
);
}
}
#[test]
fn vgclust_time_reversal_flips_sign_within_1e12() {
for n in [23usize, 61, 200] {
let closes = vgclust_lcg_closes(n);
let v = compute_bar_vg_time_directed_clustering_meangap(&closes);
let rev: Vec<f64> = closes.iter().rev().copied().collect();
let vr = compute_bar_vg_time_directed_clustering_meangap(&rev);
assert!(
(vr + v).abs() <= 1e-12,
"reversal flip violated at n={n}: {v} vs {vr}"
);
}
}
#[test]
fn vgclust_closed_form_anchors() {
let v = compute_bar_vg_time_directed_clustering_meangap(&[2.0, 1.0, 2.0, 1.0, 2.0]);
assert_eq!(v, 0.0, "symmetric sawtooth must give exactly 0.0, got {v}");
let inc: Vec<f64> = (1..=200).map(|i| i as f64).collect();
assert!(compute_bar_vg_time_directed_clustering_meangap(&inc).is_nan());
let dec: Vec<f64> = (1..=200).rev().map(|i| i as f64).collect();
assert!(compute_bar_vg_time_directed_clustering_meangap(&dec).is_nan());
}
#[test]
fn vgclust_scale_invariant_deterministic_bounded() {
let closes = vgclust_lcg_closes(200);
let base = compute_bar_vg_time_directed_clustering_meangap(&closes);
for &k in &[2.0_f64, 0.5, 4.0, 0.25] {
let scaled: Vec<f64> = closes.iter().map(|&c| c * k).collect();
let got = compute_bar_vg_time_directed_clustering_meangap(&scaled);
assert_eq!(
base.to_bits(),
got.to_bits(),
"pow2 rescale x{k} changed bits"
);
}
let again = compute_bar_vg_time_directed_clustering_meangap(&closes);
assert_eq!(base.to_bits(), again.to_bits(), "non-deterministic");
assert!((-1.0..=1.0).contains(&base), "out of [-1,1]: {base}");
}
#[test]
fn vgclust_nan_policy_and_never_inf() {
let f = compute_bar_vg_time_directed_clustering_meangap;
assert!(f(&[]).is_nan());
assert!(f(&[1.0, 2.0, 3.0, 4.0]).is_nan()); assert!(f(&[1.0, f64::NAN, 3.0, 4.0, 5.0]).is_nan());
assert!(f(&[1.0, f64::INFINITY, 3.0, 4.0, 5.0]).is_nan());
let cases: Vec<Vec<f64>> = vec![
vec![7.0; 200],
vec![0.0, 1.0, 0.0, 1.0, 0.0],
vec![-3.0, -1.0, -2.0, -0.5, -4.0],
];
for w in cases {
assert!(!f(&w).is_infinite(), "±Inf on {w:?}");
}
}
}