use quant_primitives::Candle;
use rust_decimal::Decimal;
use crate::error::IndicatorError;
#[derive(Debug, Clone)]
pub struct HurstExponent {
window: usize,
}
const SUBSERIES_SIZES: [usize; 3] = [16, 32, 64];
const MIN_RETURNS: usize = 20;
impl HurstExponent {
pub fn new(window: usize) -> Result<Self, IndicatorError> {
if window < 64 {
return Err(IndicatorError::InvalidParameter {
message: format!("HurstExponent window must be >= 64, got {window}"),
});
}
Ok(Self { window })
}
pub fn compute_from_candles(&self, candles: &[Candle]) -> Option<Decimal> {
if candles.len() < self.window {
return None;
}
let closes: Vec<Decimal> = candles[candles.len() - self.window..]
.iter()
.map(|c| c.close())
.collect();
let returns = compute_log_returns(&closes);
if returns.len() < MIN_RETURNS {
return None;
}
estimate_hurst_from_returns(&returns)
}
}
pub fn compute_log_returns(closes: &[Decimal]) -> Vec<Decimal> {
closes
.windows(2)
.filter_map(|w| {
if w[0].is_zero() {
None
} else {
Some((w[1] - w[0]) / w[0])
}
})
.collect()
}
pub fn rescaled_range_for_n(returns: &[Decimal], n: usize) -> Option<Decimal> {
if n > returns.len() || n == 0 {
return None;
}
let num_subseries = returns.len() / n;
if num_subseries == 0 {
return None;
}
let mut rs_sum = Decimal::ZERO;
let mut valid_count = 0u32;
for k in 0..num_subseries {
let sub = &returns[k * n..(k + 1) * n];
let n_dec = Decimal::from(sub.len());
let mean: Decimal = sub.iter().copied().sum::<Decimal>() / n_dec;
let deviations: Vec<Decimal> = sub.iter().map(|r| r - mean).collect();
let mut cum = Vec::with_capacity(sub.len());
let mut running = Decimal::ZERO;
for d in &deviations {
running += d;
cum.push(running);
}
let r = cum.iter().copied().max().unwrap_or(Decimal::ZERO)
- cum.iter().copied().min().unwrap_or(Decimal::ZERO);
let var: Decimal = deviations.iter().map(|d| d * d).sum::<Decimal>() / n_dec;
if var.is_zero() {
continue;
}
let s = newton_sqrt(var);
if s.is_zero() {
continue;
}
rs_sum += r / s;
valid_count += 1;
}
if valid_count == 0 {
return None;
}
Some(rs_sum / Decimal::from(valid_count))
}
pub fn estimate_slope(log_ns: &[usize], log_rs: &[Decimal]) -> Option<Decimal> {
if log_ns.len() < 2 || log_rs.len() < 2 {
return None;
}
let rs1 = log_rs[0];
let rs2 = log_rs[log_rs.len() - 1];
let n1 = Decimal::from(log_ns[0]);
let n2 = Decimal::from(log_ns[log_ns.len() - 1]);
if rs1.is_zero() || n1.is_zero() {
return None;
}
let rs_ratio = rs2 / rs1;
let n_ratio = n2 / n1;
let h = approx_log2(rs_ratio) / approx_log2(n_ratio);
Some(h.max(Decimal::ZERO).min(Decimal::ONE))
}
fn estimate_hurst_from_returns(returns: &[Decimal]) -> Option<Decimal> {
let mut log_ns = Vec::new();
let mut log_rs = Vec::new();
for &n in &SUBSERIES_SIZES {
if let Some(avg_rs) = rescaled_range_for_n(returns, n) {
log_ns.push(n);
log_rs.push(avg_rs);
}
}
estimate_slope(&log_ns, &log_rs)
}
fn newton_sqrt(val: Decimal) -> Decimal {
if val.is_zero() || val < Decimal::ZERO {
return Decimal::ZERO;
}
let mut guess = val / Decimal::TWO;
for _ in 0..10 {
if guess.is_zero() {
return Decimal::ZERO;
}
guess = (guess + val / guess) / Decimal::TWO;
}
guess
}
fn approx_log2(x: Decimal) -> Decimal {
if x <= Decimal::ZERO {
return Decimal::ZERO;
}
let ln2 = Decimal::new(6931, 4);
let mut reduced = x;
let mut shifts = 0i64;
while reduced >= Decimal::TWO {
reduced /= Decimal::TWO;
shifts += 1;
}
while reduced < Decimal::ONE && !reduced.is_zero() {
reduced *= Decimal::TWO;
shifts -= 1;
}
let u = (reduced - Decimal::ONE) / (reduced + Decimal::ONE);
let u2 = u * u;
let mut term = u;
let mut sum = u;
for k in 1..10 {
term *= u2;
sum += term / Decimal::from(2 * k + 1);
}
let ln_reduced = sum * Decimal::TWO;
let ln_x = ln_reduced + Decimal::from(shifts) * ln2;
ln_x / ln2
}
#[cfg(test)]
#[path = "hurst_tests.rs"]
mod tests;