1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::{PyDict, PyList};
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15#[cfg(all(feature = "python", feature = "cuda"))]
16use crate::cuda::{cuda_available, CudaQqe};
17#[cfg(all(feature = "python", feature = "cuda"))]
18use crate::indicators::moving_averages::alma::{make_device_array_py, DeviceArrayF32Py};
19use crate::indicators::moving_averages::ema::{ema, EmaInput, EmaParams};
20use crate::indicators::rsi::{rsi, RsiInput, RsiParams};
21use crate::utilities::data_loader::{source_type, Candles};
22use crate::utilities::enums::Kernel;
23use crate::utilities::helpers::{
24 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
25 make_uninit_matrix,
26};
27#[cfg(feature = "python")]
28use crate::utilities::kernel_validation::validate_kernel;
29use aligned_vec::{AVec, CACHELINE_ALIGN};
30
31#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
32use core::arch::x86_64::*;
33
34#[cfg(not(target_arch = "wasm32"))]
35use rayon::prelude::*;
36
37use std::convert::AsRef;
38use std::error::Error;
39use std::mem::MaybeUninit;
40use thiserror::Error;
41
42impl<'a> AsRef<[f64]> for QqeInput<'a> {
43 #[inline(always)]
44 fn as_ref(&self) -> &[f64] {
45 match &self.data {
46 QqeData::Slice(slice) => slice,
47 QqeData::Candles { candles, source } => source_type(candles, source),
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
53pub enum QqeData<'a> {
54 Candles {
55 candles: &'a Candles,
56 source: &'a str,
57 },
58 Slice(&'a [f64]),
59}
60
61#[derive(Debug, Clone)]
62pub struct QqeOutput {
63 pub fast: Vec<f64>,
64 pub slow: Vec<f64>,
65}
66
67#[derive(Debug, Clone)]
68#[cfg_attr(
69 all(target_arch = "wasm32", feature = "wasm"),
70 derive(Serialize, Deserialize)
71)]
72pub struct QqeParams {
73 pub rsi_period: Option<usize>,
74 pub smoothing_factor: Option<usize>,
75 pub fast_factor: Option<f64>,
76}
77
78impl Default for QqeParams {
79 fn default() -> Self {
80 Self {
81 rsi_period: Some(14),
82 smoothing_factor: Some(5),
83 fast_factor: Some(4.236),
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
89pub struct QqeInput<'a> {
90 pub data: QqeData<'a>,
91 pub params: QqeParams,
92}
93
94impl<'a> QqeInput<'a> {
95 #[inline]
96 pub fn from_candles(c: &'a Candles, s: &'a str, p: QqeParams) -> Self {
97 Self {
98 data: QqeData::Candles {
99 candles: c,
100 source: s,
101 },
102 params: p,
103 }
104 }
105
106 #[inline]
107 pub fn from_slice(sl: &'a [f64], p: QqeParams) -> Self {
108 Self {
109 data: QqeData::Slice(sl),
110 params: p,
111 }
112 }
113
114 #[inline]
115 pub fn with_default_candles(c: &'a Candles) -> Self {
116 Self::from_candles(c, "close", QqeParams::default())
117 }
118
119 #[inline]
120 pub fn get_rsi_period(&self) -> usize {
121 self.params.rsi_period.unwrap_or(14)
122 }
123
124 #[inline]
125 pub fn get_smoothing_factor(&self) -> usize {
126 self.params.smoothing_factor.unwrap_or(5)
127 }
128
129 #[inline]
130 pub fn get_fast_factor(&self) -> f64 {
131 self.params.fast_factor.unwrap_or(4.236)
132 }
133}
134
135#[derive(Copy, Clone, Debug)]
136pub struct QqeBuilder {
137 rsi_period: Option<usize>,
138 smoothing_factor: Option<usize>,
139 fast_factor: Option<f64>,
140 kernel: Kernel,
141}
142
143impl Default for QqeBuilder {
144 fn default() -> Self {
145 Self {
146 rsi_period: None,
147 smoothing_factor: None,
148 fast_factor: None,
149 kernel: Kernel::Auto,
150 }
151 }
152}
153
154impl QqeBuilder {
155 #[inline(always)]
156 pub fn new() -> Self {
157 Self::default()
158 }
159
160 #[inline(always)]
161 pub fn rsi_period(mut self, val: usize) -> Self {
162 self.rsi_period = Some(val);
163 self
164 }
165
166 #[inline(always)]
167 pub fn smoothing_factor(mut self, val: usize) -> Self {
168 self.smoothing_factor = Some(val);
169 self
170 }
171
172 #[inline(always)]
173 pub fn fast_factor(mut self, val: f64) -> Self {
174 self.fast_factor = Some(val);
175 self
176 }
177
178 #[inline(always)]
179 pub fn kernel(mut self, k: Kernel) -> Self {
180 self.kernel = k;
181 self
182 }
183
184 #[inline(always)]
185 pub fn apply(self, c: &Candles) -> Result<QqeOutput, QqeError> {
186 let p = QqeParams {
187 rsi_period: self.rsi_period,
188 smoothing_factor: self.smoothing_factor,
189 fast_factor: self.fast_factor,
190 };
191 let i = QqeInput::from_candles(c, "close", p);
192 qqe_with_kernel(&i, self.kernel)
193 }
194
195 #[inline(always)]
196 pub fn apply_slice(self, d: &[f64]) -> Result<QqeOutput, QqeError> {
197 let p = QqeParams {
198 rsi_period: self.rsi_period,
199 smoothing_factor: self.smoothing_factor,
200 fast_factor: self.fast_factor,
201 };
202 let i = QqeInput::from_slice(d, p);
203 qqe_with_kernel(&i, self.kernel)
204 }
205
206 #[inline(always)]
207 pub fn into_stream(self) -> Result<QqeStream, QqeError> {
208 let p = QqeParams {
209 rsi_period: self.rsi_period,
210 smoothing_factor: self.smoothing_factor,
211 fast_factor: self.fast_factor,
212 };
213 QqeStream::try_new(p)
214 }
215}
216
217#[derive(Debug, Error)]
218pub enum QqeError {
219 #[error("qqe: Input data slice is empty.")]
220 EmptyInputData,
221
222 #[error("qqe: All values are NaN.")]
223 AllValuesNaN,
224
225 #[error("qqe: Invalid period: period = {period}, data length = {data_len}")]
226 InvalidPeriod { period: usize, data_len: usize },
227
228 #[error("qqe: Not enough valid data: needed = {needed}, valid = {valid}")]
229 NotEnoughValidData { needed: usize, valid: usize },
230
231 #[error("qqe: Output slice length mismatch: expected = {expected}, got = {got}")]
232 OutputLengthMismatch { expected: usize, got: usize },
233
234 #[error("qqe: Invalid range: start = {start}, end = {end}, step = {step}")]
235 InvalidRange {
236 start: usize,
237 end: usize,
238 step: usize,
239 },
240
241 #[error("qqe: Invalid kernel type for batch operation: {0:?}")]
242 InvalidKernelForBatch(Kernel),
243
244 #[error("qqe: Error in dependent indicator: {message}")]
245 DependentIndicatorError { message: String },
246}
247
248#[inline]
249pub fn qqe(input: &QqeInput) -> Result<QqeOutput, QqeError> {
250 qqe_with_kernel(input, Kernel::Auto)
251}
252
253pub fn qqe_with_kernel(input: &QqeInput, kernel: Kernel) -> Result<QqeOutput, QqeError> {
254 let (data, rsi_p, ema_p, fast_k, first, chosen) = qqe_prepare(input, kernel)?;
255 let warm = first + rsi_p + ema_p - 2;
256
257 if chosen == Kernel::Scalar && rsi_p == 14 && ema_p == 5 && fast_k == 4.236 {
258 let mut fast = alloc_with_nan_prefix(data.len(), warm);
259 let mut slow = alloc_with_nan_prefix(data.len(), warm);
260 unsafe {
261 qqe_scalar_classic(data, rsi_p, ema_p, fast_k, first, &mut fast, &mut slow)?;
262 }
263 return Ok(QqeOutput { fast, slow });
264 }
265
266 let mut fast = alloc_with_nan_prefix(data.len(), warm);
267 let mut slow = alloc_with_nan_prefix(data.len(), warm);
268
269 qqe_into_slices(&mut fast, &mut slow, input, chosen)?;
270 Ok(QqeOutput { fast, slow })
271}
272
273fn qqe_scalar(
274 data: &[f64],
275 rsi_p: usize,
276 ema_p: usize,
277 fast_k: f64,
278 first: usize,
279 fast_warm: usize,
280) -> Result<QqeOutput, QqeError> {
281 let mut fast = alloc_with_nan_prefix(data.len(), fast_warm);
282 let mut slow = alloc_with_nan_prefix(data.len(), fast_warm);
283
284 if rsi_p == 14 && ema_p == 5 && fast_k == 4.236 {
285 unsafe {
286 qqe_scalar_classic(data, rsi_p, ema_p, fast_k, first, &mut fast, &mut slow)?;
287 }
288 } else {
289 qqe_into_slices(
290 &mut fast,
291 &mut slow,
292 &QqeInput::from_slice(
293 data,
294 QqeParams {
295 rsi_period: Some(rsi_p),
296 smoothing_factor: Some(ema_p),
297 fast_factor: Some(fast_k),
298 },
299 ),
300 Kernel::Scalar,
301 )?;
302 }
303 Ok(QqeOutput { fast, slow })
304}
305
306#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
307#[target_feature(enable = "avx2,fma")]
308unsafe fn qqe_avx2(
309 data: &[f64],
310 rsi_p: usize,
311 ema_p: usize,
312 fast_k: f64,
313 first: usize,
314 fast_warm: usize,
315) -> Result<QqeOutput, QqeError> {
316 qqe_scalar(data, rsi_p, ema_p, fast_k, first, fast_warm)
317}
318
319#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
320#[target_feature(enable = "avx512f,fma")]
321unsafe fn qqe_avx512(
322 data: &[f64],
323 rsi_p: usize,
324 ema_p: usize,
325 fast_k: f64,
326 first: usize,
327 fast_warm: usize,
328) -> Result<QqeOutput, QqeError> {
329 qqe_scalar(data, rsi_p, ema_p, fast_k, first, fast_warm)
330}
331
332#[inline]
333pub fn qqe_into_slices(
334 dst_fast: &mut [f64],
335 dst_slow: &mut [f64],
336 input: &QqeInput,
337 kern: Kernel,
338) -> Result<(), QqeError> {
339 use crate::indicators::moving_averages::ema::ema_into_slice;
340 use crate::indicators::rsi::rsi_into_slice;
341
342 let (data, rsi_p, ema_p, fast_k, first, chosen) = qqe_prepare(input, kern)?;
343 if dst_fast.len() != data.len() || dst_slow.len() != data.len() {
344 let got = core::cmp::min(dst_fast.len(), dst_slow.len());
345 return Err(QqeError::OutputLengthMismatch {
346 expected: data.len(),
347 got,
348 });
349 }
350 let warm = first + rsi_p + ema_p - 2;
351
352 if chosen == Kernel::Scalar && rsi_p == 14 && ema_p == 5 && fast_k == 4.236 {
353 let prefix = warm.min(dst_fast.len());
354 for v in &mut dst_fast[..prefix] {
355 *v = f64::NAN;
356 }
357 for v in &mut dst_slow[..prefix] {
358 *v = f64::NAN;
359 }
360 unsafe {
361 qqe_scalar_classic(data, rsi_p, ema_p, fast_k, first, dst_fast, dst_slow)?;
362 }
363 return Ok(());
364 }
365
366 let mut tmp_mu = make_uninit_matrix(1, data.len());
367 let tmp: &mut [f64] =
368 unsafe { core::slice::from_raw_parts_mut(tmp_mu.as_mut_ptr() as *mut f64, data.len()) };
369
370 let rsi_in = RsiInput::from_slice(
371 data,
372 RsiParams {
373 period: Some(rsi_p),
374 },
375 );
376 rsi_into_slice(tmp, &rsi_in, chosen).map_err(|e| QqeError::DependentIndicatorError {
377 message: e.to_string(),
378 })?;
379
380 let ema_in = EmaInput::from_slice(
381 tmp,
382 EmaParams {
383 period: Some(ema_p),
384 },
385 );
386 ema_into_slice(dst_fast, &ema_in, chosen).map_err(|e| QqeError::DependentIndicatorError {
387 message: e.to_string(),
388 })?;
389
390 for v in &mut dst_slow[..warm] {
391 *v = f64::NAN;
392 }
393
394 qqe_compute_slow_from(dst_fast, fast_k, warm, dst_slow);
395 Ok(())
396}
397
398#[inline]
399pub fn qqe_into_pair(
400 dst: (&mut [f64], &mut [f64]),
401 input: &QqeInput,
402 kern: Kernel,
403) -> Result<(), QqeError> {
404 qqe_into_slices(dst.0, dst.1, input, kern)
405}
406
407#[inline]
408pub fn qqe_into_slice(
409 dst_fast: &mut [f64],
410 dst_slow: &mut [f64],
411 input: &QqeInput,
412 kern: Kernel,
413) -> Result<(), QqeError> {
414 qqe_into_slices(dst_fast, dst_slow, input, kern)
415}
416
417#[inline(always)]
418fn qqe_prepare<'a>(
419 input: &'a QqeInput,
420 kernel: Kernel,
421) -> Result<(&'a [f64], usize, usize, f64, usize, Kernel), QqeError> {
422 let data: &[f64] = input.as_ref();
423 let len = data.len();
424
425 if len == 0 {
426 return Err(QqeError::EmptyInputData);
427 }
428
429 let first = data
430 .iter()
431 .position(|x| !x.is_nan())
432 .ok_or(QqeError::AllValuesNaN)?;
433
434 let rsi_period = input.get_rsi_period();
435 let smoothing_factor = input.get_smoothing_factor();
436 let fast_factor = input.get_fast_factor();
437
438 if rsi_period == 0 || rsi_period > len {
439 return Err(QqeError::InvalidPeriod {
440 period: rsi_period,
441 data_len: len,
442 });
443 }
444
445 if smoothing_factor == 0 || smoothing_factor > len {
446 return Err(QqeError::InvalidPeriod {
447 period: smoothing_factor,
448 data_len: len,
449 });
450 }
451
452 let needed = rsi_period + smoothing_factor;
453 if len - first < needed {
454 return Err(QqeError::NotEnoughValidData {
455 needed,
456 valid: len - first,
457 });
458 }
459
460 let chosen = match kernel {
461 Kernel::Auto => detect_best_kernel(),
462 k => k,
463 };
464
465 Ok((
466 data,
467 rsi_period,
468 smoothing_factor,
469 fast_factor,
470 first,
471 chosen,
472 ))
473}
474
475#[inline]
476fn qqe_compute_slow_from(qqef: &[f64], fast_factor: f64, start: usize, qqes: &mut [f64]) {
477 let len = qqef.len();
478 debug_assert!(start < len);
479
480 qqes[start] = qqef[start];
481
482 let alpha = 1.0 / 14.0;
483 let mut wwma = 0.0;
484 let mut atrrsi = 0.0;
485
486 for i in (start + 1)..len {
487 let tr = (qqef[i] - qqef[i - 1]).abs();
488 wwma = alpha * tr + (1.0 - alpha) * wwma;
489 atrrsi = alpha * wwma + (1.0 - alpha) * atrrsi;
490
491 let qup = qqef[i] + atrrsi * fast_factor;
492 let qdn = qqef[i] - atrrsi * fast_factor;
493
494 let prev = qqes[i - 1];
495
496 if qup < prev {
497 qqes[i] = qup;
498 } else if qqef[i] > prev && qqef[i - 1] < prev {
499 qqes[i] = qdn;
500 } else if qdn > prev {
501 qqes[i] = qdn;
502 } else if qqef[i] < prev && qqef[i - 1] > prev {
503 qqes[i] = qup;
504 } else {
505 qqes[i] = prev;
506 }
507 }
508}
509
510#[inline(always)]
511pub unsafe fn qqe_scalar_classic(
512 data: &[f64],
513 rsi_period: usize,
514 smoothing_factor: usize,
515 fast_factor: f64,
516 first: usize,
517 dst_fast: &mut [f64],
518 dst_slow: &mut [f64],
519) -> Result<(), QqeError> {
520 let len = data.len();
521 if dst_fast.len() != len || dst_slow.len() != len {
522 let got = core::cmp::min(dst_fast.len(), dst_slow.len());
523 return Err(QqeError::OutputLengthMismatch { expected: len, got });
524 }
525
526 let rsi_start = first + rsi_period;
527 if rsi_start >= len {
528 return Ok(());
529 }
530 let warm = first + rsi_period + smoothing_factor - 2;
531 let ema_warmup_end = (rsi_start + smoothing_factor).min(len);
532
533 let inv_rsi = 1.0 / rsi_period as f64;
534 let beta_rsi = 1.0 - inv_rsi;
535
536 let mut avg_gain = 0.0f64;
537 let mut avg_loss = 0.0f64;
538 let mut any_nan = false;
539
540 let init_end = (first + rsi_period).min(len - 1);
541 {
542 let mut i = first + 1;
543 while i <= init_end {
544 let delta = *data.get_unchecked(i) - *data.get_unchecked(i - 1);
545 if !delta.is_finite() {
546 any_nan = true;
547 break;
548 }
549 if delta > 0.0 {
550 avg_gain += delta;
551 } else if delta < 0.0 {
552 avg_loss -= delta;
553 }
554 i += 1;
555 }
556 }
557
558 if any_nan {
559 return Ok(());
560 }
561
562 avg_gain *= inv_rsi;
563 avg_loss *= inv_rsi;
564
565 let mut rsi = if avg_gain + avg_loss == 0.0 {
566 50.0
567 } else {
568 100.0 * avg_gain / (avg_gain + avg_loss)
569 };
570
571 *dst_fast.get_unchecked_mut(rsi_start) = rsi;
572
573 if warm <= rsi_start {
574 *dst_slow.get_unchecked_mut(rsi_start) = rsi;
575 }
576
577 let mut mean = rsi;
578 let ema_alpha = 2.0 / (smoothing_factor as f64 + 1.0);
579 let ema_beta = 1.0 - ema_alpha;
580
581 const ATR_ALPHA: f64 = 1.0 / 14.0;
582 const ATR_BETA: f64 = 1.0 - ATR_ALPHA;
583 let mut wwma = 0.0f64;
584 let mut atrrsi = 0.0f64;
585 let mut last_fast = rsi;
586
587 let mut prev_ema = rsi;
588 let mut i = rsi_start + 1;
589 while i < len {
590 let delta = *data.get_unchecked(i) - *data.get_unchecked(i - 1);
591 let gain = if delta > 0.0 { delta } else { 0.0 };
592 let loss = if delta < 0.0 { -delta } else { 0.0 };
593 avg_gain = inv_rsi * gain + beta_rsi * avg_gain;
594 avg_loss = inv_rsi * loss + beta_rsi * avg_loss;
595
596 rsi = if avg_gain + avg_loss == 0.0 {
597 50.0
598 } else {
599 100.0 * avg_gain / (avg_gain + avg_loss)
600 };
601
602 let fast_i = if i < ema_warmup_end {
603 let n = (i - rsi_start + 1) as f64;
604 mean = ((n - 1.0) * mean + rsi) / n;
605
606 prev_ema = mean;
607 mean
608 } else {
609 prev_ema = ema_beta.mul_add(prev_ema, ema_alpha * rsi);
610 prev_ema
611 };
612 *dst_fast.get_unchecked_mut(i) = fast_i;
613
614 if i == warm {
615 *dst_slow.get_unchecked_mut(i) = fast_i;
616 last_fast = fast_i;
617 } else if i > warm {
618 let tr = (fast_i - last_fast).abs();
619 wwma = ATR_ALPHA * tr + ATR_BETA * wwma;
620 atrrsi = ATR_ALPHA * wwma + ATR_BETA * atrrsi;
621
622 let qup = fast_i + atrrsi * fast_factor;
623 let qdn = fast_i - atrrsi * fast_factor;
624
625 let prev_slow = *dst_slow.get_unchecked(i - 1);
626 let prev_fast = *dst_fast.get_unchecked(i - 1);
627 let slow_i = if qup < prev_slow {
628 qup
629 } else if fast_i > prev_slow && prev_fast < prev_slow {
630 qdn
631 } else if qdn > prev_slow {
632 qdn
633 } else if fast_i < prev_slow && prev_fast > prev_slow {
634 qup
635 } else {
636 prev_slow
637 };
638 *dst_slow.get_unchecked_mut(i) = slow_i;
639 last_fast = fast_i;
640 }
641
642 i += 1;
643 }
644
645 Ok(())
646}
647
648#[derive(Debug, Clone)]
649pub struct QqeStream {
650 rsi_period: usize,
651 smoothing_factor: usize,
652 fast_factor: f64,
653
654 rsi_alpha: f64,
655 rsi_beta: f64,
656 ema_alpha: f64,
657 ema_beta: f64,
658 atr_alpha: f64,
659 atr_beta: f64,
660
661 have_prev: bool,
662 prev_price: f64,
663 deltas: usize,
664
665 sum_gain: f64,
666 sum_loss: f64,
667
668 avg_gain: f64,
669 avg_loss: f64,
670
671 rsi_count: usize,
672 running_mean: f64,
673 prev_ema: f64,
674
675 anchored: bool,
676 prev_fast: f64,
677 prev_slow: f64,
678 wwma: f64,
679 atrrsi: f64,
680}
681
682impl QqeStream {
683 #[inline]
684 pub fn try_new(params: QqeParams) -> Result<Self, QqeError> {
685 let rsi_period = params.rsi_period.unwrap_or(14);
686 let smoothing_factor = params.smoothing_factor.unwrap_or(5);
687 let fast_factor = params.fast_factor.unwrap_or(4.236);
688
689 if rsi_period == 0 || smoothing_factor == 0 {
690 return Err(QqeError::InvalidPeriod {
691 period: 0,
692 data_len: 0,
693 });
694 }
695
696 let rsi_alpha = 1.0 / rsi_period as f64;
697 let rsi_beta = 1.0 - rsi_alpha;
698 let ema_alpha = 2.0 / (smoothing_factor as f64 + 1.0);
699 let ema_beta = 1.0 - ema_alpha;
700 let atr_alpha = 1.0 / 14.0;
701 let atr_beta = 1.0 - atr_alpha;
702
703 Ok(Self {
704 rsi_period,
705 smoothing_factor,
706 fast_factor,
707
708 rsi_alpha,
709 rsi_beta,
710 ema_alpha,
711 ema_beta,
712 atr_alpha,
713 atr_beta,
714
715 have_prev: false,
716 prev_price: 0.0,
717 deltas: 0,
718
719 sum_gain: 0.0,
720 sum_loss: 0.0,
721
722 avg_gain: 0.0,
723 avg_loss: 0.0,
724
725 rsi_count: 0,
726 running_mean: 0.0,
727 prev_ema: f64::NAN,
728
729 anchored: false,
730 prev_fast: 0.0,
731 prev_slow: 0.0,
732 wwma: 0.0,
733 atrrsi: 0.0,
734 })
735 }
736
737 #[inline(always)]
738 pub fn update(&mut self, value: f64) -> Option<(f64, f64)> {
739 if !self.have_prev {
740 self.have_prev = true;
741 self.prev_price = value;
742 return None;
743 }
744
745 let delta = value - self.prev_price;
746 self.prev_price = value;
747 self.deltas += 1;
748
749 if self.deltas <= self.rsi_period {
750 if delta > 0.0 {
751 self.sum_gain += delta;
752 } else {
753 self.sum_loss -= delta;
754 }
755
756 if self.deltas < self.rsi_period {
757 return None;
758 }
759
760 self.avg_gain = self.sum_gain * self.rsi_alpha;
761 self.avg_loss = self.sum_loss * self.rsi_alpha;
762
763 let denom = self.avg_gain + self.avg_loss;
764 let rsi = if denom == 0.0 {
765 50.0
766 } else {
767 100.0 * self.avg_gain / denom
768 };
769
770 self.rsi_count = 1;
771 self.running_mean = rsi;
772 self.prev_ema = rsi;
773 self.prev_fast = rsi;
774
775 let anchor_count = self.smoothing_factor.saturating_sub(1);
776 if self.rsi_count >= anchor_count && !self.anchored {
777 self.prev_slow = rsi;
778 self.anchored = true;
779 }
780 return Some((rsi, if self.anchored { self.prev_slow } else { rsi }));
781 }
782
783 let gain = if delta > 0.0 { delta } else { 0.0 };
784 let loss = if delta < 0.0 { -delta } else { 0.0 };
785
786 self.avg_gain = self.rsi_beta.mul_add(self.avg_gain, self.rsi_alpha * gain);
787 self.avg_loss = self.rsi_beta.mul_add(self.avg_loss, self.rsi_alpha * loss);
788
789 let denom = self.avg_gain + self.avg_loss;
790 let rsi = if denom == 0.0 {
791 50.0
792 } else {
793 100.0 * self.avg_gain / denom
794 };
795
796 self.rsi_count += 1;
797
798 let fast = if self.rsi_count <= self.smoothing_factor {
799 let n = self.rsi_count as f64;
800 self.running_mean = ((n - 1.0) * self.running_mean + rsi) / n;
801 self.prev_ema = self.running_mean;
802 self.running_mean
803 } else {
804 self.prev_ema = self.ema_beta.mul_add(self.prev_ema, self.ema_alpha * rsi);
805 self.prev_ema
806 };
807
808 let anchor_count = self.smoothing_factor.saturating_sub(1);
809 if !self.anchored && self.rsi_count >= anchor_count {
810 self.prev_slow = fast;
811 self.prev_fast = fast;
812 self.anchored = true;
813 return Some((fast, fast));
814 }
815
816 if self.anchored {
817 let tr = (fast - self.prev_fast).abs();
818 self.wwma = self.atr_beta.mul_add(self.wwma, self.atr_alpha * tr);
819 self.atrrsi = self
820 .atr_beta
821 .mul_add(self.atrrsi, self.atr_alpha * self.wwma);
822
823 let qup = fast + self.atrrsi * self.fast_factor;
824 let qdn = fast - self.atrrsi * self.fast_factor;
825
826 let prev = self.prev_slow;
827 let slow = if qup < prev {
828 qup
829 } else if fast > prev && self.prev_fast < prev {
830 qdn
831 } else if qdn > prev {
832 qdn
833 } else if fast < prev && self.prev_fast > prev {
834 qup
835 } else {
836 prev
837 };
838
839 self.prev_slow = slow;
840 self.prev_fast = fast;
841 Some((fast, slow))
842 } else {
843 self.prev_fast = fast;
844 Some((fast, fast))
845 }
846 }
847}
848
849#[derive(Clone, Debug)]
850pub struct QqeBatchRange {
851 pub rsi_period: (usize, usize, usize),
852 pub smoothing_factor: (usize, usize, usize),
853 pub fast_factor: (f64, f64, f64),
854}
855
856impl Default for QqeBatchRange {
857 fn default() -> Self {
858 Self {
859 rsi_period: (14, 263, 1),
860 smoothing_factor: (5, 5, 0),
861 fast_factor: (4.236, 4.236, 0.0),
862 }
863 }
864}
865
866#[derive(Clone, Debug, Default)]
867pub struct QqeBatchBuilder {
868 range: QqeBatchRange,
869 kernel: Kernel,
870}
871
872impl QqeBatchBuilder {
873 pub fn new() -> Self {
874 Self::default()
875 }
876
877 pub fn kernel(mut self, k: Kernel) -> Self {
878 self.kernel = k;
879 self
880 }
881
882 #[inline]
883 pub fn rsi_period_range(mut self, start: usize, end: usize, step: usize) -> Self {
884 self.range.rsi_period = (start, end, step);
885 self
886 }
887
888 #[inline]
889 pub fn rsi_period_static(mut self, val: usize) -> Self {
890 self.range.rsi_period = (val, val, 0);
891 self
892 }
893
894 #[inline]
895 pub fn smoothing_factor_range(mut self, start: usize, end: usize, step: usize) -> Self {
896 self.range.smoothing_factor = (start, end, step);
897 self
898 }
899
900 #[inline]
901 pub fn smoothing_factor_static(mut self, val: usize) -> Self {
902 self.range.smoothing_factor = (val, val, 0);
903 self
904 }
905
906 #[inline]
907 pub fn fast_factor_range(mut self, start: f64, end: f64, step: f64) -> Self {
908 self.range.fast_factor = (start, end, step);
909 self
910 }
911
912 #[inline]
913 pub fn fast_factor_static(mut self, val: f64) -> Self {
914 self.range.fast_factor = (val, val, 0.0);
915 self
916 }
917
918 pub fn apply_slice(self, data: &[f64]) -> Result<QqeBatchOutput, QqeError> {
919 qqe_batch_with_kernel(data, &self.range, self.kernel)
920 }
921
922 pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<QqeBatchOutput, QqeError> {
923 QqeBatchBuilder::new().kernel(k).apply_slice(data)
924 }
925
926 pub fn apply_candles(self, c: &Candles, src: &str) -> Result<QqeBatchOutput, QqeError> {
927 let slice = source_type(c, src);
928 self.apply_slice(slice)
929 }
930
931 pub fn with_default_candles(c: &Candles) -> Result<QqeBatchOutput, QqeError> {
932 QqeBatchBuilder::new()
933 .kernel(Kernel::Auto)
934 .apply_candles(c, "close")
935 }
936}
937
938#[derive(Clone, Debug)]
939pub struct QqeBatchOutput {
940 pub fast_values: Vec<f64>,
941 pub slow_values: Vec<f64>,
942 pub combos: Vec<QqeParams>,
943 pub rows: usize,
944 pub cols: usize,
945}
946
947impl QqeBatchOutput {
948 pub fn row_for_params(&self, p: &QqeParams) -> Option<usize> {
949 self.combos.iter().position(|c| {
950 c.rsi_period.unwrap_or(14) == p.rsi_period.unwrap_or(14)
951 && c.smoothing_factor.unwrap_or(5) == p.smoothing_factor.unwrap_or(5)
952 && (c.fast_factor.unwrap_or(4.236) - p.fast_factor.unwrap_or(4.236)).abs() < 1e-12
953 })
954 }
955
956 pub fn values_for(&self, p: &QqeParams) -> Option<(&[f64], &[f64])> {
957 self.row_for_params(p).map(|row| {
958 let start = row * self.cols;
959 let end = start + self.cols;
960 (&self.fast_values[start..end], &self.slow_values[start..end])
961 })
962 }
963}
964
965fn expand_grid(r: &QqeBatchRange) -> Vec<QqeParams> {
966 fn axis_usize((s, e, st): (usize, usize, usize)) -> Vec<usize> {
967 if st == 0 || s == e {
968 return vec![s];
969 }
970 if s < e {
971 return (s..=e).step_by(st.max(1)).collect();
972 }
973 let mut v = Vec::new();
974 let step = st.max(1);
975 let mut cur = s;
976 while cur >= e {
977 v.push(cur);
978 if cur < step {
979 break;
980 }
981 cur -= step;
982 if cur == usize::MAX {
983 break;
984 }
985 }
986 v
987 }
988
989 fn axis_f64((s, e, st): (f64, f64, f64)) -> Vec<f64> {
990 let step = if st.is_sign_negative() { -st } else { st };
991 if step.abs() < 1e-12 || (s - e).abs() < 1e-12 {
992 return vec![s];
993 }
994 let mut v = Vec::new();
995 if s <= e {
996 let mut x = s;
997 while x <= e + 1e-12 {
998 v.push(x);
999 x += step;
1000 }
1001 } else {
1002 let mut x = s;
1003 while x + 1e-12 >= e {
1004 v.push(x);
1005 x -= step;
1006 }
1007 }
1008 v
1009 }
1010
1011 let rs = axis_usize(r.rsi_period);
1012 let sm = axis_usize(r.smoothing_factor);
1013 let ff = axis_f64(r.fast_factor);
1014 let cap = rs
1015 .len()
1016 .checked_mul(sm.len())
1017 .and_then(|x| x.checked_mul(ff.len()))
1018 .unwrap_or(0);
1019 let mut out = Vec::with_capacity(cap);
1020
1021 for &rp in &rs {
1022 for &sp in &sm {
1023 for &fk in &ff {
1024 out.push(QqeParams {
1025 rsi_period: Some(rp),
1026 smoothing_factor: Some(sp),
1027 fast_factor: Some(fk),
1028 });
1029 }
1030 }
1031 }
1032 out
1033}
1034
1035pub fn qqe_batch_with_kernel(
1036 data: &[f64],
1037 sweep: &QqeBatchRange,
1038 k: Kernel,
1039) -> Result<QqeBatchOutput, QqeError> {
1040 use crate::indicators::moving_averages::ema::ema_into_slice;
1041 use crate::indicators::rsi::rsi_into_slice;
1042
1043 let combos = expand_grid(sweep);
1044 if combos.is_empty() {
1045 return Err(QqeError::InvalidRange {
1046 start: sweep.rsi_period.0,
1047 end: sweep.rsi_period.1,
1048 step: sweep.rsi_period.2,
1049 });
1050 }
1051 let cols = data.len();
1052 if cols == 0 {
1053 return Err(QqeError::EmptyInputData);
1054 }
1055
1056 let first = data
1057 .iter()
1058 .position(|x| !x.is_nan())
1059 .ok_or(QqeError::AllValuesNaN)?;
1060 let worst_needed = combos
1061 .iter()
1062 .map(|c| c.rsi_period.unwrap() + c.smoothing_factor.unwrap())
1063 .max()
1064 .unwrap();
1065 if cols - first < worst_needed {
1066 return Err(QqeError::NotEnoughValidData {
1067 needed: worst_needed,
1068 valid: cols - first,
1069 });
1070 }
1071
1072 let actual = match k {
1073 Kernel::Auto => detect_best_batch_kernel(),
1074 other if other.is_batch() => other,
1075 _ => return Err(QqeError::InvalidKernelForBatch(k)),
1076 };
1077 let simd = match actual {
1078 Kernel::Avx512Batch => Kernel::Avx512,
1079 Kernel::Avx2Batch => Kernel::Avx2,
1080 Kernel::ScalarBatch => Kernel::Scalar,
1081 _ => unreachable!(),
1082 };
1083
1084 let rows = combos.len();
1085 let total = rows.checked_mul(cols).ok_or(QqeError::InvalidRange {
1086 start: sweep.rsi_period.0,
1087 end: sweep.rsi_period.1,
1088 step: sweep.rsi_period.2,
1089 })?;
1090 let mut fast_mu = make_uninit_matrix(rows, cols);
1091 let mut slow_mu = make_uninit_matrix(rows, cols);
1092
1093 let warm: Vec<usize> = combos
1094 .iter()
1095 .map(|c| first + c.rsi_period.unwrap() + c.smoothing_factor.unwrap() - 2)
1096 .collect();
1097
1098 init_matrix_prefixes(&mut fast_mu, cols, &warm);
1099 init_matrix_prefixes(&mut slow_mu, cols, &warm);
1100
1101 let fast_out: &mut [f64] =
1102 unsafe { core::slice::from_raw_parts_mut(fast_mu.as_mut_ptr() as *mut f64, total) };
1103 let slow_out: &mut [f64] =
1104 unsafe { core::slice::from_raw_parts_mut(slow_mu.as_mut_ptr() as *mut f64, total) };
1105
1106 let mut tmp_mu = make_uninit_matrix(1, cols);
1107 let tmp: &mut [f64] =
1108 unsafe { core::slice::from_raw_parts_mut(tmp_mu.as_mut_ptr() as *mut f64, cols) };
1109
1110 for (row, combo) in combos.iter().enumerate() {
1111 let rsi_p = combo.rsi_period.unwrap();
1112 let ema_p = combo.smoothing_factor.unwrap();
1113 let fast_k = combo.fast_factor.unwrap();
1114 let start = warm[row];
1115
1116 let dst_fast = &mut fast_out[row * cols..(row + 1) * cols];
1117 let dst_slow = &mut slow_out[row * cols..(row + 1) * cols];
1118
1119 let rsi_in = RsiInput::from_slice(
1120 data,
1121 RsiParams {
1122 period: Some(rsi_p),
1123 },
1124 );
1125 rsi_into_slice(tmp, &rsi_in, simd).map_err(|e| QqeError::DependentIndicatorError {
1126 message: e.to_string(),
1127 })?;
1128
1129 let ema_in = EmaInput::from_slice(
1130 tmp,
1131 EmaParams {
1132 period: Some(ema_p),
1133 },
1134 );
1135 ema_into_slice(dst_fast, &ema_in, simd).map_err(|e| QqeError::DependentIndicatorError {
1136 message: e.to_string(),
1137 })?;
1138
1139 qqe_compute_slow_from(dst_fast, fast_k, start, dst_slow);
1140 }
1141
1142 let fast_values =
1143 unsafe { Vec::from_raw_parts(fast_mu.as_mut_ptr() as *mut f64, total, total) };
1144 let slow_values =
1145 unsafe { Vec::from_raw_parts(slow_mu.as_mut_ptr() as *mut f64, total, total) };
1146 core::mem::forget(fast_mu);
1147 core::mem::forget(slow_mu);
1148
1149 Ok(QqeBatchOutput {
1150 fast_values,
1151 slow_values,
1152 combos,
1153 rows,
1154 cols,
1155 })
1156}
1157
1158fn qqe_batch_inner(
1159 data: &[f64],
1160 sweep: &QqeBatchRange,
1161 kern: Kernel,
1162 parallel: bool,
1163) -> Result<QqeBatchOutput, QqeError> {
1164 let combos = expand_grid(sweep);
1165 if combos.is_empty() {
1166 return Err(QqeError::InvalidRange {
1167 start: sweep.rsi_period.0,
1168 end: sweep.rsi_period.1,
1169 step: sweep.rsi_period.2,
1170 });
1171 }
1172 let cols = data.len();
1173 if cols == 0 {
1174 return Err(QqeError::EmptyInputData);
1175 }
1176 let first = data
1177 .iter()
1178 .position(|x| !x.is_nan())
1179 .ok_or(QqeError::AllValuesNaN)?;
1180 let worst_needed = combos
1181 .iter()
1182 .map(|c| c.rsi_period.unwrap() + c.smoothing_factor.unwrap())
1183 .max()
1184 .unwrap();
1185 if cols - first < worst_needed {
1186 return Err(QqeError::NotEnoughValidData {
1187 needed: worst_needed,
1188 valid: cols - first,
1189 });
1190 }
1191
1192 let rows = combos.len();
1193 let total = rows.checked_mul(cols).ok_or(QqeError::InvalidRange {
1194 start: sweep.rsi_period.0,
1195 end: sweep.rsi_period.1,
1196 step: sweep.rsi_period.2,
1197 })?;
1198 let mut fast_mu = make_uninit_matrix(rows, cols);
1199 let mut slow_mu = make_uninit_matrix(rows, cols);
1200
1201 let warm: Vec<usize> = combos
1202 .iter()
1203 .map(|c| first + c.rsi_period.unwrap() + c.smoothing_factor.unwrap() - 2)
1204 .collect();
1205 init_matrix_prefixes(&mut fast_mu, cols, &warm);
1206 init_matrix_prefixes(&mut slow_mu, cols, &warm);
1207
1208 let actual = match kern {
1209 Kernel::Auto => detect_best_batch_kernel(),
1210 other if other.is_batch() => other,
1211 _ => return Err(QqeError::InvalidKernelForBatch(kern)),
1212 };
1213 let simd = match actual {
1214 Kernel::Avx512Batch => Kernel::Avx512,
1215 Kernel::Avx2Batch => Kernel::Avx2,
1216 Kernel::ScalarBatch => Kernel::Scalar,
1217 _ => unreachable!(),
1218 };
1219
1220 let do_row = |row: usize, f_mu: &mut [MaybeUninit<f64>], s_mu: &mut [MaybeUninit<f64>]| {
1221 use crate::indicators::moving_averages::ema::ema_into_slice;
1222 use crate::indicators::rsi::rsi_into_slice;
1223
1224 let mut tmp_mu = make_uninit_matrix(1, cols);
1225 let tmp: &mut [f64] =
1226 unsafe { core::slice::from_raw_parts_mut(tmp_mu.as_mut_ptr() as *mut f64, cols) };
1227
1228 let rsi_p = combos[row].rsi_period.unwrap();
1229 let ema_p = combos[row].smoothing_factor.unwrap();
1230 let fast_k = combos[row].fast_factor.unwrap();
1231 let start = warm[row];
1232
1233 let dst_fast =
1234 unsafe { core::slice::from_raw_parts_mut(f_mu.as_mut_ptr() as *mut f64, cols) };
1235 let dst_slow =
1236 unsafe { core::slice::from_raw_parts_mut(s_mu.as_mut_ptr() as *mut f64, cols) };
1237
1238 let rsi_in = RsiInput::from_slice(
1239 data,
1240 RsiParams {
1241 period: Some(rsi_p),
1242 },
1243 );
1244 rsi_into_slice(tmp, &rsi_in, simd).map_err(|e| QqeError::DependentIndicatorError {
1245 message: e.to_string(),
1246 })?;
1247
1248 let ema_in = EmaInput::from_slice(
1249 tmp,
1250 EmaParams {
1251 period: Some(ema_p),
1252 },
1253 );
1254 ema_into_slice(dst_fast, &ema_in, simd).map_err(|e| QqeError::DependentIndicatorError {
1255 message: e.to_string(),
1256 })?;
1257
1258 qqe_compute_slow_from(dst_fast, fast_k, start, dst_slow);
1259
1260 Ok::<(), QqeError>(())
1261 };
1262
1263 if parallel {
1264 #[cfg(not(target_arch = "wasm32"))]
1265 {
1266 use rayon::prelude::*;
1267 fast_mu
1268 .par_chunks_mut(cols)
1269 .zip(slow_mu.par_chunks_mut(cols))
1270 .enumerate()
1271 .try_for_each(|(row, (f_mu, s_mu))| do_row(row, f_mu, s_mu))?;
1272 }
1273 #[cfg(target_arch = "wasm32")]
1274 {
1275 for (row, (f_mu, s_mu)) in fast_mu
1276 .chunks_mut(cols)
1277 .zip(slow_mu.chunks_mut(cols))
1278 .enumerate()
1279 {
1280 do_row(row, f_mu, s_mu)?;
1281 }
1282 }
1283 } else {
1284 for (row, (f_mu, s_mu)) in fast_mu
1285 .chunks_mut(cols)
1286 .zip(slow_mu.chunks_mut(cols))
1287 .enumerate()
1288 {
1289 do_row(row, f_mu, s_mu)?;
1290 }
1291 }
1292
1293 let fast_values =
1294 unsafe { Vec::from_raw_parts(fast_mu.as_mut_ptr() as *mut f64, total, total) };
1295 let slow_values =
1296 unsafe { Vec::from_raw_parts(slow_mu.as_mut_ptr() as *mut f64, total, total) };
1297 core::mem::forget(fast_mu);
1298 core::mem::forget(slow_mu);
1299
1300 Ok(QqeBatchOutput {
1301 fast_values,
1302 slow_values,
1303 combos,
1304 rows,
1305 cols,
1306 })
1307}
1308
1309#[inline(always)]
1310pub fn qqe_batch_slice(
1311 data: &[f64],
1312 sweep: &QqeBatchRange,
1313 kern: Kernel,
1314) -> Result<QqeBatchOutput, QqeError> {
1315 qqe_batch_inner(data, sweep, kern, false)
1316}
1317
1318#[inline(always)]
1319pub fn qqe_batch_par_slice(
1320 data: &[f64],
1321 sweep: &QqeBatchRange,
1322 kern: Kernel,
1323) -> Result<QqeBatchOutput, QqeError> {
1324 qqe_batch_inner(data, sweep, kern, true)
1325}
1326
1327#[cfg(feature = "python")]
1328#[pyfunction(name = "qqe")]
1329#[pyo3(signature = (data, rsi_period=14, smoothing_factor=5, fast_factor=4.236, kernel=None))]
1330pub fn qqe_py<'py>(
1331 py: Python<'py>,
1332 data: PyReadonlyArray1<'py, f64>,
1333 rsi_period: usize,
1334 smoothing_factor: usize,
1335 fast_factor: f64,
1336 kernel: Option<&str>,
1337) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
1338 let slice_in = data.as_slice()?;
1339 let kern = validate_kernel(kernel, false)?;
1340 let params = QqeParams {
1341 rsi_period: Some(rsi_period),
1342 smoothing_factor: Some(smoothing_factor),
1343 fast_factor: Some(fast_factor),
1344 };
1345 let input = QqeInput::from_slice(slice_in, params);
1346
1347 let result = py
1348 .allow_threads(|| qqe_with_kernel(&input, kern))
1349 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1350
1351 Ok((result.fast.into_pyarray(py), result.slow.into_pyarray(py)))
1352}
1353
1354#[cfg(feature = "python")]
1355#[pyclass(name = "QqeStream")]
1356pub struct QqeStreamPy {
1357 stream: QqeStream,
1358}
1359
1360#[cfg(feature = "python")]
1361#[pymethods]
1362impl QqeStreamPy {
1363 #[new]
1364 fn new(rsi_period: usize, smoothing_factor: usize, fast_factor: f64) -> PyResult<Self> {
1365 let params = QqeParams {
1366 rsi_period: Some(rsi_period),
1367 smoothing_factor: Some(smoothing_factor),
1368 fast_factor: Some(fast_factor),
1369 };
1370 let stream =
1371 QqeStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
1372 Ok(QqeStreamPy { stream })
1373 }
1374
1375 fn update(&mut self, value: f64) -> Option<(f64, f64)> {
1376 self.stream.update(value)
1377 }
1378}
1379
1380#[cfg(feature = "python")]
1381#[pyfunction(name = "qqe_batch")]
1382#[pyo3(signature = (data, rsi_period_range, smoothing_factor_range, fast_factor_range, kernel=None))]
1383pub fn qqe_batch_py<'py>(
1384 py: Python<'py>,
1385 data: PyReadonlyArray1<'py, f64>,
1386 rsi_period_range: (usize, usize, usize),
1387 smoothing_factor_range: (usize, usize, usize),
1388 fast_factor_range: (f64, f64, f64),
1389 kernel: Option<&str>,
1390) -> PyResult<Bound<'py, PyDict>> {
1391 use numpy::{IntoPyArray, PyArray2, PyArrayMethods};
1392 let slice_in = data.as_slice()?;
1393 let sweep = QqeBatchRange {
1394 rsi_period: rsi_period_range,
1395 smoothing_factor: smoothing_factor_range,
1396 fast_factor: fast_factor_range,
1397 };
1398 let kern = validate_kernel(kernel, true)?;
1399
1400 let combos = expand_grid(&sweep);
1401 if combos.is_empty() {
1402 return Err(PyValueError::new_err("Empty parameter combination"));
1403 }
1404 let rows = combos.len();
1405 let cols = slice_in.len();
1406
1407 let fast_arr = unsafe { PyArray2::<f64>::new(py, [rows, cols], false) };
1408 let slow_arr = unsafe { PyArray2::<f64>::new(py, [rows, cols], false) };
1409 let fast_slice = unsafe { fast_arr.as_slice_mut()? };
1410 let slow_slice = unsafe { slow_arr.as_slice_mut()? };
1411
1412 let first = slice_in.iter().position(|x| !x.is_nan()).unwrap_or(0);
1413 let warm: Vec<usize> = combos
1414 .iter()
1415 .map(|c| first + c.rsi_period.unwrap() + c.smoothing_factor.unwrap() - 2)
1416 .collect();
1417
1418 let mut tmp_mu = make_uninit_matrix(1, cols);
1419 let tmp: &mut [f64] =
1420 unsafe { core::slice::from_raw_parts_mut(tmp_mu.as_mut_ptr() as *mut f64, cols) };
1421
1422 use crate::indicators::moving_averages::ema::ema_into_slice;
1423 use crate::indicators::rsi::rsi_into_slice;
1424
1425 let simd = match kern {
1426 Kernel::Avx512Batch => Kernel::Avx512,
1427 Kernel::Avx2Batch => Kernel::Avx2,
1428 Kernel::ScalarBatch => Kernel::Scalar,
1429 _ => Kernel::Scalar,
1430 };
1431
1432 py.allow_threads(|| -> PyResult<()> {
1433 for (row, combo) in combos.iter().enumerate() {
1434 let rsi_p = combo.rsi_period.unwrap();
1435 let ema_p = combo.smoothing_factor.unwrap();
1436 let fast_k = combo.fast_factor.unwrap();
1437 let start = warm[row];
1438
1439 let dst_fast = &mut fast_slice[row * cols..(row + 1) * cols];
1440 let dst_slow = &mut slow_slice[row * cols..(row + 1) * cols];
1441
1442 rsi_into_slice(
1443 tmp,
1444 &RsiInput::from_slice(
1445 slice_in,
1446 RsiParams {
1447 period: Some(rsi_p),
1448 },
1449 ),
1450 simd,
1451 )
1452 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1453
1454 ema_into_slice(
1455 dst_fast,
1456 &EmaInput::from_slice(
1457 tmp,
1458 EmaParams {
1459 period: Some(ema_p),
1460 },
1461 ),
1462 simd,
1463 )
1464 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1465
1466 for v in &mut dst_fast[..start] {
1467 *v = f64::NAN;
1468 }
1469 for v in &mut dst_slow[..start] {
1470 *v = f64::NAN;
1471 }
1472
1473 qqe_compute_slow_from(dst_fast, fast_k, start, dst_slow);
1474 }
1475 Ok(())
1476 })?;
1477
1478 let dict = PyDict::new(py);
1479 dict.set_item("fast", fast_arr)?;
1480 dict.set_item("slow", slow_arr)?;
1481 dict.set_item(
1482 "rsi_periods",
1483 combos
1484 .iter()
1485 .map(|c| c.rsi_period.unwrap() as u64)
1486 .collect::<Vec<_>>()
1487 .into_pyarray(py),
1488 )?;
1489 dict.set_item(
1490 "smoothing_factors",
1491 combos
1492 .iter()
1493 .map(|c| c.smoothing_factor.unwrap() as u64)
1494 .collect::<Vec<_>>()
1495 .into_pyarray(py),
1496 )?;
1497 dict.set_item(
1498 "fast_factors",
1499 combos
1500 .iter()
1501 .map(|c| c.fast_factor.unwrap())
1502 .collect::<Vec<_>>()
1503 .into_pyarray(py),
1504 )?;
1505 Ok(dict)
1506}
1507
1508#[cfg(all(feature = "python", feature = "cuda"))]
1509#[pyfunction(name = "qqe_cuda_batch_dev")]
1510#[pyo3(signature = (data_f32, rsi_period_range, smoothing_factor_range, fast_factor_range, device_id=0))]
1511pub fn qqe_cuda_batch_dev_py<'py>(
1512 py: Python<'py>,
1513 data_f32: numpy::PyReadonlyArray1<'py, f32>,
1514 rsi_period_range: (usize, usize, usize),
1515 smoothing_factor_range: (usize, usize, usize),
1516 fast_factor_range: (f64, f64, f64),
1517 device_id: usize,
1518) -> PyResult<(DeviceArrayF32Py, Bound<'py, pyo3::types::PyDict>)> {
1519 use numpy::IntoPyArray;
1520 if !cuda_available() {
1521 return Err(PyValueError::new_err("CUDA not available"));
1522 }
1523 let slice = data_f32.as_slice()?;
1524 let sweep = QqeBatchRange {
1525 rsi_period: rsi_period_range,
1526 smoothing_factor: smoothing_factor_range,
1527 fast_factor: fast_factor_range,
1528 };
1529 let (inner, combos) = py.allow_threads(|| {
1530 let cuda = CudaQqe::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1531 cuda.qqe_batch_dev(slice, &sweep)
1532 .map_err(|e| PyValueError::new_err(e.to_string()))
1533 })?;
1534 let handle = make_device_array_py(device_id, inner)?;
1535 let dict = pyo3::types::PyDict::new(py);
1536 dict.set_item(
1537 "rsi_periods",
1538 combos
1539 .iter()
1540 .map(|c| c.rsi_period.unwrap() as u64)
1541 .collect::<Vec<_>>()
1542 .into_pyarray(py),
1543 )?;
1544 dict.set_item(
1545 "smoothing_factors",
1546 combos
1547 .iter()
1548 .map(|c| c.smoothing_factor.unwrap() as u64)
1549 .collect::<Vec<_>>()
1550 .into_pyarray(py),
1551 )?;
1552 dict.set_item(
1553 "fast_factors",
1554 combos
1555 .iter()
1556 .map(|c| c.fast_factor.unwrap() as f64)
1557 .collect::<Vec<_>>()
1558 .into_pyarray(py),
1559 )?;
1560 dict.set_item("rows", 2 * combos.len())?;
1561 dict.set_item("cols", slice.len())?;
1562 Ok((handle, dict))
1563}
1564
1565#[cfg(all(feature = "python", feature = "cuda"))]
1566#[pyfunction(name = "qqe_cuda_many_series_one_param_dev")]
1567#[pyo3(signature = (data_tm_f32, rsi_period, smoothing_factor, fast_factor, device_id=0))]
1568pub fn qqe_cuda_many_series_one_param_dev_py<'py>(
1569 py: Python<'py>,
1570 data_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
1571 rsi_period: usize,
1572 smoothing_factor: usize,
1573 fast_factor: f64,
1574 device_id: usize,
1575) -> PyResult<DeviceArrayF32Py> {
1576 use numpy::PyUntypedArrayMethods;
1577 if !cuda_available() {
1578 return Err(PyValueError::new_err("CUDA not available"));
1579 }
1580 let shape = data_tm_f32.shape();
1581 if shape.len() != 2 {
1582 return Err(PyValueError::new_err("expected 2D array (rows x cols)"));
1583 }
1584 let rows = shape[0];
1585 let cols = shape[1];
1586 let flat = data_tm_f32.as_slice()?;
1587 let params = QqeParams {
1588 rsi_period: Some(rsi_period),
1589 smoothing_factor: Some(smoothing_factor),
1590 fast_factor: Some(fast_factor),
1591 };
1592 let inner = py.allow_threads(|| {
1593 let cuda = CudaQqe::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1594 cuda.qqe_many_series_one_param_time_major_dev(flat, cols, rows, ¶ms)
1595 .map_err(|e| PyValueError::new_err(e.to_string()))
1596 })?;
1597 let handle = make_device_array_py(device_id, inner)?;
1598 Ok(handle)
1599}
1600
1601#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1602#[derive(Serialize, Deserialize)]
1603pub struct QqeJsResult {
1604 pub values: Vec<f64>,
1605 pub rows: usize,
1606 pub cols: usize,
1607}
1608
1609#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1610#[wasm_bindgen]
1611pub fn qqe_js(
1612 data: &[f64],
1613 rsi_period: usize,
1614 smoothing_factor: usize,
1615 fast_factor: f64,
1616) -> Result<JsValue, JsValue> {
1617 let params = QqeParams {
1618 rsi_period: Some(rsi_period),
1619 smoothing_factor: Some(smoothing_factor),
1620 fast_factor: Some(fast_factor),
1621 };
1622 let input = QqeInput::from_slice(data, params);
1623
1624 let mut values = vec![f64::NAN; data.len() * 2];
1625
1626 let (fast_slice, slow_slice) = values.split_at_mut(data.len());
1627
1628 qqe_into_slices(fast_slice, slow_slice, &input, detect_best_kernel())
1629 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1630
1631 let result = QqeJsResult {
1632 values,
1633 rows: 2,
1634 cols: data.len(),
1635 };
1636
1637 serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
1638}
1639
1640#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1641#[wasm_bindgen]
1642pub fn qqe_unified_js(
1643 data: &[f64],
1644 rsi_period: usize,
1645 smoothing_factor: usize,
1646 fast_factor: f64,
1647) -> Result<Vec<f64>, JsValue> {
1648 let params = QqeParams {
1649 rsi_period: Some(rsi_period),
1650 smoothing_factor: Some(smoothing_factor),
1651 fast_factor: Some(fast_factor),
1652 };
1653 let input = QqeInput::from_slice(data, params);
1654
1655 let mut result = vec![f64::NAN; data.len() * 2];
1656
1657 let (fast_slice, slow_slice) = result.split_at_mut(data.len());
1658
1659 qqe_into_slices(fast_slice, slow_slice, &input, detect_best_kernel())
1660 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1661
1662 Ok(result)
1663}
1664
1665#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1666#[wasm_bindgen]
1667pub fn qqe_alloc(len: usize) -> *mut f64 {
1668 let mut vec = Vec::<f64>::with_capacity(len * 2);
1669 let ptr = vec.as_mut_ptr();
1670 std::mem::forget(vec);
1671 ptr
1672}
1673
1674#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1675#[wasm_bindgen]
1676pub fn qqe_free(ptr: *mut f64, len: usize) {
1677 unsafe {
1678 let _ = Vec::from_raw_parts(ptr, len * 2, len * 2);
1679 }
1680}
1681
1682#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1683#[wasm_bindgen]
1684pub fn qqe_into(
1685 in_ptr: *const f64,
1686 out_ptr: *mut f64,
1687 len: usize,
1688 rsi_period: usize,
1689 smoothing_factor: usize,
1690 fast_factor: f64,
1691) -> Result<(), JsValue> {
1692 if in_ptr.is_null() || out_ptr.is_null() {
1693 return Err(JsValue::from_str("null pointer passed to qqe_into"));
1694 }
1695 unsafe {
1696 let data = std::slice::from_raw_parts(in_ptr, len);
1697 let params = QqeParams {
1698 rsi_period: Some(rsi_period),
1699 smoothing_factor: Some(smoothing_factor),
1700 fast_factor: Some(fast_factor),
1701 };
1702 let input = QqeInput::from_slice(data, params);
1703
1704 if in_ptr == out_ptr {
1705 let mut tmp = vec![f64::NAN; len * 2];
1706 let (tmp_fast, tmp_slow) = tmp.split_at_mut(len);
1707 qqe_into_slices(tmp_fast, tmp_slow, &input, detect_best_kernel())
1708 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1709 let dst = std::slice::from_raw_parts_mut(out_ptr, len * 2);
1710 dst.copy_from_slice(&tmp);
1711 } else {
1712 let dst = std::slice::from_raw_parts_mut(out_ptr, len * 2);
1713 let (dst_fast, dst_slow) = dst.split_at_mut(len);
1714 qqe_into_slices(dst_fast, dst_slow, &input, detect_best_kernel())
1715 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1716 }
1717 Ok(())
1718 }
1719}
1720
1721#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1722#[derive(Serialize, Deserialize)]
1723pub struct QqeBatchConfig {
1724 pub rsi_period_range: (usize, usize, usize),
1725 pub smoothing_factor_range: (usize, usize, usize),
1726 pub fast_factor_range: (f64, f64, f64),
1727}
1728
1729#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1730#[derive(Serialize)]
1731pub struct QqeBatchJsOutput {
1732 pub fast_values: Vec<f64>,
1733 pub slow_values: Vec<f64>,
1734 pub combos: Vec<QqeParams>,
1735 pub rows: usize,
1736 pub cols: usize,
1737}
1738
1739#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1740#[wasm_bindgen(js_name = qqe_batch)]
1741pub fn qqe_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1742 let config: QqeBatchConfig = serde_wasm_bindgen::from_value(config)
1743 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
1744
1745 let sweep = QqeBatchRange {
1746 rsi_period: config.rsi_period_range,
1747 smoothing_factor: config.smoothing_factor_range,
1748 fast_factor: config.fast_factor_range,
1749 };
1750
1751 let kernel = detect_best_batch_kernel();
1752 let result = qqe_batch_with_kernel(data, &sweep, kernel)
1753 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1754
1755 let output = QqeBatchJsOutput {
1756 fast_values: result.fast_values,
1757 slow_values: result.slow_values,
1758 combos: result.combos,
1759 rows: result.rows,
1760 cols: result.cols,
1761 };
1762
1763 serde_wasm_bindgen::to_value(&output).map_err(|e| JsValue::from_str(&e.to_string()))
1764}
1765
1766#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1767#[wasm_bindgen]
1768pub fn qqe_batch_into(
1769 in_ptr: *const f64,
1770 out_ptr: *mut f64,
1771 len: usize,
1772 rsi_period_start: usize,
1773 rsi_period_end: usize,
1774 rsi_period_step: usize,
1775 smoothing_start: usize,
1776 smoothing_end: usize,
1777 smoothing_step: usize,
1778 fast_factor_start: f64,
1779 fast_factor_end: f64,
1780 fast_factor_step: f64,
1781) -> Result<usize, JsValue> {
1782 if in_ptr.is_null() || out_ptr.is_null() {
1783 return Err(JsValue::from_str("null pointer passed to qqe_batch_into"));
1784 }
1785 unsafe {
1786 let data = core::slice::from_raw_parts(in_ptr, len);
1787 let sweep = QqeBatchRange {
1788 rsi_period: (rsi_period_start, rsi_period_end, rsi_period_step),
1789 smoothing_factor: (smoothing_start, smoothing_end, smoothing_step),
1790 fast_factor: (fast_factor_start, fast_factor_end, fast_factor_step),
1791 };
1792 let combos = expand_grid(&sweep);
1793 let rows = combos.len();
1794 if rows == 0 {
1795 return Err(JsValue::from_str("Empty parameter combination"));
1796 }
1797
1798 let total = rows * len * 2;
1799 let dst = core::slice::from_raw_parts_mut(out_ptr, total);
1800 let (dst_fast_all, dst_slow_all) = dst.split_at_mut(rows * len);
1801
1802 let mut tmp_mu = make_uninit_matrix(1, len);
1803 let tmp: &mut [f64] = core::slice::from_raw_parts_mut(tmp_mu.as_mut_ptr() as *mut f64, len);
1804
1805 let simd = match detect_best_batch_kernel() {
1806 Kernel::Avx512Batch => Kernel::Avx512,
1807 Kernel::Avx2Batch => Kernel::Avx2,
1808 Kernel::ScalarBatch => Kernel::Scalar,
1809 _ => Kernel::Scalar,
1810 };
1811
1812 use crate::indicators::moving_averages::ema::ema_into_slice;
1813 use crate::indicators::rsi::rsi_into_slice;
1814
1815 let first = data.iter().position(|x| !x.is_nan()).unwrap_or(0);
1816
1817 for (row, combo) in combos.iter().enumerate() {
1818 let rsi_p = combo.rsi_period.unwrap();
1819 let ema_p = combo.smoothing_factor.unwrap();
1820 let fast_k = combo.fast_factor.unwrap();
1821
1822 let start = first + rsi_p + ema_p - 2;
1823
1824 let dst_fast = &mut dst_fast_all[row * len..(row + 1) * len];
1825 let dst_slow = &mut dst_slow_all[row * len..(row + 1) * len];
1826
1827 rsi_into_slice(
1828 tmp,
1829 &RsiInput::from_slice(
1830 data,
1831 RsiParams {
1832 period: Some(rsi_p),
1833 },
1834 ),
1835 simd,
1836 )
1837 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1838
1839 ema_into_slice(
1840 dst_fast,
1841 &EmaInput::from_slice(
1842 tmp,
1843 EmaParams {
1844 period: Some(ema_p),
1845 },
1846 ),
1847 simd,
1848 )
1849 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1850
1851 for v in &mut dst_fast[..start] {
1852 *v = f64::NAN;
1853 }
1854 for v in &mut dst_slow[..start] {
1855 *v = f64::NAN;
1856 }
1857
1858 qqe_compute_slow_from(dst_fast, fast_k, start, dst_slow);
1859 }
1860 Ok(rows)
1861 }
1862}
1863
1864#[cfg(test)]
1865mod tests {
1866 use super::*;
1867 use crate::skip_if_unsupported;
1868 use crate::utilities::data_loader::read_candles_from_csv;
1869 use paste::paste;
1870 #[cfg(feature = "proptest")]
1871 use proptest::prelude::*;
1872 use std::error::Error;
1873
1874 fn check_qqe_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1875 skip_if_unsupported!(kernel, test_name);
1876 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1877 let candles = read_candles_from_csv(file_path)?;
1878
1879 let input = QqeInput::from_candles(&candles, "close", QqeParams::default());
1880 let result = qqe_with_kernel(&input, kernel)?;
1881
1882 let expected_fast = [
1883 42.68548144,
1884 42.68200826,
1885 42.32797706,
1886 42.50623375,
1887 41.34014948,
1888 ];
1889
1890 let expected_slow = [
1891 36.49339135,
1892 36.59103557,
1893 36.59103557,
1894 36.64790896,
1895 36.64790896,
1896 ];
1897
1898 let start = result.fast.len().saturating_sub(5);
1899
1900 for (i, (&fast_val, &slow_val)) in result.fast[start..]
1901 .iter()
1902 .zip(result.slow[start..].iter())
1903 .enumerate()
1904 {
1905 let fast_diff = (fast_val - expected_fast[i]).abs();
1906 let slow_diff = (slow_val - expected_slow[i]).abs();
1907
1908 assert!(
1909 fast_diff < 1e-6,
1910 "[{}] QQE fast {:?} mismatch at idx {}: got {}, expected {}",
1911 test_name,
1912 kernel,
1913 i,
1914 fast_val,
1915 expected_fast[i]
1916 );
1917
1918 assert!(
1919 slow_diff < 1e-6,
1920 "[{}] QQE slow {:?} mismatch at idx {}: got {}, expected {}",
1921 test_name,
1922 kernel,
1923 i,
1924 slow_val,
1925 expected_slow[i]
1926 );
1927 }
1928 Ok(())
1929 }
1930
1931 fn check_qqe_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1932 skip_if_unsupported!(kernel, test_name);
1933 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1934 let candles = read_candles_from_csv(file_path)?;
1935
1936 let default_params = QqeParams {
1937 rsi_period: None,
1938 smoothing_factor: None,
1939 fast_factor: None,
1940 };
1941 let input = QqeInput::from_candles(&candles, "close", default_params);
1942 let output = qqe_with_kernel(&input, kernel)?;
1943 assert_eq!(output.fast.len(), candles.close.len());
1944 assert_eq!(output.slow.len(), candles.close.len());
1945
1946 Ok(())
1947 }
1948
1949 fn check_qqe_default_candles(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1950 skip_if_unsupported!(kernel, test_name);
1951 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1952 let candles = read_candles_from_csv(file_path)?;
1953
1954 let input = QqeInput::with_default_candles(&candles);
1955 match input.data {
1956 QqeData::Candles { source, .. } => assert_eq!(source, "close"),
1957 _ => panic!("[{}] Expected QqeData::Candles", test_name),
1958 }
1959 let output = qqe_with_kernel(&input, kernel)?;
1960 assert_eq!(output.fast.len(), candles.close.len());
1961 assert_eq!(output.slow.len(), candles.close.len());
1962
1963 Ok(())
1964 }
1965
1966 fn check_qqe_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1967 skip_if_unsupported!(kernel, test_name);
1968 let input_data = [10.0, 20.0, 30.0];
1969 let params = QqeParams {
1970 rsi_period: Some(0),
1971 smoothing_factor: None,
1972 fast_factor: None,
1973 };
1974 let input = QqeInput::from_slice(&input_data, params);
1975 let res = qqe_with_kernel(&input, kernel);
1976 assert!(
1977 res.is_err(),
1978 "[{}] QQE should fail with zero period",
1979 test_name
1980 );
1981 Ok(())
1982 }
1983
1984 fn check_qqe_period_exceeds_length(
1985 test_name: &str,
1986 kernel: Kernel,
1987 ) -> Result<(), Box<dyn Error>> {
1988 skip_if_unsupported!(kernel, test_name);
1989 let data_small = [10.0, 20.0, 30.0];
1990 let params = QqeParams {
1991 rsi_period: Some(10),
1992 smoothing_factor: None,
1993 fast_factor: None,
1994 };
1995 let input = QqeInput::from_slice(&data_small, params);
1996 let res = qqe_with_kernel(&input, kernel);
1997 assert!(
1998 res.is_err(),
1999 "[{}] QQE should fail with period exceeding length",
2000 test_name
2001 );
2002 Ok(())
2003 }
2004
2005 fn check_qqe_very_small_dataset(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2006 skip_if_unsupported!(kernel, test_name);
2007 let single_point = [42.0];
2008 let params = QqeParams::default();
2009 let input = QqeInput::from_slice(&single_point, params);
2010 let res = qqe_with_kernel(&input, kernel);
2011 assert!(
2012 res.is_err(),
2013 "[{}] QQE should fail with insufficient data",
2014 test_name
2015 );
2016 Ok(())
2017 }
2018
2019 fn check_qqe_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2020 skip_if_unsupported!(kernel, test_name);
2021 let empty: [f64; 0] = [];
2022 let params = QqeParams::default();
2023 let input = QqeInput::from_slice(&empty, params);
2024 let res = qqe_with_kernel(&input, kernel);
2025 assert!(
2026 res.is_err(),
2027 "[{}] QQE should fail with empty input",
2028 test_name
2029 );
2030 Ok(())
2031 }
2032
2033 fn check_qqe_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2034 skip_if_unsupported!(kernel, test_name);
2035 let nan_data = [f64::NAN, f64::NAN, f64::NAN];
2036 let params = QqeParams::default();
2037 let input = QqeInput::from_slice(&nan_data, params);
2038 let res = qqe_with_kernel(&input, kernel);
2039 assert!(
2040 res.is_err(),
2041 "[{}] QQE should fail with all NaN values",
2042 test_name
2043 );
2044 Ok(())
2045 }
2046
2047 fn check_qqe_batch(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2048 skip_if_unsupported!(kernel, test_name);
2049 let data: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64).sin() * 10.0).collect();
2050
2051 let sweep = QqeBatchRange {
2052 rsi_period: (10, 20, 5),
2053 smoothing_factor: (3, 5, 1),
2054 fast_factor: (3.0, 5.0, 1.0),
2055 };
2056
2057 let result = qqe_batch_with_kernel(&data, &sweep, kernel)?;
2058
2059 assert_eq!(result.combos.len(), 27);
2060 assert_eq!(result.rows, 27);
2061 assert_eq!(result.cols, 100);
2062 assert_eq!(result.fast_values.len(), 27 * 100);
2063 assert_eq!(result.slow_values.len(), 27 * 100);
2064
2065 Ok(())
2066 }
2067
2068 fn check_qqe_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2069 skip_if_unsupported!(kernel, test_name);
2070 let mut stream = QqeStream::try_new(QqeParams::default())?;
2071
2072 let data: Vec<f64> = (0..50).map(|i| 50.0 + (i as f64).sin() * 10.0).collect();
2073 let mut results = Vec::new();
2074
2075 for &val in &data {
2076 if let Some(result) = stream.update(val) {
2077 results.push(result);
2078 }
2079 }
2080
2081 assert!(
2082 !results.is_empty(),
2083 "[{}] Should have streaming results",
2084 test_name
2085 );
2086
2087 for (fast, slow) in &results {
2088 assert!(
2089 !fast.is_nan(),
2090 "[{}] Fast value should not be NaN",
2091 test_name
2092 );
2093 assert!(
2094 !slow.is_nan(),
2095 "[{}] Slow value should not be NaN",
2096 test_name
2097 );
2098 }
2099
2100 Ok(())
2101 }
2102
2103 fn check_qqe_into_slices(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2104 skip_if_unsupported!(kernel, test_name);
2105 let data: Vec<f64> = (0..100).map(|i| 50.0 + (i as f64).sin() * 10.0).collect();
2106 let params = QqeParams::default();
2107 let input = QqeInput::from_slice(&data, params);
2108
2109 let mut dst_fast = vec![0.0; data.len()];
2110 let mut dst_slow = vec![0.0; data.len()];
2111
2112 qqe_into_slices(&mut dst_fast, &mut dst_slow, &input, kernel)?;
2113
2114 let regular = qqe_with_kernel(&input, kernel)?;
2115
2116 for i in 0..data.len() {
2117 if dst_fast[i].is_nan() && regular.fast[i].is_nan() {
2118 } else {
2119 assert_eq!(
2120 dst_fast[i], regular.fast[i],
2121 "[{}] Fast mismatch at {}",
2122 test_name, i
2123 );
2124 }
2125
2126 if dst_slow[i].is_nan() && regular.slow[i].is_nan() {
2127 } else {
2128 assert_eq!(
2129 dst_slow[i], regular.slow[i],
2130 "[{}] Slow mismatch at {}",
2131 test_name, i
2132 );
2133 }
2134 }
2135
2136 Ok(())
2137 }
2138
2139 fn check_qqe_poison_sentinel(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2140 skip_if_unsupported!(kernel, test_name);
2141
2142 let test_data = vec![
2143 50.0, 51.0, 52.0, 51.5, 50.5, 49.5, 50.0, 51.0, 52.0, 53.0, 52.5, 51.5, 50.5, 51.0,
2144 52.0, 53.0, 54.0, 53.5, 52.5, 51.5, 50.5, 51.5, 52.5, 53.5, 54.5, 55.0, 54.5, 53.5,
2145 52.5, 51.5,
2146 ];
2147
2148 {
2149 const POISON: f64 = f64::from_bits(0xDEADBEEF_DEADBEEF);
2150 let mut fast = vec![POISON; test_data.len()];
2151 let mut slow = vec![POISON; test_data.len()];
2152
2153 let params = QqeParams::default();
2154 let input = QqeInput::from_slice(&test_data, params);
2155
2156 qqe_into_slices(&mut fast[..], &mut slow[..], &input, kernel)?;
2157
2158 for (i, &val) in fast.iter().enumerate() {
2159 assert!(
2160 val.is_nan() || (val.is_finite() && val != POISON),
2161 "[{}] Uninitialized memory detected in fast at index {}: {:?}",
2162 test_name,
2163 i,
2164 val
2165 );
2166 }
2167
2168 for (i, &val) in slow.iter().enumerate() {
2169 assert!(
2170 val.is_nan() || (val.is_finite() && val != POISON),
2171 "[{}] Uninitialized memory detected in slow at index {}: {:?}",
2172 test_name,
2173 i,
2174 val
2175 );
2176 }
2177 }
2178
2179 {
2180 let sweep = QqeBatchRange {
2181 rsi_period: (10, 14, 2),
2182 smoothing_factor: (3, 5, 2),
2183 fast_factor: (3.0, 4.0, 1.0),
2184 };
2185
2186 let batch_out = qqe_batch_with_kernel(&test_data, &sweep, kernel)?;
2187
2188 for (i, &val) in batch_out.fast_values.iter().enumerate() {
2189 assert!(
2190 val.is_nan() || val.is_finite(),
2191 "[{}] Invalid value in batch fast at index {}: {:?}",
2192 test_name,
2193 i,
2194 val
2195 );
2196 }
2197
2198 for (i, &val) in batch_out.slow_values.iter().enumerate() {
2199 assert!(
2200 val.is_nan() || val.is_finite(),
2201 "[{}] Invalid value in batch slow at index {}: {:?}",
2202 test_name,
2203 i,
2204 val
2205 );
2206 }
2207 }
2208
2209 Ok(())
2210 }
2211
2212 fn check_qqe_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2213 skip_if_unsupported!(kernel, test_name);
2214 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2215 let c = read_candles_from_csv(file)?;
2216 let p = QqeParams::default();
2217
2218 let out1 = qqe_with_kernel(&QqeInput::from_candles(&c, "close", p.clone()), kernel)?;
2219
2220 let out2 = qqe_with_kernel(&QqeInput::from_slice(&out1.fast, p), kernel)?;
2221
2222 assert_eq!(out1.fast.len(), out2.fast.len());
2223 assert_eq!(out1.slow.len(), out2.slow.len());
2224 Ok(())
2225 }
2226
2227 fn check_qqe_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2228 skip_if_unsupported!(kernel, test_name);
2229 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2230 let c = read_candles_from_csv(file)?;
2231
2232 let p = QqeParams::default();
2233 let res = qqe_with_kernel(&QqeInput::from_candles(&c, "close", p.clone()), kernel)?;
2234 let first = c.close.iter().position(|x| !x.is_nan()).unwrap_or(0);
2235 let warm = first + p.rsi_period.unwrap_or(14) + p.smoothing_factor.unwrap_or(5) - 2;
2236
2237 for (i, &v) in res.fast.iter().enumerate().skip(warm) {
2238 assert!(!v.is_nan(), "[{}] fast NaN @ {}", test_name, i);
2239 }
2240 for (i, &v) in res.slow.iter().enumerate().skip(warm) {
2241 assert!(!v.is_nan(), "[{}] slow NaN @ {}", test_name, i);
2242 }
2243 Ok(())
2244 }
2245
2246 fn check_batch_default_row(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2247 skip_if_unsupported!(kernel, test_name);
2248 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2249 let c = read_candles_from_csv(file)?;
2250
2251 let out = QqeBatchBuilder::new()
2252 .kernel(kernel)
2253 .apply_candles(&c, "close")?;
2254 let def = QqeParams::default();
2255 let row = out.row_for_params(&def).expect("default row missing");
2256
2257 let start = row * out.cols;
2258 assert_eq!(
2259 out.fast_values[start..start + out.cols].len(),
2260 c.close.len()
2261 );
2262 assert_eq!(
2263 out.slow_values[start..start + out.cols].len(),
2264 c.close.len()
2265 );
2266 Ok(())
2267 }
2268
2269 #[cfg(debug_assertions)]
2270 fn check_batch_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2271 skip_if_unsupported!(kernel, test_name);
2272 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2273 let c = read_candles_from_csv(file)?;
2274 let out = QqeBatchBuilder::new()
2275 .kernel(kernel)
2276 .rsi_period_range(10, 14, 2)
2277 .smoothing_factor_range(3, 5, 1)
2278 .fast_factor_range(3.0, 5.0, 1.0)
2279 .apply_candles(&c, "close")?;
2280
2281 for (idx, &v) in out.fast_values.iter().enumerate() {
2282 if v.is_nan() {
2283 continue;
2284 }
2285 let b = v.to_bits();
2286 assert!(
2287 b != 0x1111_1111_1111_1111
2288 && b != 0x2222_2222_2222_2222
2289 && b != 0x3333_3333_3333_3333,
2290 "[{}] poison in fast @ {}",
2291 test_name,
2292 idx
2293 );
2294 }
2295 for (idx, &v) in out.slow_values.iter().enumerate() {
2296 if v.is_nan() {
2297 continue;
2298 }
2299 let b = v.to_bits();
2300 assert!(
2301 b != 0x1111_1111_1111_1111
2302 && b != 0x2222_2222_2222_2222
2303 && b != 0x3333_3333_3333_3333,
2304 "[{}] poison in slow @ {}",
2305 test_name,
2306 idx
2307 );
2308 }
2309 Ok(())
2310 }
2311
2312 #[cfg(not(debug_assertions))]
2313 fn check_batch_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2314 Ok(())
2315 }
2316
2317 #[cfg(feature = "proptest")]
2318 fn check_qqe_property(
2319 test_name: &str,
2320 kernel: Kernel,
2321 ) -> Result<(), Box<dyn std::error::Error>> {
2322 use proptest::prelude::*;
2323 skip_if_unsupported!(kernel, test_name);
2324
2325 let strat = (1usize..=64).prop_flat_map(|rsi_p| {
2326 (1usize..=32).prop_flat_map(move |ema_p| {
2327 let need = rsi_p + ema_p + 8;
2328 (
2329 prop::collection::vec(
2330 (-1e6f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
2331 need..400,
2332 ),
2333 Just(rsi_p),
2334 Just(ema_p),
2335 0.5f64..8.0f64,
2336 )
2337 })
2338 });
2339
2340 proptest::test_runner::TestRunner::default().run(
2341 &strat,
2342 |(data, rsi_p, ema_p, fast_k)| {
2343 let p = QqeParams {
2344 rsi_period: Some(rsi_p),
2345 smoothing_factor: Some(ema_p),
2346 fast_factor: Some(fast_k),
2347 };
2348 let input = QqeInput::from_slice(&data, p);
2349
2350 let ref_out = qqe_with_kernel(&input, Kernel::Scalar).unwrap();
2351
2352 let mut f = vec![0.0; data.len()];
2353 let mut s = vec![0.0; data.len()];
2354 qqe_into_slices(&mut f, &mut s, &input, Kernel::Scalar).unwrap();
2355
2356 for i in 0..data.len() {
2357 let a = ref_out.fast[i];
2358 let b = f[i];
2359 if a.is_nan() {
2360 prop_assert!(b.is_nan());
2361 } else {
2362 prop_assert!((a - b).abs() <= 1e-9);
2363 }
2364
2365 let c = ref_out.slow[i];
2366 let d = s[i];
2367 if c.is_nan() {
2368 prop_assert!(d.is_nan());
2369 } else {
2370 prop_assert!((c - d).abs() <= 1e-9);
2371 }
2372
2373 if !a.is_nan() {
2374 prop_assert!(a >= 0.0 && a <= 100.0);
2375 }
2376 }
2377 Ok(())
2378 },
2379 )?;
2380 Ok(())
2381 }
2382
2383 macro_rules! generate_all_qqe_tests {
2384 ($($test_fn:ident),+ $(,)?) => {
2385
2386 paste! {
2387 $(
2388 #[test]
2389 fn [<$test_fn _scalar>]() {
2390 let _ = $test_fn(stringify!([<$test_fn _scalar>]), Kernel::Scalar);
2391 }
2392 )*
2393
2394 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2395 $(
2396 #[test]
2397 fn [<$test_fn _avx2>]() {
2398 let _ = $test_fn(stringify!([<$test_fn _avx2>]), Kernel::Avx2);
2399 }
2400
2401 #[test]
2402 fn [<$test_fn _avx512>]() {
2403 let _ = $test_fn(stringify!([<$test_fn _avx512>]), Kernel::Avx512);
2404 }
2405 )*
2406
2407 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
2408 $(
2409 #[test]
2410 fn [<$test_fn _simd128>]() {
2411 let _ = $test_fn(stringify!([<$test_fn _simd128>]), Kernel::Scalar);
2412 }
2413 )*
2414 }
2415 };
2416 }
2417
2418 generate_all_qqe_tests!(
2419 check_qqe_accuracy,
2420 check_qqe_partial_params,
2421 check_qqe_default_candles,
2422 check_qqe_zero_period,
2423 check_qqe_period_exceeds_length,
2424 check_qqe_very_small_dataset,
2425 check_qqe_empty_input,
2426 check_qqe_all_nan,
2427 check_qqe_batch,
2428 check_qqe_streaming,
2429 check_qqe_into_slices,
2430 check_qqe_poison_sentinel,
2431 check_qqe_reinput,
2432 check_qqe_nan_handling,
2433 check_batch_default_row,
2434 check_batch_no_poison,
2435 );
2436
2437 #[cfg(feature = "proptest")]
2438 generate_all_qqe_tests!(check_qqe_property);
2439}