use alloc::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct HurstF64 {
window: VecDeque<f64>,
window_size: usize,
count: u64,
}
#[derive(Debug, Clone)]
pub struct HurstF64Builder {
window_size: Option<usize>,
}
impl HurstF64 {
#[inline]
#[must_use]
pub fn builder() -> HurstF64Builder {
HurstF64Builder { window_size: None }
}
#[inline]
pub fn update(&mut self, value: f64) -> Result<(), crate::DataError> {
check_finite!(value);
self.count += 1;
self.window.push_back(value);
if self.window.len() > self.window_size {
self.window.pop_front();
}
Ok(())
}
#[must_use]
pub fn hurst(&self) -> Option<f64> {
let n = self.window.len();
if n < 20 {
return None;
}
let sum: f64 = self.window.iter().sum();
let mean = sum / n as f64;
let var: f64 = self
.window
.iter()
.map(|x| {
let d = x - mean;
d * d
})
.sum::<f64>()
/ n as f64;
if var <= 0.0 {
return None;
}
let std_dev = crate::math::sqrt(var);
let mut cum_dev = 0.0;
let mut max_cum = f64::NEG_INFINITY;
let mut min_cum = f64::INFINITY;
for &x in &self.window {
cum_dev += x - mean;
if cum_dev > max_cum {
max_cum = cum_dev;
}
if cum_dev < min_cum {
min_cum = cum_dev;
}
}
let range = max_cum - min_cum;
if range <= 0.0 {
return None;
}
let rs = range / std_dev;
let h = crate::math::ln(rs) / crate::math::ln(n as f64);
Some(h)
}
#[inline]
#[must_use]
pub fn is_mean_reverting(&self) -> bool {
self.hurst().is_some_and(|h| h < 0.5)
}
#[inline]
#[must_use]
pub fn is_trending(&self) -> bool {
self.hurst().is_some_and(|h| h > 0.5)
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.window.len() >= 20
}
pub fn reset(&mut self) {
self.window.clear();
self.count = 0;
}
}
impl HurstF64Builder {
#[inline]
#[must_use]
pub fn window_size(mut self, size: usize) -> Self {
self.window_size = Some(size);
self
}
pub fn build(self) -> Result<HurstF64, crate::ConfigError> {
let window_size = self.window_size.unwrap_or(100);
if window_size < 20 {
return Err(crate::ConfigError::Invalid(
"window_size must be >= 20 for meaningful R/S analysis",
));
}
let mut window = VecDeque::new();
window.reserve_exact(window_size);
Ok(HurstF64 {
window,
window_size,
count: 0,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alternating_series_low_hurst() {
let mut h = HurstF64::builder().window_size(200).build().unwrap();
for i in 0..200 {
let val = if i % 2 == 0 { 1.0 } else { -1.0 };
h.update(val).unwrap();
}
let hurst = h.hurst().unwrap();
assert!(
hurst < 0.5,
"alternating series should have H < 0.5, got {hurst}"
);
assert!(h.is_mean_reverting());
}
#[test]
fn trending_series_high_hurst() {
let mut h = HurstF64::builder().window_size(200).build().unwrap();
for i in 0..200 {
h.update(i as f64).unwrap();
}
let hurst = h.hurst().unwrap();
assert!(
hurst > 0.5,
"trending series should have H > 0.5, got {hurst}"
);
assert!(h.is_trending());
}
#[test]
fn not_primed_before_20() {
let mut h = HurstF64::builder().window_size(100).build().unwrap();
for i in 0..19 {
h.update(i as f64).unwrap();
}
assert!(!h.is_primed());
assert!(h.hurst().is_none());
}
#[test]
fn primed_at_20() {
let mut h = HurstF64::builder().window_size(100).build().unwrap();
for i in 0..20 {
h.update(i as f64).unwrap();
}
assert!(h.is_primed());
assert!(h.hurst().is_some());
}
#[test]
fn reset_clears_state() {
let mut h = HurstF64::builder().window_size(100).build().unwrap();
for i in 0..50 {
h.update(i as f64).unwrap();
}
h.reset();
assert_eq!(h.count(), 0);
assert!(!h.is_primed());
}
#[test]
fn nan_rejected() {
let mut h = HurstF64::builder().window_size(100).build().unwrap();
assert!(h.update(f64::NAN).is_err());
}
#[test]
fn inf_rejected() {
let mut h = HurstF64::builder().window_size(100).build().unwrap();
assert!(h.update(f64::INFINITY).is_err());
}
#[test]
fn constant_series_no_hurst() {
let mut h = HurstF64::builder().window_size(50).build().unwrap();
for _ in 0..50 {
h.update(5.0).unwrap();
}
assert!(h.hurst().is_none());
}
}