Skip to main content

vector_ta/indicators/
qqe.rs

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, &params)
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}