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 }
}
#[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]
);
}
}
}
}