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 fn warmup_period(&self) -> usize {
173 self.rsi.warmup_period()
177 + self.rsi_ma.warmup_period()
178 + self.ma_atr.warmup_period()
179 + self.dar_ema.warmup_period()
180 - 2
181 }
182
183 fn is_ready(&self) -> bool {
184 self.last_value.is_some()
185 }
186
187 fn name(&self) -> &'static str {
188 "QQE"
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::traits::BatchExt;
196 use approx::assert_relative_eq;
197
198 fn naive(
200 prices: &[f64],
201 rsi_period: usize,
202 smoothing: usize,
203 factor: f64,
204 ) -> Vec<Option<QqeOutput>> {
205 let mut rsi = Rsi::new(rsi_period).unwrap();
206 let mut rsi_ma = Ema::new(smoothing).unwrap();
207 let wilders = 2 * rsi_period - 1;
208 let mut ma_atr = Ema::new(wilders).unwrap();
209 let mut dar_ema = Ema::new(wilders).unwrap();
210 let mut prev_ma: Option<f64> = None;
211 let mut bands: Option<(f64, f64, i8)> = None;
212 let mut out = Vec::with_capacity(prices.len());
213 for &p in prices {
214 let v = (|| {
215 let r = rsi.update(p)?;
216 let m = rsi_ma.update(r)?;
217 let Some(pm) = prev_ma else {
218 prev_ma = Some(m);
219 return None;
220 };
221 let atr = (m - pm).abs();
222 prev_ma = Some(m);
223 let ma = ma_atr.update(atr)?;
224 let dar = dar_ema.update(ma)? * factor;
225 let nl = m - dar;
226 let ns = m + dar;
227 let (lb, sb, tr) = match bands {
228 Some((lbp, sbp, trp)) => {
229 let lb = if pm > lbp && m > lbp { lbp.max(nl) } else { nl };
230 let sb = if pm < sbp && m < sbp { sbp.min(ns) } else { ns };
231 let tr = if pm <= sbp && m > sbp {
232 1
233 } else if pm >= lbp && m < lbp {
234 -1
235 } else {
236 trp
237 };
238 (lb, sb, tr)
239 }
240 None => (nl, ns, 1),
241 };
242 bands = Some((lb, sb, tr));
243 Some(QqeOutput {
244 rsi_ma: m,
245 trailing_line: if tr == 1 { lb } else { sb },
246 })
247 })();
248 out.push(v);
249 }
250 out
251 }
252
253 #[test]
254 fn rejects_bad_params() {
255 assert!(matches!(Qqe::new(0, 5, 4.236), Err(Error::PeriodZero)));
256 assert!(matches!(Qqe::new(14, 0, 4.236), Err(Error::PeriodZero)));
257 assert!(matches!(
258 Qqe::new(14, 5, 0.0),
259 Err(Error::InvalidPeriod { .. })
260 ));
261 assert!(matches!(
262 Qqe::new(14, 5, f64::NAN),
263 Err(Error::InvalidPeriod { .. })
264 ));
265 }
266
267 #[test]
270 fn accessors_and_metadata() {
271 let qqe = Qqe::new(14, 5, 4.236).unwrap();
272 assert_relative_eq!(qqe.factor(), 4.236, epsilon = 1e-12);
273 assert_eq!(qqe.value(), None);
274 assert_eq!(qqe.name(), "QQE");
275 }
276
277 #[test]
278 fn first_emission_matches_warmup() {
279 let prices: Vec<f64> = (0..200)
282 .map(|i| 100.0 + (f64::from(i) * 0.06).sin() * 20.0)
283 .collect();
284 let mut qqe = Qqe::new(14, 5, 4.236).unwrap();
285 let out = qqe.batch(&prices);
286 let warmup = qqe.warmup_period();
287 for (i, v) in out.iter().enumerate().take(warmup - 1) {
288 assert!(v.is_none(), "index {i} must be None during warmup");
289 }
290 assert!(
291 out[warmup - 1].is_some(),
292 "first value at warmup_period - 1"
293 );
294 }
295
296 #[test]
297 fn matches_naive_over_full_cycle() {
298 let prices: Vec<f64> = (0..220)
300 .map(|i| {
301 let t = f64::from(i);
302 100.0 + (t * 0.05).sin() * 18.0 + (t * 0.2).cos() * 4.0
303 })
304 .collect();
305 let mut qqe = Qqe::new(14, 5, 4.236).unwrap();
306 let got = qqe.batch(&prices);
307 let want = naive(&prices, 14, 5, 4.236);
308 for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
309 assert_eq!(g.is_some(), w.is_some(), "readiness mismatch at {i}");
310 if let (Some(a), Some(b)) = (g, w) {
311 assert_relative_eq!(a.rsi_ma, b.rsi_ma, epsilon = 1e-9);
312 assert_relative_eq!(a.trailing_line, b.trailing_line, epsilon = 1e-9);
313 }
314 }
315 }
316
317 #[test]
318 fn trailing_line_below_rsi_ma_in_uptrend() {
319 let prices: Vec<f64> = (1..=120).map(f64::from).collect();
322 let mut qqe = Qqe::new(14, 5, 4.236).unwrap();
323 let last = qqe.batch(&prices).into_iter().flatten().last().unwrap();
324 assert!(
325 last.trailing_line <= last.rsi_ma,
326 "uptrend trailing {} should sit at/below rsi_ma {}",
327 last.trailing_line,
328 last.rsi_ma
329 );
330 }
331
332 #[test]
333 fn reset_clears_state() {
334 let mut qqe = Qqe::new(14, 5, 4.236).unwrap();
335 qqe.batch(
336 &(0..120)
337 .map(|i| 100.0 + (f64::from(i) * 0.1).sin() * 8.0)
338 .collect::<Vec<_>>(),
339 );
340 assert!(qqe.is_ready());
341 qqe.reset();
342 assert!(!qqe.is_ready());
343 assert_eq!(qqe.update(1.0), None);
344 }
345
346 #[test]
347 fn batch_equals_streaming() {
348 let prices: Vec<f64> = (0..150)
349 .map(|i| 50.0 + (f64::from(i) * 0.12).sin() * 12.0)
350 .collect();
351 let mut a = Qqe::new(14, 5, 4.236).unwrap();
352 let mut b = Qqe::new(14, 5, 4.236).unwrap();
353 assert_eq!(
354 a.batch(&prices),
355 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
356 );
357 }
358}