Skip to main content

vector_ta/indicators/
rvi.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
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
15use crate::utilities::data_loader::{source_type, Candles};
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19    make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23use aligned_vec::{AVec, CACHELINE_ALIGN};
24#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
25use core::arch::x86_64::*;
26#[cfg(not(target_arch = "wasm32"))]
27use rayon::prelude::*;
28use std::cmp::Ordering;
29use std::cmp::Reverse;
30use std::collections::BinaryHeap;
31use thiserror::Error;
32
33impl<'a> AsRef<[f64]> for RviInput<'a> {
34    #[inline(always)]
35    fn as_ref(&self) -> &[f64] {
36        match &self.data {
37            RviData::Slice(slice) => slice,
38            RviData::Candles { candles, source } => source_type(candles, source),
39        }
40    }
41}
42
43#[derive(Debug, Clone)]
44pub enum RviData<'a> {
45    Candles {
46        candles: &'a Candles,
47        source: &'a str,
48    },
49    Slice(&'a [f64]),
50}
51
52#[derive(Debug, Clone)]
53pub struct RviOutput {
54    pub values: Vec<f64>,
55}
56
57#[derive(Debug, Clone)]
58#[cfg_attr(
59    all(target_arch = "wasm32", feature = "wasm"),
60    derive(Serialize, Deserialize)
61)]
62pub struct RviParams {
63    pub period: Option<usize>,
64    pub ma_len: Option<usize>,
65    pub matype: Option<usize>,
66    pub devtype: Option<usize>,
67}
68
69impl Default for RviParams {
70    fn default() -> Self {
71        Self {
72            period: Some(10),
73            ma_len: Some(14),
74            matype: Some(1),
75            devtype: Some(0),
76        }
77    }
78}
79
80#[derive(Debug, Clone)]
81pub struct RviInput<'a> {
82    pub data: RviData<'a>,
83    pub params: RviParams,
84}
85
86impl<'a> RviInput<'a> {
87    #[inline]
88    pub fn from_candles(c: &'a Candles, s: &'a str, p: RviParams) -> Self {
89        Self {
90            data: RviData::Candles {
91                candles: c,
92                source: s,
93            },
94            params: p,
95        }
96    }
97    #[inline]
98    pub fn from_slice(sl: &'a [f64], p: RviParams) -> Self {
99        Self {
100            data: RviData::Slice(sl),
101            params: p,
102        }
103    }
104    #[inline]
105    pub fn with_default_candles(c: &'a Candles) -> Self {
106        Self::from_candles(c, "close", RviParams::default())
107    }
108    #[inline]
109    pub fn get_period(&self) -> usize {
110        self.params.period.unwrap_or(10)
111    }
112    #[inline]
113    pub fn get_ma_len(&self) -> usize {
114        self.params.ma_len.unwrap_or(14)
115    }
116    #[inline]
117    pub fn get_matype(&self) -> usize {
118        self.params.matype.unwrap_or(1)
119    }
120    #[inline]
121    pub fn get_devtype(&self) -> usize {
122        self.params.devtype.unwrap_or(0)
123    }
124}
125
126#[derive(Copy, Clone, Debug)]
127pub struct RviBuilder {
128    period: Option<usize>,
129    ma_len: Option<usize>,
130    matype: Option<usize>,
131    devtype: Option<usize>,
132    kernel: Kernel,
133}
134
135impl Default for RviBuilder {
136    fn default() -> Self {
137        Self {
138            period: None,
139            ma_len: None,
140            matype: None,
141            devtype: None,
142            kernel: Kernel::Auto,
143        }
144    }
145}
146
147impl RviBuilder {
148    #[inline(always)]
149    pub fn new() -> Self {
150        Self::default()
151    }
152    #[inline(always)]
153    pub fn period(mut self, n: usize) -> Self {
154        self.period = Some(n);
155        self
156    }
157    #[inline(always)]
158    pub fn ma_len(mut self, n: usize) -> Self {
159        self.ma_len = Some(n);
160        self
161    }
162    #[inline(always)]
163    pub fn matype(mut self, n: usize) -> Self {
164        self.matype = Some(n);
165        self
166    }
167    #[inline(always)]
168    pub fn devtype(mut self, n: usize) -> Self {
169        self.devtype = Some(n);
170        self
171    }
172    #[inline(always)]
173    pub fn kernel(mut self, k: Kernel) -> Self {
174        self.kernel = k;
175        self
176    }
177    #[inline(always)]
178    pub fn apply(self, c: &Candles) -> Result<RviOutput, RviError> {
179        let p = RviParams {
180            period: self.period,
181            ma_len: self.ma_len,
182            matype: self.matype,
183            devtype: self.devtype,
184        };
185        let i = RviInput::from_candles(c, "close", p);
186        rvi_with_kernel(&i, self.kernel)
187    }
188    #[inline(always)]
189    pub fn apply_slice(self, d: &[f64]) -> Result<RviOutput, RviError> {
190        let p = RviParams {
191            period: self.period,
192            ma_len: self.ma_len,
193            matype: self.matype,
194            devtype: self.devtype,
195        };
196        let i = RviInput::from_slice(d, p);
197        rvi_with_kernel(&i, self.kernel)
198    }
199    #[inline(always)]
200    pub fn into_stream(self) -> Result<RviStream, RviError> {
201        let p = RviParams {
202            period: self.period,
203            ma_len: self.ma_len,
204            matype: self.matype,
205            devtype: self.devtype,
206        };
207        RviStream::try_new(p)
208    }
209}
210
211#[derive(Debug, Error)]
212pub enum RviError {
213    #[error("rvi: Empty data provided.")]
214    EmptyInputData,
215    #[error(
216        "rvi: Invalid period or ma_len: period = {period}, ma_len = {ma_len}, data length = {data_len}"
217    )]
218    InvalidPeriod {
219        period: usize,
220        ma_len: usize,
221        data_len: usize,
222    },
223    #[error("rvi: Not enough valid data: needed = {needed}, valid = {valid}")]
224    NotEnoughValidData { needed: usize, valid: usize },
225    #[error("rvi: All values are NaN.")]
226    AllValuesNaN,
227    #[error("rvi: Output slice length {got} != data length {expected}.")]
228    OutputLengthMismatch { expected: usize, got: usize },
229    #[error("rvi: invalid range: start = {start}, end = {end}, step = {step}")]
230    InvalidRange { start: i128, end: i128, step: i128 },
231    #[error("rvi: invalid kernel for batch: {0:?}")]
232    InvalidKernelForBatch(Kernel),
233    #[error("rvi: invalid input: {0}")]
234    InvalidInput(String),
235}
236
237#[inline]
238pub fn rvi(input: &RviInput) -> Result<RviOutput, RviError> {
239    rvi_with_kernel(input, Kernel::Auto)
240}
241
242pub fn rvi_with_kernel(input: &RviInput, kernel: Kernel) -> Result<RviOutput, RviError> {
243    let data: &[f64] = input.as_ref();
244    if data.is_empty() {
245        return Err(RviError::EmptyInputData);
246    }
247
248    let first = data
249        .iter()
250        .position(|x| !x.is_nan())
251        .ok_or(RviError::AllValuesNaN)?;
252
253    let period = input.get_period();
254    let ma_len = input.get_ma_len();
255    let matype = input.get_matype();
256    let devtype = input.get_devtype();
257    if period == 0 || ma_len == 0 || period > data.len() || ma_len > data.len() {
258        return Err(RviError::InvalidPeriod {
259            period,
260            ma_len,
261            data_len: data.len(),
262        });
263    }
264    let max_needed = period.saturating_sub(1) + ma_len.saturating_sub(1);
265    if (data.len() - first) <= max_needed {
266        return Err(RviError::NotEnoughValidData {
267            needed: max_needed + 1,
268            valid: data.len() - first,
269        });
270    }
271    let warmup_period = first + period.saturating_sub(1) + ma_len.saturating_sub(1);
272    let mut out = alloc_with_nan_prefix(data.len(), warmup_period);
273    let chosen = match kernel {
274        Kernel::Auto => Kernel::Scalar,
275        other => other,
276    };
277    unsafe {
278        match chosen {
279            Kernel::Scalar | Kernel::ScalarBatch => {
280                rvi_scalar(data, period, ma_len, matype, devtype, first, &mut out)
281            }
282            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
283            Kernel::Avx2 | Kernel::Avx2Batch => {
284                rvi_avx2(data, period, ma_len, matype, devtype, first, &mut out)
285            }
286            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
287            Kernel::Avx512 | Kernel::Avx512Batch => {
288                rvi_avx512(data, period, ma_len, matype, devtype, first, &mut out)
289            }
290            _ => unreachable!(),
291        }
292    }
293    Ok(RviOutput { values: out })
294}
295
296#[inline]
297pub fn rvi_into_slice(dst: &mut [f64], input: &RviInput, kern: Kernel) -> Result<(), RviError> {
298    let data: &[f64] = input.as_ref();
299    if data.is_empty() {
300        return Err(RviError::EmptyInputData);
301    }
302
303    let first = data
304        .iter()
305        .position(|x| !x.is_nan())
306        .ok_or(RviError::AllValuesNaN)?;
307
308    let period = input.get_period();
309    let ma_len = input.get_ma_len();
310    let matype = input.get_matype();
311    let devtype = input.get_devtype();
312
313    if period == 0 || ma_len == 0 {
314        return Err(RviError::InvalidPeriod {
315            period,
316            ma_len,
317            data_len: data.len(),
318        });
319    }
320
321    if dst.len() != data.len() {
322        return Err(RviError::OutputLengthMismatch {
323            expected: data.len(),
324            got: dst.len(),
325        });
326    }
327
328    let max_needed = period.saturating_sub(1) + ma_len.saturating_sub(1);
329    let valid_len = data.len() - first;
330
331    if period > data.len() || ma_len > data.len() {
332        return Err(RviError::InvalidPeriod {
333            period,
334            ma_len,
335            data_len: data.len(),
336        });
337    }
338
339    if valid_len <= max_needed {
340        return Err(RviError::NotEnoughValidData {
341            needed: max_needed + 1,
342            valid: valid_len,
343        });
344    }
345
346    let warmup_period = first + period.saturating_sub(1) + ma_len.saturating_sub(1);
347
348    for v in &mut dst[..warmup_period] {
349        *v = f64::NAN;
350    }
351
352    let chosen = match kern {
353        Kernel::Auto => Kernel::Scalar,
354        other => other,
355    };
356
357    unsafe {
358        match chosen {
359            Kernel::Scalar | Kernel::ScalarBatch => {
360                rvi_scalar(data, period, ma_len, matype, devtype, first, dst)
361            }
362            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
363            Kernel::Avx2 | Kernel::Avx2Batch => {
364                rvi_avx2(data, period, ma_len, matype, devtype, first, dst)
365            }
366            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
367            Kernel::Avx512 | Kernel::Avx512Batch => {
368                rvi_avx512(data, period, ma_len, matype, devtype, first, dst)
369            }
370            _ => unreachable!(),
371        }
372    }
373
374    Ok(())
375}
376
377#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
378#[inline]
379pub fn rvi_into(input: &RviInput, out: &mut [f64]) -> Result<(), RviError> {
380    rvi_into_slice(out, input, Kernel::Auto)
381}
382
383#[inline]
384pub fn rvi_scalar(
385    data: &[f64],
386    period: usize,
387    ma_len: usize,
388    matype: usize,
389    devtype: usize,
390    first: usize,
391    out: &mut [f64],
392) {
393    debug_assert_eq!(out.len(), data.len());
394    let n = data.len();
395    if n == 0 {
396        return;
397    }
398
399    let warmup = first + period.saturating_sub(1) + ma_len.saturating_sub(1);
400
401    let inv_p = 1.0f64 / period as f64;
402    let inv_m = 1.0f64 / ma_len as f64;
403
404    let mut sum = 0.0f64;
405    let mut sumsq = 0.0f64;
406
407    let mut ring = vec![f64::NAN; period];
408    let mut r_head = 0usize;
409    let mut r_filled = false;
410
411    let mut scratch = if devtype == 2 {
412        vec![0.0f64; period]
413    } else {
414        Vec::new()
415    };
416
417    let use_sma = matype == 0;
418
419    let mut up_sum = 0.0f64;
420    let mut dn_sum = 0.0f64;
421    let mut up_ring = if use_sma {
422        vec![0.0f64; ma_len]
423    } else {
424        Vec::new()
425    };
426    let mut dn_ring = if use_sma {
427        vec![0.0f64; ma_len]
428    } else {
429        Vec::new()
430    };
431    let mut up_h = 0usize;
432    let mut dn_h = 0usize;
433    let mut up_cnt = 0usize;
434    let mut dn_cnt = 0usize;
435
436    let alpha = if !use_sma {
437        2.0 / (ma_len as f64 + 1.0)
438    } else {
439        0.0
440    };
441    let one_m_alpha = 1.0 - alpha;
442    let mut up_prev = 0.0f64;
443    let mut dn_prev = 0.0f64;
444    let mut up_started = false;
445    let mut dn_started = false;
446    let mut up_seed_sum = 0.0f64;
447    let mut dn_seed_sum = 0.0f64;
448    let mut up_seed_cnt = 0usize;
449    let mut dn_seed_cnt = 0usize;
450
451    let mut prev = data[0];
452
453    for i in 0..period.min(n) {
454        let x = data[i];
455        if x.is_nan() {
456            sum = f64::NAN;
457            sumsq = f64::NAN;
458            break;
459        }
460        sum += x;
461        sumsq += x * x;
462        if devtype != 0 {
463            ring[i] = x;
464            if i + 1 == period {
465                r_filled = true;
466                r_head = 0;
467            }
468        }
469    }
470
471    if devtype == 0 {
472        let mut prev = data[0];
473        if use_sma {
474            for i in 0..n {
475                let x = data[i];
476                let d = if i == 0 || x.is_nan() || prev.is_nan() {
477                    f64::NAN
478                } else {
479                    x - prev
480                };
481                prev = x;
482
483                let dev = if i + 1 < period {
484                    f64::NAN
485                } else if i == period - 1 {
486                    if sum.is_nan() {
487                        f64::NAN
488                    } else {
489                        let mean = sum * inv_p;
490                        let mean_sq = sumsq * inv_p;
491                        (mean_sq - mean * mean).sqrt()
492                    }
493                } else {
494                    let leaving = data[i - period];
495                    if leaving.is_nan() || x.is_nan() || sum.is_nan() || sumsq.is_nan() {
496                        sum = 0.0;
497                        sumsq = 0.0;
498                        let start = i + 1 - period;
499                        let mut bad = false;
500                        for k in start..=i {
501                            let v = data[k];
502                            if v.is_nan() {
503                                bad = true;
504                                break;
505                            }
506                            sum += v;
507                            sumsq += v * v;
508                        }
509                        if bad {
510                            sum = f64::NAN;
511                            sumsq = f64::NAN;
512                            f64::NAN
513                        } else {
514                            let mean = sum * inv_p;
515                            let mean_sq = sumsq * inv_p;
516                            (mean_sq - mean * mean).sqrt()
517                        }
518                    } else {
519                        sum += x - leaving;
520                        sumsq += x * x - leaving * leaving;
521                        let mean = sum * inv_p;
522                        let mean_sq = sumsq * inv_p;
523                        (mean_sq - mean * mean).sqrt()
524                    }
525                };
526
527                let (up_i, dn_i) = if d.is_nan() || dev.is_nan() {
528                    (f64::NAN, f64::NAN)
529                } else if d > 0.0 {
530                    (dev, 0.0)
531                } else if d < 0.0 {
532                    (0.0, dev)
533                } else {
534                    (0.0, 0.0)
535                };
536
537                let up_s = if up_i.is_nan() {
538                    up_sum = 0.0;
539                    up_cnt = 0;
540                    up_h = 0;
541                    f64::NAN
542                } else if up_cnt < ma_len {
543                    unsafe {
544                        *up_ring.get_unchecked_mut(up_h) = up_i;
545                    }
546                    up_sum += up_i;
547                    up_h = (up_h + 1) % ma_len;
548                    up_cnt += 1;
549                    if up_cnt == ma_len {
550                        up_sum * inv_m
551                    } else {
552                        f64::NAN
553                    }
554                } else {
555                    let old = unsafe { *up_ring.get_unchecked(up_h) };
556                    unsafe {
557                        *up_ring.get_unchecked_mut(up_h) = up_i;
558                    }
559                    up_h = (up_h + 1) % ma_len;
560                    up_sum += up_i - old;
561                    up_sum * inv_m
562                };
563
564                let dn_s = if dn_i.is_nan() {
565                    dn_sum = 0.0;
566                    dn_cnt = 0;
567                    dn_h = 0;
568                    f64::NAN
569                } else if dn_cnt < ma_len {
570                    unsafe {
571                        *dn_ring.get_unchecked_mut(dn_h) = dn_i;
572                    }
573                    dn_sum += dn_i;
574                    dn_h = (dn_h + 1) % ma_len;
575                    dn_cnt += 1;
576                    if dn_cnt == ma_len {
577                        dn_sum * inv_m
578                    } else {
579                        f64::NAN
580                    }
581                } else {
582                    let old = unsafe { *dn_ring.get_unchecked(dn_h) };
583                    unsafe {
584                        *dn_ring.get_unchecked_mut(dn_h) = dn_i;
585                    }
586                    dn_h = (dn_h + 1) % ma_len;
587                    dn_sum += dn_i - old;
588                    dn_sum * inv_m
589                };
590
591                if i >= warmup {
592                    if up_s.is_nan() || dn_s.is_nan() {
593                        out[i] = f64::NAN;
594                    } else {
595                        let denom = up_s + dn_s;
596                        out[i] = if denom.abs() < f64::EPSILON {
597                            f64::NAN
598                        } else {
599                            100.0 * (up_s / denom)
600                        };
601                    }
602                }
603            }
604        } else {
605            for i in 0..n {
606                let x = data[i];
607                let d = if i == 0 || x.is_nan() || prev.is_nan() {
608                    f64::NAN
609                } else {
610                    x - prev
611                };
612                prev = x;
613
614                let dev = if i + 1 < period {
615                    f64::NAN
616                } else if i == period - 1 {
617                    if sum.is_nan() {
618                        f64::NAN
619                    } else {
620                        let mean = sum * inv_p;
621                        let mean_sq = sumsq * inv_p;
622                        (mean_sq - mean * mean).sqrt()
623                    }
624                } else {
625                    let leaving = data[i - period];
626                    if leaving.is_nan() || x.is_nan() || sum.is_nan() || sumsq.is_nan() {
627                        sum = 0.0;
628                        sumsq = 0.0;
629                        let start = i + 1 - period;
630                        let mut bad = false;
631                        for k in start..=i {
632                            let v = data[k];
633                            if v.is_nan() {
634                                bad = true;
635                                break;
636                            }
637                            sum += v;
638                            sumsq += v * v;
639                        }
640                        if bad {
641                            sum = f64::NAN;
642                            sumsq = f64::NAN;
643                            f64::NAN
644                        } else {
645                            let mean = sum * inv_p;
646                            let mean_sq = sumsq * inv_p;
647                            (mean_sq - mean * mean).sqrt()
648                        }
649                    } else {
650                        sum += x - leaving;
651                        sumsq += x * x - leaving * leaving;
652                        let mean = sum * inv_p;
653                        let mean_sq = sumsq * inv_p;
654                        (mean_sq - mean * mean).sqrt()
655                    }
656                };
657
658                let (up_i, dn_i) = if d.is_nan() || dev.is_nan() {
659                    (f64::NAN, f64::NAN)
660                } else if d > 0.0 {
661                    (dev, 0.0)
662                } else if d < 0.0 {
663                    (0.0, dev)
664                } else {
665                    (0.0, 0.0)
666                };
667
668                let up_s = if up_i.is_nan() {
669                    up_started = false;
670                    up_seed_sum = 0.0;
671                    up_seed_cnt = 0;
672                    f64::NAN
673                } else if !up_started {
674                    up_seed_sum += up_i;
675                    up_seed_cnt += 1;
676                    if up_seed_cnt == ma_len {
677                        up_prev = up_seed_sum * inv_m;
678                        up_started = true;
679                        up_prev
680                    } else {
681                        f64::NAN
682                    }
683                } else {
684                    up_prev = alpha.mul_add(up_i, one_m_alpha * up_prev);
685                    up_prev
686                };
687
688                let dn_s = if dn_i.is_nan() {
689                    dn_started = false;
690                    dn_seed_sum = 0.0;
691                    dn_seed_cnt = 0;
692                    f64::NAN
693                } else if !dn_started {
694                    dn_seed_sum += dn_i;
695                    dn_seed_cnt += 1;
696                    if dn_seed_cnt == ma_len {
697                        dn_prev = dn_seed_sum * inv_m;
698                        dn_started = true;
699                        dn_prev
700                    } else {
701                        f64::NAN
702                    }
703                } else {
704                    dn_prev = alpha.mul_add(dn_i, one_m_alpha * dn_prev);
705                    dn_prev
706                };
707
708                if i >= warmup {
709                    if up_s.is_nan() || dn_s.is_nan() {
710                        out[i] = f64::NAN;
711                    } else {
712                        let denom = up_s + dn_s;
713                        out[i] = if denom.abs() < f64::EPSILON {
714                            f64::NAN
715                        } else {
716                            100.0 * (up_s / denom)
717                        };
718                    }
719                }
720            }
721        }
722        return;
723    }
724
725    for i in 0..n {
726        let x = data[i];
727
728        let d = if i == 0 || x.is_nan() || prev.is_nan() {
729            f64::NAN
730        } else {
731            x - prev
732        };
733        prev = x;
734
735        let dev = if i + 1 < period {
736            f64::NAN
737        } else {
738            match devtype {
739                0 => {
740                    if i == period - 1 {
741                        if sum.is_nan() {
742                            f64::NAN
743                        } else {
744                            let mean = sum * inv_p;
745                            let mean_sq = sumsq * inv_p;
746                            (mean_sq - mean * mean).sqrt()
747                        }
748                    } else {
749                        let leaving = data[i - period];
750                        let incoming = x;
751                        if leaving.is_nan() || incoming.is_nan() || sum.is_nan() || sumsq.is_nan() {
752                            sum = 0.0;
753                            sumsq = 0.0;
754                            let start = i + 1 - period;
755                            let mut bad = false;
756                            for k in start..=i {
757                                let v = data[k];
758                                if v.is_nan() {
759                                    bad = true;
760                                    break;
761                                }
762                                sum += v;
763                                sumsq += v * v;
764                            }
765                            if bad {
766                                sum = f64::NAN;
767                                sumsq = f64::NAN;
768                                f64::NAN
769                            } else {
770                                let mean = sum * inv_p;
771                                let mean_sq = sumsq * inv_p;
772                                (mean_sq - mean * mean).sqrt()
773                            }
774                        } else {
775                            sum += incoming - leaving;
776                            sumsq += incoming * incoming - leaving * leaving;
777                            let mean = sum * inv_p;
778                            let mean_sq = sumsq * inv_p;
779                            (mean_sq - mean * mean).sqrt()
780                        }
781                    }
782                }
783                1 => {
784                    let incoming = x;
785                    if i < period {
786                        if !incoming.is_nan() {
787                            ring[i] = incoming;
788                            if i + 1 == period {
789                                r_filled = true;
790                                r_head = 0;
791                            }
792                        }
793                        if i + 1 < period {
794                            f64::NAN
795                        } else {
796                            let mut s = 0.0;
797                            unsafe {
798                                for k in 0..period {
799                                    s += *ring.get_unchecked(k);
800                                }
801                            }
802                            let mean = s * inv_p;
803                            let mut abs_sum = 0.0;
804                            unsafe {
805                                for k in 0..period {
806                                    abs_sum += (*ring.get_unchecked(k) - mean).abs();
807                                }
808                            }
809                            abs_sum * inv_p
810                        }
811                    } else {
812                        let leaving = data[i - period];
813                        if incoming.is_nan() || leaving.is_nan() {
814                            r_filled = false;
815                            for j in 0..period {
816                                ring[j] = f64::NAN;
817                            }
818                            f64::NAN
819                        } else {
820                            unsafe {
821                                *ring.get_unchecked_mut(r_head) = incoming;
822                            }
823                            r_head = (r_head + 1) % period;
824                            r_filled = true;
825
826                            let mut s = 0.0;
827                            unsafe {
828                                for k in 0..period {
829                                    s += *ring.get_unchecked(k);
830                                }
831                            }
832                            let mean = s * inv_p;
833                            let mut abs_sum = 0.0;
834                            unsafe {
835                                for k in 0..period {
836                                    abs_sum += (*ring.get_unchecked(k) - mean).abs();
837                                }
838                            }
839                            abs_sum * inv_p
840                        }
841                    }
842                }
843                _ => {
844                    let incoming = x;
845                    if i < period {
846                        if !incoming.is_nan() {
847                            ring[i] = incoming;
848                            if i + 1 == period {
849                                r_filled = true;
850                                r_head = 0;
851                            }
852                        }
853                        if i + 1 < period {
854                            f64::NAN
855                        } else {
856                            unsafe {
857                                for k in 0..period {
858                                    *scratch.get_unchecked_mut(k) = *ring.get_unchecked(k);
859                                }
860                            }
861                            scratch.sort_by(|a, b| {
862                                a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
863                            });
864                            let median = if period & 1 == 1 {
865                                scratch[period / 2]
866                            } else {
867                                (scratch[period / 2 - 1] + scratch[period / 2]) * 0.5
868                            };
869                            let mut abs_sum = 0.0;
870                            unsafe {
871                                for k in 0..period {
872                                    abs_sum += (*ring.get_unchecked(k) - median).abs();
873                                }
874                            }
875                            abs_sum * inv_p
876                        }
877                    } else {
878                        let leaving = data[i - period];
879                        if incoming.is_nan() || leaving.is_nan() {
880                            r_filled = false;
881                            for j in 0..period {
882                                ring[j] = f64::NAN;
883                            }
884                            f64::NAN
885                        } else {
886                            unsafe {
887                                *ring.get_unchecked_mut(r_head) = incoming;
888                            }
889                            r_head = (r_head + 1) % period;
890                            r_filled = true;
891
892                            unsafe {
893                                for k in 0..period {
894                                    *scratch.get_unchecked_mut(k) =
895                                        *ring.get_unchecked((r_head + k) % period);
896                                }
897                            }
898                            scratch.sort_by(|a, b| {
899                                a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
900                            });
901                            let median = if period & 1 == 1 {
902                                scratch[period / 2]
903                            } else {
904                                (scratch[period / 2 - 1] + scratch[period / 2]) * 0.5
905                            };
906                            let mut abs_sum = 0.0;
907
908                            unsafe {
909                                for k in 0..period {
910                                    abs_sum +=
911                                        (*ring.get_unchecked((r_head + k) % period) - median).abs();
912                                }
913                            }
914                            abs_sum * inv_p
915                        }
916                    }
917                }
918            }
919        };
920
921        let (up_i, dn_i) = if d.is_nan() || dev.is_nan() {
922            (f64::NAN, f64::NAN)
923        } else if d > 0.0 {
924            (dev, 0.0)
925        } else if d < 0.0 {
926            (0.0, dev)
927        } else {
928            (0.0, 0.0)
929        };
930
931        let (up_s, dn_s) = if use_sma {
932            let up_smooth = if up_i.is_nan() {
933                up_sum = 0.0;
934                up_cnt = 0;
935                up_h = 0;
936                f64::NAN
937            } else {
938                if up_cnt < ma_len {
939                    unsafe {
940                        *up_ring.get_unchecked_mut(up_h) = up_i;
941                    }
942                    up_sum += up_i;
943                    up_h = (up_h + 1) % ma_len;
944                    up_cnt += 1;
945                    if up_cnt == ma_len {
946                        up_sum * inv_m
947                    } else {
948                        f64::NAN
949                    }
950                } else {
951                    let old = unsafe { *up_ring.get_unchecked(up_h) };
952                    unsafe {
953                        *up_ring.get_unchecked_mut(up_h) = up_i;
954                    }
955                    up_h = (up_h + 1) % ma_len;
956                    up_sum += up_i - old;
957                    up_sum * inv_m
958                }
959            };
960
961            let dn_smooth = if dn_i.is_nan() {
962                dn_sum = 0.0;
963                dn_cnt = 0;
964                dn_h = 0;
965                f64::NAN
966            } else {
967                if dn_cnt < ma_len {
968                    unsafe {
969                        *dn_ring.get_unchecked_mut(dn_h) = dn_i;
970                    }
971                    dn_sum += dn_i;
972                    dn_h = (dn_h + 1) % ma_len;
973                    dn_cnt += 1;
974                    if dn_cnt == ma_len {
975                        dn_sum * inv_m
976                    } else {
977                        f64::NAN
978                    }
979                } else {
980                    let old = unsafe { *dn_ring.get_unchecked(dn_h) };
981                    unsafe {
982                        *dn_ring.get_unchecked_mut(dn_h) = dn_i;
983                    }
984                    dn_h = (dn_h + 1) % ma_len;
985                    dn_sum += dn_i - old;
986                    dn_sum * inv_m
987                }
988            };
989            (up_smooth, dn_smooth)
990        } else {
991            let up_smooth = if up_i.is_nan() {
992                up_started = false;
993                up_seed_sum = 0.0;
994                up_seed_cnt = 0;
995                f64::NAN
996            } else if !up_started {
997                up_seed_sum += up_i;
998                up_seed_cnt += 1;
999                if up_seed_cnt == ma_len {
1000                    up_prev = up_seed_sum * inv_m;
1001                    up_started = true;
1002                    up_prev
1003                } else {
1004                    f64::NAN
1005                }
1006            } else {
1007                up_prev = alpha.mul_add(up_i, one_m_alpha * up_prev);
1008                up_prev
1009            };
1010            let dn_smooth = if dn_i.is_nan() {
1011                dn_started = false;
1012                dn_seed_sum = 0.0;
1013                dn_seed_cnt = 0;
1014                f64::NAN
1015            } else if !dn_started {
1016                dn_seed_sum += dn_i;
1017                dn_seed_cnt += 1;
1018                if dn_seed_cnt == ma_len {
1019                    dn_prev = dn_seed_sum * inv_m;
1020                    dn_started = true;
1021                    dn_prev
1022                } else {
1023                    f64::NAN
1024                }
1025            } else {
1026                dn_prev = alpha.mul_add(dn_i, one_m_alpha * dn_prev);
1027                dn_prev
1028            };
1029            (up_smooth, dn_smooth)
1030        };
1031
1032        if i >= warmup {
1033            if up_s.is_nan() || dn_s.is_nan() {
1034                out[i] = f64::NAN;
1035            } else {
1036                let denom = up_s + dn_s;
1037                out[i] = if denom.abs() < f64::EPSILON {
1038                    f64::NAN
1039                } else {
1040                    100.0 * (up_s / denom)
1041                };
1042            }
1043        }
1044    }
1045}
1046
1047#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1048#[inline]
1049pub fn rvi_avx512(
1050    data: &[f64],
1051    period: usize,
1052    ma_len: usize,
1053    matype: usize,
1054    devtype: usize,
1055    first: usize,
1056    out: &mut [f64],
1057) {
1058    if period <= 32 {
1059        unsafe { rvi_avx512_short(data, period, ma_len, matype, devtype, first, out) }
1060    } else {
1061        unsafe { rvi_avx512_long(data, period, ma_len, matype, devtype, first, out) }
1062    }
1063}
1064
1065#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1066#[inline]
1067pub unsafe fn rvi_avx2(
1068    data: &[f64],
1069    period: usize,
1070    ma_len: usize,
1071    matype: usize,
1072    devtype: usize,
1073    first: usize,
1074    out: &mut [f64],
1075) {
1076    rvi_scalar(data, period, ma_len, matype, devtype, first, out)
1077}
1078
1079#[inline]
1080pub fn rvi_scalar_opt(
1081    data: &[f64],
1082    period: usize,
1083    ma_len: usize,
1084    matype: usize,
1085    devtype: usize,
1086    first: usize,
1087    out: &mut [f64],
1088) {
1089    debug_assert_eq!(out.len(), data.len());
1090    let n = data.len();
1091    if n == 0 {
1092        return;
1093    }
1094
1095    let warmup = first + period.saturating_sub(1) + ma_len.saturating_sub(1);
1096    let inv_p = 1.0 / (period as f64);
1097    let inv_m = 1.0 / (ma_len as f64);
1098    let use_sma = matype == 0;
1099
1100    let mut up_sum = 0.0f64;
1101    let mut dn_sum = 0.0f64;
1102    let mut up_ring = if use_sma {
1103        vec![0.0f64; ma_len]
1104    } else {
1105        Vec::new()
1106    };
1107    let mut dn_ring = if use_sma {
1108        vec![0.0f64; ma_len]
1109    } else {
1110        Vec::new()
1111    };
1112    let mut up_h: usize = 0;
1113    let mut dn_h: usize = 0;
1114    let mut up_cnt: usize = 0;
1115    let mut dn_cnt: usize = 0;
1116    let alpha = if !use_sma {
1117        2.0 / (ma_len as f64 + 1.0)
1118    } else {
1119        0.0
1120    };
1121    let one_m_alpha = 1.0 - alpha;
1122    let mut up_prev = 0.0f64;
1123    let mut dn_prev = 0.0f64;
1124    let mut up_started = false;
1125    let mut dn_started = false;
1126    let mut up_seed_sum = 0.0f64;
1127    let mut dn_seed_sum = 0.0f64;
1128    let mut up_seed_cnt = 0usize;
1129    let mut dn_seed_cnt = 0usize;
1130
1131    #[inline(always)]
1132    fn bump_idx(idx: &mut usize, len: usize) {
1133        *idx += 1;
1134        if *idx == len {
1135            *idx = 0;
1136        }
1137    }
1138
1139    if devtype == 0 {
1140        let mut prev = data[0];
1141        let mut sum = 0.0f64;
1142        let mut sumsq = 0.0f64;
1143        let mut vflag = vec![0u8; period];
1144        let mut vcnt: usize = 0;
1145        let mut head: usize = 0;
1146        for i in 0..period.min(n) {
1147            let x = unsafe { *data.get_unchecked(i) };
1148            if !x.is_nan() {
1149                sum += x;
1150                sumsq += x * x;
1151                vflag[i] = 1;
1152                vcnt += 1;
1153            }
1154        }
1155        for i in 0..n {
1156            let x = unsafe { *data.get_unchecked(i) };
1157            let d = if i == 0 || x.is_nan() || prev.is_nan() {
1158                f64::NAN
1159            } else {
1160                x - prev
1161            };
1162            prev = x;
1163            let dev = if i + 1 < period {
1164                f64::NAN
1165            } else if i == period - 1 {
1166                if vcnt == period {
1167                    let mean = sum * inv_p;
1168                    let mean_sq = sumsq * inv_p;
1169                    (mean_sq - mean * mean).sqrt()
1170                } else {
1171                    f64::NAN
1172                }
1173            } else {
1174                let leaving_valid = unsafe { *vflag.get_unchecked(head) } != 0;
1175                if leaving_valid {
1176                    let leaving = unsafe { *data.get_unchecked(i - period) };
1177                    sum -= leaving;
1178                    sumsq -= leaving * leaving;
1179                    vcnt -= 1;
1180                }
1181                if !x.is_nan() {
1182                    sum += x;
1183                    sumsq += x * x;
1184                    unsafe { *vflag.get_unchecked_mut(head) = 1 };
1185                    vcnt += 1;
1186                } else {
1187                    unsafe { *vflag.get_unchecked_mut(head) = 0 };
1188                }
1189                bump_idx(&mut head, period);
1190                if vcnt == period {
1191                    let mean = sum * inv_p;
1192                    let mean_sq = sumsq * inv_p;
1193                    (mean_sq - mean * mean).sqrt()
1194                } else {
1195                    f64::NAN
1196                }
1197            };
1198
1199            let (up_i, dn_i) = if d.is_nan() || dev.is_nan() {
1200                (f64::NAN, f64::NAN)
1201            } else if d > 0.0 {
1202                (dev, 0.0)
1203            } else if d < 0.0 {
1204                (0.0, dev)
1205            } else {
1206                (0.0, 0.0)
1207            };
1208            let up_s = if use_sma {
1209                if up_i.is_nan() {
1210                    up_sum = 0.0;
1211                    up_cnt = 0;
1212                    up_h = 0;
1213                    f64::NAN
1214                } else if up_cnt < ma_len {
1215                    unsafe {
1216                        *up_ring.get_unchecked_mut(up_h) = up_i;
1217                    }
1218                    up_sum += up_i;
1219                    bump_idx(&mut up_h, ma_len);
1220                    up_cnt += 1;
1221                    if up_cnt == ma_len {
1222                        up_sum * inv_m
1223                    } else {
1224                        f64::NAN
1225                    }
1226                } else {
1227                    let old = unsafe { *up_ring.get_unchecked(up_h) };
1228                    unsafe {
1229                        *up_ring.get_unchecked_mut(up_h) = up_i;
1230                    }
1231                    up_sum += up_i - old;
1232                    bump_idx(&mut up_h, ma_len);
1233                    up_sum * inv_m
1234                }
1235            } else {
1236                if up_i.is_nan() {
1237                    up_started = false;
1238                    up_seed_sum = 0.0;
1239                    up_seed_cnt = 0;
1240                    f64::NAN
1241                } else if !up_started {
1242                    up_seed_sum += up_i;
1243                    up_seed_cnt += 1;
1244                    if up_seed_cnt == ma_len {
1245                        up_prev = up_seed_sum * inv_m;
1246                        up_started = true;
1247                        up_prev
1248                    } else {
1249                        f64::NAN
1250                    }
1251                } else {
1252                    up_prev = alpha.mul_add(up_i, one_m_alpha * up_prev);
1253                    up_prev
1254                }
1255            };
1256            let dn_s = if use_sma {
1257                if dn_i.is_nan() {
1258                    dn_sum = 0.0;
1259                    dn_cnt = 0;
1260                    dn_h = 0;
1261                    f64::NAN
1262                } else if dn_cnt < ma_len {
1263                    unsafe {
1264                        *dn_ring.get_unchecked_mut(dn_h) = dn_i;
1265                    }
1266                    dn_sum += dn_i;
1267                    bump_idx(&mut dn_h, ma_len);
1268                    dn_cnt += 1;
1269                    if dn_cnt == ma_len {
1270                        dn_sum * inv_m
1271                    } else {
1272                        f64::NAN
1273                    }
1274                } else {
1275                    let old = unsafe { *dn_ring.get_unchecked(dn_h) };
1276                    unsafe {
1277                        *dn_ring.get_unchecked_mut(dn_h) = dn_i;
1278                    }
1279                    dn_sum += dn_i - old;
1280                    bump_idx(&mut dn_h, ma_len);
1281                    dn_sum * inv_m
1282                }
1283            } else {
1284                if dn_i.is_nan() {
1285                    dn_started = false;
1286                    dn_seed_sum = 0.0;
1287                    dn_seed_cnt = 0;
1288                    f64::NAN
1289                } else if !dn_started {
1290                    dn_seed_sum += dn_i;
1291                    dn_seed_cnt += 1;
1292                    if dn_seed_cnt == ma_len {
1293                        dn_prev = dn_seed_sum * inv_m;
1294                        dn_started = true;
1295                        dn_prev
1296                    } else {
1297                        f64::NAN
1298                    }
1299                } else {
1300                    dn_prev = alpha.mul_add(dn_i, one_m_alpha * dn_prev);
1301                    dn_prev
1302                }
1303            };
1304            if i >= warmup {
1305                if up_s.is_nan() || dn_s.is_nan() {
1306                    out[i] = f64::NAN;
1307                } else {
1308                    let denom = up_s + dn_s;
1309                    out[i] = if denom.abs() < f64::EPSILON {
1310                        f64::NAN
1311                    } else {
1312                        100.0 * (up_s / denom)
1313                    };
1314                }
1315            }
1316        }
1317        return;
1318    }
1319
1320    let mut prev = data[0];
1321    let mut ring = vec![0.0f64; period];
1322    let mut head: usize = 0;
1323    let mut filled_cnt: usize = 0;
1324    let mut ring_sum = 0.0f64;
1325    let mut scratch = if devtype == 2 {
1326        vec![0.0f64; period]
1327    } else {
1328        Vec::new()
1329    };
1330
1331    #[inline(always)]
1332    fn abs_dev_mean_unrolled(r: &[f64], mean: f64) -> f64 {
1333        let mut acc = 0.0f64;
1334        let len = r.len();
1335        let mut k = 0usize;
1336        while k + 4 <= len {
1337            unsafe {
1338                let a0 = *r.get_unchecked(k) - mean;
1339                let a1 = *r.get_unchecked(k + 1) - mean;
1340                let a2 = *r.get_unchecked(k + 2) - mean;
1341                let a3 = *r.get_unchecked(k + 3) - mean;
1342                acc += a0.abs() + a1.abs() + a2.abs() + a3.abs();
1343            }
1344            k += 4;
1345        }
1346        while k < len {
1347            unsafe {
1348                acc += (*r.get_unchecked(k) - mean).abs();
1349            }
1350            k += 1;
1351        }
1352        acc
1353    }
1354
1355    for i in 0..n {
1356        let x = unsafe { *data.get_unchecked(i) };
1357        let d = if i == 0 || x.is_nan() || prev.is_nan() {
1358            f64::NAN
1359        } else {
1360            x - prev
1361        };
1362        prev = x;
1363        let dev = if filled_cnt < period {
1364            if !x.is_nan() {
1365                unsafe {
1366                    *ring.get_unchecked_mut(head) = x;
1367                }
1368                ring_sum += x;
1369                bump_idx(&mut head, period);
1370                filled_cnt += 1;
1371                if filled_cnt == period {
1372                    if devtype == 1 {
1373                        let mean = ring_sum * inv_p;
1374                        abs_dev_mean_unrolled(&ring, mean) * inv_p
1375                    } else {
1376                        unsafe {
1377                            core::ptr::copy_nonoverlapping(
1378                                ring.as_ptr(),
1379                                scratch.as_mut_ptr(),
1380                                period,
1381                            );
1382                        }
1383                        let mid = period >> 1;
1384                        let cmp = |a: &f64, b: &f64| {
1385                            a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
1386                        };
1387                        let median = if period & 1 == 1 {
1388                            let (_lt, m, _gt) = scratch.select_nth_unstable_by(mid, cmp);
1389                            *m
1390                        } else {
1391                            let m_hi_val = {
1392                                let (_lt, m_hi, _gt) = scratch.select_nth_unstable_by(mid, cmp);
1393                                *m_hi
1394                            };
1395                            let lo = {
1396                                let lower = &mut scratch[..mid];
1397                                let (_lt2, m_lo, _gt2) = lower.select_nth_unstable_by(mid - 1, cmp);
1398                                *m_lo
1399                            };
1400                            (lo + m_hi_val) * 0.5
1401                        };
1402                        abs_dev_mean_unrolled(&ring, median) * inv_p
1403                    }
1404                } else {
1405                    f64::NAN
1406                }
1407            } else {
1408                head = 0;
1409                filled_cnt = 0;
1410                ring_sum = 0.0;
1411                f64::NAN
1412            }
1413        } else {
1414            if x.is_nan() {
1415                head = 0;
1416                filled_cnt = 0;
1417                ring_sum = 0.0;
1418                f64::NAN
1419            } else {
1420                let leaving = unsafe { *ring.get_unchecked(head) };
1421                unsafe {
1422                    *ring.get_unchecked_mut(head) = x;
1423                }
1424                bump_idx(&mut head, period);
1425                ring_sum += x - leaving;
1426                if devtype == 1 {
1427                    let mean = ring_sum * inv_p;
1428                    abs_dev_mean_unrolled(&ring, mean) * inv_p
1429                } else {
1430                    unsafe {
1431                        core::ptr::copy_nonoverlapping(ring.as_ptr(), scratch.as_mut_ptr(), period);
1432                    }
1433                    let mid = period >> 1;
1434                    let cmp =
1435                        |a: &f64, b: &f64| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal);
1436                    let median = if period & 1 == 1 {
1437                        let (_lt, m, _gt) = scratch.select_nth_unstable_by(mid, cmp);
1438                        *m
1439                    } else {
1440                        let m_hi_val = {
1441                            let (_lt, m_hi, _gt) = scratch.select_nth_unstable_by(mid, cmp);
1442                            *m_hi
1443                        };
1444                        let lo = {
1445                            let lower = &mut scratch[..mid];
1446                            let (_lt2, m_lo, _gt2) = lower.select_nth_unstable_by(mid - 1, cmp);
1447                            *m_lo
1448                        };
1449                        (lo + m_hi_val) * 0.5
1450                    };
1451                    abs_dev_mean_unrolled(&ring, median) * inv_p
1452                }
1453            }
1454        };
1455
1456        let (up_i, dn_i) = if d.is_nan() || dev.is_nan() {
1457            (f64::NAN, f64::NAN)
1458        } else if d > 0.0 {
1459            (dev, 0.0)
1460        } else if d < 0.0 {
1461            (0.0, dev)
1462        } else {
1463            (0.0, 0.0)
1464        };
1465        let up_s = if use_sma {
1466            if up_i.is_nan() {
1467                up_sum = 0.0;
1468                up_cnt = 0;
1469                up_h = 0;
1470                f64::NAN
1471            } else if up_cnt < ma_len {
1472                unsafe {
1473                    *up_ring.get_unchecked_mut(up_h) = up_i;
1474                }
1475                up_sum += up_i;
1476                bump_idx(&mut up_h, ma_len);
1477                up_cnt += 1;
1478                if up_cnt == ma_len {
1479                    up_sum * inv_m
1480                } else {
1481                    f64::NAN
1482                }
1483            } else {
1484                let old = unsafe { *up_ring.get_unchecked(up_h) };
1485                unsafe {
1486                    *up_ring.get_unchecked_mut(up_h) = up_i;
1487                }
1488                up_sum += up_i - old;
1489                bump_idx(&mut up_h, ma_len);
1490                up_sum * inv_m
1491            }
1492        } else {
1493            if up_i.is_nan() {
1494                up_started = false;
1495                up_seed_sum = 0.0;
1496                up_seed_cnt = 0;
1497                f64::NAN
1498            } else if !up_started {
1499                up_seed_sum += up_i;
1500                up_seed_cnt += 1;
1501                if up_seed_cnt == ma_len {
1502                    up_prev = up_seed_sum * inv_m;
1503                    up_started = true;
1504                    up_prev
1505                } else {
1506                    f64::NAN
1507                }
1508            } else {
1509                up_prev = alpha.mul_add(up_i, one_m_alpha * up_prev);
1510                up_prev
1511            }
1512        };
1513        let dn_s = if use_sma {
1514            if dn_i.is_nan() {
1515                dn_sum = 0.0;
1516                dn_cnt = 0;
1517                dn_h = 0;
1518                f64::NAN
1519            } else if dn_cnt < ma_len {
1520                unsafe {
1521                    *dn_ring.get_unchecked_mut(dn_h) = dn_i;
1522                }
1523                dn_sum += dn_i;
1524                bump_idx(&mut dn_h, ma_len);
1525                dn_cnt += 1;
1526                if dn_cnt == ma_len {
1527                    dn_sum * inv_m
1528                } else {
1529                    f64::NAN
1530                }
1531            } else {
1532                let old = unsafe { *dn_ring.get_unchecked(dn_h) };
1533                unsafe {
1534                    *dn_ring.get_unchecked_mut(dn_h) = dn_i;
1535                }
1536                dn_sum += dn_i - old;
1537                bump_idx(&mut dn_h, ma_len);
1538                dn_sum * inv_m
1539            }
1540        } else {
1541            if dn_i.is_nan() {
1542                dn_started = false;
1543                dn_seed_sum = 0.0;
1544                dn_seed_cnt = 0;
1545                f64::NAN
1546            } else if !dn_started {
1547                dn_seed_sum += dn_i;
1548                dn_seed_cnt += 1;
1549                if dn_seed_cnt == ma_len {
1550                    dn_prev = dn_seed_sum * inv_m;
1551                    dn_started = true;
1552                    dn_prev
1553                } else {
1554                    f64::NAN
1555                }
1556            } else {
1557                dn_prev = alpha.mul_add(dn_i, one_m_alpha * dn_prev);
1558                dn_prev
1559            }
1560        };
1561        if i >= warmup {
1562            if up_s.is_nan() || dn_s.is_nan() {
1563                out[i] = f64::NAN;
1564            } else {
1565                let denom = up_s + dn_s;
1566                out[i] = if denom.abs() < f64::EPSILON {
1567                    f64::NAN
1568                } else {
1569                    100.0 * (up_s / denom)
1570                };
1571            }
1572        }
1573    }
1574}
1575
1576#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1577#[inline]
1578pub unsafe fn rvi_avx512_short(
1579    data: &[f64],
1580    period: usize,
1581    ma_len: usize,
1582    matype: usize,
1583    devtype: usize,
1584    first: usize,
1585    out: &mut [f64],
1586) {
1587    rvi_scalar(data, period, ma_len, matype, devtype, first, out)
1588}
1589
1590#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1591#[inline]
1592pub unsafe fn rvi_avx512_long(
1593    data: &[f64],
1594    period: usize,
1595    ma_len: usize,
1596    matype: usize,
1597    devtype: usize,
1598    first: usize,
1599    out: &mut [f64],
1600) {
1601    rvi_scalar(data, period, ma_len, matype, devtype, first, out)
1602}
1603
1604#[derive(Copy, Clone, Debug)]
1605struct HeapItem {
1606    val: f64,
1607    id: usize,
1608}
1609impl PartialEq for HeapItem {
1610    #[inline(always)]
1611    fn eq(&self, other: &Self) -> bool {
1612        self.id == other.id && self.val.to_bits() == other.val.to_bits()
1613    }
1614}
1615impl Eq for HeapItem {}
1616impl Ord for HeapItem {
1617    #[inline(always)]
1618    fn cmp(&self, other: &Self) -> Ordering {
1619        match self.val.partial_cmp(&other.val).unwrap() {
1620            Ordering::Equal => self.id.cmp(&other.id),
1621            ord => ord,
1622        }
1623    }
1624}
1625impl PartialOrd for HeapItem {
1626    #[inline(always)]
1627    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1628        Some(self.cmp(other))
1629    }
1630}
1631
1632#[derive(Clone, Debug)]
1633pub struct RviStream {
1634    period: usize,
1635    ma_len: usize,
1636    matype: usize,
1637    devtype: usize,
1638
1639    inv_p: f64,
1640    inv_m: f64,
1641    use_sma: bool,
1642    alpha: f64,
1643    one_m_alpha: f64,
1644
1645    prev_x: f64,
1646    have_prev: bool,
1647
1648    win: Vec<f64>,
1649    head: usize,
1650    filled: usize,
1651
1652    sum: f64,
1653    sumsq: f64,
1654
1655    mad_sum: f64,
1656
1657    left: BinaryHeap<HeapItem>,
1658    right: BinaryHeap<Reverse<HeapItem>>,
1659    side_of_id: Vec<u8>,
1660    deleted: Vec<u8>,
1661    n_left: usize,
1662    n_right: usize,
1663    s_left: f64,
1664    s_right: f64,
1665
1666    up_ring: Vec<f64>,
1667    dn_ring: Vec<f64>,
1668    up_sum: f64,
1669    dn_sum: f64,
1670    up_h: usize,
1671    dn_h: usize,
1672    up_cnt: usize,
1673    dn_cnt: usize,
1674
1675    up_prev: f64,
1676    dn_prev: f64,
1677    up_started: bool,
1678    dn_started: bool,
1679    up_seed_sum: f64,
1680    dn_seed_sum: f64,
1681    up_seed_cnt: usize,
1682    dn_seed_cnt: usize,
1683}
1684
1685impl RviStream {
1686    pub fn try_new(params: RviParams) -> Result<Self, RviError> {
1687        let period = params.period.unwrap_or(10);
1688        let ma_len = params.ma_len.unwrap_or(14);
1689        let matype = params.matype.unwrap_or(1);
1690        let devtype = params.devtype.unwrap_or(0);
1691        if period == 0 || ma_len == 0 {
1692            return Err(RviError::InvalidPeriod {
1693                period,
1694                ma_len,
1695                data_len: 0,
1696            });
1697        }
1698
1699        let inv_p = 1.0 / period as f64;
1700        let inv_m = 1.0 / ma_len as f64;
1701        let use_sma = matype == 0;
1702        let alpha = if use_sma {
1703            0.0
1704        } else {
1705            2.0 / (ma_len as f64 + 1.0)
1706        };
1707        let one_m_alpha = 1.0 - alpha;
1708
1709        Ok(Self {
1710            period,
1711            ma_len,
1712            matype,
1713            devtype,
1714
1715            inv_p,
1716            inv_m,
1717            use_sma,
1718            alpha,
1719            one_m_alpha,
1720
1721            prev_x: f64::NAN,
1722            have_prev: false,
1723
1724            win: vec![f64::NAN; period],
1725            head: 0,
1726            filled: 0,
1727
1728            sum: 0.0,
1729            sumsq: 0.0,
1730
1731            mad_sum: 0.0,
1732
1733            left: BinaryHeap::new(),
1734            right: BinaryHeap::new(),
1735            side_of_id: vec![0; period],
1736            deleted: vec![1; period],
1737            n_left: 0,
1738            n_right: 0,
1739            s_left: 0.0,
1740            s_right: 0.0,
1741
1742            up_ring: if use_sma {
1743                vec![0.0; ma_len]
1744            } else {
1745                Vec::new()
1746            },
1747            dn_ring: if use_sma {
1748                vec![0.0; ma_len]
1749            } else {
1750                Vec::new()
1751            },
1752            up_sum: 0.0,
1753            dn_sum: 0.0,
1754            up_h: 0,
1755            dn_h: 0,
1756            up_cnt: 0,
1757            dn_cnt: 0,
1758            up_prev: 0.0,
1759            dn_prev: 0.0,
1760            up_started: false,
1761            dn_started: false,
1762            up_seed_sum: 0.0,
1763            dn_seed_sum: 0.0,
1764            up_seed_cnt: 0,
1765            dn_seed_cnt: 0,
1766        })
1767    }
1768
1769    #[inline(always)]
1770    fn reset_smoothing(&mut self) {
1771        if self.use_sma {
1772            self.up_sum = 0.0;
1773            self.dn_sum = 0.0;
1774            self.up_h = 0;
1775            self.dn_h = 0;
1776            self.up_cnt = 0;
1777            self.dn_cnt = 0;
1778        }
1779        self.up_prev = 0.0;
1780        self.dn_prev = 0.0;
1781        self.up_started = false;
1782        self.dn_started = false;
1783        self.up_seed_sum = 0.0;
1784        self.dn_seed_sum = 0.0;
1785        self.up_seed_cnt = 0;
1786        self.dn_seed_cnt = 0;
1787    }
1788
1789    #[inline(always)]
1790    fn reset_all(&mut self) {
1791        self.prev_x = f64::NAN;
1792        self.have_prev = false;
1793        self.head = 0;
1794        self.filled = 0;
1795        self.sum = 0.0;
1796        self.sumsq = 0.0;
1797        self.mad_sum = 0.0;
1798        for i in 0..self.period {
1799            self.win[i] = f64::NAN;
1800            self.deleted[i] = 1;
1801        }
1802        self.left.clear();
1803        self.right.clear();
1804        self.n_left = 0;
1805        self.n_right = 0;
1806        self.s_left = 0.0;
1807        self.s_right = 0.0;
1808        self.reset_smoothing();
1809    }
1810
1811    #[inline(always)]
1812    fn prune_left(&mut self) {
1813        while let Some(top) = self.left.peek() {
1814            if self.deleted[top.id] != 0 {
1815                self.left.pop();
1816            } else {
1817                break;
1818            }
1819        }
1820    }
1821    #[inline(always)]
1822    fn prune_right(&mut self) {
1823        while let Some(Reverse(top)) = self.right.peek() {
1824            if self.deleted[top.id] != 0 {
1825                self.right.pop();
1826            } else {
1827                break;
1828            }
1829        }
1830    }
1831    #[inline(always)]
1832    fn rebalance(&mut self) {
1833        self.prune_left();
1834        self.prune_right();
1835        if self.n_left > self.n_right + 1 {
1836            let item = self.left.pop().unwrap();
1837            self.prune_left();
1838            self.n_left -= 1;
1839            self.s_left -= item.val;
1840            self.n_right += 1;
1841            self.s_right += item.val;
1842            self.side_of_id[item.id] = 1;
1843            self.right.push(Reverse(item));
1844            self.prune_right();
1845        } else if self.n_left < self.n_right {
1846            let Reverse(item) = self.right.pop().unwrap();
1847            self.prune_right();
1848            self.n_right -= 1;
1849            self.s_right -= item.val;
1850            self.n_left += 1;
1851            self.s_left += item.val;
1852            self.side_of_id[item.id] = 0;
1853            self.left.push(item);
1854            self.prune_left();
1855        }
1856    }
1857    #[inline(always)]
1858    fn median_insert(&mut self, id: usize, val: f64) {
1859        self.prune_left();
1860        if self.n_left == 0 || self.left.peek().map(|t| val <= t.val).unwrap_or(true) {
1861            self.left.push(HeapItem { val, id });
1862            self.side_of_id[id] = 0;
1863            self.deleted[id] = 0;
1864            self.n_left += 1;
1865            self.s_left += val;
1866        } else {
1867            self.right.push(Reverse(HeapItem { val, id }));
1868            self.side_of_id[id] = 1;
1869            self.deleted[id] = 0;
1870            self.n_right += 1;
1871            self.s_right += val;
1872        }
1873        self.rebalance();
1874    }
1875    #[inline(always)]
1876    fn median_remove(&mut self, id: usize, val: f64) {
1877        self.deleted[id] = 1;
1878        if self.side_of_id[id] == 0 {
1879            if self.n_left > 0 {
1880                self.n_left -= 1;
1881                self.s_left -= val;
1882            }
1883        } else {
1884            if self.n_right > 0 {
1885                self.n_right -= 1;
1886                self.s_right -= val;
1887            }
1888        }
1889        self.rebalance();
1890    }
1891    #[inline(always)]
1892    fn median_value(&mut self) -> Option<f64> {
1893        self.prune_left();
1894        self.left.peek().map(|t| t.val)
1895    }
1896    #[inline(always)]
1897    fn mean_abs_dev_about_median(&mut self, m: f64) -> f64 {
1898        let l = self.n_left as f64;
1899        let r = self.n_right as f64;
1900        let l1 = m * l - self.s_left + self.s_right - m * r;
1901        l1 * self.inv_p
1902    }
1903
1904    #[inline(always)]
1905    fn stddev_current(&self) -> f64 {
1906        let mean = self.sum * self.inv_p;
1907        let mean_sq = self.sumsq * self.inv_p;
1908        (mean_sq - mean * mean).sqrt()
1909    }
1910
1911    #[inline(always)]
1912    fn push_sma(
1913        sum: &mut f64,
1914        ring: &mut [f64],
1915        head: &mut usize,
1916        cnt: &mut usize,
1917        inv_m: f64,
1918        x: f64,
1919    ) -> Option<f64> {
1920        if *cnt < ring.len() {
1921            ring[*head] = x;
1922            *sum += x;
1923            *head += 1;
1924            if *head == ring.len() {
1925                *head = 0;
1926            }
1927            *cnt += 1;
1928            if *cnt == ring.len() {
1929                Some(*sum * inv_m)
1930            } else {
1931                None
1932            }
1933        } else {
1934            let old = ring[*head];
1935            ring[*head] = x;
1936            *head += 1;
1937            if *head == ring.len() {
1938                *head = 0;
1939            }
1940            *sum += x - old;
1941            Some(*sum * inv_m)
1942        }
1943    }
1944
1945    #[inline(always)]
1946    fn push_ema(
1947        prev: &mut f64,
1948        started: &mut bool,
1949        seed_sum: &mut f64,
1950        seed_cnt: &mut usize,
1951        ma_len: usize,
1952        inv_m: f64,
1953        alpha: f64,
1954        one_m_alpha: f64,
1955        x: f64,
1956    ) -> Option<f64> {
1957        if !*started {
1958            *seed_sum += x;
1959            *seed_cnt += 1;
1960            if *seed_cnt == ma_len {
1961                *prev = *seed_sum * inv_m;
1962                *started = true;
1963                Some(*prev)
1964            } else {
1965                None
1966            }
1967        } else {
1968            *prev = alpha.mul_add(x, one_m_alpha * *prev);
1969            Some(*prev)
1970        }
1971    }
1972
1973    #[inline(always)]
1974    fn reset_smoothers_on_gap(&mut self) {
1975        self.reset_smoothing();
1976    }
1977
1978    #[inline(always)]
1979    fn combine_rvi(us: Option<f64>, ds: Option<f64>) -> Option<f64> {
1980        match (us, ds) {
1981            (Some(u), Some(d)) => {
1982                let denom = u + d;
1983                if denom.abs() < f64::EPSILON {
1984                    None
1985                } else {
1986                    Some(100.0 * (u / denom))
1987                }
1988            }
1989            _ => None,
1990        }
1991    }
1992
1993    #[inline(always)]
1994    pub fn update(&mut self, value: f64) -> Option<f64> {
1995        if value.is_nan() {
1996            self.reset_all();
1997            return None;
1998        }
1999
2000        let d = if self.have_prev {
2001            value - self.prev_x
2002        } else {
2003            f64::NAN
2004        };
2005        self.prev_x = value;
2006        self.have_prev = true;
2007
2008        let id = self.head;
2009        if self.filled < self.period {
2010            self.win[id] = value;
2011            self.head += 1;
2012            if self.head == self.period {
2013                self.head = 0;
2014            }
2015            self.filled += 1;
2016            match self.devtype {
2017                0 => {
2018                    self.sum += value;
2019                    self.sumsq += value * value;
2020                }
2021                1 => {
2022                    self.mad_sum += value;
2023                }
2024                2 => {
2025                    self.median_insert(id, value);
2026                }
2027                _ => {}
2028            }
2029        } else {
2030            let leaving = self.win[id];
2031            self.win[id] = value;
2032            self.head += 1;
2033            if self.head == self.period {
2034                self.head = 0;
2035            }
2036            match self.devtype {
2037                0 => {
2038                    self.sum += value - leaving;
2039                    self.sumsq += value * value - leaving * leaving;
2040                }
2041                1 => {
2042                    self.mad_sum += value - leaving;
2043                }
2044                2 => {
2045                    self.median_remove(id, leaving);
2046                    self.median_insert(id, value);
2047                }
2048                _ => {}
2049            }
2050        }
2051
2052        if self.filled < self.period {
2053            self.reset_smoothers_on_gap();
2054            return None;
2055        }
2056
2057        let dev = match self.devtype {
2058            0 => {
2059                let sd = self.stddev_current();
2060                if !sd.is_finite() {
2061                    self.reset_smoothers_on_gap();
2062                    return None;
2063                }
2064                sd
2065            }
2066            1 => {
2067                let mean = self.mad_sum * self.inv_p;
2068                let mut abs_sum = 0.0;
2069                for k in 0..self.period {
2070                    abs_sum += (self.win[k] - mean).abs();
2071                }
2072                abs_sum * self.inv_p
2073            }
2074            2 => {
2075                if let Some(med) = self.median_value() {
2076                    self.mean_abs_dev_about_median(med)
2077                } else {
2078                    self.reset_smoothers_on_gap();
2079                    return None;
2080                }
2081            }
2082            _ => unreachable!(),
2083        };
2084
2085        if !d.is_finite() || !dev.is_finite() {
2086            self.reset_smoothers_on_gap();
2087            return None;
2088        }
2089        let (up_i, dn_i) = if d > 0.0 {
2090            (dev, 0.0)
2091        } else if d < 0.0 {
2092            (0.0, dev)
2093        } else {
2094            (0.0, 0.0)
2095        };
2096
2097        let (up_s, dn_s) = if self.use_sma {
2098            let up_s = Self::push_sma(
2099                &mut self.up_sum,
2100                &mut self.up_ring,
2101                &mut self.up_h,
2102                &mut self.up_cnt,
2103                self.inv_m,
2104                up_i,
2105            );
2106            let dn_s = Self::push_sma(
2107                &mut self.dn_sum,
2108                &mut self.dn_ring,
2109                &mut self.dn_h,
2110                &mut self.dn_cnt,
2111                self.inv_m,
2112                dn_i,
2113            );
2114            (up_s, dn_s)
2115        } else {
2116            let up_s = Self::push_ema(
2117                &mut self.up_prev,
2118                &mut self.up_started,
2119                &mut self.up_seed_sum,
2120                &mut self.up_seed_cnt,
2121                self.ma_len,
2122                self.inv_m,
2123                self.alpha,
2124                self.one_m_alpha,
2125                up_i,
2126            );
2127            let dn_s = Self::push_ema(
2128                &mut self.dn_prev,
2129                &mut self.dn_started,
2130                &mut self.dn_seed_sum,
2131                &mut self.dn_seed_cnt,
2132                self.ma_len,
2133                self.inv_m,
2134                self.alpha,
2135                self.one_m_alpha,
2136                dn_i,
2137            );
2138            (up_s, dn_s)
2139        };
2140
2141        Self::combine_rvi(up_s, dn_s)
2142    }
2143}
2144
2145#[derive(Clone, Debug)]
2146pub struct RviBatchRange {
2147    pub period: (usize, usize, usize),
2148    pub ma_len: (usize, usize, usize),
2149    pub matype: (usize, usize, usize),
2150    pub devtype: (usize, usize, usize),
2151}
2152
2153impl Default for RviBatchRange {
2154    fn default() -> Self {
2155        Self {
2156            period: (10, 259, 1),
2157            ma_len: (14, 14, 0),
2158            matype: (1, 1, 0),
2159            devtype: (0, 0, 0),
2160        }
2161    }
2162}
2163
2164#[derive(Clone, Debug, Default)]
2165pub struct RviBatchBuilder {
2166    range: RviBatchRange,
2167    kernel: Kernel,
2168}
2169
2170impl RviBatchBuilder {
2171    pub fn new() -> Self {
2172        Self::default()
2173    }
2174    pub fn kernel(mut self, k: Kernel) -> Self {
2175        self.kernel = k;
2176        self
2177    }
2178    #[inline]
2179    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
2180        self.range.period = (start, end, step);
2181        self
2182    }
2183    #[inline]
2184    pub fn period_static(mut self, p: usize) -> Self {
2185        self.range.period = (p, p, 0);
2186        self
2187    }
2188    #[inline]
2189    pub fn ma_len_range(mut self, start: usize, end: usize, step: usize) -> Self {
2190        self.range.ma_len = (start, end, step);
2191        self
2192    }
2193    #[inline]
2194    pub fn ma_len_static(mut self, p: usize) -> Self {
2195        self.range.ma_len = (p, p, 0);
2196        self
2197    }
2198    #[inline]
2199    pub fn matype_range(mut self, start: usize, end: usize, step: usize) -> Self {
2200        self.range.matype = (start, end, step);
2201        self
2202    }
2203    #[inline]
2204    pub fn matype_static(mut self, p: usize) -> Self {
2205        self.range.matype = (p, p, 0);
2206        self
2207    }
2208    #[inline]
2209    pub fn devtype_range(mut self, start: usize, end: usize, step: usize) -> Self {
2210        self.range.devtype = (start, end, step);
2211        self
2212    }
2213    #[inline]
2214    pub fn devtype_static(mut self, p: usize) -> Self {
2215        self.range.devtype = (p, p, 0);
2216        self
2217    }
2218
2219    pub fn apply_slice(self, data: &[f64]) -> Result<RviBatchOutput, RviError> {
2220        rvi_batch_with_kernel(data, &self.range, self.kernel)
2221    }
2222
2223    pub fn with_default_slice(data: &[f64], k: Kernel) -> Result<RviBatchOutput, RviError> {
2224        RviBatchBuilder::new().kernel(k).apply_slice(data)
2225    }
2226
2227    pub fn apply_candles(self, c: &Candles, src: &str) -> Result<RviBatchOutput, RviError> {
2228        let slice = source_type(c, src);
2229        self.apply_slice(slice)
2230    }
2231
2232    pub fn with_default_candles(c: &Candles) -> Result<RviBatchOutput, RviError> {
2233        RviBatchBuilder::new()
2234            .kernel(Kernel::Auto)
2235            .apply_candles(c, "close")
2236    }
2237}
2238
2239#[derive(Clone, Debug)]
2240pub struct RviBatchOutput {
2241    pub values: Vec<f64>,
2242    pub combos: Vec<RviParams>,
2243    pub rows: usize,
2244    pub cols: usize,
2245}
2246
2247impl RviBatchOutput {
2248    pub fn row_for_params(&self, p: &RviParams) -> Option<usize> {
2249        self.combos.iter().position(|c| {
2250            c.period.unwrap_or(10) == p.period.unwrap_or(10)
2251                && c.ma_len.unwrap_or(14) == p.ma_len.unwrap_or(14)
2252                && c.matype.unwrap_or(1) == p.matype.unwrap_or(1)
2253                && c.devtype.unwrap_or(0) == p.devtype.unwrap_or(0)
2254        })
2255    }
2256    pub fn values_for(&self, p: &RviParams) -> Option<&[f64]> {
2257        self.row_for_params(p).map(|row| {
2258            let start = row * self.cols;
2259            &self.values[start..start + self.cols]
2260        })
2261    }
2262}
2263
2264#[inline(always)]
2265fn expand_grid(r: &RviBatchRange) -> Result<Vec<RviParams>, RviError> {
2266    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, RviError> {
2267        let s = start as i128;
2268        let e = end as i128;
2269        let st = step as i128;
2270        if step == 0 || start == end {
2271            return Ok(vec![start]);
2272        }
2273        let mut v = Vec::new();
2274        if start <= end {
2275            let stp = step.max(1);
2276            let mut cur = start;
2277            while cur <= end {
2278                v.push(cur);
2279                cur = match cur.checked_add(stp) {
2280                    Some(n) => n,
2281                    None => break,
2282                };
2283            }
2284        } else {
2285            let stp = step.max(1);
2286            let mut cur = start;
2287            loop {
2288                v.push(cur);
2289                if cur <= end {
2290                    break;
2291                }
2292                cur = match cur.checked_sub(stp) {
2293                    Some(n) => n,
2294                    None => break,
2295                };
2296                if cur < end {
2297                    break;
2298                }
2299            }
2300        }
2301        if v.is_empty() {
2302            Err(RviError::InvalidRange {
2303                start: s,
2304                end: e,
2305                step: st,
2306            })
2307        } else {
2308            Ok(v)
2309        }
2310    }
2311
2312    let periods = axis_usize(r.period)?;
2313    let ma_lens = axis_usize(r.ma_len)?;
2314    let matypes = axis_usize(r.matype)?;
2315    let devtypes = axis_usize(r.devtype)?;
2316
2317    let cap = periods
2318        .len()
2319        .checked_mul(ma_lens.len())
2320        .and_then(|x| x.checked_mul(matypes.len()))
2321        .and_then(|x| x.checked_mul(devtypes.len()))
2322        .ok_or_else(|| RviError::InvalidInput("parameter grid too large".into()))?;
2323
2324    let mut out = Vec::with_capacity(cap);
2325    for &p in &periods {
2326        for &m in &ma_lens {
2327            for &t in &matypes {
2328                for &d in &devtypes {
2329                    out.push(RviParams {
2330                        period: Some(p),
2331                        ma_len: Some(m),
2332                        matype: Some(t),
2333                        devtype: Some(d),
2334                    });
2335                }
2336            }
2337        }
2338    }
2339    Ok(out)
2340}
2341
2342#[inline(always)]
2343pub fn rvi_batch_with_kernel(
2344    data: &[f64],
2345    sweep: &RviBatchRange,
2346    k: Kernel,
2347) -> Result<RviBatchOutput, RviError> {
2348    let kernel = match k {
2349        Kernel::Auto => detect_best_batch_kernel(),
2350        other if other.is_batch() => other,
2351        _ => return Err(RviError::InvalidKernelForBatch(k)),
2352    };
2353    let simd = match kernel {
2354        Kernel::Avx512Batch => Kernel::Avx512,
2355        Kernel::Avx2Batch => Kernel::Avx2,
2356        Kernel::ScalarBatch => Kernel::Scalar,
2357        _ => unreachable!(),
2358    };
2359    rvi_batch_par_slice(data, sweep, simd)
2360}
2361
2362#[inline(always)]
2363pub fn rvi_batch_slice(
2364    data: &[f64],
2365    sweep: &RviBatchRange,
2366    kern: Kernel,
2367) -> Result<RviBatchOutput, RviError> {
2368    rvi_batch_inner(data, sweep, kern, false)
2369}
2370
2371#[inline(always)]
2372pub fn rvi_batch_par_slice(
2373    data: &[f64],
2374    sweep: &RviBatchRange,
2375    kern: Kernel,
2376) -> Result<RviBatchOutput, RviError> {
2377    rvi_batch_inner(data, sweep, kern, true)
2378}
2379
2380#[inline(always)]
2381fn rvi_batch_inner(
2382    data: &[f64],
2383    sweep: &RviBatchRange,
2384    kern: Kernel,
2385    parallel: bool,
2386) -> Result<RviBatchOutput, RviError> {
2387    if data.is_empty() {
2388        return Err(RviError::EmptyInputData);
2389    }
2390    let combos = expand_grid(sweep)?;
2391    if combos.is_empty() {
2392        return Err(RviError::InvalidRange {
2393            start: 0,
2394            end: 0,
2395            step: 0,
2396        });
2397    }
2398    let first = data
2399        .iter()
2400        .position(|x| !x.is_nan())
2401        .ok_or(RviError::AllValuesNaN)?;
2402    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
2403    let max_m = combos.iter().map(|c| c.ma_len.unwrap()).max().unwrap();
2404    let need = max_p.saturating_sub(1) + max_m.saturating_sub(1) + 1;
2405    if (data.len() - first) <= (max_p.saturating_sub(1) + max_m.saturating_sub(1)) {
2406        return Err(RviError::NotEnoughValidData {
2407            needed: need,
2408            valid: data.len() - first,
2409        });
2410    }
2411    let rows = combos.len();
2412    let cols = data.len();
2413    let _ = rows
2414        .checked_mul(cols)
2415        .ok_or_else(|| RviError::InvalidInput("rows * cols overflow".into()))?;
2416
2417    let mut buf_mu = make_uninit_matrix(rows, cols);
2418
2419    let warm: Vec<usize> = combos
2420        .iter()
2421        .map(|c| first + c.period.unwrap().saturating_sub(1) + c.ma_len.unwrap().saturating_sub(1))
2422        .collect();
2423    init_matrix_prefixes(&mut buf_mu, cols, &warm);
2424
2425    let mut buf_guard = core::mem::ManuallyDrop::new(buf_mu);
2426    let out: &mut [f64] = unsafe {
2427        core::slice::from_raw_parts_mut(buf_guard.as_mut_ptr() as *mut f64, buf_guard.len())
2428    };
2429
2430    let chosen_kernel = match kern {
2431        Kernel::Auto => detect_best_batch_kernel(),
2432        other => other,
2433    };
2434
2435    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
2436        let prm = &combos[row];
2437        match chosen_kernel {
2438            Kernel::Scalar | Kernel::ScalarBatch => rvi_row_scalar(data, first, prm, out_row),
2439            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2440            Kernel::Avx2 | Kernel::Avx2Batch => rvi_row_avx2(data, first, prm, out_row),
2441            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2442            Kernel::Avx512 | Kernel::Avx512Batch => rvi_row_avx512(data, first, prm, out_row),
2443            _ => rvi_row_scalar(data, first, prm, out_row),
2444        }
2445    };
2446    if parallel {
2447        #[cfg(not(target_arch = "wasm32"))]
2448        {
2449            out.par_chunks_mut(cols)
2450                .enumerate()
2451                .for_each(|(row, slice)| do_row(row, slice));
2452        }
2453
2454        #[cfg(target_arch = "wasm32")]
2455        {
2456            for (row, slice) in out.chunks_mut(cols).enumerate() {
2457                do_row(row, slice);
2458            }
2459        }
2460    } else {
2461        for (row, slice) in out.chunks_mut(cols).enumerate() {
2462            do_row(row, slice);
2463        }
2464    }
2465
2466    let values = unsafe {
2467        Vec::from_raw_parts(
2468            buf_guard.as_mut_ptr() as *mut f64,
2469            buf_guard.len(),
2470            buf_guard.capacity(),
2471        )
2472    };
2473
2474    Ok(RviBatchOutput {
2475        values,
2476        combos,
2477        rows,
2478        cols,
2479    })
2480}
2481
2482#[inline(always)]
2483fn rvi_batch_inner_into(
2484    data: &[f64],
2485    sweep: &RviBatchRange,
2486    kern: Kernel,
2487    parallel: bool,
2488    output: &mut [f64],
2489) -> Result<Vec<RviParams>, RviError> {
2490    if data.is_empty() {
2491        return Err(RviError::EmptyInputData);
2492    }
2493    let combos = expand_grid(sweep)?;
2494    if combos.is_empty() {
2495        return Err(RviError::InvalidRange {
2496            start: 0,
2497            end: 0,
2498            step: 0,
2499        });
2500    }
2501    let first = data
2502        .iter()
2503        .position(|x| !x.is_nan())
2504        .ok_or(RviError::AllValuesNaN)?;
2505    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
2506    let max_m = combos.iter().map(|c| c.ma_len.unwrap()).max().unwrap();
2507    let need = max_p.saturating_sub(1) + max_m.saturating_sub(1) + 1;
2508    if (data.len() - first) <= (max_p.saturating_sub(1) + max_m.saturating_sub(1)) {
2509        return Err(RviError::NotEnoughValidData {
2510            needed: need,
2511            valid: data.len() - first,
2512        });
2513    }
2514    let rows = combos.len();
2515    let cols = data.len();
2516    let expected_len = rows
2517        .checked_mul(cols)
2518        .ok_or_else(|| RviError::InvalidInput("rows * cols overflow".into()))?;
2519    if output.len() != expected_len {
2520        return Err(RviError::OutputLengthMismatch {
2521            expected: expected_len,
2522            got: output.len(),
2523        });
2524    }
2525
2526    let chosen_kernel = match kern {
2527        Kernel::Auto => detect_best_batch_kernel(),
2528        other => other,
2529    };
2530
2531    for (row, combo) in combos.iter().enumerate() {
2532        let warmup = first
2533            + combo.period.unwrap().saturating_sub(1)
2534            + combo.ma_len.unwrap().saturating_sub(1);
2535        let row_start = row * cols;
2536        for i in 0..warmup.min(cols) {
2537            output[row_start + i] = f64::NAN;
2538        }
2539    }
2540    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
2541        let prm = &combos[row];
2542        match chosen_kernel {
2543            Kernel::Scalar | Kernel::ScalarBatch => rvi_row_scalar(data, first, prm, out_row),
2544            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2545            Kernel::Avx2 | Kernel::Avx2Batch => rvi_row_avx2(data, first, prm, out_row),
2546            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2547            Kernel::Avx512 | Kernel::Avx512Batch => rvi_row_avx512(data, first, prm, out_row),
2548            _ => rvi_row_scalar(data, first, prm, out_row),
2549        }
2550    };
2551    if parallel {
2552        #[cfg(not(target_arch = "wasm32"))]
2553        {
2554            output
2555                .par_chunks_mut(cols)
2556                .enumerate()
2557                .for_each(|(row, slice)| do_row(row, slice));
2558        }
2559
2560        #[cfg(target_arch = "wasm32")]
2561        {
2562            for (row, slice) in output.chunks_mut(cols).enumerate() {
2563                do_row(row, slice);
2564            }
2565        }
2566    } else {
2567        for (row, slice) in output.chunks_mut(cols).enumerate() {
2568            do_row(row, slice);
2569        }
2570    }
2571    Ok(combos)
2572}
2573
2574#[inline(always)]
2575unsafe fn rvi_row_scalar(data: &[f64], first: usize, params: &RviParams, out: &mut [f64]) {
2576    rvi_scalar(
2577        data,
2578        params.period.unwrap(),
2579        params.ma_len.unwrap(),
2580        params.matype.unwrap(),
2581        params.devtype.unwrap(),
2582        first,
2583        out,
2584    )
2585}
2586
2587#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2588#[inline(always)]
2589unsafe fn rvi_row_avx2(data: &[f64], first: usize, params: &RviParams, out: &mut [f64]) {
2590    rvi_avx2(
2591        data,
2592        params.period.unwrap(),
2593        params.ma_len.unwrap(),
2594        params.matype.unwrap(),
2595        params.devtype.unwrap(),
2596        first,
2597        out,
2598    )
2599}
2600
2601#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2602#[inline(always)]
2603unsafe fn rvi_row_avx512(data: &[f64], first: usize, params: &RviParams, out: &mut [f64]) {
2604    rvi_avx512(
2605        data,
2606        params.period.unwrap(),
2607        params.ma_len.unwrap(),
2608        params.matype.unwrap(),
2609        params.devtype.unwrap(),
2610        first,
2611        out,
2612    )
2613}
2614
2615#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2616#[inline(always)]
2617unsafe fn rvi_row_avx512_short(data: &[f64], first: usize, params: &RviParams, out: &mut [f64]) {
2618    rvi_avx512_short(
2619        data,
2620        params.period.unwrap(),
2621        params.ma_len.unwrap(),
2622        params.matype.unwrap(),
2623        params.devtype.unwrap(),
2624        first,
2625        out,
2626    )
2627}
2628
2629#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2630#[inline(always)]
2631unsafe fn rvi_row_avx512_long(data: &[f64], first: usize, params: &RviParams, out: &mut [f64]) {
2632    rvi_avx512_long(
2633        data,
2634        params.period.unwrap(),
2635        params.ma_len.unwrap(),
2636        params.matype.unwrap(),
2637        params.devtype.unwrap(),
2638        first,
2639        out,
2640    )
2641}
2642
2643#[cfg(test)]
2644mod tests {
2645    use super::*;
2646    use crate::skip_if_unsupported;
2647    use crate::utilities::data_loader::read_candles_from_csv;
2648
2649    fn check_rvi_partial_params(
2650        test_name: &str,
2651        kernel: Kernel,
2652    ) -> Result<(), Box<dyn std::error::Error>> {
2653        skip_if_unsupported!(kernel, test_name);
2654        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2655        let candles = read_candles_from_csv(file_path)?;
2656        let partial_params = RviParams {
2657            period: Some(10),
2658            ma_len: None,
2659            matype: None,
2660            devtype: None,
2661        };
2662        let input = RviInput::from_candles(&candles, "close", partial_params);
2663        let output = rvi_with_kernel(&input, kernel)?;
2664        assert_eq!(output.values.len(), candles.close.len());
2665        Ok(())
2666    }
2667
2668    fn check_rvi_default_params(
2669        test_name: &str,
2670        kernel: Kernel,
2671    ) -> Result<(), Box<dyn std::error::Error>> {
2672        skip_if_unsupported!(kernel, test_name);
2673        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2674        let candles = read_candles_from_csv(file_path)?;
2675        let input = RviInput::with_default_candles(&candles);
2676        let output = rvi_with_kernel(&input, kernel)?;
2677        assert_eq!(output.values.len(), candles.close.len());
2678        Ok(())
2679    }
2680
2681    fn check_rvi_error_zero_period(
2682        test_name: &str,
2683        kernel: Kernel,
2684    ) -> Result<(), Box<dyn std::error::Error>> {
2685        skip_if_unsupported!(kernel, test_name);
2686        let data = [10.0, 20.0, 30.0, 40.0];
2687        let params = RviParams {
2688            period: Some(0),
2689            ma_len: Some(14),
2690            matype: Some(1),
2691            devtype: Some(0),
2692        };
2693        let input = RviInput::from_slice(&data, params);
2694        let result = rvi_with_kernel(&input, kernel);
2695        assert!(result.is_err());
2696        Ok(())
2697    }
2698
2699    fn check_rvi_error_zero_ma_len(
2700        test_name: &str,
2701        kernel: Kernel,
2702    ) -> Result<(), Box<dyn std::error::Error>> {
2703        skip_if_unsupported!(kernel, test_name);
2704        let data = [10.0, 20.0, 30.0, 40.0];
2705        let params = RviParams {
2706            period: Some(10),
2707            ma_len: Some(0),
2708            matype: Some(1),
2709            devtype: Some(0),
2710        };
2711        let input = RviInput::from_slice(&data, params);
2712        let result = rvi_with_kernel(&input, kernel);
2713        assert!(result.is_err());
2714        Ok(())
2715    }
2716
2717    fn check_rvi_error_period_exceeds_data_length(
2718        test_name: &str,
2719        kernel: Kernel,
2720    ) -> Result<(), Box<dyn std::error::Error>> {
2721        skip_if_unsupported!(kernel, test_name);
2722        let data = [10.0, 20.0, 30.0];
2723        let params = RviParams {
2724            period: Some(10),
2725            ma_len: Some(14),
2726            matype: Some(1),
2727            devtype: Some(0),
2728        };
2729        let input = RviInput::from_slice(&data, params);
2730        let result = rvi_with_kernel(&input, kernel);
2731        assert!(result.is_err());
2732        Ok(())
2733    }
2734
2735    fn check_rvi_all_nan_input(
2736        test_name: &str,
2737        kernel: Kernel,
2738    ) -> Result<(), Box<dyn std::error::Error>> {
2739        skip_if_unsupported!(kernel, test_name);
2740        let data = [f64::NAN, f64::NAN, f64::NAN];
2741        let params = RviParams::default();
2742        let input = RviInput::from_slice(&data, params);
2743        let result = rvi_with_kernel(&input, kernel);
2744        assert!(result.is_err());
2745        Ok(())
2746    }
2747
2748    fn check_rvi_not_enough_valid_data(
2749        test_name: &str,
2750        kernel: Kernel,
2751    ) -> Result<(), Box<dyn std::error::Error>> {
2752        skip_if_unsupported!(kernel, test_name);
2753        let data = [f64::NAN, 1.0, 2.0, 3.0];
2754        let params = RviParams {
2755            period: Some(3),
2756            ma_len: Some(5),
2757            matype: Some(1),
2758            devtype: Some(0),
2759        };
2760        let input = RviInput::from_slice(&data, params);
2761        let result = rvi_with_kernel(&input, kernel);
2762        assert!(result.is_err());
2763        Ok(())
2764    }
2765
2766    fn check_rvi_example_values(
2767        test_name: &str,
2768        kernel: Kernel,
2769    ) -> Result<(), Box<dyn std::error::Error>> {
2770        skip_if_unsupported!(kernel, test_name);
2771        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2772        let candles = read_candles_from_csv(file_path)?;
2773        let params = RviParams {
2774            period: Some(10),
2775            ma_len: Some(14),
2776            matype: Some(1),
2777            devtype: Some(0),
2778        };
2779        let input = RviInput::from_candles(&candles, "close", params);
2780        let output = rvi_with_kernel(&input, kernel)?;
2781        assert_eq!(output.values.len(), candles.close.len());
2782        let last_five = &output.values[output.values.len().saturating_sub(5)..];
2783        let expected = [
2784            67.48579363423423,
2785            62.03322230763894,
2786            56.71819195768154,
2787            60.487299747927636,
2788            55.022521428674175,
2789        ];
2790        for (i, &val) in last_five.iter().enumerate() {
2791            let exp = expected[i];
2792            assert!(
2793                val.is_finite(),
2794                "Expected a finite RVI value, got NaN at index {}",
2795                i
2796            );
2797            let diff = (val - exp).abs();
2798            assert!(
2799                diff < 1e-1,
2800                "Mismatch at index {} -> got: {}, expected: {}, diff: {}",
2801                i,
2802                val,
2803                exp,
2804                diff
2805            );
2806        }
2807        Ok(())
2808    }
2809
2810    #[cfg(debug_assertions)]
2811    fn check_rvi_no_poison(
2812        test_name: &str,
2813        kernel: Kernel,
2814    ) -> Result<(), Box<dyn std::error::Error>> {
2815        skip_if_unsupported!(kernel, test_name);
2816
2817        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2818        let candles = read_candles_from_csv(file_path)?;
2819
2820        let test_params = vec![
2821            RviParams::default(),
2822            RviParams {
2823                period: Some(2),
2824                ma_len: Some(2),
2825                matype: Some(0),
2826                devtype: Some(0),
2827            },
2828            RviParams {
2829                period: Some(5),
2830                ma_len: Some(5),
2831                matype: Some(1),
2832                devtype: Some(1),
2833            },
2834            RviParams {
2835                period: Some(10),
2836                ma_len: Some(20),
2837                matype: Some(0),
2838                devtype: Some(2),
2839            },
2840            RviParams {
2841                period: Some(20),
2842                ma_len: Some(30),
2843                matype: Some(1),
2844                devtype: Some(0),
2845            },
2846            RviParams {
2847                period: Some(50),
2848                ma_len: Some(50),
2849                matype: Some(0),
2850                devtype: Some(1),
2851            },
2852            RviParams {
2853                period: Some(100),
2854                ma_len: Some(20),
2855                matype: Some(1),
2856                devtype: Some(2),
2857            },
2858            RviParams {
2859                period: Some(14),
2860                ma_len: Some(100),
2861                matype: Some(0),
2862                devtype: Some(0),
2863            },
2864        ];
2865
2866        for (param_idx, params) in test_params.iter().enumerate() {
2867            let input = RviInput::from_candles(&candles, "close", params.clone());
2868            let output = rvi_with_kernel(&input, kernel)?;
2869
2870            for (i, &val) in output.values.iter().enumerate() {
2871                if val.is_nan() {
2872                    continue;
2873                }
2874
2875                let bits = val.to_bits();
2876
2877                if bits == 0x11111111_11111111 {
2878                    panic!(
2879                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
2880						 with params: {:?} (param set {})",
2881                        test_name, val, bits, i, params, param_idx
2882                    );
2883                }
2884
2885                if bits == 0x22222222_22222222 {
2886                    panic!(
2887                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
2888						 with params: {:?} (param set {})",
2889                        test_name, val, bits, i, params, param_idx
2890                    );
2891                }
2892
2893                if bits == 0x33333333_33333333 {
2894                    panic!(
2895                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
2896						 with params: {:?} (param set {})",
2897                        test_name, val, bits, i, params, param_idx
2898                    );
2899                }
2900            }
2901        }
2902
2903        Ok(())
2904    }
2905
2906    #[cfg(not(debug_assertions))]
2907    fn check_rvi_no_poison(
2908        _test_name: &str,
2909        _kernel: Kernel,
2910    ) -> Result<(), Box<dyn std::error::Error>> {
2911        Ok(())
2912    }
2913
2914    #[cfg(feature = "proptest")]
2915    #[allow(clippy::float_cmp)]
2916    fn check_rvi_property(
2917        test_name: &str,
2918        kernel: Kernel,
2919    ) -> Result<(), Box<dyn std::error::Error>> {
2920        use proptest::prelude::*;
2921        skip_if_unsupported!(kernel, test_name);
2922
2923        let strat = (2usize..=30, 2usize..=30, 0usize..=1, 0usize..=2).prop_flat_map(
2924            |(period, ma_len, matype, devtype)| {
2925                (
2926                    prop::collection::vec(
2927                        (0.01f64..1e6f64).prop_filter("finite", |x| x.is_finite()),
2928                        (period + ma_len)..400,
2929                    ),
2930                    Just(period),
2931                    Just(ma_len),
2932                    Just(matype),
2933                    Just(devtype),
2934                )
2935            },
2936        );
2937
2938        proptest::test_runner::TestRunner::default().run(
2939            &strat,
2940            |(data, period, ma_len, matype, devtype)| {
2941                let params = RviParams {
2942                    period: Some(period),
2943                    ma_len: Some(ma_len),
2944                    matype: Some(matype),
2945                    devtype: Some(devtype),
2946                };
2947                let input = RviInput::from_slice(&data, params.clone());
2948
2949                let RviOutput { values: out } = rvi_with_kernel(&input, kernel).unwrap();
2950
2951                let RviOutput { values: ref_out } =
2952                    rvi_with_kernel(&input, Kernel::Scalar).unwrap();
2953
2954                let warmup = period.saturating_sub(1) + ma_len.saturating_sub(1);
2955
2956                for i in 0..warmup.min(data.len()) {
2957                    prop_assert!(
2958                        out[i].is_nan(),
2959                        "Expected NaN during warmup at index {}, got {}",
2960                        i,
2961                        out[i]
2962                    );
2963                }
2964
2965                for i in warmup..data.len() {
2966                    let y = out[i];
2967                    let r = ref_out[i];
2968
2969                    if y.is_finite() {
2970                        prop_assert!(
2971                            y >= -1e-9 && y <= 100.0 + 1e-9,
2972                            "RVI out of bounds at idx {}: {} (should be 0-100)",
2973                            i,
2974                            y
2975                        );
2976                    }
2977
2978                    if !y.is_finite() || !r.is_finite() {
2979                        prop_assert!(
2980                            y.to_bits() == r.to_bits(),
2981                            "finite/NaN mismatch idx {}: {} vs {}",
2982                            i,
2983                            y,
2984                            r
2985                        );
2986                    } else {
2987                        let y_bits = y.to_bits();
2988                        let r_bits = r.to_bits();
2989                        let ulp_diff: u64 = y_bits.abs_diff(r_bits);
2990
2991                        prop_assert!(
2992                            (y - r).abs() <= 1e-9 || ulp_diff <= 4,
2993                            "Kernel mismatch at idx {}: {} vs {} (ULP={})",
2994                            i,
2995                            y,
2996                            r,
2997                            ulp_diff
2998                        );
2999                    }
3000                }
3001
3002                let is_monotonic_increasing = data.windows(2).all(|w| w[1] >= w[0] - f64::EPSILON);
3003
3004                if is_monotonic_increasing && out.len() > warmup + 10 {
3005                    let last_values = &out[out.len().saturating_sub(10)..];
3006                    let finite_values: Vec<f64> = last_values
3007                        .iter()
3008                        .filter(|v| v.is_finite())
3009                        .copied()
3010                        .collect();
3011
3012                    if !finite_values.is_empty() {
3013                        let avg_rvi =
3014                            finite_values.iter().sum::<f64>() / finite_values.len() as f64;
3015                        prop_assert!(
3016                            avg_rvi >= 90.0,
3017                            "RVI should be high for monotonic increasing data, got avg {}",
3018                            avg_rvi
3019                        );
3020                    }
3021                }
3022
3023                let is_monotonic_decreasing = data.windows(2).all(|w| w[1] <= w[0] + f64::EPSILON);
3024
3025                if is_monotonic_decreasing && out.len() > warmup + 10 {
3026                    let last_values = &out[out.len().saturating_sub(10)..];
3027                    let finite_values: Vec<f64> = last_values
3028                        .iter()
3029                        .filter(|v| v.is_finite())
3030                        .copied()
3031                        .collect();
3032
3033                    if !finite_values.is_empty() {
3034                        let avg_rvi =
3035                            finite_values.iter().sum::<f64>() / finite_values.len() as f64;
3036                        prop_assert!(
3037                            avg_rvi <= 10.0,
3038                            "RVI should be low for monotonic decreasing data, got avg {}",
3039                            avg_rvi
3040                        );
3041                    }
3042                }
3043
3044                let is_constant = data
3045                    .windows(2)
3046                    .all(|w| (w[0] - w[1]).abs() <= f64::EPSILON * w[0].abs().max(1.0));
3047
3048                if is_constant && out.len() > warmup {
3049                    for i in warmup..out.len() {
3050                        prop_assert!(
3051                            out[i].is_nan(),
3052                            "RVI should be NaN for constant data at idx {}, got {}",
3053                            i,
3054                            out[i]
3055                        );
3056                    }
3057                }
3058
3059                let mut is_alternating = data.len() >= 4;
3060                if is_alternating {
3061                    for i in 1..data.len().saturating_sub(1) {
3062                        let diff1 = data[i] - data[i - 1];
3063                        let diff2 = data[i + 1] - data[i];
3064
3065                        if diff1 * diff2 >= 0.0 && diff1.abs() > f64::EPSILON {
3066                            is_alternating = false;
3067                            break;
3068                        }
3069                    }
3070                }
3071
3072                if is_alternating && out.len() > warmup + 10 {
3073                    let last_values = &out[out.len().saturating_sub(10)..];
3074                    let finite_values: Vec<f64> = last_values
3075                        .iter()
3076                        .filter(|v| v.is_finite())
3077                        .copied()
3078                        .collect();
3079
3080                    if !finite_values.is_empty() {
3081                        let avg_rvi =
3082                            finite_values.iter().sum::<f64>() / finite_values.len() as f64;
3083                        prop_assert!(
3084                            avg_rvi >= 35.0 && avg_rvi <= 65.0,
3085                            "RVI should be near 50 for alternating data, got avg {}",
3086                            avg_rvi
3087                        );
3088                    }
3089                }
3090
3091                Ok(())
3092            },
3093        )?;
3094
3095        Ok(())
3096    }
3097
3098    macro_rules! generate_all_rvi_tests {
3099        ($($test_fn:ident),*) => {
3100            paste::paste! {
3101                $(
3102                    #[test]
3103                    fn [<$test_fn _scalar_f64>]() {
3104                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
3105                    }
3106                )*
3107                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3108                $(
3109                    #[test]
3110                    fn [<$test_fn _avx2_f64>]() {
3111                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
3112                    }
3113                    #[test]
3114                    fn [<$test_fn _avx512_f64>]() {
3115                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
3116                    }
3117                )*
3118            }
3119        }
3120    }
3121
3122    generate_all_rvi_tests!(
3123        check_rvi_partial_params,
3124        check_rvi_default_params,
3125        check_rvi_error_zero_period,
3126        check_rvi_error_zero_ma_len,
3127        check_rvi_error_period_exceeds_data_length,
3128        check_rvi_all_nan_input,
3129        check_rvi_not_enough_valid_data,
3130        check_rvi_example_values,
3131        check_rvi_no_poison
3132    );
3133
3134    #[cfg(feature = "proptest")]
3135    generate_all_rvi_tests!(check_rvi_property);
3136
3137    fn check_batch_default_row(
3138        test: &str,
3139        kernel: Kernel,
3140    ) -> Result<(), Box<dyn std::error::Error>> {
3141        skip_if_unsupported!(kernel, test);
3142        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3143        let c = read_candles_from_csv(file)?;
3144        let output = RviBatchBuilder::new()
3145            .kernel(kernel)
3146            .apply_candles(&c, "close")?;
3147        let def = RviParams::default();
3148        let row = output.values_for(&def).expect("default row missing");
3149        assert_eq!(row.len(), c.close.len());
3150        Ok(())
3151    }
3152
3153    #[cfg(debug_assertions)]
3154    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
3155        skip_if_unsupported!(kernel, test);
3156
3157        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3158        let c = read_candles_from_csv(file)?;
3159
3160        let test_configs = vec![
3161            (2, 10, 2, 2, 10, 2),
3162            (5, 25, 5, 10, 30, 5),
3163            (30, 60, 15, 20, 40, 10),
3164            (2, 5, 1, 2, 5, 1),
3165            (10, 20, 5, 50, 100, 25),
3166            (50, 100, 50, 10, 20, 10),
3167        ];
3168
3169        for (cfg_idx, &(p_start, p_end, p_step, m_start, m_end, m_step)) in
3170            test_configs.iter().enumerate()
3171        {
3172            for matype in [0, 1].iter() {
3173                for devtype in [0, 1, 2].iter() {
3174                    let output = RviBatchBuilder::new()
3175                        .kernel(kernel)
3176                        .period_range(p_start, p_end, p_step)
3177                        .ma_len_range(m_start, m_end, m_step)
3178                        .matype_static(*matype)
3179                        .devtype_static(*devtype)
3180                        .apply_candles(&c, "close")?;
3181
3182                    for (idx, &val) in output.values.iter().enumerate() {
3183                        if val.is_nan() {
3184                            continue;
3185                        }
3186
3187                        let bits = val.to_bits();
3188                        let row = idx / output.cols;
3189                        let col = idx % output.cols;
3190                        let combo = &output.combos[row];
3191
3192                        if bits == 0x11111111_11111111 {
3193                            panic!(
3194								"[{}] Config {} (matype={}, devtype={}): Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
3195								 at row {} col {} (flat index {}) with params: {:?}",
3196								test, cfg_idx, matype, devtype, val, bits, row, col, idx, combo
3197							);
3198                        }
3199
3200                        if bits == 0x22222222_22222222 {
3201                            panic!(
3202								"[{}] Config {} (matype={}, devtype={}): Found init_matrix_prefixes poison value {} (0x{:016X}) \
3203								 at row {} col {} (flat index {}) with params: {:?}",
3204								test, cfg_idx, matype, devtype, val, bits, row, col, idx, combo
3205							);
3206                        }
3207
3208                        if bits == 0x33333333_33333333 {
3209                            panic!(
3210								"[{}] Config {} (matype={}, devtype={}): Found make_uninit_matrix poison value {} (0x{:016X}) \
3211								 at row {} col {} (flat index {}) with params: {:?}",
3212								test, cfg_idx, matype, devtype, val, bits, row, col, idx, combo
3213							);
3214                        }
3215                    }
3216                }
3217            }
3218        }
3219
3220        Ok(())
3221    }
3222
3223    #[cfg(not(debug_assertions))]
3224    fn check_batch_no_poison(
3225        _test: &str,
3226        _kernel: Kernel,
3227    ) -> Result<(), Box<dyn std::error::Error>> {
3228        Ok(())
3229    }
3230
3231    macro_rules! gen_batch_tests {
3232        ($fn_name:ident) => {
3233            paste::paste! {
3234                #[test] fn [<$fn_name _scalar>]()      {
3235                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
3236                }
3237                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3238                #[test] fn [<$fn_name _avx2>]()        {
3239                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
3240                }
3241                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3242                #[test] fn [<$fn_name _avx512>]()      {
3243                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
3244                }
3245                #[test] fn [<$fn_name _auto_detect>]() {
3246                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
3247                }
3248            }
3249        };
3250    }
3251    gen_batch_tests!(check_batch_default_row);
3252    gen_batch_tests!(check_batch_no_poison);
3253
3254    #[test]
3255    fn test_rvi_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
3256        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3257        let candles = read_candles_from_csv(file)?;
3258        let input = RviInput::with_default_candles(&candles);
3259
3260        let baseline = rvi(&input)?.values;
3261
3262        let mut out = vec![0.0f64; candles.close.len()];
3263        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
3264        {
3265            rvi_into(&input, &mut out)?;
3266        }
3267        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3268        {
3269            rvi_into_slice(&mut out, &input, Kernel::Auto)?;
3270        }
3271
3272        assert_eq!(baseline.len(), out.len());
3273
3274        #[inline]
3275        fn eq_or_both_nan(a: f64, b: f64) -> bool {
3276            (a.is_nan() && b.is_nan()) || (a - b).abs() <= 1e-12
3277        }
3278
3279        for i in 0..out.len() {
3280            assert!(
3281                eq_or_both_nan(baseline[i], out[i]),
3282                "mismatch at index {}: baseline={}, into={}",
3283                i,
3284                baseline[i],
3285                out[i]
3286            );
3287        }
3288
3289        Ok(())
3290    }
3291}
3292
3293#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3294#[wasm_bindgen]
3295pub fn rvi_js(
3296    data: &[f64],
3297    period: usize,
3298    ma_len: usize,
3299    matype: usize,
3300    devtype: usize,
3301) -> Result<Vec<f64>, JsValue> {
3302    if data.is_empty() {
3303        return Err(JsValue::from_str("rvi: Empty data provided."));
3304    }
3305
3306    if data.iter().all(|&x| x.is_nan()) {
3307        return Err(JsValue::from_str("rvi: All values are NaN."));
3308    }
3309
3310    if period == 0 || ma_len == 0 {
3311        return Err(JsValue::from_str("rvi: Invalid period"));
3312    }
3313
3314    let first = data.iter().position(|&x| !x.is_nan()).unwrap_or(0);
3315    let needed = period.saturating_sub(1) + ma_len.saturating_sub(1) + 1;
3316    let valid_len = data.len() - first;
3317
3318    if period > data.len() || ma_len > data.len() {
3319        return Err(JsValue::from_str("rvi: Invalid period"));
3320    } else if valid_len < needed {
3321        return Err(JsValue::from_str("rvi: Not enough valid data"));
3322    }
3323
3324    let params = RviParams {
3325        period: Some(period),
3326        ma_len: Some(ma_len),
3327        matype: Some(matype),
3328        devtype: Some(devtype),
3329    };
3330    let input = RviInput::from_slice(data, params);
3331    let mut out = vec![f64::NAN; data.len()];
3332    rvi_into_slice(&mut out, &input, detect_best_kernel())
3333        .map_err(|e| JsValue::from_str(&e.to_string()))?;
3334    Ok(out)
3335}
3336
3337#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3338#[wasm_bindgen]
3339pub fn rvi_alloc(len: usize) -> *mut f64 {
3340    let mut vec = Vec::<f64>::with_capacity(len);
3341    let ptr = vec.as_mut_ptr();
3342    std::mem::forget(vec);
3343    ptr
3344}
3345
3346#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3347#[wasm_bindgen]
3348pub fn rvi_free(ptr: *mut f64, len: usize) {
3349    if !ptr.is_null() {
3350        unsafe {
3351            let _ = Vec::from_raw_parts(ptr, len, len);
3352        }
3353    }
3354}
3355
3356#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3357#[wasm_bindgen]
3358pub fn rvi_into(
3359    in_ptr: *const f64,
3360    out_ptr: *mut f64,
3361    len: usize,
3362    period: usize,
3363    ma_len: usize,
3364    matype: usize,
3365    devtype: usize,
3366) -> Result<(), JsValue> {
3367    if len == 0 {
3368        return Err(JsValue::from_str("rvi_into: len cannot be 0"));
3369    }
3370
3371    unsafe {
3372        let data = std::slice::from_raw_parts(in_ptr, len);
3373        let out = std::slice::from_raw_parts_mut(out_ptr, len);
3374        let params = RviParams {
3375            period: Some(period),
3376            ma_len: Some(ma_len),
3377            matype: Some(matype),
3378            devtype: Some(devtype),
3379        };
3380        let input = RviInput::from_slice(data, params);
3381
3382        if std::ptr::eq(in_ptr, out_ptr) {
3383            let mut tmp = vec![f64::NAN; len];
3384            rvi_into_slice(&mut tmp, &input, detect_best_kernel())
3385                .map_err(|e| JsValue::from_str(&e.to_string()))?;
3386            out.copy_from_slice(&tmp);
3387        } else {
3388            rvi_into_slice(out, &input, detect_best_kernel())
3389                .map_err(|e| JsValue::from_str(&e.to_string()))?;
3390        }
3391        Ok(())
3392    }
3393}
3394
3395#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3396#[derive(Serialize, Deserialize)]
3397pub struct RviBatchConfig {
3398    pub period_range: (usize, usize, usize),
3399    pub ma_len_range: (usize, usize, usize),
3400    pub matype_range: (usize, usize, usize),
3401    pub devtype_range: (usize, usize, usize),
3402}
3403
3404#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3405#[derive(Serialize, Deserialize)]
3406pub struct RviBatchJsOutput {
3407    pub values: Vec<f64>,
3408    pub periods: Vec<usize>,
3409    pub ma_lens: Vec<usize>,
3410    pub matypes: Vec<usize>,
3411    pub devtypes: Vec<usize>,
3412    pub rows: usize,
3413    pub cols: usize,
3414}
3415
3416#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3417#[wasm_bindgen(js_name = rvi_batch)]
3418pub fn rvi_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
3419    let cfg: RviBatchConfig = serde_wasm_bindgen::from_value(config)
3420        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
3421
3422    let sweep = RviBatchRange {
3423        period: cfg.period_range,
3424        ma_len: cfg.ma_len_range,
3425        matype: cfg.matype_range,
3426        devtype: cfg.devtype_range,
3427    };
3428
3429    let output = rvi_batch_inner(data, &sweep, detect_best_kernel(), false)
3430        .map_err(|e| JsValue::from_str(&e.to_string()))?;
3431
3432    let js_out = RviBatchJsOutput {
3433        values: output.values,
3434        periods: output.combos.iter().map(|c| c.period.unwrap()).collect(),
3435        ma_lens: output.combos.iter().map(|c| c.ma_len.unwrap()).collect(),
3436        matypes: output.combos.iter().map(|c| c.matype.unwrap()).collect(),
3437        devtypes: output.combos.iter().map(|c| c.devtype.unwrap()).collect(),
3438        rows: output.rows,
3439        cols: output.cols,
3440    };
3441    serde_wasm_bindgen::to_value(&js_out)
3442        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
3443}
3444
3445#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3446#[wasm_bindgen]
3447pub fn rvi_batch_into(
3448    in_ptr: *const f64,
3449    out_ptr: *mut f64,
3450    len: usize,
3451    p_start: usize,
3452    p_end: usize,
3453    p_step: usize,
3454    m_start: usize,
3455    m_end: usize,
3456    m_step: usize,
3457    t_start: usize,
3458    t_end: usize,
3459    t_step: usize,
3460    d_start: usize,
3461    d_end: usize,
3462    d_step: usize,
3463) -> Result<usize, JsValue> {
3464    if in_ptr.is_null() || out_ptr.is_null() {
3465        return Err(JsValue::from_str("null pointer to rvi_batch_into"));
3466    }
3467    unsafe {
3468        let data = std::slice::from_raw_parts(in_ptr, len);
3469        let sweep = RviBatchRange {
3470            period: (p_start, p_end, p_step),
3471            ma_len: (m_start, m_end, m_step),
3472            matype: (t_start, t_end, t_step),
3473            devtype: (d_start, d_end, d_step),
3474        };
3475        let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
3476        let rows = combos.len();
3477        let cols = len;
3478        let total = rows
3479            .checked_mul(cols)
3480            .ok_or_else(|| JsValue::from_str("rvi_batch_into: rows * cols overflow"))?;
3481        let out = std::slice::from_raw_parts_mut(out_ptr, total);
3482
3483        let simd = match detect_best_batch_kernel() {
3484            Kernel::Avx512Batch => Kernel::Avx512,
3485            Kernel::Avx2Batch => Kernel::Avx2,
3486            _ => Kernel::Scalar,
3487        };
3488        rvi_batch_inner_into(data, &sweep, simd, false, out)
3489            .map_err(|e| JsValue::from_str(&e.to_string()))?;
3490        Ok(rows)
3491    }
3492}
3493
3494#[cfg(feature = "python")]
3495#[pyfunction(name = "rvi")]
3496#[pyo3(signature = (data, period, ma_len, matype, devtype, kernel=None))]
3497pub fn rvi_py<'py>(
3498    py: Python<'py>,
3499    data: numpy::PyReadonlyArray1<'py, f64>,
3500    period: usize,
3501    ma_len: usize,
3502    matype: usize,
3503    devtype: usize,
3504    kernel: Option<&str>,
3505) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
3506    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
3507
3508    let slice_in = data.as_slice()?;
3509    let kern = validate_kernel(kernel, false)?;
3510
3511    let out_arr = unsafe { PyArray1::<f64>::new(py, [slice_in.len()], false) };
3512    let out_slice = unsafe { out_arr.as_slice_mut()? };
3513
3514    let params = RviParams {
3515        period: Some(period),
3516        ma_len: Some(ma_len),
3517        matype: Some(matype),
3518        devtype: Some(devtype),
3519    };
3520    let input = RviInput::from_slice(slice_in, params);
3521
3522    py.allow_threads(|| rvi_into_slice(out_slice, &input, kern))
3523        .map_err(|e| PyValueError::new_err(e.to_string()))?;
3524
3525    Ok(out_arr)
3526}
3527
3528#[cfg(feature = "python")]
3529#[pyfunction(name = "rvi_batch")]
3530#[pyo3(signature = (data, period_range, ma_len_range, matype_range, devtype_range, kernel=None))]
3531pub fn rvi_batch_py<'py>(
3532    py: Python<'py>,
3533    data: numpy::PyReadonlyArray1<'py, f64>,
3534    period_range: (usize, usize, usize),
3535    ma_len_range: (usize, usize, usize),
3536    matype_range: (usize, usize, usize),
3537    devtype_range: (usize, usize, usize),
3538    kernel: Option<&str>,
3539) -> PyResult<Bound<'py, PyDict>> {
3540    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
3541
3542    let slice_in = data.as_slice()?;
3543
3544    let sweep = RviBatchRange {
3545        period: period_range,
3546        ma_len: ma_len_range,
3547        matype: matype_range,
3548        devtype: devtype_range,
3549    };
3550
3551    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
3552    let rows = combos.len();
3553    let cols = slice_in.len();
3554    let total = rows
3555        .checked_mul(cols)
3556        .ok_or_else(|| PyValueError::new_err("rvi_batch: rows * cols overflow"))?;
3557
3558    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
3559    let out_slice = unsafe { out_arr.as_slice_mut()? };
3560
3561    let kern = validate_kernel(kernel, true)?;
3562    let simd = match kern {
3563        Kernel::Auto => detect_best_batch_kernel(),
3564        k => k,
3565    };
3566    let simd = match simd {
3567        Kernel::Avx512Batch => Kernel::Avx512,
3568        Kernel::Avx2Batch => Kernel::Avx2,
3569        Kernel::ScalarBatch => Kernel::Scalar,
3570        _ => Kernel::Scalar,
3571    };
3572
3573    let combos_back = py
3574        .allow_threads(|| rvi_batch_inner_into(slice_in, &sweep, simd, true, out_slice))
3575        .map_err(|e| PyValueError::new_err(e.to_string()))?;
3576
3577    let d = PyDict::new(py);
3578    d.set_item("values", out_arr.reshape((rows, cols))?)?;
3579    d.set_item(
3580        "periods",
3581        combos_back
3582            .iter()
3583            .map(|p| p.period.unwrap() as u64)
3584            .collect::<Vec<_>>()
3585            .into_pyarray(py),
3586    )?;
3587    d.set_item(
3588        "ma_lens",
3589        combos_back
3590            .iter()
3591            .map(|p| p.ma_len.unwrap() as u64)
3592            .collect::<Vec<_>>()
3593            .into_pyarray(py),
3594    )?;
3595    d.set_item(
3596        "matypes",
3597        combos_back
3598            .iter()
3599            .map(|p| p.matype.unwrap() as u64)
3600            .collect::<Vec<_>>()
3601            .into_pyarray(py),
3602    )?;
3603    d.set_item(
3604        "devtypes",
3605        combos_back
3606            .iter()
3607            .map(|p| p.devtype.unwrap() as u64)
3608            .collect::<Vec<_>>()
3609            .into_pyarray(py),
3610    )?;
3611    Ok(d)
3612}
3613
3614#[cfg(feature = "python")]
3615#[pyclass(name = "RviStream")]
3616pub struct RviStreamPy {
3617    stream: RviStream,
3618}
3619
3620#[cfg(feature = "python")]
3621#[pymethods]
3622impl RviStreamPy {
3623    #[new]
3624    fn new(period: usize, ma_len: usize, matype: usize, devtype: usize) -> PyResult<Self> {
3625        let params = RviParams {
3626            period: Some(period),
3627            ma_len: Some(ma_len),
3628            matype: Some(matype),
3629            devtype: Some(devtype),
3630        };
3631        let stream =
3632            RviStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
3633        Ok(RviStreamPy { stream })
3634    }
3635
3636    fn update(&mut self, value: f64) -> Option<f64> {
3637        self.stream.update(value)
3638    }
3639}
3640
3641#[cfg(feature = "python")]
3642pub fn register_rvi_module(m: &Bound<'_, pyo3::types::PyModule>) -> PyResult<()> {
3643    m.add_function(wrap_pyfunction!(rvi_py, m)?)?;
3644    m.add_function(wrap_pyfunction!(rvi_batch_py, m)?)?;
3645    m.add_class::<RviStreamPy>()?;
3646    Ok(())
3647}
3648
3649#[cfg(all(feature = "python", feature = "cuda"))]
3650use crate::cuda::oscillators::CudaRvi;
3651#[cfg(all(feature = "python", feature = "cuda"))]
3652use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
3653
3654#[cfg(all(feature = "python", feature = "cuda"))]
3655#[pyfunction(name = "rvi_cuda_batch_dev")]
3656#[pyo3(signature = (data_f32, period_range, ma_len_range, matype_range, devtype_range, device_id=0))]
3657pub fn rvi_cuda_batch_dev_py<'py>(
3658    py: Python<'py>,
3659    data_f32: numpy::PyReadonlyArray1<'py, f32>,
3660    period_range: (usize, usize, usize),
3661    ma_len_range: (usize, usize, usize),
3662    matype_range: (usize, usize, usize),
3663    devtype_range: (usize, usize, usize),
3664    device_id: usize,
3665) -> PyResult<(DeviceArrayF32Py, Bound<'py, PyDict>)> {
3666    use crate::cuda::cuda_available;
3667    use numpy::{IntoPyArray, PyArrayMethods};
3668    if !cuda_available() {
3669        return Err(PyValueError::new_err("CUDA not available"));
3670    }
3671    let d = data_f32.as_slice()?;
3672    let sweep = RviBatchRange {
3673        period: period_range,
3674        ma_len: ma_len_range,
3675        matype: matype_range,
3676        devtype: devtype_range,
3677    };
3678    let (inner, combos) = py.allow_threads(|| {
3679        let cuda = CudaRvi::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
3680        cuda.rvi_batch_dev(d, &sweep)
3681            .map_err(|e| PyValueError::new_err(e.to_string()))
3682    })?;
3683    let dict = PyDict::new(py);
3684    dict.set_item(
3685        "periods",
3686        combos
3687            .iter()
3688            .map(|p| p.period.unwrap() as u64)
3689            .collect::<Vec<_>>()
3690            .into_pyarray(py),
3691    )?;
3692    dict.set_item(
3693        "ma_lens",
3694        combos
3695            .iter()
3696            .map(|p| p.ma_len.unwrap() as u64)
3697            .collect::<Vec<_>>()
3698            .into_pyarray(py),
3699    )?;
3700    dict.set_item(
3701        "matypes",
3702        combos
3703            .iter()
3704            .map(|p| p.matype.unwrap() as u64)
3705            .collect::<Vec<_>>()
3706            .into_pyarray(py),
3707    )?;
3708    dict.set_item(
3709        "devtypes",
3710        combos
3711            .iter()
3712            .map(|p| p.devtype.unwrap() as u64)
3713            .collect::<Vec<_>>()
3714            .into_pyarray(py),
3715    )?;
3716    let handle = make_device_array_py(device_id, inner)?;
3717    Ok((handle, dict))
3718}
3719
3720#[cfg(all(feature = "python", feature = "cuda"))]
3721#[pyfunction(name = "rvi_cuda_many_series_one_param_dev")]
3722#[pyo3(signature = (data_tm_f32, cols, rows, period, ma_len, matype, devtype, device_id=0))]
3723pub fn rvi_cuda_many_series_one_param_dev_py(
3724    py: Python<'_>,
3725    data_tm_f32: numpy::PyReadonlyArray1<'_, f32>,
3726    cols: usize,
3727    rows: usize,
3728    period: usize,
3729    ma_len: usize,
3730    matype: usize,
3731    devtype: usize,
3732    device_id: usize,
3733) -> PyResult<DeviceArrayF32Py> {
3734    use crate::cuda::cuda_available;
3735    if !cuda_available() {
3736        return Err(PyValueError::new_err("CUDA not available"));
3737    }
3738    let tm = data_tm_f32.as_slice()?;
3739    let params = RviParams {
3740        period: Some(period),
3741        ma_len: Some(ma_len),
3742        matype: Some(matype),
3743        devtype: Some(devtype),
3744    };
3745    let inner = py.allow_threads(|| {
3746        let cuda = CudaRvi::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
3747        cuda.rvi_many_series_one_param_time_major_dev(tm, cols, rows, &params)
3748            .map_err(|e| PyValueError::new_err(e.to_string()))
3749    })?;
3750    Ok(make_device_array_py(device_id, inner)?)
3751}