quant_indicators/
hurst.rs1use quant_primitives::Candle;
17use rust_decimal::Decimal;
18
19use crate::error::IndicatorError;
20
21#[derive(Debug, Clone)]
26pub struct HurstExponent {
27 window: usize,
28}
29
30const SUBSERIES_SIZES: [usize; 3] = [16, 32, 64];
32
33const MIN_RETURNS: usize = 20;
35
36impl HurstExponent {
37 pub fn new(window: usize) -> Result<Self, IndicatorError> {
46 if window < 64 {
47 return Err(IndicatorError::InvalidParameter {
48 message: format!("HurstExponent window must be >= 64, got {window}"),
49 });
50 }
51 Ok(Self { window })
52 }
53
54 pub fn compute_from_candles(&self, candles: &[Candle]) -> Option<Decimal> {
58 if candles.len() < self.window {
59 return None;
60 }
61 let closes: Vec<Decimal> = candles[candles.len() - self.window..]
62 .iter()
63 .map(|c| c.close())
64 .collect();
65 let returns = compute_log_returns(&closes);
66 if returns.len() < MIN_RETURNS {
67 return None;
68 }
69 estimate_hurst_from_returns(&returns)
70 }
71}
72
73pub fn compute_log_returns(closes: &[Decimal]) -> Vec<Decimal> {
77 closes
78 .windows(2)
79 .filter_map(|w| {
80 if w[0].is_zero() {
81 None
82 } else {
83 Some((w[1] - w[0]) / w[0])
84 }
85 })
86 .collect()
87}
88
89pub fn rescaled_range_for_n(returns: &[Decimal], n: usize) -> Option<Decimal> {
95 if n > returns.len() || n == 0 {
96 return None;
97 }
98 let num_subseries = returns.len() / n;
99 if num_subseries == 0 {
100 return None;
101 }
102
103 let mut rs_sum = Decimal::ZERO;
104 let mut valid_count = 0u32;
105
106 for k in 0..num_subseries {
107 let sub = &returns[k * n..(k + 1) * n];
108 let n_dec = Decimal::from(sub.len());
109 let mean: Decimal = sub.iter().copied().sum::<Decimal>() / n_dec;
110 let deviations: Vec<Decimal> = sub.iter().map(|r| r - mean).collect();
111
112 let mut cum = Vec::with_capacity(sub.len());
114 let mut running = Decimal::ZERO;
115 for d in &deviations {
116 running += d;
117 cum.push(running);
118 }
119
120 let r = cum.iter().copied().max().unwrap_or(Decimal::ZERO)
121 - cum.iter().copied().min().unwrap_or(Decimal::ZERO);
122
123 let var: Decimal = deviations.iter().map(|d| d * d).sum::<Decimal>() / n_dec;
125 if var.is_zero() {
126 continue;
127 }
128 let s = newton_sqrt(var);
129 if s.is_zero() {
130 continue;
131 }
132
133 rs_sum += r / s;
134 valid_count += 1;
135 }
136
137 if valid_count == 0 {
138 return None;
139 }
140 Some(rs_sum / Decimal::from(valid_count))
141}
142
143pub fn estimate_slope(log_ns: &[usize], log_rs: &[Decimal]) -> Option<Decimal> {
150 if log_ns.len() < 2 || log_rs.len() < 2 {
151 return None;
152 }
153
154 let rs1 = log_rs[0];
155 let rs2 = log_rs[log_rs.len() - 1];
156 let n1 = Decimal::from(log_ns[0]);
157 let n2 = Decimal::from(log_ns[log_ns.len() - 1]);
158
159 if rs1.is_zero() || n1.is_zero() {
160 return None;
161 }
162
163 let rs_ratio = rs2 / rs1;
164 let n_ratio = n2 / n1;
165 let h = approx_log2(rs_ratio) / approx_log2(n_ratio);
166 Some(h.max(Decimal::ZERO).min(Decimal::ONE))
167}
168
169fn estimate_hurst_from_returns(returns: &[Decimal]) -> Option<Decimal> {
171 let mut log_ns = Vec::new();
172 let mut log_rs = Vec::new();
173
174 for &n in &SUBSERIES_SIZES {
175 if let Some(avg_rs) = rescaled_range_for_n(returns, n) {
176 log_ns.push(n);
177 log_rs.push(avg_rs);
178 }
179 }
180
181 estimate_slope(&log_ns, &log_rs)
182}
183
184fn newton_sqrt(val: Decimal) -> Decimal {
186 if val.is_zero() || val < Decimal::ZERO {
187 return Decimal::ZERO;
188 }
189 let mut guess = val / Decimal::TWO;
190 for _ in 0..10 {
191 if guess.is_zero() {
192 return Decimal::ZERO;
193 }
194 guess = (guess + val / guess) / Decimal::TWO;
195 }
196 guess
197}
198
199fn approx_log2(x: Decimal) -> Decimal {
201 if x <= Decimal::ZERO {
202 return Decimal::ZERO;
203 }
204 let ln2 = Decimal::new(6931, 4); let mut reduced = x;
207 let mut shifts = 0i64;
208 while reduced >= Decimal::TWO {
209 reduced /= Decimal::TWO;
210 shifts += 1;
211 }
212 while reduced < Decimal::ONE && !reduced.is_zero() {
213 reduced *= Decimal::TWO;
214 shifts -= 1;
215 }
216
217 let u = (reduced - Decimal::ONE) / (reduced + Decimal::ONE);
218 let u2 = u * u;
219 let mut term = u;
220 let mut sum = u;
221 for k in 1..10 {
222 term *= u2;
223 sum += term / Decimal::from(2 * k + 1);
224 }
225 let ln_reduced = sum * Decimal::TWO;
226 let ln_x = ln_reduced + Decimal::from(shifts) * ln2;
227 ln_x / ln2
228}
229
230#[cfg(test)]
231#[path = "hurst_tests.rs"]
232mod tests;