use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::ohlcv_field::OhlcvField;
#[derive(Clone)]
pub struct SampleEntropy {
period: usize, m: usize, r: f64, source: OhlcvField,
data: Vec<f64>,
sampen: f64, complexity_score: f64,
count: usize,
initialized: bool,
std_dev: f64,
}
impl SampleEntropy {
pub fn new(period: usize, m: usize, r: f64) -> Self {
Self::with_source(period, m, r, OhlcvField::Close)
}
pub fn with_source(period: usize, m: usize, r: f64, source: OhlcvField) -> Self {
Self {
period: period.min(512),
m: m.clamp(1, 5), r: r.clamp(0.01, 1.0), source,
data: Vec::with_capacity(period.min(512)),
sampen: 0.0,
complexity_score: 0.5,
count: 0,
initialized: false,
std_dev: 0.0,
}
}
pub fn new_default(period: usize) -> Self {
Self::new(period, 2, 0.0) }
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 self.data.len() >= self.period {
self.data.remove(0);
}
self.data.push(value);
self.count += 1;
let min_required = (self.m + 1).max(3).min(self.period);
if self.data.len() >= min_required {
self.calculate_std_dev();
self.calculate_sampen();
self.initialized = true;
}
self.sampen
}
fn calculate_std_dev(&mut self) {
if self.data.len() < 2 {
return;
}
let mean = self.data.iter().sum::<f64>() / self.data.len() as f64;
let variance = self.data.iter()
.map(|x| (x - mean).powi(2))
.sum::<f64>() / self.data.len() as f64;
self.std_dev = variance.sqrt();
if self.r < 0.01 {
self.r = 0.15 * self.std_dev; }
}
fn calculate_sampen(&mut self) {
if self.data.len() < self.m + 1 {
return;
}
let matches_m = self.count_matches(self.m);
let matches_m_plus_1 = self.count_matches(self.m + 1);
if matches_m > 0 && matches_m_plus_1 > 0 {
self.sampen = -((matches_m_plus_1 as f64) / (matches_m as f64)).ln();
} else if matches_m > 0 {
self.sampen = 3.0; } else {
self.sampen = 1.5;
}
self.complexity_score = (self.sampen / 3.0).clamp(0.0, 1.0);
}
fn count_matches(&self, pattern_length: usize) -> usize {
if pattern_length >= self.data.len() {
return 0;
}
let n = self.data.len();
let mut matches = 0;
for i in 0..=(n - pattern_length) {
for j in 0..=(n - pattern_length) {
if i != j && self.patterns_match(i, j, pattern_length) {
matches += 1;
}
}
}
matches
}
fn patterns_match(&self, i: usize, j: usize, length: usize) -> bool {
for k in 0..length {
if i + k >= self.data.len() || j + k >= self.data.len() {
return false;
}
if (self.data[i + k] - self.data[j + k]).abs() > self.r {
return false;
}
}
true
}
pub fn sampen(&self) -> f64 {
self.sampen
}
pub fn complexity_score(&self) -> f64 {
self.complexity_score
}
pub fn market_state(&self) -> &'static str {
match self.sampen {
s if s < 0.5 => "Highly Predictable",
s if s < 1.0 => "Moderately Complex",
s if s < 2.0 => "Complex",
_ => "Highly Complex",
}
}
pub fn trading_signal(&self) -> i8 {
match self.sampen {
s if s < 0.3 => 1, s if s > 2.0 => -1, _ => 0, }
}
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Single(self.sampen)
}
pub fn is_ready(&self) -> bool {
self.initialized
}
pub fn period(&self) -> usize {
self.period
}
pub fn pattern_length(&self) -> usize {
self.m
}
pub fn tolerance(&self) -> f64 {
self.r
}
pub fn std_dev(&self) -> f64 {
self.std_dev
}
pub fn reset(&mut self) {
self.data.clear();
self.sampen = 0.0;
self.complexity_score = 0.5;
self.count = 0;
self.initialized = false;
self.std_dev = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sample_entropy_creation() {
let sampen = SampleEntropy::new(50, 2, 0.15);
assert!(!sampen.is_ready());
assert_eq!(sampen.value().main(), 0.0);
assert_eq!(sampen.period(), 50);
assert_eq!(sampen.pattern_length(), 2);
}
#[test]
fn test_sample_entropy_default() {
let sampen = SampleEntropy::new_default(30);
assert!(!sampen.is_ready());
assert_eq!(sampen.period(), 30);
}
#[test]
fn test_sample_entropy_warmup() {
let mut sampen = SampleEntropy::new_default(20);
for i in 0..25 {
let price = 100.0 + (i as f64 * 0.1).sin() * 5.0;
sampen.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
}
assert!(sampen.is_ready());
}
#[test]
fn test_sample_entropy_values_finite() {
let mut sampen = SampleEntropy::new_default(20);
for i in 0..30 {
let price = 100.0 + (i as f64 * 0.2).sin() * 10.0;
let value = sampen.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
assert!(value.is_finite());
}
}
#[test]
fn test_sample_entropy_reset() {
let mut sampen = SampleEntropy::new_default(20);
for i in 0..25 {
sampen.update_bar(100.0 + i as f64, 105.0, 95.0, 101.0, 1000.0);
}
sampen.reset();
assert!(!sampen.is_ready());
assert_eq!(sampen.value().main(), 0.0);
}
#[test]
fn test_sample_entropy_complexity_score() {
let mut sampen = SampleEntropy::new_default(20);
for i in 0..25 {
let price = 100.0 + (i as f64 * 0.1).sin() * 5.0;
sampen.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
}
let score = sampen.complexity_score();
assert!(score >= 0.0 && score <= 1.0);
}
}