use crate::error::FinError;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ShannonEntropy {
window: usize,
num_bins: usize,
buffer: Vec<f64>,
}
impl ShannonEntropy {
pub fn new(window: usize, num_bins: usize) -> Result<Self, FinError> {
if window == 0 {
return Err(FinError::InvalidPeriod(window));
}
if num_bins == 0 {
return Err(FinError::InvalidInput("num_bins must be at least 1".to_owned()));
}
Ok(Self { window, num_bins, buffer: Vec::with_capacity(window + 1) })
}
pub fn update(&mut self, price: f64) -> Option<f64> {
self.buffer.push(price);
if self.buffer.len() > self.window + 1 {
self.buffer.remove(0);
}
if self.buffer.len() < 2 {
return None;
}
let returns: Vec<f64> = self
.buffer
.windows(2)
.map(|w| {
if w[0] != 0.0 { (w[1] / w[0]).ln() } else { 0.0 }
})
.collect();
if returns.len() < self.window {
return None;
}
let min = returns.iter().cloned().fold(f64::INFINITY, f64::min);
let max = returns.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let range = max - min;
if range < f64::EPSILON {
return Some(0.0);
}
let bin_width = range / self.num_bins as f64;
let mut counts = vec![0usize; self.num_bins];
for &r in &returns {
let idx = ((r - min) / bin_width).floor() as usize;
let idx = idx.min(self.num_bins - 1);
counts[idx] += 1;
}
let n = returns.len() as f64;
let entropy = counts
.iter()
.filter(|&&c| c > 0)
.map(|&c| {
let p = c as f64 / n;
-p * p.ln()
})
.sum();
Some(entropy)
}
pub fn warmup_period(&self) -> usize {
self.window + 1
}
}
#[derive(Debug, Clone)]
pub struct PermutationEntropy {
order: usize,
window: usize,
buffer: Vec<f64>,
}
impl PermutationEntropy {
pub fn new(order: usize, window: usize) -> Result<Self, FinError> {
if order < 2 {
return Err(FinError::InvalidInput(
"Permutation entropy order must be at least 2".to_owned(),
));
}
if window == 0 {
return Err(FinError::InvalidPeriod(window));
}
Ok(Self { order, window, buffer: Vec::with_capacity(window + order) })
}
pub fn update(&mut self, price: f64) -> Option<f64> {
self.buffer.push(price);
let needed = self.window + self.order - 1;
if self.buffer.len() > needed {
self.buffer.remove(0);
}
if self.buffer.len() < needed {
return None;
}
let mut pattern_counts: HashMap<Vec<usize>, usize> = HashMap::new();
let total_patterns = self.buffer.len() - self.order + 1;
for i in 0..total_patterns {
let slice = &self.buffer[i..i + self.order];
let mut indexed: Vec<(usize, f64)> =
slice.iter().copied().enumerate().collect();
indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let pattern: Vec<usize> = indexed.iter().map(|(idx, _)| *idx).collect();
*pattern_counts.entry(pattern).or_insert(0) += 1;
}
let n = total_patterns as f64;
let raw_entropy: f64 = pattern_counts
.values()
.map(|&c| {
let p = c as f64 / n;
-p * p.ln()
})
.sum();
let max_entropy = ln_factorial(self.order);
if max_entropy < f64::EPSILON {
return Some(0.0);
}
Some((raw_entropy / max_entropy).clamp(0.0, 1.0))
}
pub fn warmup_period(&self) -> usize {
self.window + self.order - 1
}
}
fn ln_factorial(n: usize) -> f64 {
(1..=n).map(|k| (k as f64).ln()).sum()
}
#[derive(Debug, Clone)]
pub struct ApproximateEntropy {
m: usize,
r: f64,
capacity: usize,
buffer: Vec<f64>,
}
impl ApproximateEntropy {
pub fn new(m: usize, r: f64, capacity: usize) -> Result<Self, FinError> {
if m == 0 {
return Err(FinError::InvalidInput("m must be at least 1".to_owned()));
}
if r <= 0.0 {
return Err(FinError::InvalidInput("r must be positive".to_owned()));
}
if capacity < m + 2 {
return Err(FinError::InvalidInput(format!(
"capacity must be at least m+2 = {}",
m + 2
)));
}
Ok(Self { m, r, capacity, buffer: Vec::with_capacity(capacity) })
}
pub fn update(&mut self, value: f64) -> Option<f64> {
if self.buffer.len() == self.capacity {
self.buffer.remove(0);
}
self.buffer.push(value);
if self.buffer.len() < self.capacity {
return None;
}
Some(self.compute())
}
pub fn warmup_period(&self) -> usize {
self.capacity
}
fn compute(&self) -> f64 {
let phi_m = self.phi(self.m);
let phi_m1 = self.phi(self.m + 1);
phi_m - phi_m1
}
fn phi(&self, m: usize) -> f64 {
let n = self.buffer.len();
if n < m {
return 0.0;
}
let count_sum: f64 = (0..=(n - m))
.map(|i| {
let matches = (0..=(n - m))
.filter(|&j| self.max_dist(i, j, m) <= self.r)
.count();
(matches as f64 / (n - m + 1) as f64).ln()
})
.sum();
count_sum / (n - m + 1) as f64
}
fn max_dist(&self, i: usize, j: usize, m: usize) -> f64 {
(0..m)
.map(|k| (self.buffer[i + k] - self.buffer[j + k]).abs())
.fold(0.0_f64, f64::max)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shannon_warmup() {
let mut se = ShannonEntropy::new(5, 8).unwrap();
for _ in 0..5 {
assert!(se.update(100.0).is_none());
}
let v = se.update(101.0);
assert!(v.is_some());
}
#[test]
fn test_shannon_constant_series_zero_entropy() {
let mut se = ShannonEntropy::new(10, 8).unwrap();
let mut last = None;
for _ in 0..20 {
last = se.update(100.0);
}
assert_eq!(last.unwrap(), 0.0);
}
#[test]
fn test_shannon_invalid_params() {
assert!(ShannonEntropy::new(0, 8).is_err());
assert!(ShannonEntropy::new(5, 0).is_err());
}
#[test]
fn test_permutation_entropy_warmup() {
let mut pe = PermutationEntropy::new(3, 10).unwrap();
for i in 0..11 {
assert!(pe.update(i as f64).is_none());
}
assert!(pe.update(12.0).is_some());
}
#[test]
fn test_permutation_entropy_monotone_low() {
let mut pe = PermutationEntropy::new(3, 20).unwrap();
let mut last = None;
for i in 0..50 {
last = pe.update(i as f64); }
assert!(last.unwrap() < 0.1);
}
#[test]
fn test_approximate_entropy_warmup() {
let mut ae = ApproximateEntropy::new(2, 0.1, 10).unwrap();
for i in 0..9 {
assert!(ae.update(i as f64).is_none());
}
assert!(ae.update(10.0).is_some());
}
#[test]
fn test_approximate_entropy_invalid_params() {
assert!(ApproximateEntropy::new(0, 0.1, 10).is_err());
assert!(ApproximateEntropy::new(2, -0.1, 10).is_err());
assert!(ApproximateEntropy::new(2, 0.1, 2).is_err()); }
}