use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::ohlcv_field::OhlcvField;
#[derive(Clone)]
pub struct ApproximateEntropy {
period: usize, m: usize, r: f64, source: OhlcvField,
data: Vec<f64>,
apen: f64, regularity_score: f64,
count: usize,
initialized: bool,
std_dev: f64,
}
impl ApproximateEntropy {
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)),
apen: 0.0,
regularity_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_apen();
self.initialized = true;
}
self.apen
}
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_apen(&mut self) {
if self.data.len() < self.m + 1 {
return;
}
let _n = self.data.len();
let phi_m = self.calculate_phi(self.m);
let phi_m_plus_1 = self.calculate_phi(self.m + 1);
self.apen = phi_m - phi_m_plus_1;
self.regularity_score = 1.0 - (self.apen / 2.0).clamp(0.0, 1.0);
}
fn calculate_phi(&self, pattern_length: usize) -> f64 {
if pattern_length >= self.data.len() {
return 0.0;
}
let n = self.data.len();
let mut sum = 0.0;
let mut valid_patterns = 0;
for i in 0..=(n - pattern_length) {
let mut matches = 0;
for j in 0..=(n - pattern_length) {
if self.patterns_match(i, j, pattern_length) {
matches += 1;
}
}
if matches > 0 {
let probability = matches as f64 / (n - pattern_length + 1) as f64;
sum += probability.ln();
valid_patterns += 1;
}
}
if valid_patterns > 0 {
sum / valid_patterns as f64
} else {
0.0
}
}
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 apen(&self) -> f64 {
self.apen
}
pub fn regularity_score(&self) -> f64 {
self.regularity_score
}
pub fn market_state(&self) -> &'static str {
match self.apen {
a if a < 0.3 => "Highly Regular",
a if a < 0.7 => "Moderately Regular",
a if a < 1.2 => "Irregular",
_ => "Highly Irregular",
}
}
pub fn trading_signal(&self) -> i8 {
match self.apen {
a if a < 0.5 => 1, a if a > 1.5 => -1, _ => 0, }
}
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Single(self.apen)
}
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.apen = 0.0;
self.regularity_score = 0.5;
self.count = 0;
self.initialized = false;
self.std_dev = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_approximate_entropy_creation() {
let apen = ApproximateEntropy::new(50, 2, 0.15);
assert!(!apen.is_ready());
assert_eq!(apen.value().main(), 0.0);
assert_eq!(apen.period(), 50);
assert_eq!(apen.pattern_length(), 2);
}
#[test]
fn test_approximate_entropy_default() {
let apen = ApproximateEntropy::new_default(30);
assert!(!apen.is_ready());
assert_eq!(apen.period(), 30);
}
#[test]
fn test_approximate_entropy_warmup() {
let mut apen = ApproximateEntropy::new_default(20);
for i in 0..25 {
let price = 100.0 + (i as f64 * 0.1).sin() * 5.0;
apen.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
}
assert!(apen.is_ready());
}
#[test]
fn test_approximate_entropy_values_finite() {
let mut apen = ApproximateEntropy::new_default(20);
for i in 0..30 {
let price = 100.0 + (i as f64 * 0.2).sin() * 10.0;
let value = apen.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
assert!(value.is_finite());
}
}
#[test]
fn test_approximate_entropy_reset() {
let mut apen = ApproximateEntropy::new_default(20);
for i in 0..25 {
apen.update_bar(100.0 + i as f64, 105.0, 95.0, 101.0, 1000.0);
}
apen.reset();
assert!(!apen.is_ready());
assert_eq!(apen.value().main(), 0.0);
}
#[test]
fn test_approximate_entropy_regularity_score() {
let mut apen = ApproximateEntropy::new_default(20);
for i in 0..25 {
let price = 100.0 + (i as f64 * 0.1).sin() * 5.0;
apen.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
}
let score = apen.regularity_score();
assert!(score >= 0.0 && score <= 1.0);
}
}