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>
35 lines
1.4 KiB
Python
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
|