use std::collections::HashMap;
use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::ohlcv_field::OhlcvField;
#[derive(Debug, Clone, PartialEq)]
pub enum MarketEntropyState {
HighlyPredictable, Moderate, Random, Chaotic, }
#[derive(Clone)]
pub struct ShannonEntropy {
period: usize, bins: usize, source: OhlcvField,
returns: Vec<f64>, prev_close: Option<f64>,
entropy: f64, normalized_entropy: f64, predictability_score: f64,
count: usize,
initialized: bool,
}
impl ShannonEntropy {
pub fn new(period: usize, bins: usize) -> Self {
Self::with_source(period, bins, OhlcvField::Close)
}
pub fn with_source(period: usize, bins: usize, source: OhlcvField) -> Self {
Self {
period: period.min(512),
bins: bins.clamp(5, 50), source,
returns: Vec::with_capacity(period.min(512)),
prev_close: None,
entropy: 0.0,
normalized_entropy: 0.0,
predictability_score: 1.0,
count: 0,
initialized: false,
}
}
pub fn new_default(period: usize) -> Self {
Self::new(period, 20) }
pub fn update_bar(&mut self, open: f64, high: f64, low: f64, close: f64, volume: f64) -> f64 {
let value = self.source.extract(open, high, low, close, volume);
if let Some(prev_close) = self.prev_close {
let log_return = (value / prev_close).ln();
if log_return.is_finite() && log_return.abs() < 1.0 {
if self.returns.len() >= self.period {
self.returns.remove(0);
}
self.returns.push(log_return);
self.count += 1;
if self.returns.len() >= self.period.min(10) {
self.calculate_entropy();
self.initialized = true;
}
}
}
self.prev_close = Some(value);
self.normalized_entropy
}
fn calculate_entropy(&mut self) {
if self.returns.is_empty() {
return;
}
let min_return = self.returns.iter().copied().fold(f64::INFINITY, f64::min);
let max_return = self.returns.iter().copied().fold(f64::NEG_INFINITY, f64::max);
if (max_return - min_return).abs() < 1e-10 {
self.entropy = 0.0;
self.normalized_entropy = 0.0;
self.predictability_score = 1.0;
return;
}
let bin_width = (max_return - min_return) / self.bins as f64;
let mut histogram = HashMap::new();
for &return_val in &self.returns {
let bin_index = ((return_val - min_return) / bin_width).floor() as usize;
let bin_index = bin_index.min(self.bins - 1);
*histogram.entry(bin_index).or_insert(0) += 1;
}
let total_count = self.returns.len() as f64;
let mut entropy = 0.0;
for &count in histogram.values() {
if count > 0 {
let probability = count as f64 / total_count;
entropy -= probability * probability.log2();
}
}
let max_entropy = (self.bins as f64).log2();
self.entropy = entropy;
self.normalized_entropy = if max_entropy > 0.0 {
(entropy / max_entropy).clamp(0.0, 1.0)
} else {
0.0
};
self.predictability_score = 1.0 - self.normalized_entropy;
}
pub fn entropy(&self) -> f64 {
self.entropy
}
pub fn normalized_entropy(&self) -> f64 {
self.normalized_entropy
}
pub fn predictability_score(&self) -> f64 {
self.predictability_score
}
pub fn market_state(&self) -> MarketEntropyState {
match self.normalized_entropy {
e if e < 0.3 => MarketEntropyState::HighlyPredictable,
e if e < 0.7 => MarketEntropyState::Moderate,
e if e < 0.9 => MarketEntropyState::Random,
_ => MarketEntropyState::Chaotic,
}
}
pub fn trading_signal(&self) -> i8 {
match self.market_state() {
MarketEntropyState::HighlyPredictable => 1, MarketEntropyState::Moderate => 0, MarketEntropyState::Random => -1, MarketEntropyState::Chaotic => 0, }
}
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Single(self.normalized_entropy)
}
pub fn is_ready(&self) -> bool {
self.initialized
}
pub fn period(&self) -> usize {
self.period
}
pub fn bins(&self) -> usize {
self.bins
}
pub fn reset(&mut self) {
self.returns.clear();
self.prev_close = None;
self.entropy = 0.0;
self.normalized_entropy = 0.0;
self.predictability_score = 1.0;
self.count = 0;
self.initialized = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shannon_entropy_creation() {
let se = ShannonEntropy::new(50, 20);
assert!(!se.is_ready());
assert_eq!(se.value().main(), 0.0);
assert_eq!(se.period(), 50);
assert_eq!(se.bins(), 20);
}
#[test]
fn test_shannon_entropy_default() {
let se = ShannonEntropy::new_default(30);
assert!(!se.is_ready());
assert_eq!(se.period(), 30);
assert_eq!(se.bins(), 20);
}
#[test]
fn test_shannon_entropy_warmup() {
let mut se = ShannonEntropy::new_default(15);
for i in 0..25 {
let price = 100.0 + (i as f64 * 0.1).sin() * 5.0;
se.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
}
assert!(se.is_ready());
}
#[test]
fn test_shannon_entropy_values_range() {
let mut se = ShannonEntropy::new_default(15);
for i in 0..30 {
let price = 100.0 + (i as f64 * 0.2).sin() * 10.0;
let value = se.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
assert!(value >= 0.0 && value <= 1.0);
}
}
#[test]
fn test_shannon_entropy_reset() {
let mut se = ShannonEntropy::new_default(15);
for i in 0..25 {
se.update_bar(100.0 + i as f64, 105.0, 95.0, 101.0, 1000.0);
}
se.reset();
assert!(!se.is_ready());
assert_eq!(se.value().main(), 0.0);
}
#[test]
fn test_shannon_entropy_predictability() {
let mut se = ShannonEntropy::new_default(15);
for i in 0..25 {
let price = 100.0 + (i as f64 * 0.1).sin() * 5.0;
se.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
}
let score = se.predictability_score();
assert!(score >= 0.0 && score <= 1.0);
}
#[test]
fn test_shannon_entropy_market_state() {
let mut se = ShannonEntropy::new_default(15);
for i in 0..25 {
let price = 100.0 + (i as f64 * 0.1).sin() * 5.0;
se.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
}
let state = se.market_state();
assert!(matches!(state, MarketEntropyState::HighlyPredictable | MarketEntropyState::Moderate | MarketEntropyState::Random | MarketEntropyState::Chaotic));
}
}