use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct HurstExponent {
period: usize,
window: VecDeque<f64>,
}
impl HurstExponent {
pub fn new(period: usize) -> Self {
Self {
period: period.max(2),
window: VecDeque::with_capacity(period),
}
}
}
impl Next<f64> for HurstExponent {
type Output = f64;
fn next(&mut self, input: f64) -> Self::Output {
self.window.push_back(input);
if self.window.len() > self.period {
self.window.pop_front();
}
if self.window.len() < self.period {
return f64::NAN;
}
let sum: f64 = self.window.iter().sum();
let mean = sum / self.period as f64;
let mut cumulative_deviate = 0.0;
let mut max_z = f64::MIN;
let mut min_z = f64::MAX;
for &val in self.window.iter() {
cumulative_deviate += val - mean;
if cumulative_deviate > max_z {
max_z = cumulative_deviate;
}
if cumulative_deviate < min_z {
min_z = cumulative_deviate;
}
}
let range = max_z - min_z;
let mut variance_sum = 0.0;
for &val in self.window.iter() {
let diff = val - mean;
variance_sum += diff * diff;
}
let std_dev = (variance_sum / self.period as f64).sqrt();
if std_dev == 0.0 {
return 0.5; }
let rs = range / std_dev;
(rs.ln()) / (self.period as f64).ln()
}
}
pub const HURST_EXPONENT_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "Hurst Exponent",
description: "Measures the persistence or anti-persistence of a time series using R/S analysis.",
usage: "Use to classify the current market regime. H > 0.5 suggests a trending market (persistent); H < 0.5 suggests a mean-reverting market (anti-persistent). Useful as a filter for trend-following or mean-reversion strategies.",
keywords: &["statistics", "regime-detection", "hurst", "ml", "trending", "mean-reversion"],
ehlers_summary: "The Hurst Exponent, pioneered by Harold Edwin Hurst in 1951, quantifies the 'memory' of a time series. In technical analysis, it distinguishes between trending, mean-reverting, and random walk price action. It is a critical feature for machine learning models to adapt their logic to the underlying market structure.",
params: &[
ParamDef {
name: "period",
default: "100",
description: "Lookback period for R/S analysis",
},
],
formula_source: "https://en.wikipedia.org/wiki/Hurst_exponent",
formula_latex: r#"
\[
H = \frac{\ln(R/S)}{\ln(N)}
\]
"#,
gold_standard_file: "hurst_exponent.json",
category: "ML Features",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
use proptest::prelude::*;
#[test]
fn test_hurst_basic() {
let period = 10;
let mut hurst = HurstExponent::new(period);
for i in 0..period {
let res = hurst.next(i as f64);
if i < period - 1 {
assert!(res.is_nan());
} else {
assert!(res > 0.5);
}
}
}
proptest! {
#[test]
fn test_hurst_parity(
inputs in prop::collection::vec(1.0..100.0, 50..100),
) {
let period = 20;
let mut hurst = HurstExponent::new(period);
let streaming_results: Vec<f64> = inputs.iter().map(|&x| hurst.next(x)).collect();
let mut batch_results = Vec::with_capacity(inputs.len());
for i in 0..inputs.len() {
if i < period - 1 {
batch_results.push(f64::NAN);
continue;
}
let slice = &inputs[i + 1 - period..=i];
let sum: f64 = slice.iter().sum();
let mean = sum / period as f64;
let mut cum_dev = 0.0;
let mut max_z = f64::MIN;
let mut min_z = f64::MAX;
for &val in slice {
cum_dev += val - mean;
if cum_dev > max_z { max_z = cum_dev; }
if cum_dev < min_z { min_z = cum_dev; }
}
let range = max_z - min_z;
let var_sum: f64 = slice.iter().map(|&v| (v - mean).powi(2)).sum();
let std_dev = (var_sum / period as f64).sqrt();
let res = if std_dev == 0.0 { 0.5 } else { (range / std_dev).ln() / (period as f64).ln() };
batch_results.push(res);
}
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
if s.is_nan() {
assert!(b.is_nan());
} else {
approx::assert_relative_eq!(s, b, epsilon = 1e-10);
}
}
}
}
}