1use quant_primitives::Candle;
4use rust_decimal::Decimal;
5
6use crate::error::IndicatorError;
7use crate::indicator::Indicator;
8use crate::series::Series;
9use crate::wma::Wma;
10
11#[derive(Debug, Clone)]
36pub struct HullMa {
37 period: usize,
38 half_period: usize,
39 sqrt_period: usize,
40 name: String,
41}
42
43impl HullMa {
44 pub fn new(period: usize) -> Result<Self, IndicatorError> {
50 if period < 2 {
51 return Err(IndicatorError::InvalidParameter {
52 message: "HullMA period must be >= 2".to_string(),
53 });
54 }
55
56 let half_period = period / 2;
57 let sqrt_period = (period as f64).sqrt().round() as usize;
58
59 Ok(Self {
60 period,
61 half_period,
62 sqrt_period,
63 name: format!("HullMA({})", period),
64 })
65 }
66}
67
68impl Indicator for HullMa {
69 fn name(&self) -> &str {
70 &self.name
71 }
72
73 fn warmup_period(&self) -> usize {
74 self.period + self.sqrt_period - 1
76 }
77
78 fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
79 let required = self.warmup_period();
80 if candles.len() < required {
81 return Err(IndicatorError::InsufficientData {
82 required,
83 actual: candles.len(),
84 });
85 }
86
87 let wma_half = Wma::new(self.half_period)?;
89 let half_series = wma_half.compute(candles)?;
90
91 let wma_full = Wma::new(self.period)?;
93 let full_series = wma_full.compute(candles)?;
94
95 let offset = self.period - self.half_period;
98 let half_values = half_series.values();
99 let full_values = full_series.values();
100
101 let mut intermediate = Vec::with_capacity(full_values.len());
103 for (i, (ts, full_val)) in full_values.iter().enumerate() {
104 let half_val = half_values[i + offset].1;
105 let raw = Decimal::TWO * half_val - *full_val;
106 intermediate.push((*ts, raw));
107 }
108
109 if intermediate.len() < self.sqrt_period {
111 return Err(IndicatorError::InsufficientData {
112 required,
113 actual: candles.len(),
114 });
115 }
116
117 let mut values = Vec::with_capacity(intermediate.len() - self.sqrt_period + 1);
118 let wma_sqrt = Wma::new(self.sqrt_period)?;
119
120 let weight_sum = Decimal::from(self.sqrt_period as u64)
122 * Decimal::from(self.sqrt_period as u64 + 1)
123 / Decimal::TWO;
124
125 for window in intermediate.windows(self.sqrt_period) {
126 let weighted_sum: Decimal = window
127 .iter()
128 .enumerate()
129 .map(|(i, (_, v))| *v * Decimal::from((i + 1) as u64))
130 .sum();
131
132 let hull = weighted_sum / weight_sum;
133 let ts = window[self.sqrt_period - 1].0;
135 values.push((ts, hull));
136 }
137
138 let _ = wma_sqrt;
140
141 Ok(Series::new(values))
142 }
143}
144
145#[cfg(test)]
146#[path = "hull_tests.rs"]
147mod tests;