1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
//! Hull Moving Average (HullMA) indicator.
use quant_primitives::Candle;
use rust_decimal::Decimal;
use crate::error::IndicatorError;
use crate::indicator::Indicator;
use crate::series::Series;
use crate::wma::Wma;
/// Hull Moving Average indicator.
///
/// A responsive moving average that reduces lag while maintaining smoothness.
/// Developed by Alan Hull.
///
/// # Formula
///
/// HullMA = WMA(2 * WMA(n/2) - WMA(n), sqrt(n))
///
/// # Example
///
/// ```
/// use quant_indicators::{Indicator, HullMa};
/// use quant_primitives::Candle;
/// use chrono::Utc;
/// use rust_decimal_macros::dec;
///
/// let ts = Utc::now();
/// let candles: Vec<Candle> = (0..25).map(|i| {
/// Candle::new(dec!(100), dec!(110), dec!(90), dec!(100) + rust_decimal::Decimal::from(i), dec!(1000), ts).unwrap()
/// }).collect();
/// let hull = HullMa::new(20).unwrap();
/// let series = hull.compute(&candles).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct HullMa {
period: usize,
half_period: usize,
sqrt_period: usize,
name: String,
}
impl HullMa {
/// Create a new Hull MA indicator with the specified period.
///
/// # Errors
///
/// Returns `InvalidParameter` if period < 2.
pub fn new(period: usize) -> Result<Self, IndicatorError> {
if period < 2 {
return Err(IndicatorError::InvalidParameter {
message: "HullMA period must be >= 2".to_string(),
});
}
let half_period = period / 2;
let sqrt_period = (period as f64).sqrt().round() as usize;
Ok(Self {
period,
half_period,
sqrt_period,
name: format!("HullMA({})", period),
})
}
}
impl Indicator for HullMa {
fn name(&self) -> &str {
&self.name
}
fn warmup_period(&self) -> usize {
// Need enough data for WMA(n) plus WMA(sqrt(n)) on the result
self.period + self.sqrt_period - 1
}
fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
let required = self.warmup_period();
if candles.len() < required {
return Err(IndicatorError::InsufficientData {
required,
actual: candles.len(),
});
}
// Step 1: Compute WMA(n/2)
let wma_half = Wma::new(self.half_period)?;
let half_series = wma_half.compute(candles)?;
// Step 2: Compute WMA(n)
let wma_full = Wma::new(self.period)?;
let full_series = wma_full.compute(candles)?;
// Step 3: Calculate 2 * WMA(n/2) - WMA(n) for overlapping region
// The full_series starts later than half_series, so we need to align them
let offset = self.period - self.half_period;
let half_values = half_series.values();
let full_values = full_series.values();
// Create intermediate values: 2 * WMA(n/2) - WMA(n)
let mut intermediate = Vec::with_capacity(full_values.len());
for (i, (ts, full_val)) in full_values.iter().enumerate() {
let half_val = half_values[i + offset].1;
let raw = Decimal::TWO * half_val - *full_val;
intermediate.push((*ts, raw));
}
// Step 4: Apply WMA(sqrt(n)) to the intermediate values
if intermediate.len() < self.sqrt_period {
return Err(IndicatorError::InsufficientData {
required,
actual: candles.len(),
});
}
let mut values = Vec::with_capacity(intermediate.len() - self.sqrt_period + 1);
let wma_sqrt = Wma::new(self.sqrt_period)?;
// Manual WMA calculation on intermediate values
let weight_sum = Decimal::from(self.sqrt_period as u64)
* Decimal::from(self.sqrt_period as u64 + 1)
/ Decimal::TWO;
for window in intermediate.windows(self.sqrt_period) {
let weighted_sum: Decimal = window
.iter()
.enumerate()
.map(|(i, (_, v))| *v * Decimal::from((i + 1) as u64))
.sum();
let hull = weighted_sum / weight_sum;
// Safe: windows(n) always yields slices of length n when n > 0
let ts = window[self.sqrt_period - 1].0;
values.push((ts, hull));
}
// Suppress unused warning since we constructed it for clarity
let _ = wma_sqrt;
Ok(Series::new(values))
}
}
#[cfg(test)]
#[path = "hull_tests.rs"]
mod tests;