Files
fxhnt/tests/unit/test_hurst.py
jgrusewski 0722e65c02 feat: Hurst exponent (R/S) ported — the persistence signal for regime classification
domain/regime/hurst.hurst_exponent: faithful port of Rust price_features compute_hurst_exponent (R/S,
H=ln(R/S)/ln(n), close-only so it fits PriceSeries). H>0.5 persistent/trending, H<0.5 mean-reverting/
ranging, ~0.5 random walk — the principled core of the Hurst/ADX regime classifier (ADX leg needs HL
bars, a future extension). 4 TDD tests (persistent-blocks>0.6, alternating<0.4, degenerate=0.5, bounded).
57 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:09:08 +02:00

35 lines
1.4 KiB
Python

"""Hurst exponent — TDD of the R/S port. Persistent → H>0.5, anti-persistent → H<0.5, degenerate → 0.5."""
from __future__ import annotations
import numpy as np
from fxhnt.domain.regime import hurst_exponent
def _close_from_logrets(logrets: list[float]) -> np.ndarray:
return 100.0 * np.exp(np.concatenate([[0.0], np.cumsum(logrets)]))
def test_persistent_blocks_give_high_hurst() -> None:
# one big up-block then a down-block: cumulative deviations make a large excursion → R/S large → H>0.5
close = _close_from_logrets([0.02] * 10 + [-0.02] * 10)
assert hurst_exponent(close, period=21) > 0.6
def test_anti_persistent_alternating_gives_low_hurst() -> None:
close = _close_from_logrets([0.02, -0.02] * 10) # oscillation → cumulative stays near 0 → R/S small
assert hurst_exponent(close, period=21) < 0.4
def test_degenerate_returns_random_walk() -> None:
assert hurst_exponent(np.full(5, 100.0), period=20) == 0.5 # too few points
assert hurst_exponent(np.full(30, 100.0), period=8) == 0.5 # period < 10
assert hurst_exponent(np.full(30, 100.0), period=20) == 0.5 # zero range/std (flat)
def test_hurst_bounded_unit_interval() -> None:
rng = np.random.default_rng(0)
close = 100.0 * np.cumprod(1.0 + rng.normal(0.0003, 0.01, 200))
h = hurst_exponent(close, period=100)
assert 0.0 <= h <= 1.0