1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::indicators::rsi::Rsi;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct QqeOutput {
11 pub rsi_ma: f64,
13 pub trailing_line: f64,
16}
17
18#[derive(Debug, Clone)]
56pub struct Qqe {
57 rsi: Rsi,
58 rsi_ma: Ema,
59 ma_atr: Ema,
60 dar_ema: Ema,
61 factor: f64,
62 prev_rsi_ma: Option<f64>,
63 bands: Option<(f64, f64, i8)>, last_value: Option<QqeOutput>,
65}
66
67impl Qqe {
68 pub fn new(rsi_period: usize, smoothing: usize, factor: f64) -> Result<Self> {
75 if rsi_period == 0 || smoothing == 0 {
76 return Err(Error::PeriodZero);
77 }
78 if !factor.is_finite() || factor <= 0.0 {
79 return Err(Error::InvalidPeriod {
80 message: "QQE factor must be a finite positive value",
81 });
82 }
83 let wilders = 2 * rsi_period - 1;
84 Ok(Self {
85 rsi: Rsi::new(rsi_period)?,
86 rsi_ma: Ema::new(smoothing)?,
87 ma_atr: Ema::new(wilders)?,
88 dar_ema: Ema::new(wilders)?,
89 factor,
90 prev_rsi_ma: None,
91 bands: None,
92 last_value: None,
93 })
94 }
95
96 pub const fn factor(&self) -> f64 {
98 self.factor
99 }
100
101 pub const fn value(&self) -> Option<QqeOutput> {
103 self.last_value
104 }
105}
106
107impl Indicator for Qqe {
108 type Input = f64;
109 type Output = QqeOutput;
110
111 fn update(&mut self, price: f64) -> Option<QqeOutput> {
112 let rsi = self.rsi.update(price)?;
113 let rsi_ma = self.rsi_ma.update(rsi)?;
114
115 let Some(prev_ma) = self.prev_rsi_ma else {
116 self.prev_rsi_ma = Some(rsi_ma);
117 return None;
118 };
119 let atr_rsi = (rsi_ma - prev_ma).abs();
120 self.prev_rsi_ma = Some(rsi_ma);
121
122 let ma_atr = self.ma_atr.update(atr_rsi)?;
123 let dar = self.dar_ema.update(ma_atr)? * self.factor;
124
125 let new_long = rsi_ma - dar;
126 let new_short = rsi_ma + dar;
127
128 let (long_band, short_band, trend) = match self.bands {
129 Some((lb_prev, sb_prev, tr_prev)) => {
130 let lb = if prev_ma > lb_prev && rsi_ma > lb_prev {
131 lb_prev.max(new_long)
132 } else {
133 new_long
134 };
135 let sb = if prev_ma < sb_prev && rsi_ma < sb_prev {
136 sb_prev.min(new_short)
137 } else {
138 new_short
139 };
140 let tr = if prev_ma <= sb_prev && rsi_ma > sb_prev {
141 1
142 } else if prev_ma >= lb_prev && rsi_ma < lb_prev {
143 -1
144 } else {
145 tr_prev
146 };
147 (lb, sb, tr)
148 }
149 None => (new_long, new_short, 1),
150 };
151 self.bands = Some((long_band, short_band, trend));
152
153 let trailing_line = if trend == 1 { long_band } else { short_band };
154 let out = QqeOutput {
155 rsi_ma,
156 trailing_line,
157 };
158 self.last_value = Some(out);
159 Some(out)
160 }
161
162 fn reset(&mut self) {
163 self.rsi.reset();
164 self.rsi_ma.reset();
165 self.ma_atr.reset();
166 self.dar_ema.reset();
167 self.prev_rsi_ma = None;
168 self.bands = None;
169 self.last_value = None;
170 }
171
172 #[inline]
173 fn warmup_period(&self) -> usize {
174 self.rsi.warmup_period()
178 + self.rsi_ma.warmup_period()
179 + self.ma_atr.warmup_period()
180 + self.dar_ema.warmup_period()
181 - 2
182 }
183
184 #[inline]
185 fn is_ready(&self) -> bool {
186 self.last_value.is_some()
187 }
188
189 #[inline]
190 fn name(&self) -> &'static str {
191 "QQE"
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::traits::BatchExt;
199 use approx::assert_relative_eq;
200
201 fn naive(
203 prices: &[f64],
204 rsi_period: usize,
205 smoothing: usize,
206 factor: f64,
207 ) -> Vec<Option<QqeOutput>> {
208 let mut rsi = Rsi::new(rsi_period).unwrap();
209 let mut rsi_ma = Ema::new(smoothing).unwrap();
210 let wilders = 2 * rsi_period - 1;
211 let mut ma_atr = Ema::new(wilders).unwrap();
212 let mut dar_ema = Ema::new(wilders).unwrap();
213 let mut prev_ma: Option<f64> = None;
214 let mut bands: Option<(f64, f64, i8)> = None;
215 let mut out = Vec::with_capacity(prices.len());
216 for &p in prices {
217 let v = (|| {
218 let r = rsi.update(p)?;
219 let m = rsi_ma.update(r)?;
220 let Some(pm) = prev_ma else {
221 prev_ma = Some(m);
222 return None;
223 };
224 let atr = (m - pm).abs();
225 prev_ma = Some(m);
226 let ma = ma_atr.update(atr)?;
227 let dar = dar_ema.update(ma)? * factor;
228 let nl = m - dar;
229 let ns = m + dar;
230 let (lb, sb, tr) = match bands {
231 Some((lbp, sbp, trp)) => {
232 let lb = if pm > lbp && m > lbp { lbp.max(nl) } else { nl };
233 let sb = if pm < sbp && m < sbp { sbp.min(ns) } else { ns };
234 let tr = if pm <= sbp && m > sbp {
235 1
236 } else if pm >= lbp && m < lbp {
237 -1
238 } else {
239 trp
240 };
241 (lb, sb, tr)
242 }
243 None => (nl, ns, 1),
244 };
245 bands = Some((lb, sb, tr));
246 Some(QqeOutput {
247 rsi_ma: m,
248 trailing_line: if tr == 1 { lb } else { sb },
249 })
250 })();
251 out.push(v);
252 }
253 out
254 }
255
256 #[test]
257 fn rejects_bad_params() {
258 assert!(matches!(Qqe::new(0, 5, 4.236), Err(Error::PeriodZero)));
259 assert!(matches!(Qqe::new(14, 0, 4.236), Err(Error::PeriodZero)));
260 assert!(matches!(
261 Qqe::new(14, 5, 0.0),
262 Err(Error::InvalidPeriod { .. })
263 ));
264 assert!(matches!(
265 Qqe::new(14, 5, f64::NAN),
266 Err(Error::InvalidPeriod { .. })
267 ));
268 }
269
270 #[test]
273 fn accessors_and_metadata() {
274 let qqe = Qqe::new(14, 5, 4.236).unwrap();
275 assert_relative_eq!(qqe.factor(), 4.236, epsilon = 1e-12);
276 assert_eq!(qqe.value(), None);
277 assert_eq!(qqe.name(), "QQE");
278 }
279
280 #[test]
281 fn first_emission_matches_warmup() {
282 let prices: Vec<f64> = (0..200)
285 .map(|i| 100.0 + (f64::from(i) * 0.06).sin() * 20.0)
286 .collect();
287 let mut qqe = Qqe::new(14, 5, 4.236).unwrap();
288 let out = qqe.batch(&prices);
289 let warmup = qqe.warmup_period();
290 for (i, v) in out.iter().enumerate().take(warmup - 1) {
291 assert!(v.is_none(), "index {i} must be None during warmup");
292 }
293 assert!(
294 out[warmup - 1].is_some(),
295 "first value at warmup_period - 1"
296 );
297 }
298
299 #[test]
300 fn matches_naive_over_full_cycle() {
301 let prices: Vec<f64> = (0..220)
303 .map(|i| {
304 let t = f64::from(i);
305 100.0 + (t * 0.05).sin() * 18.0 + (t * 0.2).cos() * 4.0
306 })
307 .collect();
308 let mut qqe = Qqe::new(14, 5, 4.236).unwrap();
309 let got = qqe.batch(&prices);
310 let want = naive(&prices, 14, 5, 4.236);
311 for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
312 assert_eq!(g.is_some(), w.is_some(), "readiness mismatch at {i}");
313 if let (Some(a), Some(b)) = (g, w) {
314 assert_relative_eq!(a.rsi_ma, b.rsi_ma, epsilon = 1e-9);
315 assert_relative_eq!(a.trailing_line, b.trailing_line, epsilon = 1e-9);
316 }
317 }
318 }
319
320 #[test]
321 fn trailing_line_below_rsi_ma_in_uptrend() {
322 let prices: Vec<f64> = (1..=120).map(f64::from).collect();
325 let mut qqe = Qqe::new(14, 5, 4.236).unwrap();
326 let last = qqe.batch(&prices).into_iter().flatten().last().unwrap();
327 assert!(
328 last.trailing_line <= last.rsi_ma,
329 "uptrend trailing {} should sit at/below rsi_ma {}",
330 last.trailing_line,
331 last.rsi_ma
332 );
333 }
334
335 #[test]
336 fn reset_clears_state() {
337 let mut qqe = Qqe::new(14, 5, 4.236).unwrap();
338 qqe.batch(
339 &(0..120)
340 .map(|i| 100.0 + (f64::from(i) * 0.1).sin() * 8.0)
341 .collect::<Vec<_>>(),
342 );
343 assert!(qqe.is_ready());
344 qqe.reset();
345 assert!(!qqe.is_ready());
346 assert_eq!(qqe.update(1.0), None);
347 }
348
349 #[test]
350 fn batch_equals_streaming() {
351 let prices: Vec<f64> = (0..150)
352 .map(|i| 50.0 + (f64::from(i) * 0.12).sin() * 12.0)
353 .collect();
354 let mut a = Qqe::new(14, 5, 4.236).unwrap();
355 let mut b = Qqe::new(14, 5, 4.236).unwrap();
356 assert_eq!(
357 a.batch(&prices),
358 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
359 );
360 }
361}