Skip to main content

rangebar_hurst/
lib.rs

1//! Hurst Exponent estimator functions for Rust
2//!
3//! Originally based on evrom/hurst (GPL-3.0), forked to MIT for license compatibility.
4//! This provides core R/S (Rescaled Range) analysis for Hurst exponent estimation.
5//!
6//! # License Resolution
7//!
8//! Issue #96 Task #149/150: GPL-3.0 license conflict resolution.
9//! The external evrom/hurst crate (v0.1.0) was GPL-3.0 licensed, blocking PyPI distribution.
10//! This internal fork enables MIT-licensed Hurst calculations without GPL restrictions.
11//!
12//! # Examples
13//!
14//! ```
15//! # use rangebar_hurst::rssimple;
16//! let prices = vec![100.0, 101.0, 99.5, 102.0, 101.5];
17//! let hurst = rssimple(&prices);
18//! assert!(hurst >= 0.0 && hurst <= 1.0);
19//! ```
20
21use linreg::linear_regression;
22
23pub(crate) mod utils;
24
25use utils::*;
26
27/// Simple R/S Hurst estimation
28///
29/// Computes the Hurst exponent using basic Rescaled Range analysis.
30/// Issue #96: Optimized from 5 passes + 2 Vec allocations to 2 passes + 0 allocations.
31///
32/// # Arguments
33///
34/// * `x` - Input time series data (prices or returns)
35///
36/// # Returns
37///
38/// Hurst exponent value, typically in range [0, 1]
39pub fn rssimple(x: &[f64]) -> f64 {
40    let n = x.len();
41    if n == 0 {
42        return 0.0;
43    }
44    let n_f64 = n as f64;
45    let inv_n = 1.0 / n_f64;
46
47    // Pass 1: mean
48    let x_mean: f64 = x.iter().sum::<f64>() * inv_n;
49
50    // Pass 2: fused cumsum-minmax + variance (zero allocations)
51    // Simultaneously tracks:
52    // - Running cumulative sum of deviations (for R/S range)
53    // - Min/max of cumulative sum (for rescaled range)
54    // - Sum of squared deviations (for standard deviation)
55    let mut cumsum = 0.0_f64;
56    let mut cum_min = 0.0_f64;
57    let mut cum_max = 0.0_f64;
58    let mut sum_sq = 0.0_f64;
59
60    for &val in x {
61        let d = val - x_mean;
62        cumsum += d;
63        cum_min = cum_min.min(cumsum);
64        cum_max = cum_max.max(cumsum);
65        sum_sq += d * d;
66    }
67
68    let std_dev = (sum_sq * inv_n).sqrt();
69    if std_dev < f64::EPSILON {
70        return 0.5; // Constant series
71    }
72
73    let rs = (cum_max - cum_min) / std_dev;
74    if rs < f64::EPSILON {
75        return 0.5;
76    }
77
78    rs.log2() / n_f64.log2()
79}
80
81/// Corrected R over S Hurst exponent
82///
83/// Computes Hurst exponent with interval averaging correction for improved stability.
84///
85/// # Arguments
86///
87/// * `x` - Input time series data
88///
89/// # Returns
90///
91/// Corrected Hurst exponent value, typically in range [0, 1]
92pub fn rs_corrected(x: Vec<f64>) -> f64 {
93    let mut cap_x: Vec<f64> = vec![x.len() as f64];
94    let mut cap_y: Vec<f64> = vec![rscalc(&x)];
95    let mut n: Vec<u64> = vec![0, x.len() as u64 / 2, x.len() as u64];
96
97    // compute averaged R/S for halved intervals
98    while n[1] >= 8 {
99        let mut xl: Vec<f64> = vec![];
100        let mut yl: Vec<f64> = vec![];
101        for i in 1..n.len() {
102            let rs: f64 = rscalc(&x[((n[i - 1] + 1) as usize)..(n[i] as usize)]);
103            xl.push((n[i] - n[i - 1]) as f64);
104            yl.push(rs);
105        }
106        cap_x.push(mean(&xl));
107        cap_y.push(mean(&yl));
108        // next step
109        n = half(&n, x.len() as u64);
110    }
111
112    // apply linear regression
113    let cap_x_log: Vec<f64> = cap_x.iter().map(|a| a.ln()).collect();
114    let cap_y_log: Vec<f64> = cap_y.iter().map(|a| a.ln()).collect();
115    let (slope, _): (f64, f64) = linear_regression(&cap_x_log, &cap_y_log).unwrap();
116    slope
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_rssimple_trending_series() {
125        // Monotonically increasing → H > 0.5 (trending)
126        let prices: Vec<f64> = (0..100).map(|i| 100.0 + i as f64 * 0.5).collect();
127        let h = rssimple(&prices);
128        assert!(h.is_finite(), "Hurst must be finite: {}", h);
129        assert!(h > 0.4, "Trending series should have H > 0.4: {}", h);
130    }
131
132    #[test]
133    fn test_rssimple_constant_series() {
134        let prices = vec![100.0; 50];
135        let h = rssimple(&prices);
136        assert_eq!(h, 0.5, "Constant series → H = 0.5");
137    }
138
139    #[test]
140    fn test_rssimple_empty() {
141        let h = rssimple(&[]);
142        assert_eq!(h, 0.0, "Empty series → 0.0");
143    }
144
145    #[test]
146    fn test_rssimple_single_element() {
147        let h = rssimple(&[100.0]);
148        // Single element: std_dev = 0 → returns 0.5
149        assert_eq!(h, 0.5, "Single element → H = 0.5");
150    }
151
152    #[test]
153    fn test_rssimple_alternating_series() {
154        // Mean-reverting: H < 0.5
155        let prices: Vec<f64> = (0..200).map(|i| if i % 2 == 0 { 100.0 } else { 101.0 }).collect();
156        let h = rssimple(&prices);
157        assert!(h.is_finite(), "Hurst must be finite: {}", h);
158        assert!(h < 0.6, "Alternating series should have low H: {}", h);
159    }
160
161    #[test]
162    fn test_rssimple_five_elements_doc_example() {
163        let prices = vec![100.0, 101.0, 99.5, 102.0, 101.5];
164        let h = rssimple(&prices);
165        assert!(h >= 0.0 && h <= 1.5, "Hurst should be in reasonable range: {}", h);
166    }
167
168    #[test]
169    fn test_rssimple_two_elements() {
170        let h = rssimple(&[100.0, 200.0]);
171        assert!(h.is_finite(), "Two-element series must be finite: {}", h);
172    }
173
174    #[test]
175    fn test_rssimple_nan_in_series() {
176        // NaN values should not cause panic, but result may be NaN/non-finite
177        let prices = vec![100.0, f64::NAN, 102.0, 101.0, 99.0];
178        let h = rssimple(&prices);
179        // Just ensure no panic — NaN propagation is acceptable
180        let _ = h;
181    }
182
183    #[test]
184    fn test_rssimple_negative_prices() {
185        // Negative prices (e.g., futures spread) should work
186        let prices: Vec<f64> = (0..50).map(|i| -10.0 + i as f64 * 0.1).collect();
187        let h = rssimple(&prices);
188        assert!(h.is_finite(), "Negative prices must produce finite H: {}", h);
189    }
190}