From 3749a3616dd37cda9b53204ba9d234ce50003f2a Mon Sep 17 00:00:00 2001 From: gord chung <5091603+chungg@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:46:20 -0400 Subject: [PATCH] hurst exponent --- benches/traquer.rs | 3 ++ src/trend.rs | 95 +++++++++++++++++++++++++++++++++++++++ tests/correlation_test.rs | 8 ++-- tests/trend_test.rs | 35 +++++++++++++++ 4 files changed, 137 insertions(+), 4 deletions(-) diff --git a/benches/traquer.rs b/benches/traquer.rs index 6fb53d8..019e615 100644 --- a/benches/traquer.rs +++ b/benches/traquer.rs @@ -377,6 +377,9 @@ fn criterion_benchmark(c: &mut Criterion) { ) }) }); + c.bench_function("sig-trend-hurst", |b| { + b.iter(|| black_box(trend::hurst(&stats.close, 100, None).collect::>())) + }); c.bench_function("sig-volume-bw_mfi", |b| { b.iter(|| { black_box(volume::bw_mfi(&stats.high, &stats.low, &stats.volume).collect::>()) diff --git a/src/trend.rs b/src/trend.rs index e5c4bf4..742b5e6 100644 --- a/src/trend.rs +++ b/src/trend.rs @@ -1002,3 +1002,98 @@ pub fn ichimoku<'a>( lag.chain(iter::repeat(f64::NAN).take(base_win)), ) } + +fn hurst_rs(data: &[f64]) -> f64 { + let avg = data.iter().sum::() / data.len() as f64; + let mut max_z = f64::MIN; + let mut min_z = f64::MAX; + let mut dev_cumsum = 0.0; + let mut var = 0.0; + for x in data { + let dev = x - avg; + var += dev.powi(2); + dev_cumsum += dev; + max_z = max_z.max(dev_cumsum); + min_z = min_z.min(dev_cumsum); + } + + let r = max_z - min_z; + let s = (var / (data.len() - 1) as f64).sqrt(); + r / s +} + +fn linreg(x: &[f64], y: &[f64]) -> f64 { + let x_avg = x.iter().fold(0.0, |acc, &x| acc + x) / x.len() as f64; + let y_avg = y.iter().fold(0.0, |acc, &y| acc + y) / y.len() as f64; + let mut covar = 0.0; + let mut var = 0.0; + for (&xi, &yi) in x.iter().zip(y) { + covar += (xi - x_avg) * (yi - y_avg); + var += (xi - x_avg).powi(2); + } + covar / var +} + +/// Hurst Exponent +/// +/// Measures the long-term memory of time series, and the rate at which these decrease as the +/// lag between pairs of values increases. +/// +/// Configured to accept delta / return values as input rather than price. Window size should be +/// significantly larger than min_win size to ensure enough datapoints are computed for estimation +/// or else values less than 0 or greater than 1 may result. +/// +/// ## Usage +/// +/// A value above 0.5 suggests a positive autocorrelation or that there is a trend and +/// it will continue (either up or down). A value below 0.5 suggests it will mean-revert +/// or that it will break trend. +/// +/// ## Sources +/// +/// [[1]](https://en.wikipedia.org/wiki/Hurst_exponent) +/// [[2]](https://github.com/Mottl/hurst/) +/// [[3]](https://mahowald.github.io/hurst/) +/// +/// # Examples +/// +/// ``` +/// use traquer::trend; +/// +/// trend::hurst(&vec![1.0,2.0,3.0,4.0,5.0,6.0,4.0,5.0], 5, None).collect::>(); +/// +/// ``` +pub fn hurst( + data: &[f64], + window: usize, + min_win: Option, +) -> impl Iterator + '_ { + // create sub window sizes + let mut win_sizes = Vec::new(); + let mut win = (min_win.unwrap_or(4) as f64).log10(); + loop { + let win_size = 10.0_f64.powf(win) as usize; + if win_size >= window { + break; + } + win_sizes.push(win_size); + win += 0.25; + } + win_sizes.push(window); + let win_size_log10 = win_sizes + .iter() + .map(|&x| (x as f64).log10()) + .collect::>(); + + iter::repeat(f64::NAN) + .take(window - 1) + .chain(data.windows(window).map(move |w| { + let mut rs_vals = Vec::new(); + for &x in win_sizes.iter() { + let chunks = w.chunks_exact(x); + let cnt = chunks.len(); + rs_vals.push((chunks.fold(0.0, |acc, x| acc + hurst_rs(x)) / cnt as f64).log10()); + } + linreg(&win_size_log10, &rs_vals) + })) +} diff --git a/tests/correlation_test.rs b/tests/correlation_test.rs index d5d83be..ca9498f 100644 --- a/tests/correlation_test.rs +++ b/tests/correlation_test.rs @@ -190,15 +190,15 @@ fn test_krcc() { result[16 - 1..] ); let result = correlation::krcc( - &vec![7.1, 7.1, 7.2, 8.3, 9.4, 10.5, 11.4], - &vec![2.8, 2.9, 2.8, 2.6, 3.5, 4.6, 5.0], + &[7.1, 7.1, 7.2, 8.3, 9.4, 10.5, 11.4], + &[2.8, 2.9, 2.8, 2.6, 3.5, 4.6, 5.0], 7, ) .collect::>(); assert_eq!(0.55, result[7 - 1]); let result = correlation::krcc( - &vec![7.1, 7.1, 7.2, 8.3, 9.4, 10.5, 11.4], - &vec![2.8, 2.8, 2.8, 2.6, 3.5, 4.6, 5.0], + &[7.1, 7.1, 7.2, 8.3, 9.4, 10.5, 11.4], + &[2.8, 2.8, 2.8, 2.6, 3.5, 4.6, 5.0], 7, ) .collect::>(); diff --git a/tests/trend_test.rs b/tests/trend_test.rs index a87a984..fa9e256 100644 --- a/tests/trend_test.rs +++ b/tests/trend_test.rs @@ -975,3 +975,38 @@ fn test_ichimoku() { result.4[..result.0.len() - b_win - lag_win] ); } + +#[test] +fn test_hurst() { + let stats = common::test_data(); + let rets = stats + .close + .windows(2) + .map(|w| w[1] / w[0] - 1.0) + .collect::>(); + let result = trend::hurst(&rets, 16, None).collect::>(); + assert_eq!(rets.len(), result.len()); + assert_eq!( + vec![ + 0.717300003513944, + 0.7040433063878732, + 0.7870815790649716, + 0.5852648888827561, + 0.4372401978043943, + 0.6284984091135501, + 0.5407697086351437, + 0.5950129675824667, + 0.8070520438846511, + 0.7513875353620354, + 0.7370004550770194, + 0.6775389374178914, + 0.7450996515301547, + 0.773309179173034, + 0.5946959711202044, + 0.23772640082555682, + 0.20834665213422418, + 0.10884312104017504, + ], + result[16 - 1..] + ); +}