Skip to main content

vector_ta/indicators/
dvdiqqe.rs

1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::{PyNotImplementedError, PyValueError};
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::{PyDict, PyList};
9
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15#[cfg(all(feature = "python", feature = "cuda"))]
16use crate::cuda::dvdiqqe_wrapper::CudaDvdiqqe;
17#[cfg(all(feature = "python", feature = "cuda"))]
18use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
19#[cfg(all(feature = "python", feature = "cuda"))]
20use cust::context::Context as CudaContext;
21
22#[cfg(all(feature = "python", feature = "cuda"))]
23#[pyclass(module = "ta_indicators.cuda", unsendable)]
24pub struct DeviceDvdiqqePlanePy {
25    pub(crate) inner: crate::cuda::moving_averages::DeviceArrayF32,
26    pub(crate) _ctx: std::sync::Arc<CudaContext>,
27    pub(crate) device_id: u32,
28}
29
30#[cfg(all(feature = "python", feature = "cuda"))]
31#[pymethods]
32impl DeviceDvdiqqePlanePy {
33    #[getter]
34    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
35        let inner = &self.inner;
36        let d = PyDict::new(py);
37        d.set_item("shape", (inner.rows, inner.cols))?;
38        d.set_item("typestr", "<f4")?;
39        d.set_item(
40            "strides",
41            (
42                inner.cols * std::mem::size_of::<f32>(),
43                std::mem::size_of::<f32>(),
44            ),
45        )?;
46        d.set_item("data", (inner.device_ptr() as usize, false))?;
47        d.set_item("version", 3)?;
48        Ok(d)
49    }
50
51    fn __dlpack_device__(&self) -> PyResult<(i32, i32)> {
52        Ok((2, self.device_id as i32))
53    }
54
55    #[pyo3(signature = (stream=None, max_version=None, dl_device=None, copy=None))]
56    fn __dlpack__<'py>(
57        &mut self,
58        py: Python<'py>,
59        stream: Option<PyObject>,
60        max_version: Option<PyObject>,
61        dl_device: Option<PyObject>,
62        copy: Option<PyObject>,
63    ) -> PyResult<PyObject> {
64        let (kdl, alloc_dev) = self.__dlpack_device__()?;
65        if let Some(dev_obj) = dl_device.as_ref() {
66            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
67                if dev_ty != kdl || dev_id != alloc_dev {
68                    let wants_copy = copy
69                        .as_ref()
70                        .and_then(|c| c.extract::<bool>(py).ok())
71                        .unwrap_or(false);
72                    if wants_copy {
73                        return Err(PyNotImplementedError::new_err(
74                            "__dlpack__ copy path is not implemented for dvdiqqe CUDA buffers",
75                        ));
76                    } else {
77                        return Err(PyValueError::new_err(
78                            "dl_device mismatch and copy not requested",
79                        ));
80                    }
81                }
82            }
83        }
84
85        if let Some(obj) = stream.as_ref() {
86            if !obj.is_none(py) {
87                if let Ok(i) = obj.extract::<i64>(py) {
88                    if i == 0 {
89                        return Err(PyValueError::new_err(
90                            "__dlpack__: stream 0 is disallowed for CUDA",
91                        ));
92                    }
93                }
94            }
95        }
96
97        let inner = std::mem::replace(
98            &mut self.inner,
99            crate::cuda::moving_averages::DeviceArrayF32 {
100                buf: cust::memory::DeviceBuffer::from_slice(&[])
101                    .map_err(|e| PyValueError::new_err(e.to_string()))?,
102                rows: 0,
103                cols: 0,
104            },
105        );
106
107        let rows = inner.rows;
108        let cols = inner.cols;
109        let buf = inner.buf;
110
111        let max_version_bound = max_version.map(|obj| obj.into_bound(py));
112
113        export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
114    }
115}
116use crate::indicators::moving_averages::ema::{ema_with_kernel, EmaInput, EmaParams};
117use crate::utilities::data_loader::{source_type, Candles};
118use crate::utilities::enums::Kernel;
119use crate::utilities::helpers::{
120    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
121    make_uninit_matrix,
122};
123#[cfg(feature = "python")]
124use crate::utilities::kernel_validation::validate_kernel;
125use aligned_vec::{AVec, CACHELINE_ALIGN};
126
127#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
128use core::arch::x86_64::*;
129
130#[cfg(not(target_arch = "wasm32"))]
131use rayon::prelude::*;
132
133use std::convert::AsRef;
134use std::error::Error;
135use std::mem::MaybeUninit;
136use thiserror::Error;
137
138#[derive(Debug, Clone)]
139pub enum DvdiqqeData<'a> {
140    Candles {
141        candles: &'a Candles,
142    },
143    Slices {
144        open: &'a [f64],
145        high: &'a [f64],
146        low: &'a [f64],
147        close: &'a [f64],
148        volume: Option<&'a [f64]>,
149    },
150}
151
152#[derive(Debug, Clone)]
153pub struct DvdiqqeOutput {
154    pub dvdi: Vec<f64>,
155    pub fast_tl: Vec<f64>,
156    pub slow_tl: Vec<f64>,
157    pub center_line: Vec<f64>,
158}
159
160#[derive(Debug, Clone)]
161#[cfg_attr(
162    all(target_arch = "wasm32", feature = "wasm"),
163    derive(Serialize, Deserialize)
164)]
165pub struct DvdiqqeParams {
166    pub period: Option<usize>,
167    pub smoothing_period: Option<usize>,
168    pub fast_multiplier: Option<f64>,
169    pub slow_multiplier: Option<f64>,
170    pub volume_type: Option<String>,
171    pub center_type: Option<String>,
172    pub tick_size: Option<f64>,
173}
174
175impl Default for DvdiqqeParams {
176    fn default() -> Self {
177        Self {
178            period: Some(13),
179            smoothing_period: Some(6),
180            fast_multiplier: Some(2.618),
181            slow_multiplier: Some(4.236),
182            volume_type: Some("default".to_string()),
183            center_type: Some("dynamic".to_string()),
184            tick_size: Some(0.01),
185        }
186    }
187}
188
189#[derive(Debug, Clone)]
190pub struct DvdiqqeInput<'a> {
191    pub data: DvdiqqeData<'a>,
192    pub params: DvdiqqeParams,
193}
194
195impl<'a> AsRef<[f64]> for DvdiqqeInput<'a> {
196    fn as_ref(&self) -> &[f64] {
197        match &self.data {
198            DvdiqqeData::Candles { candles } => &candles.close,
199            DvdiqqeData::Slices { close, .. } => close,
200        }
201    }
202}
203
204impl<'a> DvdiqqeInput<'a> {
205    #[inline]
206    pub fn from_candles(c: &'a Candles, p: DvdiqqeParams) -> Self {
207        Self {
208            data: DvdiqqeData::Candles { candles: c },
209            params: p,
210        }
211    }
212
213    #[inline]
214    pub fn from_slices(
215        open: &'a [f64],
216        high: &'a [f64],
217        low: &'a [f64],
218        close: &'a [f64],
219        volume: Option<&'a [f64]>,
220        p: DvdiqqeParams,
221    ) -> Self {
222        Self {
223            data: DvdiqqeData::Slices {
224                open,
225                high,
226                low,
227                close,
228                volume,
229            },
230            params: p,
231        }
232    }
233
234    #[inline]
235    pub fn with_default_candles(c: &'a Candles) -> Self {
236        Self::from_candles(c, DvdiqqeParams::default())
237    }
238
239    #[inline]
240    pub fn get_period(&self) -> usize {
241        self.params.period.unwrap_or(13)
242    }
243
244    #[inline]
245    pub fn get_smoothing_period(&self) -> usize {
246        self.params.smoothing_period.unwrap_or(6)
247    }
248
249    #[inline]
250    pub fn get_fast_multiplier(&self) -> f64 {
251        self.params.fast_multiplier.unwrap_or(2.618)
252    }
253
254    #[inline]
255    pub fn get_slow_multiplier(&self) -> f64 {
256        self.params.slow_multiplier.unwrap_or(4.236)
257    }
258
259    #[inline]
260    pub fn get_volume_type(&self) -> &str {
261        self.params.volume_type.as_deref().unwrap_or("default")
262    }
263
264    #[inline]
265    pub fn get_center_type(&self) -> &str {
266        self.params.center_type.as_deref().unwrap_or("dynamic")
267    }
268
269    #[inline]
270    pub fn get_tick_size(&self) -> f64 {
271        self.params.tick_size.unwrap_or(0.01)
272    }
273}
274
275#[derive(Debug, Error)]
276pub enum DvdiqqeError {
277    #[error("dvdiqqe: Empty input data")]
278    EmptyInputData,
279
280    #[error("dvdiqqe: All values are NaN")]
281    AllValuesNaN,
282
283    #[error("dvdiqqe: Invalid period: {period}, data length: {data_len}")]
284    InvalidPeriod { period: usize, data_len: usize },
285
286    #[error("dvdiqqe: Not enough valid data: needed = {needed}, valid = {valid}")]
287    NotEnoughValidData { needed: usize, valid: usize },
288
289    #[error("Input arrays must have the same length")]
290    MissingData,
291
292    #[error("dvdiqqe: Invalid smoothing period: {smoothing}")]
293    InvalidSmoothing { smoothing: usize },
294
295    #[error("dvdiqqe: Invalid tick size: {tick}")]
296    InvalidTick { tick: f64 },
297
298    #[error("Invalid multiplier: {which} multiplier must be positive (got {multiplier})")]
299    InvalidMultiplier { multiplier: f64, which: String },
300
301    #[error("dvdiqqe: Output length mismatch: expected = {expected}, got = {got}")]
302    OutputLengthMismatch { expected: usize, got: usize },
303
304    #[error("dvdiqqe: Invalid range (usize): start={start} end={end} step={step}")]
305    InvalidRangeUsize {
306        start: usize,
307        end: usize,
308        step: usize,
309    },
310
311    #[error("dvdiqqe: Invalid range (f64): start={start} end={end} step={step}")]
312    InvalidRangeF64 { start: f64, end: f64, step: f64 },
313
314    #[error("dvdiqqe: Invalid kernel for batch: {0:?}")]
315    InvalidKernelForBatch(crate::utilities::enums::Kernel),
316
317    #[error("dvdiqqe: {0}")]
318    InvalidInput(String),
319
320    #[error("dvdiqqe: EMA computation failed: {0}")]
321    EmaError(String),
322}
323
324#[inline(always)]
325fn dvdiqqe_prepare<'a>(
326    input: &'a DvdiqqeInput,
327) -> Result<
328    (
329        &'a [f64],
330        &'a [f64],
331        &'a [f64],
332        &'a [f64],
333        Option<&'a [f64]>,
334        usize,
335        usize,
336        f64,
337        f64,
338        &'a str,
339        &'a str,
340        f64,
341        usize,
342    ),
343    DvdiqqeError,
344> {
345    let (o, h, l, c, v) = match &input.data {
346        DvdiqqeData::Candles { candles } => (
347            &candles.open[..],
348            &candles.high[..],
349            &candles.low[..],
350            &candles.close[..],
351            Some(&candles.volume[..]),
352        ),
353        DvdiqqeData::Slices {
354            open,
355            high,
356            low,
357            close,
358            volume,
359        } => (*open, *high, *low, *close, *volume),
360    };
361
362    let len = c.len();
363    if len == 0 {
364        return Err(DvdiqqeError::EmptyInputData);
365    }
366    if o.len() != len || h.len() != len || l.len() != len {
367        return Err(DvdiqqeError::MissingData);
368    }
369    if let Some(vs) = v {
370        if vs.len() != len {
371            return Err(DvdiqqeError::MissingData);
372        }
373    }
374
375    let first = c
376        .iter()
377        .position(|x| x.is_finite())
378        .ok_or(DvdiqqeError::AllValuesNaN)?;
379    let period = input.get_period();
380    if period == 0 || period > len {
381        return Err(DvdiqqeError::InvalidPeriod {
382            period,
383            data_len: len,
384        });
385    }
386
387    let smoothing = input.get_smoothing_period();
388    if smoothing == 0 {
389        return Err(DvdiqqeError::InvalidSmoothing { smoothing });
390    }
391
392    let fast_mult = input.get_fast_multiplier();
393    let slow_mult = input.get_slow_multiplier();
394    if fast_mult <= 0.0 || !fast_mult.is_finite() {
395        return Err(DvdiqqeError::InvalidMultiplier {
396            multiplier: fast_mult,
397            which: "fast".to_string(),
398        });
399    }
400    if slow_mult <= 0.0 || !slow_mult.is_finite() {
401        return Err(DvdiqqeError::InvalidMultiplier {
402            multiplier: slow_mult,
403            which: "slow".to_string(),
404        });
405    }
406
407    if len - first < period {
408        return Err(DvdiqqeError::NotEnoughValidData {
409            needed: period,
410            valid: len - first,
411        });
412    }
413
414    let tick = input.get_tick_size();
415    if !tick.is_finite() || tick <= 0.0 {
416        return Err(DvdiqqeError::InvalidTick { tick });
417    }
418
419    Ok((
420        o,
421        h,
422        l,
423        c,
424        v,
425        period,
426        smoothing,
427        fast_mult,
428        slow_mult,
429        input.get_volume_type(),
430        input.get_center_type(),
431        tick,
432        first,
433    ))
434}
435
436#[inline(always)]
437fn dvdiqqe_compute_into(
438    open: &[f64],
439    _high: &[f64],
440    _low: &[f64],
441    close: &[f64],
442    volume_opt: Option<&[f64]>,
443    period: usize,
444    smoothing_period: usize,
445    fast_mult: f64,
446    slow_mult: f64,
447    volume_type: &str,
448    center_type: &str,
449    tick: f64,
450    first_valid: usize,
451    kernel: Kernel,
452    dvdi_out: &mut [f64],
453    fast_out: &mut [f64],
454    slow_out: &mut [f64],
455    center_out: &mut [f64],
456) -> Result<(), DvdiqqeError> {
457    let len = close.len();
458    assert_eq!(dvdi_out.len(), len);
459    assert_eq!(fast_out.len(), len);
460    assert_eq!(slow_out.len(), len);
461    assert_eq!(center_out.len(), len);
462
463    if len == 0 {
464        return Ok(());
465    }
466
467    let wper = (period * 2) - 1;
468    let warmup = first_valid + wper;
469
470    let mut pvi = alloc_with_nan_prefix(len, 0);
471    let mut nvi = alloc_with_nan_prefix(len, 0);
472
473    let mut pvi_prev = 0.0f64;
474    let mut nvi_prev = 0.0f64;
475    let mut prev_vol = 0.0f64;
476    let mut prev_close = 0.0f64;
477    let mut tickrng_prev = tick;
478    let use_tick_only = volume_type.eq_ignore_ascii_case("tick");
479
480    for i in 0..len {
481        let oi = open[i];
482        let ci = close[i];
483
484        let rng = ci - oi;
485        let tickrng = if rng.abs() < tick { tickrng_prev } else { rng };
486        let tick_vol = (tickrng.abs() / tick).max(0.0);
487
488        let sel_vol = if use_tick_only {
489            tick_vol
490        } else if let Some(vs) = volume_opt {
491            let vv = vs[i];
492            if vv.is_finite() {
493                vv
494            } else {
495                tick_vol
496            }
497        } else {
498            tick_vol
499        };
500
501        let d_close = ci - prev_close;
502        if sel_vol > prev_vol {
503            pvi_prev += d_close;
504        }
505        if sel_vol < prev_vol {
506            nvi_prev -= d_close;
507        }
508
509        pvi[i] = pvi_prev;
510        nvi[i] = nvi_prev;
511        prev_close = ci;
512        prev_vol = sel_vol;
513        tickrng_prev = tickrng;
514    }
515
516    let pvi_ema = {
517        let prm = EmaParams {
518            period: Some(period),
519        };
520        let inp = EmaInput::from_slice(&pvi, prm);
521        ema_with_kernel(&inp, kernel).map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
522    };
523    let nvi_ema = {
524        let prm = EmaParams {
525            period: Some(period),
526        };
527        let inp = EmaInput::from_slice(&nvi, prm);
528        ema_with_kernel(&inp, kernel).map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
529    };
530
531    for i in 0..len {
532        pvi[i] = pvi[i] - pvi_ema.values[i];
533        nvi[i] = nvi[i] - nvi_ema.values[i];
534    }
535
536    let pdiv_ema = {
537        let prm = EmaParams {
538            period: Some(smoothing_period),
539        };
540        let inp = EmaInput::from_slice(&pvi, prm);
541        ema_with_kernel(&inp, kernel).map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
542    };
543    let ndiv_ema = {
544        let prm = EmaParams {
545            period: Some(smoothing_period),
546        };
547        let inp = EmaInput::from_slice(&nvi, prm);
548        ema_with_kernel(&inp, kernel).map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
549    };
550
551    let mut ranges = alloc_with_nan_prefix(len, 1);
552    dvdi_out[0] = pdiv_ema.values[0] - ndiv_ema.values[0];
553    for i in 1..len {
554        let dvdi_i = pdiv_ema.values[i] - ndiv_ema.values[i];
555        ranges[i] = (dvdi_i - dvdi_out[i - 1]).abs();
556        dvdi_out[i] = dvdi_i;
557    }
558
559    let avg_range = {
560        let prm = EmaParams { period: Some(wper) };
561        let inp = EmaInput::from_slice(&ranges, prm);
562        ema_with_kernel(&inp, Kernel::Auto).map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
563    };
564    let smooth_range = {
565        let prm = EmaParams { period: Some(wper) };
566        let inp = EmaInput::from_slice(&avg_range.values, prm);
567        ema_with_kernel(&inp, Kernel::Auto).map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
568    };
569
570    for i in 0..warmup.min(len) {
571        dvdi_out[i] = f64::NAN;
572        fast_out[i] = f64::NAN;
573        slow_out[i] = f64::NAN;
574    }
575
576    if warmup < len {
577        fast_out[warmup] = dvdi_out[warmup];
578        slow_out[warmup] = dvdi_out[warmup];
579
580        for i in (warmup + 1)..len {
581            let fr = smooth_range.values[i] * fast_mult;
582            let sr = smooth_range.values[i] * slow_mult;
583
584            if dvdi_out[i] > fast_out[i - 1] {
585                let nv = dvdi_out[i] - fr;
586                fast_out[i] = if nv < fast_out[i - 1] {
587                    fast_out[i - 1]
588                } else {
589                    nv
590                };
591            } else {
592                let nv = dvdi_out[i] + fr;
593                fast_out[i] = if nv > fast_out[i - 1] {
594                    fast_out[i - 1]
595                } else {
596                    nv
597                };
598            }
599
600            if dvdi_out[i] > slow_out[i - 1] {
601                let nv = dvdi_out[i] - sr;
602                slow_out[i] = if nv < slow_out[i - 1] {
603                    slow_out[i - 1]
604                } else {
605                    nv
606                };
607            } else {
608                let nv = dvdi_out[i] + sr;
609                slow_out[i] = if nv > slow_out[i - 1] {
610                    slow_out[i - 1]
611                } else {
612                    nv
613                };
614            }
615        }
616    }
617
618    for i in 0..warmup.min(len) {
619        center_out[i] = f64::NAN;
620    }
621    if center_type.eq_ignore_ascii_case("dynamic") {
622        let mut sum = 0.0f64;
623        let mut cnt = 0.0f64;
624        for i in warmup..len {
625            let v = dvdi_out[i];
626            if v.is_finite() {
627                sum += v;
628                cnt += 1.0;
629            }
630            center_out[i] = if cnt > 0.0 { sum / cnt } else { f64::NAN };
631        }
632    } else {
633        for i in warmup..len {
634            center_out[i] = 0.0;
635        }
636    }
637
638    Ok(())
639}
640
641pub fn dvdiqqe(input: &DvdiqqeInput) -> Result<DvdiqqeOutput, DvdiqqeError> {
642    dvdiqqe_with_kernel(input, Kernel::Auto)
643}
644
645pub fn dvdiqqe_with_kernel(
646    input: &DvdiqqeInput,
647    kernel: Kernel,
648) -> Result<DvdiqqeOutput, DvdiqqeError> {
649    let (_, _, _, c, _, period, _, _, _, _, _, _, first) = dvdiqqe_prepare(input)?;
650    let len = c.len();
651    let wper = (period * 2) - 1;
652    let warmup = first + wper;
653    let mut dvdi = alloc_with_nan_prefix(len, warmup);
654    let mut fast = alloc_with_nan_prefix(len, warmup);
655    let mut slow = alloc_with_nan_prefix(len, warmup);
656    let mut center = alloc_with_nan_prefix(len, warmup);
657
658    let actual_kernel = match kernel {
659        Kernel::Auto => detect_best_kernel(),
660        k => k,
661    };
662
663    match actual_kernel {
664        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
665        Kernel::Avx512 => unsafe {
666            dvdiqqe_avx512(&mut dvdi, &mut fast, &mut slow, &mut center, input)
667        },
668        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
669        Kernel::Avx2 => unsafe {
670            dvdiqqe_avx2(&mut dvdi, &mut fast, &mut slow, &mut center, input)
671        },
672        _ => dvdiqqe_into_slices(
673            &mut dvdi,
674            &mut fast,
675            &mut slow,
676            &mut center,
677            input,
678            actual_kernel,
679        ),
680    }?;
681
682    Ok(DvdiqqeOutput {
683        dvdi,
684        fast_tl: fast,
685        slow_tl: slow,
686        center_line: center,
687    })
688}
689
690#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
691#[target_feature(enable = "avx2,fma")]
692unsafe fn dvdiqqe_avx2(
693    dvdi_dst: &mut [f64],
694    fast_dst: &mut [f64],
695    slow_dst: &mut [f64],
696    center_dst: &mut [f64],
697    input: &DvdiqqeInput,
698) -> Result<(), DvdiqqeError> {
699    dvdiqqe_into_slices(
700        dvdi_dst,
701        fast_dst,
702        slow_dst,
703        center_dst,
704        input,
705        Kernel::Avx2,
706    )
707}
708
709#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
710#[target_feature(enable = "avx512f,fma")]
711unsafe fn dvdiqqe_avx512(
712    dvdi_dst: &mut [f64],
713    fast_dst: &mut [f64],
714    slow_dst: &mut [f64],
715    center_dst: &mut [f64],
716    input: &DvdiqqeInput,
717) -> Result<(), DvdiqqeError> {
718    dvdiqqe_into_slices(
719        dvdi_dst,
720        fast_dst,
721        slow_dst,
722        center_dst,
723        input,
724        Kernel::Avx512,
725    )
726}
727
728#[inline]
729fn calculate_tick_volume_pine_like(open: &[f64], close: &[f64], tick: f64) -> Vec<f64> {
730    let len = open.len();
731    let mut out = alloc_with_nan_prefix(len, 0);
732    let mut tickrng_prev = tick;
733
734    for i in 0..len {
735        let rng = close[i] - open[i];
736        let tickrng = if rng.abs() < tick { tickrng_prev } else { rng };
737        out[i] = (tickrng.abs() / tick).max(0.0);
738        tickrng_prev = tickrng;
739    }
740    out
741}
742
743#[inline]
744fn select_volume_pine_like(
745    vol_opt: Option<&[f64]>,
746    tick_vol: &[f64],
747    volume_type: &str,
748) -> Vec<f64> {
749    let len = tick_vol.len();
750    let mut out = alloc_with_nan_prefix(len, 0);
751    match (volume_type.eq_ignore_ascii_case("tick"), vol_opt) {
752        (true, _) => {
753            for i in 0..len {
754                out[i] = tick_vol[i];
755            }
756        }
757        (false, Some(v)) => {
758            for i in 0..len {
759                out[i] = if v[i].is_finite() { v[i] } else { tick_vol[i] };
760            }
761        }
762        (false, None) => {
763            for i in 0..len {
764                out[i] = tick_vol[i];
765            }
766        }
767    }
768    out
769}
770
771#[inline]
772fn build_pvi_nvi_pine_like(close: &[f64], volume: &[f64]) -> (Vec<f64>, Vec<f64>) {
773    let len = close.len();
774    let mut pvi = alloc_with_nan_prefix(len, 0);
775    let mut nvi = alloc_with_nan_prefix(len, 0);
776    let mut pvi_prev = 0.0;
777    let mut nvi_prev = 0.0;
778    let mut prev_vol = 0.0;
779    let mut prev_x = 0.0;
780
781    for i in 0..len {
782        if volume[i] > prev_vol {
783            pvi_prev += close[i] - prev_x;
784        }
785        if volume[i] < prev_vol {
786            nvi_prev -= close[i] - prev_x;
787        }
788        pvi[i] = pvi_prev;
789        nvi[i] = nvi_prev;
790        prev_vol = volume[i];
791        prev_x = close[i];
792    }
793    (pvi, nvi)
794}
795
796fn calculate_dvdi(
797    close: &[f64],
798    volume: &[f64],
799    period: usize,
800    smoothing_period: usize,
801    kernel: Kernel,
802) -> Result<Vec<f64>, DvdiqqeError> {
803    let len = close.len();
804
805    let (pvi, nvi) = build_pvi_nvi_pine_like(close, volume);
806
807    let pvi_ema_params = EmaParams {
808        period: Some(period),
809    };
810    let pvi_ema_input = EmaInput::from_slice(&pvi, pvi_ema_params);
811    let pvi_ema = ema_with_kernel(&pvi_ema_input, kernel)
812        .map_err(|e| DvdiqqeError::EmaError(e.to_string()))?;
813
814    let nvi_ema_params = EmaParams {
815        period: Some(period),
816    };
817    let nvi_ema_input = EmaInput::from_slice(&nvi, nvi_ema_params);
818    let nvi_ema = ema_with_kernel(&nvi_ema_input, kernel)
819        .map_err(|e| DvdiqqeError::EmaError(e.to_string()))?;
820
821    let mut pvi_div = alloc_with_nan_prefix(len, 0);
822    let mut nvi_div = alloc_with_nan_prefix(len, 0);
823
824    for i in 0..len {
825        pvi_div[i] = pvi[i] - pvi_ema.values[i];
826        nvi_div[i] = nvi[i] - nvi_ema.values[i];
827    }
828
829    let pdiv_ema_params = EmaParams {
830        period: Some(smoothing_period),
831    };
832    let pdiv_ema_input = EmaInput::from_slice(&pvi_div, pdiv_ema_params);
833    let pdiv_ema = ema_with_kernel(&pdiv_ema_input, kernel)
834        .map_err(|e| DvdiqqeError::EmaError(e.to_string()))?;
835
836    let ndiv_ema_params = EmaParams {
837        period: Some(smoothing_period),
838    };
839    let ndiv_ema_input = EmaInput::from_slice(&nvi_div, ndiv_ema_params);
840    let ndiv_ema = ema_with_kernel(&ndiv_ema_input, kernel)
841        .map_err(|e| DvdiqqeError::EmaError(e.to_string()))?;
842
843    let mut dvdi = alloc_with_nan_prefix(len, 0);
844    for i in 0..len {
845        dvdi[i] = pdiv_ema.values[i] - ndiv_ema.values[i];
846    }
847
848    Ok(dvdi)
849}
850
851fn calculate_trailing_levels(
852    dvdi: &[f64],
853    period: usize,
854    fast_mult: f64,
855    slow_mult: f64,
856) -> Result<(Vec<f64>, Vec<f64>), DvdiqqeError> {
857    let len = dvdi.len();
858    let wper = (period * 2) - 1;
859
860    let mut ranges = alloc_with_nan_prefix(len, 1);
861    for i in 1..len {
862        ranges[i] = (dvdi[i] - dvdi[i - 1]).abs();
863    }
864
865    let range_ema_params = EmaParams { period: Some(wper) };
866    let range_ema_input = EmaInput::from_slice(&ranges, range_ema_params);
867    let avg_range = ema_with_kernel(&range_ema_input, Kernel::Auto)
868        .map_err(|e| DvdiqqeError::EmaError(e.to_string()))?;
869
870    let smooth_range_params = EmaParams { period: Some(wper) };
871    let smooth_range_input = EmaInput::from_slice(&avg_range.values, smooth_range_params);
872    let smooth_range = ema_with_kernel(&smooth_range_input, Kernel::Auto)
873        .map_err(|e| DvdiqqeError::EmaError(e.to_string()))?;
874
875    let first_valid = dvdi.iter().position(|&x| x.is_finite()).unwrap_or(len);
876
877    let mut fast_tl = alloc_with_nan_prefix(len, first_valid);
878    let mut slow_tl = alloc_with_nan_prefix(len, first_valid);
879
880    if first_valid < len {
881        fast_tl[first_valid] = dvdi[first_valid];
882        slow_tl[first_valid] = dvdi[first_valid];
883
884        for i in (first_valid + 1)..len {
885            let fast_range = smooth_range.values[i] * fast_mult;
886            let slow_range = smooth_range.values[i] * slow_mult;
887
888            if dvdi[i] > fast_tl[i - 1] {
889                let new_val = dvdi[i] - fast_range;
890                fast_tl[i] = if new_val < fast_tl[i - 1] {
891                    fast_tl[i - 1]
892                } else {
893                    new_val
894                };
895            } else {
896                let new_val = dvdi[i] + fast_range;
897                fast_tl[i] = if new_val > fast_tl[i - 1] {
898                    fast_tl[i - 1]
899                } else {
900                    new_val
901                };
902            }
903
904            if dvdi[i] > slow_tl[i - 1] {
905                let new_val = dvdi[i] - slow_range;
906                slow_tl[i] = if new_val < slow_tl[i - 1] {
907                    slow_tl[i - 1]
908                } else {
909                    new_val
910                };
911            } else {
912                let new_val = dvdi[i] + slow_range;
913                slow_tl[i] = if new_val > slow_tl[i - 1] {
914                    slow_tl[i - 1]
915                } else {
916                    new_val
917                };
918            }
919        }
920    }
921
922    Ok((fast_tl, slow_tl))
923}
924
925fn calculate_cumulative_mean(dvdi: &[f64]) -> Vec<f64> {
926    let len = dvdi.len();
927    let first_valid = dvdi.iter().position(|&x| x.is_finite()).unwrap_or(len);
928    let mut center = alloc_with_nan_prefix(len, first_valid);
929    let mut sum = 0.0;
930    let mut cnt = 0.0;
931
932    for i in first_valid..len {
933        if dvdi[i].is_finite() {
934            sum += dvdi[i];
935            cnt += 1.0;
936        }
937        center[i] = if cnt > 0.0 { sum / cnt } else { f64::NAN };
938    }
939    center
940}
941
942pub fn dvdiqqe_into_slices(
943    dvdi_dst: &mut [f64],
944    fast_tl_dst: &mut [f64],
945    slow_tl_dst: &mut [f64],
946    center_dst: &mut [f64],
947    input: &DvdiqqeInput,
948    kernel: Kernel,
949) -> Result<(), DvdiqqeError> {
950    let (o, h, l, c, v, period, smoothing, fast, slow, vt, ct, tick, first) =
951        dvdiqqe_prepare(input)?;
952
953    let len = c.len();
954    if dvdi_dst.len() != len {
955        return Err(DvdiqqeError::OutputLengthMismatch {
956            expected: len,
957            got: dvdi_dst.len(),
958        });
959    }
960    if fast_tl_dst.len() != len {
961        return Err(DvdiqqeError::OutputLengthMismatch {
962            expected: len,
963            got: fast_tl_dst.len(),
964        });
965    }
966    if slow_tl_dst.len() != len {
967        return Err(DvdiqqeError::OutputLengthMismatch {
968            expected: len,
969            got: slow_tl_dst.len(),
970        });
971    }
972    if center_dst.len() != len {
973        return Err(DvdiqqeError::OutputLengthMismatch {
974            expected: len,
975            got: center_dst.len(),
976        });
977    }
978
979    dvdiqqe_compute_into(
980        o,
981        h,
982        l,
983        c,
984        v,
985        period,
986        smoothing,
987        fast,
988        slow,
989        vt,
990        ct,
991        tick,
992        first,
993        kernel,
994        dvdi_dst,
995        fast_tl_dst,
996        slow_tl_dst,
997        center_dst,
998    )
999}
1000
1001#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1002pub fn dvdiqqe_into(
1003    input: &DvdiqqeInput,
1004    dvdi_out: &mut [f64],
1005    fast_tl_out: &mut [f64],
1006    slow_tl_out: &mut [f64],
1007    center_out: &mut [f64],
1008) -> Result<(), DvdiqqeError> {
1009    dvdiqqe_into_slices(
1010        dvdi_out,
1011        fast_tl_out,
1012        slow_tl_out,
1013        center_out,
1014        input,
1015        Kernel::Auto,
1016    )
1017}
1018
1019#[inline]
1020pub fn dvdiqqe_into_flat(
1021    dst_4xlen: &mut [f64],
1022    input: &DvdiqqeInput,
1023    k: Kernel,
1024) -> Result<(), DvdiqqeError> {
1025    let (_, _, _, c, _, _, _, _, _, _, _, _, _) = dvdiqqe_prepare(input)?;
1026    let len = c.len();
1027    let expected = len
1028        .checked_mul(4)
1029        .ok_or_else(|| DvdiqqeError::InvalidInput("4*len overflow".into()))?;
1030    if dst_4xlen.len() != expected {
1031        return Err(DvdiqqeError::OutputLengthMismatch {
1032            expected,
1033            got: dst_4xlen.len(),
1034        });
1035    }
1036    let (dvdi, rest) = dst_4xlen.split_at_mut(len);
1037    let (fast, rest) = rest.split_at_mut(len);
1038    let (slow, cent) = rest.split_at_mut(len);
1039    dvdiqqe_into_slices(dvdi, fast, slow, cent, input, k)
1040}
1041
1042#[derive(Copy, Clone, Debug, Default)]
1043pub struct DvdiqqeBuilder {
1044    period: Option<usize>,
1045    smoothing: Option<usize>,
1046    fast: Option<f64>,
1047    slow: Option<f64>,
1048    volume_type: Option<&'static str>,
1049    center_type: Option<&'static str>,
1050    tick: Option<f64>,
1051    kernel: Kernel,
1052}
1053
1054impl DvdiqqeBuilder {
1055    #[inline(always)]
1056    pub fn new() -> Self {
1057        Self::default()
1058    }
1059
1060    pub fn period(mut self, n: usize) -> Self {
1061        self.period = Some(n);
1062        self
1063    }
1064
1065    pub fn smoothing(mut self, n: usize) -> Self {
1066        self.smoothing = Some(n);
1067        self
1068    }
1069
1070    pub fn fast(mut self, mult: f64) -> Self {
1071        self.fast = Some(mult);
1072        self
1073    }
1074
1075    pub fn slow(mut self, mult: f64) -> Self {
1076        self.slow = Some(mult);
1077        self
1078    }
1079
1080    pub fn kernel(mut self, k: Kernel) -> Self {
1081        self.kernel = k;
1082        self
1083    }
1084
1085    pub fn volume_type(mut self, vt: &'static str) -> Self {
1086        self.volume_type = Some(vt);
1087        self
1088    }
1089
1090    pub fn center_type(mut self, ct: &'static str) -> Self {
1091        self.center_type = Some(ct);
1092        self
1093    }
1094
1095    pub fn tick_size(mut self, ts: f64) -> Self {
1096        self.tick = Some(ts);
1097        self
1098    }
1099
1100    pub fn apply_slice(
1101        self,
1102        o: &[f64],
1103        h: &[f64],
1104        l: &[f64],
1105        c: &[f64],
1106        v: Option<&[f64]>,
1107    ) -> Result<DvdiqqeOutput, DvdiqqeError> {
1108        let p = DvdiqqeParams {
1109            period: self.period,
1110            smoothing_period: self.smoothing,
1111            fast_multiplier: self.fast,
1112            slow_multiplier: self.slow,
1113            volume_type: self.volume_type.map(String::from),
1114            center_type: self.center_type.map(String::from),
1115            tick_size: self.tick,
1116        };
1117        let i = DvdiqqeInput::from_slices(o, h, l, c, v, p);
1118        dvdiqqe_with_kernel(&i, self.kernel)
1119    }
1120
1121    #[inline(always)]
1122    pub fn apply_candles(self, c: &Candles) -> Result<DvdiqqeOutput, DvdiqqeError> {
1123        let p = DvdiqqeParams {
1124            period: self.period,
1125            smoothing_period: self.smoothing,
1126            fast_multiplier: self.fast,
1127            slow_multiplier: self.slow,
1128            volume_type: self.volume_type.map(str::to_string),
1129            center_type: self.center_type.map(str::to_string),
1130            tick_size: self.tick,
1131        };
1132        let i = DvdiqqeInput::from_candles(c, p);
1133        dvdiqqe_with_kernel(&i, self.kernel)
1134    }
1135
1136    #[inline(always)]
1137    pub fn into_stream(self) -> Result<DvdiqqeStream, DvdiqqeError> {
1138        let p = DvdiqqeParams {
1139            period: self.period,
1140            smoothing_period: self.smoothing,
1141            fast_multiplier: self.fast,
1142            slow_multiplier: self.slow,
1143            volume_type: self.volume_type.map(str::to_string),
1144            center_type: self.center_type.map(str::to_string),
1145            tick_size: self.tick,
1146        };
1147        DvdiqqeStream::try_new(p)
1148    }
1149}
1150
1151#[derive(Clone, Debug)]
1152pub struct DvdiqqeBatchRange {
1153    pub period: (usize, usize, usize),
1154    pub smoothing_period: (usize, usize, usize),
1155    pub fast_multiplier: (f64, f64, f64),
1156    pub slow_multiplier: (f64, f64, f64),
1157}
1158
1159impl Default for DvdiqqeBatchRange {
1160    fn default() -> Self {
1161        Self {
1162            period: (13, 262, 1),
1163            smoothing_period: (6, 6, 0),
1164            fast_multiplier: (2.618, 2.618, 0.0),
1165            slow_multiplier: (4.236, 4.236, 0.0),
1166        }
1167    }
1168}
1169
1170#[derive(Clone, Debug)]
1171pub struct DvdiqqeBatchOutput {
1172    pub dvdi_values: Vec<f64>,
1173    pub fast_tl_values: Vec<f64>,
1174    pub slow_tl_values: Vec<f64>,
1175    pub center_values: Vec<f64>,
1176    pub combos: Vec<DvdiqqeParams>,
1177    pub rows: usize,
1178    pub cols: usize,
1179}
1180
1181impl DvdiqqeBatchOutput {
1182    pub fn values_for(&self, params: &DvdiqqeParams) -> Option<DvdiqqeBatchValues> {
1183        self.combos
1184            .iter()
1185            .position(|p| {
1186                p.period == params.period
1187                    && p.smoothing_period == params.smoothing_period
1188                    && p.fast_multiplier == params.fast_multiplier
1189                    && p.slow_multiplier == params.slow_multiplier
1190            })
1191            .map(|idx| {
1192                let start = idx * self.cols;
1193                let end = start + self.cols;
1194                DvdiqqeBatchValues {
1195                    dvdi: &self.dvdi_values[start..end],
1196                    fast_tl: &self.fast_tl_values[start..end],
1197                    slow_tl: &self.slow_tl_values[start..end],
1198                    center: &self.center_values[start..end],
1199                }
1200            })
1201    }
1202
1203    pub fn row_for_params(&self, params: &DvdiqqeParams) -> Option<Vec<f64>> {
1204        self.combos
1205            .iter()
1206            .position(|p| {
1207                p.period == params.period
1208                    && p.smoothing_period == params.smoothing_period
1209                    && p.fast_multiplier == params.fast_multiplier
1210                    && p.slow_multiplier == params.slow_multiplier
1211            })
1212            .map(|idx| {
1213                let start = idx * self.cols;
1214                let end = start + self.cols;
1215
1216                let mut row = Vec::with_capacity(self.cols * 4);
1217                row.extend_from_slice(&self.dvdi_values[start..end]);
1218                row.extend_from_slice(&self.fast_tl_values[start..end]);
1219                row.extend_from_slice(&self.slow_tl_values[start..end]);
1220                row.extend_from_slice(&self.center_values[start..end]);
1221                row
1222            })
1223    }
1224}
1225
1226#[derive(Debug)]
1227pub struct DvdiqqeBatchValues<'a> {
1228    pub dvdi: &'a [f64],
1229    pub fast_tl: &'a [f64],
1230    pub slow_tl: &'a [f64],
1231    pub center: &'a [f64],
1232}
1233
1234#[derive(Clone, Debug)]
1235pub struct DvdiqqeBatchOutputFlat {
1236    pub values: Vec<f64>,
1237    pub combos: Vec<DvdiqqeParams>,
1238    pub rows: usize,
1239    pub cols: usize,
1240    pub series: usize,
1241}
1242
1243impl DvdiqqeBatchOutputFlat {
1244    #[inline]
1245    pub fn slice_series(&self, s: usize) -> &[f64] {
1246        assert!(s < self.series);
1247        let plane = self.rows * self.cols;
1248        &self.values[s * plane..(s + 1) * plane]
1249    }
1250}
1251
1252#[derive(Clone, Debug)]
1253pub struct DvdiqqeBatchBuilder {
1254    range: DvdiqqeBatchRange,
1255    kernel: Kernel,
1256    volume_type: String,
1257    center_type: String,
1258    tick_size: f64,
1259}
1260
1261impl Default for DvdiqqeBatchBuilder {
1262    fn default() -> Self {
1263        Self {
1264            range: DvdiqqeBatchRange::default(),
1265            kernel: Kernel::Auto,
1266            volume_type: "default".to_string(),
1267            center_type: "dynamic".to_string(),
1268            tick_size: 0.01,
1269        }
1270    }
1271}
1272
1273impl DvdiqqeBatchBuilder {
1274    pub fn new() -> Self {
1275        Self::default()
1276    }
1277
1278    pub fn kernel(mut self, k: Kernel) -> Self {
1279        self.kernel = k;
1280        self
1281    }
1282
1283    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
1284        self.range.period = (start, end, step);
1285        self
1286    }
1287
1288    pub fn period_static(mut self, p: usize) -> Self {
1289        self.range.period = (p, p, 0);
1290        self
1291    }
1292
1293    pub fn smoothing_range(mut self, start: usize, end: usize, step: usize) -> Self {
1294        self.range.smoothing_period = (start, end, step);
1295        self
1296    }
1297
1298    pub fn smoothing_static(mut self, s: usize) -> Self {
1299        self.range.smoothing_period = (s, s, 0);
1300        self
1301    }
1302
1303    pub fn fast_range(mut self, start: f64, end: f64, step: f64) -> Self {
1304        self.range.fast_multiplier = (start, end, step);
1305        self
1306    }
1307
1308    pub fn fast_static(mut self, f: f64) -> Self {
1309        self.range.fast_multiplier = (f, f, 0.0);
1310        self
1311    }
1312
1313    pub fn slow_range(mut self, start: f64, end: f64, step: f64) -> Self {
1314        self.range.slow_multiplier = (start, end, step);
1315        self
1316    }
1317
1318    pub fn slow_static(mut self, s: f64) -> Self {
1319        self.range.slow_multiplier = (s, s, 0.0);
1320        self
1321    }
1322
1323    pub fn volume_type(mut self, vt: &str) -> Self {
1324        self.volume_type = vt.to_string();
1325        self
1326    }
1327
1328    pub fn center_type(mut self, ct: &str) -> Self {
1329        self.center_type = ct.to_string();
1330        self
1331    }
1332
1333    pub fn tick_size(mut self, ts: f64) -> Self {
1334        self.tick_size = ts;
1335        self
1336    }
1337
1338    pub fn apply_candles(self, candles: &Candles) -> Result<DvdiqqeBatchOutput, DvdiqqeError> {
1339        dvdiqqe_batch_with_kernel(
1340            &candles.open,
1341            &candles.high,
1342            &candles.low,
1343            &candles.close,
1344            Some(&candles.volume),
1345            &self.range,
1346            self.kernel,
1347            &self.volume_type,
1348            &self.center_type,
1349            self.tick_size,
1350        )
1351    }
1352
1353    pub fn apply_slices(
1354        self,
1355        open: &[f64],
1356        high: &[f64],
1357        low: &[f64],
1358        close: &[f64],
1359        volume: Option<&[f64]>,
1360    ) -> Result<DvdiqqeBatchOutput, DvdiqqeError> {
1361        dvdiqqe_batch_with_kernel(
1362            open,
1363            high,
1364            low,
1365            close,
1366            volume,
1367            &self.range,
1368            self.kernel,
1369            &self.volume_type,
1370            &self.center_type,
1371            self.tick_size,
1372        )
1373    }
1374
1375    pub fn with_default_candles(candles: &Candles) -> Result<DvdiqqeBatchOutput, DvdiqqeError> {
1376        let builder = Self::default();
1377        dvdiqqe_batch_with_kernel(
1378            &candles.open,
1379            &candles.high,
1380            &candles.low,
1381            &candles.close,
1382            Some(&candles.volume),
1383            &builder.range,
1384            builder.kernel,
1385            &builder.volume_type,
1386            &builder.center_type,
1387            builder.tick_size,
1388        )
1389    }
1390
1391    pub fn with_default_slice(
1392        open: &[f64],
1393        high: &[f64],
1394        low: &[f64],
1395        close: &[f64],
1396        volume: Option<&[f64]>,
1397    ) -> Result<DvdiqqeBatchOutput, DvdiqqeError> {
1398        let builder = Self::default();
1399        dvdiqqe_batch_with_kernel(
1400            open,
1401            high,
1402            low,
1403            close,
1404            volume,
1405            &builder.range,
1406            builder.kernel,
1407            &builder.volume_type,
1408            &builder.center_type,
1409            builder.tick_size,
1410        )
1411    }
1412}
1413
1414#[inline(always)]
1415fn expand_grid(r: &DvdiqqeBatchRange) -> Result<Vec<DvdiqqeParams>, DvdiqqeError> {
1416    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, DvdiqqeError> {
1417        if step == 0 || start == end {
1418            return Ok(vec![start]);
1419        }
1420        let mut v = Vec::new();
1421        if start < end {
1422            let mut cur = start;
1423            while cur <= end {
1424                v.push(cur);
1425                match cur.checked_add(step) {
1426                    Some(n) => cur = n,
1427                    None => break,
1428                }
1429            }
1430        } else {
1431            let mut cur = start;
1432            while cur >= end {
1433                v.push(cur);
1434                if cur < step {
1435                    break;
1436                }
1437                cur -= step;
1438                if cur == usize::MAX {
1439                    break;
1440                }
1441                if cur == 0 && end > 0 {
1442                    break;
1443                }
1444            }
1445        }
1446        if v.is_empty() {
1447            return Err(DvdiqqeError::InvalidRangeUsize { start, end, step });
1448        }
1449        Ok(v)
1450    }
1451
1452    fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, DvdiqqeError> {
1453        if step == 0.0 || start == end {
1454            return Ok(vec![start]);
1455        }
1456        let mut v = Vec::new();
1457        if start < end {
1458            let mut curr = start;
1459            while curr <= end + 1e-12 {
1460                v.push(curr);
1461                curr += step;
1462            }
1463        } else {
1464            let mut curr = start;
1465            let step_abs = step.abs();
1466            while curr >= end - 1e-12 {
1467                v.push(curr);
1468                curr -= step_abs;
1469                if !curr.is_finite() {
1470                    break;
1471                }
1472            }
1473        }
1474        if v.is_empty() {
1475            return Err(DvdiqqeError::InvalidRangeF64 { start, end, step });
1476        }
1477        Ok(v)
1478    }
1479
1480    let periods = axis_usize(r.period)?;
1481    let smoothings = axis_usize(r.smoothing_period)?;
1482    let fasts = axis_f64(r.fast_multiplier)?;
1483    let slows = axis_f64(r.slow_multiplier)?;
1484
1485    let mut combos = Vec::new();
1486    for &p in &periods {
1487        for &s in &smoothings {
1488            for &f in &fasts {
1489                for &sl in &slows {
1490                    combos.push(DvdiqqeParams {
1491                        period: Some(p),
1492                        smoothing_period: Some(s),
1493                        fast_multiplier: Some(f),
1494                        slow_multiplier: Some(sl),
1495                        volume_type: None,
1496                        center_type: None,
1497                        tick_size: None,
1498                    });
1499                }
1500            }
1501        }
1502    }
1503    if combos.is_empty() {
1504        return Err(DvdiqqeError::InvalidInput("empty sweep".into()));
1505    }
1506    Ok(combos)
1507}
1508
1509pub fn dvdiqqe_batch_with_kernel(
1510    open: &[f64],
1511    high: &[f64],
1512    low: &[f64],
1513    close: &[f64],
1514    volume: Option<&[f64]>,
1515    sweep: &DvdiqqeBatchRange,
1516    k: Kernel,
1517    volume_type: &str,
1518    center_type: &str,
1519    tick_size: f64,
1520) -> Result<DvdiqqeBatchOutput, DvdiqqeError> {
1521    if !matches!(k, Kernel::Auto) && !k.is_batch() {
1522        return Err(DvdiqqeError::InvalidKernelForBatch(k));
1523    }
1524    let kernel = match k {
1525        Kernel::Auto => detect_best_batch_kernel(),
1526        other => other,
1527    };
1528    let simd = match kernel {
1529        Kernel::Avx512Batch => Kernel::Avx512,
1530        Kernel::Avx2Batch => Kernel::Avx2,
1531        Kernel::ScalarBatch => Kernel::Scalar,
1532        _ => kernel,
1533    };
1534    dvdiqqe_batch_par_slice(
1535        open,
1536        high,
1537        low,
1538        close,
1539        volume,
1540        sweep,
1541        simd,
1542        volume_type,
1543        center_type,
1544        tick_size,
1545    )
1546}
1547
1548pub fn dvdiqqe_batch_with_kernel_flat(
1549    open: &[f64],
1550    high: &[f64],
1551    low: &[f64],
1552    close: &[f64],
1553    volume: Option<&[f64]>,
1554    sweep: &DvdiqqeBatchRange,
1555    k: Kernel,
1556    volume_type: &str,
1557    center_type: &str,
1558    tick_size: f64,
1559) -> Result<DvdiqqeBatchOutputFlat, DvdiqqeError> {
1560    if !matches!(k, Kernel::Auto) && !k.is_batch() {
1561        return Err(DvdiqqeError::InvalidKernelForBatch(k));
1562    }
1563    let kernel = match k {
1564        Kernel::Auto => detect_best_batch_kernel(),
1565        other => other,
1566    };
1567    let simd = match kernel {
1568        Kernel::Avx512Batch => Kernel::Avx512,
1569        Kernel::Avx2Batch => Kernel::Avx2,
1570        Kernel::ScalarBatch => Kernel::Scalar,
1571        _ => kernel,
1572    };
1573    dvdiqqe_batch_inner_flat(
1574        open,
1575        high,
1576        low,
1577        close,
1578        volume,
1579        sweep,
1580        simd,
1581        true,
1582        volume_type,
1583        center_type,
1584        tick_size,
1585    )
1586}
1587
1588#[inline(always)]
1589pub fn dvdiqqe_batch_slice(
1590    open: &[f64],
1591    high: &[f64],
1592    low: &[f64],
1593    close: &[f64],
1594    volume: Option<&[f64]>,
1595    sweep: &DvdiqqeBatchRange,
1596    kern: Kernel,
1597    volume_type: &str,
1598    center_type: &str,
1599    tick_size: f64,
1600) -> Result<DvdiqqeBatchOutput, DvdiqqeError> {
1601    dvdiqqe_batch_inner(
1602        open,
1603        high,
1604        low,
1605        close,
1606        volume,
1607        sweep,
1608        kern,
1609        false,
1610        volume_type,
1611        center_type,
1612        tick_size,
1613    )
1614}
1615
1616#[inline(always)]
1617pub fn dvdiqqe_batch_par_slice(
1618    open: &[f64],
1619    high: &[f64],
1620    low: &[f64],
1621    close: &[f64],
1622    volume: Option<&[f64]>,
1623    sweep: &DvdiqqeBatchRange,
1624    kern: Kernel,
1625    volume_type: &str,
1626    center_type: &str,
1627    tick_size: f64,
1628) -> Result<DvdiqqeBatchOutput, DvdiqqeError> {
1629    dvdiqqe_batch_inner(
1630        open,
1631        high,
1632        low,
1633        close,
1634        volume,
1635        sweep,
1636        kern,
1637        true,
1638        volume_type,
1639        center_type,
1640        tick_size,
1641    )
1642}
1643
1644fn dvdiqqe_batch_inner(
1645    open: &[f64],
1646    high: &[f64],
1647    low: &[f64],
1648    close: &[f64],
1649    volume: Option<&[f64]>,
1650    sweep: &DvdiqqeBatchRange,
1651    kern: Kernel,
1652    parallel: bool,
1653    volume_type: &str,
1654    center_type: &str,
1655    tick_size: f64,
1656) -> Result<DvdiqqeBatchOutput, DvdiqqeError> {
1657    let flat = dvdiqqe_batch_inner_flat(
1658        open,
1659        high,
1660        low,
1661        close,
1662        volume,
1663        sweep,
1664        kern,
1665        parallel,
1666        volume_type,
1667        center_type,
1668        tick_size,
1669    )?;
1670
1671    let plane = flat.rows * flat.cols;
1672    Ok(DvdiqqeBatchOutput {
1673        dvdi_values: flat.values[0..plane].to_vec(),
1674        fast_tl_values: flat.values[plane..2 * plane].to_vec(),
1675        slow_tl_values: flat.values[2 * plane..3 * plane].to_vec(),
1676        center_values: flat.values[3 * plane..4 * plane].to_vec(),
1677        combos: flat.combos,
1678        rows: flat.rows,
1679        cols: flat.cols,
1680    })
1681}
1682
1683fn dvdiqqe_batch_inner_flat(
1684    open: &[f64],
1685    high: &[f64],
1686    low: &[f64],
1687    close: &[f64],
1688    volume: Option<&[f64]>,
1689    sweep: &DvdiqqeBatchRange,
1690    kern: Kernel,
1691    parallel: bool,
1692    volume_type: &str,
1693    center_type: &str,
1694    tick_size: f64,
1695) -> Result<DvdiqqeBatchOutputFlat, DvdiqqeError> {
1696    let combos = expand_grid(sweep)?;
1697    let rows = combos.len();
1698    let cols = close.len();
1699    if cols == 0 {
1700        return Err(DvdiqqeError::EmptyInputData);
1701    }
1702
1703    let series = 4usize;
1704
1705    let rows_cols = rows
1706        .checked_mul(cols)
1707        .ok_or_else(|| DvdiqqeError::InvalidInput("rows*cols overflow".into()))?;
1708    let _ = series
1709        .checked_mul(rows_cols)
1710        .ok_or_else(|| DvdiqqeError::InvalidInput("series*rows*cols overflow".into()))?;
1711    let mut buf_mu = make_uninit_matrix(series * rows, cols);
1712
1713    let first = close
1714        .iter()
1715        .position(|x| x.is_finite())
1716        .ok_or(DvdiqqeError::AllValuesNaN)?;
1717    let warms: Vec<usize> = combos
1718        .iter()
1719        .map(|p| first + p.period.unwrap_or(13) - 1)
1720        .collect();
1721
1722    for s in 0..series {
1723        let off = s * rows * cols;
1724        let plane = &mut buf_mu[off..off + rows * cols];
1725        init_matrix_prefixes(plane, cols, &warms);
1726    }
1727
1728    let mut guard = core::mem::ManuallyDrop::new(buf_mu);
1729    let out: &mut [f64] =
1730        unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
1731
1732    let tick_vol_once = calculate_tick_volume_pine_like(open, close, tick_size);
1733    let sel_vol_once = select_volume_pine_like(volume, &tick_vol_once, volume_type);
1734    let (pvi_stream, nvi_stream) = build_pvi_nvi_pine_like(close, &sel_vol_once);
1735
1736    let process_row = |row: usize, out_slice: &mut [f64]| -> Result<(), DvdiqqeError> {
1737        let prm = &combos[row];
1738        let params = DvdiqqeParams {
1739            period: prm.period,
1740            smoothing_period: prm.smoothing_period,
1741            fast_multiplier: prm.fast_multiplier,
1742            slow_multiplier: prm.slow_multiplier,
1743            volume_type: Some(volume_type.to_string()),
1744            center_type: Some(center_type.to_string()),
1745            tick_size: Some(tick_size),
1746        };
1747        let input = DvdiqqeInput::from_slices(open, high, low, close, volume, params);
1748        let (_o, _h, _l, c, _v, period, smoothing, fast, slow, _vt, ct, _tick, first_local) =
1749            dvdiqqe_prepare(&input)?;
1750
1751        let plane = rows * cols;
1752        let (dvdi_plane, rest) = out_slice.split_at_mut(plane);
1753        let (fast_plane, rest) = rest.split_at_mut(plane);
1754        let (slow_plane, center_plane) = rest.split_at_mut(plane);
1755
1756        let dvdi_dst = &mut dvdi_plane[row * cols..(row + 1) * cols];
1757        let fast_dst = &mut fast_plane[row * cols..(row + 1) * cols];
1758        let slow_dst = &mut slow_plane[row * cols..(row + 1) * cols];
1759        let center_dst = &mut center_plane[row * cols..(row + 1) * cols];
1760
1761        let pvi_ema = {
1762            let prm = EmaParams {
1763                period: Some(period),
1764            };
1765            let inp = EmaInput::from_slice(&pvi_stream, prm);
1766            ema_with_kernel(&inp, kern).map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
1767        };
1768        let nvi_ema = {
1769            let prm = EmaParams {
1770                period: Some(period),
1771            };
1772            let inp = EmaInput::from_slice(&nvi_stream, prm);
1773            ema_with_kernel(&inp, kern).map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
1774        };
1775
1776        let mut pdiv = alloc_with_nan_prefix(cols, 0);
1777        let mut ndiv = alloc_with_nan_prefix(cols, 0);
1778        for i in 0..cols {
1779            pdiv[i] = pvi_stream[i] - pvi_ema.values[i];
1780            ndiv[i] = nvi_stream[i] - nvi_ema.values[i];
1781        }
1782        let pdiv_ema = {
1783            let prm = EmaParams {
1784                period: Some(smoothing),
1785            };
1786            let inp = EmaInput::from_slice(&pdiv, prm);
1787            ema_with_kernel(&inp, kern).map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
1788        };
1789        let ndiv_ema = {
1790            let prm = EmaParams {
1791                period: Some(smoothing),
1792            };
1793            let inp = EmaInput::from_slice(&ndiv, prm);
1794            ema_with_kernel(&inp, kern).map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
1795        };
1796
1797        let wper = (period * 2) - 1;
1798        let warmup = first_local + wper;
1799
1800        for i in 0..warmup.min(cols) {
1801            dvdi_dst[i] = f64::NAN;
1802            fast_dst[i] = f64::NAN;
1803            slow_dst[i] = f64::NAN;
1804        }
1805
1806        let mut ranges = alloc_with_nan_prefix(cols, 1);
1807        if cols > 0 {
1808            dvdi_dst[0] = pdiv_ema.values[0] - ndiv_ema.values[0];
1809            for i in 1..cols {
1810                let dvdi_i = pdiv_ema.values[i] - ndiv_ema.values[i];
1811                ranges[i] = (dvdi_i - dvdi_dst[i - 1]).abs();
1812                dvdi_dst[i] = dvdi_i;
1813            }
1814        }
1815
1816        let avg_range = {
1817            let prm = EmaParams { period: Some(wper) };
1818            let inp = EmaInput::from_slice(&ranges, prm);
1819            ema_with_kernel(&inp, Kernel::Auto)
1820                .map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
1821        };
1822        let smooth_range = {
1823            let prm = EmaParams { period: Some(wper) };
1824            let inp = EmaInput::from_slice(&avg_range.values, prm);
1825            ema_with_kernel(&inp, Kernel::Auto)
1826                .map_err(|e| DvdiqqeError::EmaError(e.to_string()))?
1827        };
1828
1829        if warmup < cols {
1830            fast_dst[warmup] = dvdi_dst[warmup];
1831            slow_dst[warmup] = dvdi_dst[warmup];
1832            for i in (warmup + 1)..cols {
1833                let fr = smooth_range.values[i] * fast;
1834                let sr = smooth_range.values[i] * slow;
1835                if dvdi_dst[i] > fast_dst[i - 1] {
1836                    let nv = dvdi_dst[i] - fr;
1837                    fast_dst[i] = if nv < fast_dst[i - 1] {
1838                        fast_dst[i - 1]
1839                    } else {
1840                        nv
1841                    };
1842                } else {
1843                    let nv = dvdi_dst[i] + fr;
1844                    fast_dst[i] = if nv > fast_dst[i - 1] {
1845                        fast_dst[i - 1]
1846                    } else {
1847                        nv
1848                    };
1849                }
1850
1851                if dvdi_dst[i] > slow_dst[i - 1] {
1852                    let nv = dvdi_dst[i] - sr;
1853                    slow_dst[i] = if nv < slow_dst[i - 1] {
1854                        slow_dst[i - 1]
1855                    } else {
1856                        nv
1857                    };
1858                } else {
1859                    let nv = dvdi_dst[i] + sr;
1860                    slow_dst[i] = if nv > slow_dst[i - 1] {
1861                        slow_dst[i - 1]
1862                    } else {
1863                        nv
1864                    };
1865                }
1866            }
1867        }
1868
1869        for i in 0..warmup.min(cols) {
1870            center_dst[i] = f64::NAN;
1871        }
1872        if ct.eq_ignore_ascii_case("dynamic") {
1873            let mut sum = 0.0f64;
1874            let mut cnt = 0.0f64;
1875            for i in warmup..cols {
1876                let v = dvdi_dst[i];
1877                if v.is_finite() {
1878                    sum += v;
1879                    cnt += 1.0;
1880                }
1881                center_dst[i] = if cnt > 0.0 { sum / cnt } else { f64::NAN };
1882            }
1883        } else {
1884            for i in warmup..cols {
1885                center_dst[i] = 0.0;
1886            }
1887        }
1888
1889        Ok(())
1890    };
1891
1892    for r in 0..rows {
1893        process_row(r, out)?;
1894    }
1895
1896    let _ = parallel;
1897
1898    let values = unsafe {
1899        Vec::from_raw_parts(
1900            guard.as_mut_ptr() as *mut f64,
1901            guard.len(),
1902            guard.capacity(),
1903        )
1904    };
1905
1906    Ok(DvdiqqeBatchOutputFlat {
1907        values,
1908        combos,
1909        rows,
1910        cols,
1911        series,
1912    })
1913}
1914
1915fn dvdiqqe_batch_inner_into(
1916    open: &[f64],
1917    high: &[f64],
1918    low: &[f64],
1919    close: &[f64],
1920    volume: Option<&[f64]>,
1921    sweep: &DvdiqqeBatchRange,
1922    kern: Kernel,
1923    parallel: bool,
1924    volume_type: &str,
1925    center_type: &str,
1926    tick_size: f64,
1927    dvdi_out: &mut [f64],
1928    fast_out: &mut [f64],
1929    slow_out: &mut [f64],
1930    center_out: &mut [f64],
1931) -> Result<Vec<DvdiqqeParams>, DvdiqqeError> {
1932    let combos = expand_grid(sweep)?;
1933    let cols = close.len();
1934
1935    #[cfg(not(target_arch = "wasm32"))]
1936    {
1937        if parallel {
1938            use rayon::prelude::*;
1939
1940            let results: Result<Vec<_>, DvdiqqeError> = combos
1941                .par_iter()
1942                .map(|params| {
1943                    let mut full_params = params.clone();
1944                    full_params.volume_type = Some(volume_type.to_string());
1945                    full_params.center_type = Some(center_type.to_string());
1946                    full_params.tick_size = Some(tick_size);
1947
1948                    let input =
1949                        DvdiqqeInput::from_slices(open, high, low, close, volume, full_params);
1950                    dvdiqqe_with_kernel(&input, kern)
1951                })
1952                .collect();
1953
1954            let results = results?;
1955
1956            for (row, output) in results.iter().enumerate() {
1957                let start = row * cols;
1958                let end = start + cols;
1959                dvdi_out[start..end].copy_from_slice(&output.dvdi);
1960                fast_out[start..end].copy_from_slice(&output.fast_tl);
1961                slow_out[start..end].copy_from_slice(&output.slow_tl);
1962                center_out[start..end].copy_from_slice(&output.center_line);
1963            }
1964        } else {
1965            for (row, params) in combos.iter().enumerate() {
1966                let mut full_params = params.clone();
1967                full_params.volume_type = Some(volume_type.to_string());
1968                full_params.center_type = Some(center_type.to_string());
1969                full_params.tick_size = Some(tick_size);
1970
1971                let input = DvdiqqeInput::from_slices(open, high, low, close, volume, full_params);
1972                let output = dvdiqqe_with_kernel(&input, kern)?;
1973
1974                let start = row * cols;
1975                let end = start + cols;
1976                dvdi_out[start..end].copy_from_slice(&output.dvdi);
1977                fast_out[start..end].copy_from_slice(&output.fast_tl);
1978                slow_out[start..end].copy_from_slice(&output.slow_tl);
1979                center_out[start..end].copy_from_slice(&output.center_line);
1980            }
1981        }
1982    }
1983
1984    #[cfg(target_arch = "wasm32")]
1985    {
1986        for (row, params) in combos.iter().enumerate() {
1987            let mut full_params = params.clone();
1988            full_params.volume_type = Some(volume_type.to_string());
1989            full_params.center_type = Some(center_type.to_string());
1990            full_params.tick_size = Some(tick_size);
1991
1992            let input = DvdiqqeInput::from_slices(open, high, low, close, volume, full_params);
1993            let output = dvdiqqe_with_kernel(&input, kern)?;
1994
1995            let start = row * cols;
1996            let end = start + cols;
1997            dvdi_out[start..end].copy_from_slice(&output.dvdi);
1998            fast_out[start..end].copy_from_slice(&output.fast_tl);
1999            slow_out[start..end].copy_from_slice(&output.slow_tl);
2000            center_out[start..end].copy_from_slice(&output.center_line);
2001        }
2002    }
2003
2004    Ok(combos)
2005}
2006
2007pub struct DvdiqqeStream {
2008    period: usize,
2009    smoothing_period: usize,
2010    fast_mult: f64,
2011    slow_mult: f64,
2012    volume_type: String,
2013    center_type: String,
2014    tick_size: f64,
2015
2016    alpha_pvi: f64,
2017    alpha_div: f64,
2018    alpha_rng: f64,
2019    inv_tick: f64,
2020    use_tick_only: bool,
2021    warmup_needed: usize,
2022
2023    prev_close: f64,
2024    prev_sel_vol: f64,
2025    tickrng_prev: f64,
2026
2027    pvi: f64,
2028    nvi: f64,
2029    pvi_ema: f64,
2030    nvi_ema: f64,
2031    ema_pvi_inited: bool,
2032
2033    pdiv_ema: f64,
2034    ndiv_ema: f64,
2035    ema_div_inited: bool,
2036
2037    dvdi_prev: f64,
2038    rng_ema1: f64,
2039    rng_ema2: f64,
2040    ema_rng_inited: bool,
2041
2042    fast_tl_prev: f64,
2043    slow_tl_prev: f64,
2044    tl_seeded: bool,
2045
2046    count: usize,
2047    center_sum: f64,
2048    center_count: f64,
2049}
2050
2051impl DvdiqqeStream {
2052    pub fn try_new(params: DvdiqqeParams) -> Result<Self, DvdiqqeError> {
2053        let period = params.period.unwrap_or(13);
2054        let smoothing_period = params.smoothing_period.unwrap_or(6);
2055        if period == 0 {
2056            return Err(DvdiqqeError::InvalidPeriod {
2057                period,
2058                data_len: 0,
2059            });
2060        }
2061        if smoothing_period == 0 {
2062            return Err(DvdiqqeError::InvalidSmoothing { smoothing: 0 });
2063        }
2064
2065        let fast_mult = params.fast_multiplier.unwrap_or(2.618);
2066        let slow_mult = params.slow_multiplier.unwrap_or(4.236);
2067        if !(fast_mult.is_finite() && fast_mult > 0.0) {
2068            return Err(DvdiqqeError::InvalidMultiplier {
2069                multiplier: fast_mult,
2070                which: "fast".into(),
2071            });
2072        }
2073        if !(slow_mult.is_finite() && slow_mult > 0.0) {
2074            return Err(DvdiqqeError::InvalidMultiplier {
2075                multiplier: slow_mult,
2076                which: "slow".into(),
2077            });
2078        }
2079
2080        let volume_type = params.volume_type.unwrap_or_else(|| "default".to_string());
2081        let center_type = params.center_type.unwrap_or_else(|| "dynamic".to_string());
2082        let tick_size = params.tick_size.unwrap_or(0.01);
2083        if !(tick_size.is_finite() && tick_size > 0.0) {
2084            return Err(DvdiqqeError::InvalidTick { tick: tick_size });
2085        }
2086
2087        let alpha_pvi = 2.0 / (period as f64 + 1.0);
2088        let alpha_div = 2.0 / (smoothing_period as f64 + 1.0);
2089
2090        let alpha_rng = 1.0 / (period as f64);
2091
2092        Ok(Self {
2093            period,
2094            smoothing_period,
2095            fast_mult,
2096            slow_mult,
2097            use_tick_only: volume_type.eq_ignore_ascii_case("tick"),
2098            volume_type,
2099            center_type,
2100            tick_size,
2101            inv_tick: 1.0 / tick_size,
2102            alpha_pvi,
2103            alpha_div,
2104            alpha_rng,
2105            warmup_needed: period * 2,
2106
2107            prev_close: 0.0,
2108            prev_sel_vol: 0.0,
2109            tickrng_prev: tick_size,
2110
2111            pvi: 0.0,
2112            nvi: 0.0,
2113            pvi_ema: 0.0,
2114            nvi_ema: 0.0,
2115            ema_pvi_inited: false,
2116
2117            pdiv_ema: 0.0,
2118            ndiv_ema: 0.0,
2119            ema_div_inited: false,
2120
2121            dvdi_prev: 0.0,
2122            rng_ema1: 0.0,
2123            rng_ema2: 0.0,
2124            ema_rng_inited: false,
2125
2126            fast_tl_prev: f64::NAN,
2127            slow_tl_prev: f64::NAN,
2128            tl_seeded: false,
2129
2130            count: 0,
2131            center_sum: 0.0,
2132            center_count: 0.0,
2133        })
2134    }
2135
2136    pub fn update(
2137        &mut self,
2138        open: f64,
2139        _high: f64,
2140        _low: f64,
2141        close: f64,
2142        volume: f64,
2143    ) -> Option<DvdiqqeStreamOutput> {
2144        let rng = close - open;
2145        let tickrng = if rng.abs() < self.tick_size {
2146            self.tickrng_prev
2147        } else {
2148            rng
2149        };
2150        let tick_vol = (tickrng.abs() * self.inv_tick).max(0.0);
2151        self.tickrng_prev = tickrng;
2152
2153        let sel_vol = if self.use_tick_only {
2154            tick_vol
2155        } else if volume.is_finite() {
2156            volume
2157        } else {
2158            tick_vol
2159        };
2160
2161        let d_close = close - self.prev_close;
2162        if sel_vol > self.prev_sel_vol {
2163            self.pvi += d_close;
2164        } else if sel_vol < self.prev_sel_vol {
2165            self.nvi -= d_close;
2166        }
2167        self.prev_sel_vol = sel_vol;
2168        self.prev_close = close;
2169
2170        if !self.ema_pvi_inited {
2171            self.pvi_ema = self.pvi;
2172            self.nvi_ema = self.nvi;
2173            self.ema_pvi_inited = true;
2174        } else {
2175            self.pvi_ema += self.alpha_pvi * (self.pvi - self.pvi_ema);
2176            self.nvi_ema += self.alpha_pvi * (self.nvi - self.nvi_ema);
2177        }
2178
2179        let pdiv = self.pvi - self.pvi_ema;
2180        let ndiv = self.nvi - self.nvi_ema;
2181
2182        if !self.ema_div_inited {
2183            self.pdiv_ema = pdiv;
2184            self.ndiv_ema = ndiv;
2185            self.ema_div_inited = true;
2186        } else {
2187            self.pdiv_ema += self.alpha_div * (pdiv - self.pdiv_ema);
2188            self.ndiv_ema += self.alpha_div * (ndiv - self.ndiv_ema);
2189        }
2190
2191        let dvdi = self.pdiv_ema - self.ndiv_ema;
2192
2193        let step_rng = (dvdi - self.dvdi_prev).abs();
2194        if !self.ema_rng_inited {
2195            self.rng_ema1 = step_rng;
2196            self.rng_ema2 = self.rng_ema1;
2197            self.ema_rng_inited = true;
2198        } else {
2199            self.rng_ema1 += self.alpha_rng * (step_rng - self.rng_ema1);
2200            self.rng_ema2 += self.alpha_rng * (self.rng_ema1 - self.rng_ema2);
2201        }
2202        self.dvdi_prev = dvdi;
2203
2204        self.count = self.count.saturating_add(1);
2205        if self.count < self.warmup_needed {
2206            return None;
2207        }
2208
2209        let smooth_rng = self.rng_ema2;
2210        let fr = smooth_rng * self.fast_mult;
2211        let sr = smooth_rng * self.slow_mult;
2212
2213        if !self.tl_seeded {
2214            self.fast_tl_prev = dvdi;
2215            self.slow_tl_prev = dvdi;
2216            self.tl_seeded = true;
2217
2218            if self.center_type.eq_ignore_ascii_case("dynamic") && dvdi.is_finite() {
2219                self.center_sum = dvdi;
2220                self.center_count = 1.0;
2221            }
2222            return Some(DvdiqqeStreamOutput {
2223                dvdi,
2224                fast_tl: self.fast_tl_prev,
2225                slow_tl: self.slow_tl_prev,
2226                center_line: if self.center_type.eq_ignore_ascii_case("static") {
2227                    0.0
2228                } else if self.center_count > 0.0 {
2229                    self.center_sum / self.center_count
2230                } else {
2231                    f64::NAN
2232                },
2233            });
2234        }
2235
2236        let fast_tl = if dvdi > self.fast_tl_prev {
2237            let nv = dvdi - fr;
2238            if nv < self.fast_tl_prev {
2239                self.fast_tl_prev
2240            } else {
2241                nv
2242            }
2243        } else {
2244            let nv = dvdi + fr;
2245            if nv > self.fast_tl_prev {
2246                self.fast_tl_prev
2247            } else {
2248                nv
2249            }
2250        };
2251
2252        let slow_tl = if dvdi > self.slow_tl_prev {
2253            let nv = dvdi - sr;
2254            if nv < self.slow_tl_prev {
2255                self.slow_tl_prev
2256            } else {
2257                nv
2258            }
2259        } else {
2260            let nv = dvdi + sr;
2261            if nv > self.slow_tl_prev {
2262                self.slow_tl_prev
2263            } else {
2264                nv
2265            }
2266        };
2267
2268        self.fast_tl_prev = fast_tl;
2269        self.slow_tl_prev = slow_tl;
2270
2271        let center_val = if self.center_type.eq_ignore_ascii_case("static") {
2272            0.0
2273        } else {
2274            if dvdi.is_finite() {
2275                self.center_sum += dvdi;
2276                self.center_count += 1.0;
2277            }
2278            if self.center_count > 0.0 {
2279                self.center_sum / self.center_count
2280            } else {
2281                f64::NAN
2282            }
2283        };
2284
2285        Some(DvdiqqeStreamOutput {
2286            dvdi,
2287            fast_tl,
2288            slow_tl,
2289            center_line: center_val,
2290        })
2291    }
2292}
2293
2294#[derive(Debug, Clone, Copy)]
2295pub struct DvdiqqeStreamOutput {
2296    pub dvdi: f64,
2297    pub fast_tl: f64,
2298    pub slow_tl: f64,
2299    pub center_line: f64,
2300}
2301
2302#[cfg(feature = "python")]
2303#[pyclass]
2304pub struct DvdiqqeStreamPy {
2305    stream: DvdiqqeStream,
2306}
2307
2308#[cfg(feature = "python")]
2309#[pymethods]
2310impl DvdiqqeStreamPy {
2311    #[new]
2312    fn new(
2313        period: Option<i32>,
2314        smoothing_period: Option<i32>,
2315        fast_multiplier: Option<f64>,
2316        slow_multiplier: Option<f64>,
2317        volume_type: Option<String>,
2318        center_type: Option<String>,
2319        tick_size: Option<f64>,
2320    ) -> PyResult<Self> {
2321        let period_validated = if let Some(p) = period {
2322            if p <= 0 {
2323                return Err(PyValueError::new_err(format!(
2324                    "Invalid period: Period must be positive (got {})",
2325                    p
2326                )));
2327            }
2328            Some(p as usize)
2329        } else {
2330            None
2331        };
2332
2333        let smoothing_validated = if let Some(s) = smoothing_period {
2334            if s <= 0 {
2335                return Err(PyValueError::new_err(format!(
2336                    "Invalid smoothing period: Smoothing period must be positive (got {})",
2337                    s
2338                )));
2339            }
2340            Some(s as usize)
2341        } else {
2342            None
2343        };
2344
2345        let params = DvdiqqeParams {
2346            period: period_validated,
2347            smoothing_period: smoothing_validated,
2348            fast_multiplier,
2349            slow_multiplier,
2350            volume_type,
2351            center_type,
2352            tick_size,
2353        };
2354
2355        let stream =
2356            DvdiqqeStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2357
2358        Ok(DvdiqqeStreamPy { stream })
2359    }
2360
2361    fn update(
2362        &mut self,
2363        open: f64,
2364        high: f64,
2365        low: f64,
2366        close: f64,
2367        volume: f64,
2368    ) -> Option<(f64, f64, f64, f64)> {
2369        self.stream
2370            .update(open, high, low, close, volume)
2371            .map(|output| {
2372                (
2373                    output.dvdi,
2374                    output.fast_tl,
2375                    output.slow_tl,
2376                    output.center_line,
2377                )
2378            })
2379    }
2380}
2381
2382#[cfg(feature = "python")]
2383#[pyfunction]
2384#[pyo3(name = "dvdiqqe", signature = (
2385    open,
2386    high,
2387    low,
2388    close,
2389    volume=None,
2390    period=None,
2391    smoothing_period=None,
2392    fast_multiplier=None,
2393    slow_multiplier=None,
2394    volume_type=None,
2395    center_type=None,
2396    tick_size=None,
2397    kernel=None
2398))]
2399pub fn dvdiqqe_py<'py>(
2400    py: Python<'py>,
2401    open: Option<PyReadonlyArray1<'py, f64>>,
2402    high: Option<PyReadonlyArray1<'py, f64>>,
2403    low: Option<PyReadonlyArray1<'py, f64>>,
2404    close: Option<PyReadonlyArray1<'py, f64>>,
2405    volume: Option<PyReadonlyArray1<'py, f64>>,
2406    period: Option<i32>,
2407    smoothing_period: Option<i32>,
2408    fast_multiplier: Option<f64>,
2409    slow_multiplier: Option<f64>,
2410    volume_type: Option<String>,
2411    center_type: Option<String>,
2412    tick_size: Option<f64>,
2413    kernel: Option<&str>,
2414) -> PyResult<(
2415    Bound<'py, PyArray1<f64>>,
2416    Bound<'py, PyArray1<f64>>,
2417    Bound<'py, PyArray1<f64>>,
2418    Bound<'py, PyArray1<f64>>,
2419)> {
2420    if open.is_none() || high.is_none() || low.is_none() || close.is_none() {
2421        return Err(PyValueError::new_err(
2422            "OHLC data (open, high, low, close) is required",
2423        ));
2424    }
2425
2426    let open_arr = open.unwrap();
2427    let high_arr = high.unwrap();
2428    let low_arr = low.unwrap();
2429    let close_arr = close.unwrap();
2430
2431    let o = open_arr.as_slice()?;
2432    let h = high_arr.as_slice()?;
2433    let l = low_arr.as_slice()?;
2434    let c = close_arr.as_slice()?;
2435    let v = volume.as_ref().map(|v| v.as_slice()).transpose()?;
2436    let len = c.len();
2437
2438    let mut dvdi = unsafe { numpy::PyArray1::<f64>::new(py, [len], false) };
2439    let mut fast = unsafe { numpy::PyArray1::<f64>::new(py, [len], false) };
2440    let mut slow = unsafe { numpy::PyArray1::<f64>::new(py, [len], false) };
2441    let mut cent = unsafe { numpy::PyArray1::<f64>::new(py, [len], false) };
2442    let dvdi_s = unsafe { dvdi.as_slice_mut()? };
2443    let fast_s = unsafe { fast.as_slice_mut()? };
2444    let slow_s = unsafe { slow.as_slice_mut()? };
2445    let cent_s = unsafe { cent.as_slice_mut()? };
2446
2447    let period_validated = if let Some(p) = period {
2448        if p <= 0 {
2449            return Err(PyValueError::new_err(format!(
2450                "Invalid period: Period must be positive (got {})",
2451                p
2452            )));
2453        }
2454        Some(p as usize)
2455    } else {
2456        None
2457    };
2458
2459    let smoothing_validated = if let Some(s) = smoothing_period {
2460        if s <= 0 {
2461            return Err(PyValueError::new_err(format!(
2462                "Invalid smoothing period: Smoothing period must be positive (got {})",
2463                s
2464            )));
2465        }
2466        Some(s as usize)
2467    } else {
2468        None
2469    };
2470
2471    let params = DvdiqqeParams {
2472        period: period_validated,
2473        smoothing_period: smoothing_validated,
2474        fast_multiplier,
2475        slow_multiplier,
2476        volume_type,
2477        center_type,
2478        tick_size,
2479    };
2480    let input = DvdiqqeInput::from_slices(o, h, l, c, v, params);
2481    let kern = validate_kernel(kernel, false).map_err(|e| PyValueError::new_err(e.to_string()))?;
2482
2483    py.allow_threads(|| dvdiqqe_into_slices(dvdi_s, fast_s, slow_s, cent_s, &input, kern))
2484        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2485
2486    Ok((dvdi.into(), fast.into(), slow.into(), cent.into()))
2487}
2488
2489#[cfg(feature = "python")]
2490#[pyfunction(name = "dvdiqqe_batch")]
2491#[pyo3(signature = (
2492    open,
2493    high,
2494    low,
2495    close,
2496    period_range,
2497    smoothing_period_range,
2498    fast_mult_range,
2499    slow_mult_range,
2500    kernel=None
2501))]
2502pub fn dvdiqqe_batch_py<'py>(
2503    py: Python<'py>,
2504    open: PyReadonlyArray1<'py, f64>,
2505    high: PyReadonlyArray1<'py, f64>,
2506    low: PyReadonlyArray1<'py, f64>,
2507    close: PyReadonlyArray1<'py, f64>,
2508    period_range: (usize, usize, usize),
2509    smoothing_period_range: (usize, usize, usize),
2510    fast_mult_range: (f64, f64, f64),
2511    slow_mult_range: (f64, f64, f64),
2512    kernel: Option<&str>,
2513) -> PyResult<Bound<'py, PyDict>> {
2514    let o = open.as_slice()?;
2515    let h = high.as_slice()?;
2516    let l = low.as_slice()?;
2517    let c = close.as_slice()?;
2518    let sweep = DvdiqqeBatchRange {
2519        period: period_range,
2520        smoothing_period: smoothing_period_range,
2521        fast_multiplier: fast_mult_range,
2522        slow_multiplier: slow_mult_range,
2523    };
2524    let kern = validate_kernel(kernel, true).map_err(|e| PyValueError::new_err(e.to_string()))?;
2525    let out = py
2526        .allow_threads(|| {
2527            dvdiqqe_batch_with_kernel_flat(
2528                o, h, l, c, None, &sweep, kern, "default", "dynamic", 0.01,
2529            )
2530        })
2531        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2532
2533    let rows = out.rows;
2534    let cols = out.cols;
2535    let series = out.series;
2536    let plane = rows * cols;
2537
2538    use numpy::PyArray2;
2539    let dvdi = unsafe { PyArray2::new(py, [rows, cols], false) };
2540    let fast = unsafe { PyArray2::new(py, [rows, cols], false) };
2541    let slow = unsafe { PyArray2::new(py, [rows, cols], false) };
2542    let center = unsafe { PyArray2::new(py, [rows, cols], false) };
2543
2544    unsafe {
2545        dvdi.as_slice_mut()?
2546            .copy_from_slice(&out.values[0 * plane..1 * plane]);
2547        fast.as_slice_mut()?
2548            .copy_from_slice(&out.values[1 * plane..2 * plane]);
2549        slow.as_slice_mut()?
2550            .copy_from_slice(&out.values[2 * plane..3 * plane]);
2551        center
2552            .as_slice_mut()?
2553            .copy_from_slice(&out.values[3 * plane..4 * plane]);
2554    }
2555
2556    let d = PyDict::new(py);
2557    d.set_item("dvdi", dvdi)?;
2558    d.set_item("fast", fast)?;
2559    d.set_item("slow", slow)?;
2560    d.set_item("center", center)?;
2561    d.set_item(
2562        "periods",
2563        out.combos
2564            .iter()
2565            .map(|p| p.period.unwrap() as u64)
2566            .collect::<Vec<_>>()
2567            .into_pyarray(py),
2568    )?;
2569    d.set_item(
2570        "smoothing_periods",
2571        out.combos
2572            .iter()
2573            .map(|p| p.smoothing_period.unwrap() as u64)
2574            .collect::<Vec<_>>()
2575            .into_pyarray(py),
2576    )?;
2577    d.set_item(
2578        "fast_multipliers",
2579        out.combos
2580            .iter()
2581            .map(|p| p.fast_multiplier.unwrap())
2582            .collect::<Vec<_>>()
2583            .into_pyarray(py),
2584    )?;
2585    d.set_item(
2586        "slow_multipliers",
2587        out.combos
2588            .iter()
2589            .map(|p| p.slow_multiplier.unwrap())
2590            .collect::<Vec<_>>()
2591            .into_pyarray(py),
2592    )?;
2593    d.set_item("rows", rows)?;
2594    d.set_item("cols", cols)?;
2595    d.set_item("series", series)?;
2596    Ok(d.into())
2597}
2598
2599#[cfg(all(feature = "python", feature = "cuda"))]
2600#[pyfunction(name = "dvdiqqe_cuda_batch_dev")]
2601#[pyo3(signature = (open_f32, close_f32, volume_f32, period_range, smoothing_period_range, fast_mult_range, slow_mult_range, volume_type="default", center_type="dynamic", tick_size=0.01, device_id=0))]
2602pub fn dvdiqqe_cuda_batch_dev_py(
2603    py: Python<'_>,
2604    open_f32: PyReadonlyArray1<'_, f32>,
2605    close_f32: PyReadonlyArray1<'_, f32>,
2606    volume_f32: Option<PyReadonlyArray1<'_, f32>>,
2607    period_range: (usize, usize, usize),
2608    smoothing_period_range: (usize, usize, usize),
2609    fast_mult_range: (f64, f64, f64),
2610    slow_mult_range: (f64, f64, f64),
2611    volume_type: &str,
2612    center_type: &str,
2613    tick_size: f32,
2614    device_id: usize,
2615) -> PyResult<(
2616    DeviceDvdiqqePlanePy,
2617    DeviceDvdiqqePlanePy,
2618    DeviceDvdiqqePlanePy,
2619    DeviceDvdiqqePlanePy,
2620)> {
2621    use crate::cuda::cuda_available;
2622    if !cuda_available() {
2623        return Err(PyValueError::new_err("CUDA not available"));
2624    }
2625
2626    let o = open_f32.as_slice()?;
2627    let c = close_f32.as_slice()?;
2628    let v_opt: Option<&[f32]> = match volume_f32.as_ref() {
2629        Some(v) => Some(v.as_slice()?),
2630        None => None,
2631    };
2632    if o.len() != c.len() {
2633        return Err(PyValueError::new_err("open/close length mismatch"));
2634    }
2635    if let Some(v) = v_opt {
2636        if v.len() != c.len() {
2637            return Err(PyValueError::new_err("volume length mismatch"));
2638        }
2639    }
2640
2641    let sweep = DvdiqqeBatchRange {
2642        period: period_range,
2643        smoothing_period: smoothing_period_range,
2644        fast_multiplier: fast_mult_range,
2645        slow_multiplier: slow_mult_range,
2646    };
2647
2648    let (dvdi, fast, slow, center) = py.allow_threads(|| {
2649        let cuda = CudaDvdiqqe::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2650        let ctx = cuda.context_arc();
2651        let dev = cuda.device_id();
2652        let quad = cuda
2653            .dvdiqqe_batch_dev(o, c, v_opt, &sweep, volume_type, center_type, tick_size)
2654            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2655        Ok::<_, PyErr>((
2656            DeviceDvdiqqePlanePy {
2657                inner: quad.dvdi,
2658                _ctx: ctx.clone(),
2659                device_id: dev,
2660            },
2661            DeviceDvdiqqePlanePy {
2662                inner: quad.fast,
2663                _ctx: ctx.clone(),
2664                device_id: dev,
2665            },
2666            DeviceDvdiqqePlanePy {
2667                inner: quad.slow,
2668                _ctx: ctx.clone(),
2669                device_id: dev,
2670            },
2671            DeviceDvdiqqePlanePy {
2672                inner: quad.center,
2673                _ctx: ctx,
2674                device_id: dev,
2675            },
2676        ))
2677    })?;
2678
2679    Ok((dvdi, fast, slow, center))
2680}
2681
2682#[cfg(all(feature = "python", feature = "cuda"))]
2683#[pyfunction(name = "dvdiqqe_cuda_many_series_one_param_dev")]
2684#[pyo3(signature = (open_tm_f32, close_tm_f32, cols, rows, period, smoothing, fast_mult, slow_mult, volume_tm_f32, volume_type="default", center_type="dynamic", tick_size=0.01, device_id=0))]
2685pub fn dvdiqqe_cuda_many_series_one_param_dev_py(
2686    py: Python<'_>,
2687    open_tm_f32: PyReadonlyArray1<'_, f32>,
2688    close_tm_f32: PyReadonlyArray1<'_, f32>,
2689    cols: usize,
2690    rows: usize,
2691    period: usize,
2692    smoothing: usize,
2693    fast_mult: f32,
2694    slow_mult: f32,
2695    volume_tm_f32: Option<PyReadonlyArray1<'_, f32>>,
2696    volume_type: &str,
2697    center_type: &str,
2698    tick_size: f32,
2699    device_id: usize,
2700) -> PyResult<(
2701    DeviceDvdiqqePlanePy,
2702    DeviceDvdiqqePlanePy,
2703    DeviceDvdiqqePlanePy,
2704    DeviceDvdiqqePlanePy,
2705)> {
2706    use crate::cuda::cuda_available;
2707    if !cuda_available() {
2708        return Err(PyValueError::new_err("CUDA not available"));
2709    }
2710
2711    let o_tm = open_tm_f32.as_slice()?;
2712    let c_tm = close_tm_f32.as_slice()?;
2713    let v_tm: Option<&[f32]> = match volume_tm_f32.as_ref() {
2714        Some(v) => Some(v.as_slice()?),
2715        None => None,
2716    };
2717    let expected = cols
2718        .checked_mul(rows)
2719        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
2720    if o_tm.len() != expected || c_tm.len() != expected {
2721        return Err(PyValueError::new_err("time-major input length mismatch"));
2722    }
2723    if let Some(v) = v_tm {
2724        if v.len() != expected {
2725            return Err(PyValueError::new_err("time-major volume mismatch"));
2726        }
2727    }
2728
2729    let (dvdi, fast, slow, center) = py.allow_threads(|| {
2730        let cuda = CudaDvdiqqe::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2731        let ctx = cuda.context_arc();
2732        let dev = cuda.device_id();
2733        let quad = cuda
2734            .dvdiqqe_many_series_one_param_time_major_dev(
2735                o_tm,
2736                c_tm,
2737                v_tm,
2738                cols,
2739                rows,
2740                period,
2741                smoothing,
2742                fast_mult,
2743                slow_mult,
2744                volume_type,
2745                center_type,
2746                tick_size,
2747            )
2748            .map_err(|e| PyValueError::new_err(e.to_string()))?;
2749        Ok::<_, PyErr>((
2750            DeviceDvdiqqePlanePy {
2751                inner: quad.dvdi,
2752                _ctx: ctx.clone(),
2753                device_id: dev,
2754            },
2755            DeviceDvdiqqePlanePy {
2756                inner: quad.fast,
2757                _ctx: ctx.clone(),
2758                device_id: dev,
2759            },
2760            DeviceDvdiqqePlanePy {
2761                inner: quad.slow,
2762                _ctx: ctx.clone(),
2763                device_id: dev,
2764            },
2765            DeviceDvdiqqePlanePy {
2766                inner: quad.center,
2767                _ctx: ctx,
2768                device_id: dev,
2769            },
2770        ))
2771    })?;
2772
2773    Ok((dvdi, fast, slow, center))
2774}
2775
2776#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2777#[derive(Serialize, Deserialize)]
2778pub struct DvdiqqeJsFlat {
2779    pub values: Vec<f64>,
2780    pub rows: usize,
2781    pub cols: usize,
2782}
2783
2784#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2785#[wasm_bindgen(js_name = dvdiqqe)]
2786pub fn dvdiqqe_js(
2787    open: &[f64],
2788    high: &[f64],
2789    low: &[f64],
2790    close: &[f64],
2791    volume: Option<Vec<f64>>,
2792    period: Option<usize>,
2793    smoothing_period: Option<usize>,
2794    fast_multiplier: Option<f64>,
2795    slow_multiplier: Option<f64>,
2796    volume_type: Option<String>,
2797    center_type: Option<String>,
2798    tick_size: Option<f64>,
2799) -> Result<JsValue, JsValue> {
2800    let params = DvdiqqeParams {
2801        period: period.or(Some(13)),
2802        smoothing_period: smoothing_period.or(Some(6)),
2803        fast_multiplier: fast_multiplier.or(Some(2.618)),
2804        slow_multiplier: slow_multiplier.or(Some(4.236)),
2805        volume_type: volume_type.or_else(|| Some("default".to_string())),
2806        center_type: center_type.or_else(|| Some("dynamic".to_string())),
2807        tick_size: tick_size.or(Some(0.01)),
2808    };
2809    let input = DvdiqqeInput::from_slices(open, high, low, close, volume.as_deref(), params);
2810    let out = dvdiqqe_with_kernel(&input, detect_best_kernel())
2811        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2812
2813    let cols = close.len();
2814    let mut values = Vec::with_capacity(4 * cols);
2815    values.extend_from_slice(&out.dvdi);
2816    values.extend_from_slice(&out.fast_tl);
2817    values.extend_from_slice(&out.slow_tl);
2818    values.extend_from_slice(&out.center_line);
2819
2820    serde_wasm_bindgen::to_value(&DvdiqqeJsFlat {
2821        values,
2822        rows: 4,
2823        cols,
2824    })
2825    .map_err(|e| JsValue::from_str(&e.to_string()))
2826}
2827
2828#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2829#[wasm_bindgen]
2830pub fn dvdiqqe_alloc(len: usize) -> *mut f64 {
2831    let mut v: Vec<f64> = Vec::with_capacity(len);
2832    let ptr = v.as_mut_ptr();
2833    std::mem::forget(v);
2834    ptr
2835}
2836
2837#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2838#[wasm_bindgen]
2839pub fn dvdiqqe_free(ptr: *mut f64, len: usize) {
2840    unsafe {
2841        let _ = Vec::from_raw_parts(ptr, len, len);
2842    }
2843}
2844
2845#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2846#[wasm_bindgen(js_name = dvdiqqe_into)]
2847pub fn dvdiqqe_into(
2848    open: *const f64,
2849    high: *const f64,
2850    low: *const f64,
2851    close: *const f64,
2852    vol: *const f64,
2853    len: usize,
2854    period: usize,
2855    smoothing_period: usize,
2856    fast_multiplier: f64,
2857    slow_multiplier: f64,
2858    volume_type: String,
2859    center_type: String,
2860    tick_size: f64,
2861
2862    out_ptr: *mut f64,
2863) -> Result<(), JsValue> {
2864    if open.is_null() || high.is_null() || low.is_null() || close.is_null() || out_ptr.is_null() {
2865        return Err(JsValue::from_str("null pointer"));
2866    }
2867    unsafe {
2868        let o = std::slice::from_raw_parts(open, len);
2869        let h = std::slice::from_raw_parts(high, len);
2870        let l = std::slice::from_raw_parts(low, len);
2871        let c = std::slice::from_raw_parts(close, len);
2872        let v = if vol.is_null() {
2873            None
2874        } else {
2875            Some(std::slice::from_raw_parts(vol, len))
2876        };
2877
2878        let mut out = std::slice::from_raw_parts_mut(out_ptr, 4 * len);
2879        let (dvdi_dst, rest) = out.split_at_mut(len);
2880        let (fast_dst, rest) = rest.split_at_mut(len);
2881        let (slow_dst, cent_dst) = rest.split_at_mut(len);
2882
2883        let params = DvdiqqeParams {
2884            period: Some(period),
2885            smoothing_period: Some(smoothing_period),
2886            fast_multiplier: Some(fast_multiplier),
2887            slow_multiplier: Some(slow_multiplier),
2888            volume_type: Some(volume_type),
2889            center_type: Some(center_type),
2890            tick_size: Some(tick_size),
2891        };
2892        let input = DvdiqqeInput::from_slices(o, h, l, c, v, params);
2893        dvdiqqe_into_slices(
2894            dvdi_dst,
2895            fast_dst,
2896            slow_dst,
2897            cent_dst,
2898            &input,
2899            detect_best_kernel(),
2900        )
2901        .map_err(|e| JsValue::from_str(&e.to_string()))
2902    }
2903}
2904
2905#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2906#[derive(Serialize, Deserialize)]
2907pub struct DvdiqqeBatchConfig {
2908    pub period_range: (usize, usize, usize),
2909    pub smoothing_period_range: (usize, usize, usize),
2910    pub fast_mult_range: (f64, f64, f64),
2911    pub slow_mult_range: (f64, f64, f64),
2912}
2913
2914#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2915#[derive(Serialize, Deserialize)]
2916pub struct DvdiqqeParamsJs {
2917    pub period: usize,
2918    pub smoothing_period: usize,
2919    pub fast_multiplier: f64,
2920    pub slow_multiplier: f64,
2921}
2922
2923#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2924#[derive(Serialize, Deserialize)]
2925pub struct DvdiqqeBatchJsOutput {
2926    pub values: Vec<f64>,
2927    pub rows: usize,
2928    pub cols: usize,
2929    pub combos: Vec<DvdiqqeParamsJs>,
2930}
2931
2932#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2933#[wasm_bindgen(js_name = dvdiqqe_batch_unified)]
2934pub fn dvdiqqe_batch_unified_js(
2935    open: &[f64],
2936    high: &[f64],
2937    low: &[f64],
2938    close: &[f64],
2939    volume: Option<Vec<f64>>,
2940    config: JsValue,
2941    volume_type: String,
2942    center_type: String,
2943    tick_size: f64,
2944) -> Result<JsValue, JsValue> {
2945    let cfg: DvdiqqeBatchConfig =
2946        serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
2947
2948    let sweep = DvdiqqeBatchRange {
2949        period: cfg.period_range,
2950        smoothing_period: cfg.smoothing_period_range,
2951        fast_multiplier: cfg.fast_mult_range,
2952        slow_multiplier: cfg.slow_mult_range,
2953    };
2954
2955    let result = dvdiqqe_batch_with_kernel(
2956        open,
2957        high,
2958        low,
2959        close,
2960        volume.as_deref(),
2961        &sweep,
2962        detect_best_kernel(),
2963        &volume_type,
2964        &center_type,
2965        tick_size,
2966    )
2967    .map_err(|e| JsValue::from_str(&e.to_string()))?;
2968
2969    let cols = close.len();
2970    let rows = result.rows;
2971    let mut values = Vec::with_capacity(4 * rows * cols);
2972
2973    values.extend_from_slice(&result.dvdi_values);
2974    values.extend_from_slice(&result.fast_tl_values);
2975    values.extend_from_slice(&result.slow_tl_values);
2976    values.extend_from_slice(&result.center_values);
2977
2978    let combos: Vec<DvdiqqeParamsJs> = result
2979        .combos
2980        .iter()
2981        .map(|p| DvdiqqeParamsJs {
2982            period: p.period.unwrap_or(13),
2983            smoothing_period: p.smoothing_period.unwrap_or(6),
2984            fast_multiplier: p.fast_multiplier.unwrap_or(2.618),
2985            slow_multiplier: p.slow_multiplier.unwrap_or(4.236),
2986        })
2987        .collect();
2988
2989    let output = DvdiqqeBatchJsOutput {
2990        values,
2991        rows: rows * 4,
2992        cols,
2993        combos,
2994    };
2995
2996    serde_wasm_bindgen::to_value(&output).map_err(|e| JsValue::from_str(&e.to_string()))
2997}
2998
2999#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
3000#[wasm_bindgen(js_name = dvdiqqe_batch_into)]
3001pub fn dvdiqqe_batch_into(
3002    open: *const f64,
3003    high: *const f64,
3004    low: *const f64,
3005    close: *const f64,
3006    vol: *const f64,
3007    len: usize,
3008
3009    period_start: usize,
3010    period_end: usize,
3011    period_step: usize,
3012    smoothing_start: usize,
3013    smoothing_end: usize,
3014    smoothing_step: usize,
3015    fast_start: f64,
3016    fast_end: f64,
3017    fast_step: f64,
3018    slow_start: f64,
3019    slow_end: f64,
3020    slow_step: f64,
3021
3022    volume_type: String,
3023    center_type: String,
3024    tick_size: f64,
3025
3026    out_ptr: *mut f64,
3027) -> Result<(), JsValue> {
3028    if open.is_null() || high.is_null() || low.is_null() || close.is_null() || out_ptr.is_null() {
3029        return Err(JsValue::from_str("null pointer"));
3030    }
3031
3032    unsafe {
3033        let open = std::slice::from_raw_parts(open, len);
3034        let high = std::slice::from_raw_parts(high, len);
3035        let low = std::slice::from_raw_parts(low, len);
3036        let close = std::slice::from_raw_parts(close, len);
3037        let volume = if vol.is_null() {
3038            None
3039        } else {
3040            Some(std::slice::from_raw_parts(vol, len))
3041        };
3042
3043        let sweep = DvdiqqeBatchRange {
3044            period: (period_start, period_end, period_step),
3045            smoothing_period: (smoothing_start, smoothing_end, smoothing_step),
3046            fast_multiplier: (fast_start, fast_end, fast_step),
3047            slow_multiplier: (slow_start, slow_end, slow_step),
3048        };
3049
3050        let result = dvdiqqe_batch_with_kernel_flat(
3051            open,
3052            high,
3053            low,
3054            close,
3055            volume,
3056            &sweep,
3057            detect_best_kernel(),
3058            &volume_type,
3059            &center_type,
3060            tick_size,
3061        )
3062        .map_err(|e| JsValue::from_str(&e.to_string()))?;
3063
3064        let out_slice = std::slice::from_raw_parts_mut(out_ptr, result.values.len());
3065        out_slice.copy_from_slice(&result.values);
3066
3067        Ok(())
3068    }
3069}
3070
3071#[cfg(test)]
3072mod tests {
3073    use super::*;
3074    use crate::skip_if_unsupported;
3075    use crate::utilities::data_loader::{read_candles_from_csv, Candles};
3076    #[cfg(feature = "proptest")]
3077    use proptest::prelude::*;
3078    use std::error::Error;
3079
3080    #[test]
3081    fn test_dvdiqqe_accuracy_scalar() -> Result<(), Box<dyn Error>> {
3082        check_dvdiqqe_accuracy("test_dvdiqqe_accuracy_scalar", Kernel::Scalar)
3083    }
3084
3085    #[test]
3086    fn test_dvdiqqe_accuracy_auto() -> Result<(), Box<dyn Error>> {
3087        check_dvdiqqe_accuracy("test_dvdiqqe_accuracy_auto", Kernel::Auto)
3088    }
3089
3090    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3091    #[test]
3092    fn test_dvdiqqe_accuracy_avx2() -> Result<(), Box<dyn Error>> {
3093        check_dvdiqqe_accuracy("test_dvdiqqe_accuracy_avx2", Kernel::Avx2)
3094    }
3095
3096    #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3097    #[test]
3098    fn test_dvdiqqe_accuracy_avx512() -> Result<(), Box<dyn Error>> {
3099        check_dvdiqqe_accuracy("test_dvdiqqe_accuracy_avx512", Kernel::Avx512)
3100    }
3101
3102    #[test]
3103    fn test_dvdiqqe_with_csv_data() -> Result<(), Box<dyn Error>> {
3104        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3105        let candles = read_candles_from_csv(file_path)?;
3106
3107        let params = DvdiqqeParams::default();
3108        let input = DvdiqqeInput::from_candles(&candles, params);
3109        let result = dvdiqqe(&input)?;
3110
3111        let len = candles.close.len();
3112        assert_eq!(result.dvdi.len(), len);
3113        assert_eq!(result.fast_tl.len(), len);
3114        assert_eq!(result.slow_tl.len(), len);
3115        assert_eq!(result.center_line.len(), len);
3116
3117        let warmup = 25;
3118        for i in 0..warmup.min(len) {
3119            assert!(
3120                result.dvdi[i].is_nan(),
3121                "Expected NaN in warmup at index {}",
3122                i
3123            );
3124        }
3125        for i in warmup..len {
3126            assert!(
3127                result.dvdi[i].is_finite(),
3128                "Expected finite value after warmup at index {}",
3129                i
3130            );
3131        }
3132
3133        Ok(())
3134    }
3135
3136    #[test]
3137    fn test_dvdiqqe_into_matches_api_v2() -> Result<(), Box<dyn Error>> {
3138        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3139        let candles = read_candles_from_csv(file_path)?;
3140
3141        let input = DvdiqqeInput::with_default_candles(&candles);
3142        let baseline = dvdiqqe(&input)?;
3143
3144        let len = candles.close.len();
3145        let mut dvdi = vec![0.0; len];
3146        let mut fast = vec![0.0; len];
3147        let mut slow = vec![0.0; len];
3148        let mut center = vec![0.0; len];
3149
3150        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
3151        {
3152            dvdiqqe_into(&input, &mut dvdi, &mut fast, &mut slow, &mut center)?;
3153        }
3154
3155        assert_eq!(baseline.dvdi.len(), dvdi.len());
3156        assert_eq!(baseline.fast_tl.len(), fast.len());
3157        assert_eq!(baseline.slow_tl.len(), slow.len());
3158        assert_eq!(baseline.center_line.len(), center.len());
3159
3160        fn eq_or_both_nan_eps(a: f64, b: f64) -> bool {
3161            (a.is_nan() && b.is_nan()) || (a - b).abs() <= 1e-12
3162        }
3163
3164        for i in 0..len {
3165            assert!(
3166                eq_or_both_nan_eps(baseline.dvdi[i], dvdi[i]),
3167                "dvdi mismatch at {}: baseline={}, into={}",
3168                i,
3169                baseline.dvdi[i],
3170                dvdi[i]
3171            );
3172            assert!(
3173                eq_or_both_nan_eps(baseline.fast_tl[i], fast[i]),
3174                "fast_tl mismatch at {}: baseline={}, into={}",
3175                i,
3176                baseline.fast_tl[i],
3177                fast[i]
3178            );
3179            assert!(
3180                eq_or_both_nan_eps(baseline.slow_tl[i], slow[i]),
3181                "slow_tl mismatch at {}: baseline={}, into={}",
3182                i,
3183                baseline.slow_tl[i],
3184                slow[i]
3185            );
3186            assert!(
3187                eq_or_both_nan_eps(baseline.center_line[i], center[i]),
3188                "center_line mismatch at {}: baseline={}, into={}",
3189                i,
3190                baseline.center_line[i],
3191                center[i]
3192            );
3193        }
3194
3195        Ok(())
3196    }
3197
3198    #[test]
3199    fn test_dvdiqqe_empty_input() {
3200        let candles = Candles::new(vec![], vec![], vec![], vec![], vec![], vec![]);
3201        let params = DvdiqqeParams::default();
3202        let input = DvdiqqeInput::from_candles(&candles, params);
3203        let result = dvdiqqe(&input);
3204        assert!(result.is_err());
3205    }
3206
3207    #[test]
3208    fn test_dvdiqqe_all_nan() {
3209        let nan_vec = vec![f64::NAN; 10];
3210        let candles = Candles::new(
3211            vec![0; 10],
3212            nan_vec.clone(),
3213            nan_vec.clone(),
3214            nan_vec.clone(),
3215            nan_vec.clone(),
3216            nan_vec.clone(),
3217        );
3218        let params = DvdiqqeParams::default();
3219        let input = DvdiqqeInput::from_candles(&candles, params);
3220        let result = dvdiqqe(&input);
3221        assert!(result.is_err());
3222    }
3223
3224    #[test]
3225    fn test_dvdiqqe_period_validation() {
3226        let data = vec![1.0, 2.0, 3.0];
3227        let candles = Candles::new(
3228            vec![0, 1, 2],
3229            data.clone(),
3230            data.clone(),
3231            data.clone(),
3232            data.clone(),
3233            vec![100.0, 200.0, 300.0],
3234        );
3235
3236        let params = DvdiqqeParams {
3237            period: Some(10),
3238            ..Default::default()
3239        };
3240
3241        let input = DvdiqqeInput::from_candles(&candles, params);
3242        let result = dvdiqqe(&input);
3243        assert!(result.is_err());
3244    }
3245
3246    #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
3247    #[test]
3248    fn test_dvdiqqe_into_matches_api() -> Result<(), Box<dyn Error>> {
3249        let len = 256usize;
3250        let mut ts = Vec::with_capacity(len);
3251        let mut open = Vec::with_capacity(len);
3252        let mut high = Vec::with_capacity(len);
3253        let mut low = Vec::with_capacity(len);
3254        let mut close = Vec::with_capacity(len);
3255        let mut volume = Vec::with_capacity(len);
3256
3257        for i in 0..len {
3258            ts.push(i as i64);
3259            let base = 100.0 + (i as f64) * 0.1;
3260            let noise = ((i * 17) % 13) as f64 * 0.01;
3261            let o = base + noise;
3262            let c = base + (noise * 1.5) - 0.03;
3263            let h = o.max(c) + 0.5;
3264            let l = o.min(c) - 0.5;
3265            open.push(o);
3266            high.push(h);
3267            low.push(l);
3268            close.push(c);
3269            volume.push(1000.0 + ((i * 37) % 23) as f64);
3270        }
3271
3272        let candles = Candles::new(ts, open, high, low, close.clone(), volume);
3273        let input = DvdiqqeInput::with_default_candles(&candles);
3274
3275        let baseline = dvdiqqe(&input)?;
3276
3277        let mut dvdi = vec![0.0; len];
3278        let mut fast = vec![0.0; len];
3279        let mut slow = vec![0.0; len];
3280        let mut center = vec![0.0; len];
3281
3282        dvdiqqe_into(&input, &mut dvdi, &mut fast, &mut slow, &mut center)?;
3283
3284        assert_eq!(baseline.dvdi.len(), len);
3285        assert_eq!(baseline.fast_tl.len(), len);
3286        assert_eq!(baseline.slow_tl.len(), len);
3287        assert_eq!(baseline.center_line.len(), len);
3288
3289        fn eq_or_both_nan(a: f64, b: f64) -> bool {
3290            (a.is_nan() && b.is_nan()) || (a == b)
3291        }
3292
3293        for i in 0..len {
3294            assert!(
3295                eq_or_both_nan(baseline.dvdi[i], dvdi[i]),
3296                "dvdi mismatch at {}: api={} into={}",
3297                i,
3298                baseline.dvdi[i],
3299                dvdi[i]
3300            );
3301            assert!(
3302                eq_or_both_nan(baseline.fast_tl[i], fast[i]),
3303                "fast_tl mismatch at {}: api={} into={}",
3304                i,
3305                baseline.fast_tl[i],
3306                fast[i]
3307            );
3308            assert!(
3309                eq_or_both_nan(baseline.slow_tl[i], slow[i]),
3310                "slow_tl mismatch at {}: api={} into={}",
3311                i,
3312                baseline.slow_tl[i],
3313                slow[i]
3314            );
3315            assert!(
3316                eq_or_both_nan(baseline.center_line[i], center[i]),
3317                "center_line mismatch at {}: api={} into={}",
3318                i,
3319                baseline.center_line[i],
3320                center[i]
3321            );
3322        }
3323
3324        Ok(())
3325    }
3326
3327    fn check_dvdiqqe_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3328        skip_if_unsupported!(kernel, test_name);
3329        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3330        let candles = read_candles_from_csv(file_path)?;
3331
3332        let params = DvdiqqeParams::default();
3333        let input = DvdiqqeInput::from_candles(&candles, params);
3334        let result = dvdiqqe_with_kernel(&input, kernel)?;
3335
3336        let expected_dvdi = vec![
3337            -304.41010224,
3338            -279.48152664,
3339            -287.58723437,
3340            -252.40349484,
3341            -343.00922595,
3342        ];
3343        let expected_slow_tl = vec![
3344            -990.21769695,
3345            -955.69385266,
3346            -951.82562405,
3347            -903.39071943,
3348            -903.39071943,
3349        ];
3350        let expected_fast_tl = vec![
3351            -728.26380454,
3352            -697.40500858,
3353            -697.40500858,
3354            -654.73695895,
3355            -654.73695895,
3356        ];
3357
3358        let expected_center = vec![
3359            21.98929919135097,
3360            21.969910753134442,
3361            21.950003541229705,
3362            21.932361363982043,
3363            21.908895469736102,
3364        ];
3365
3366        let start = result.dvdi.len().saturating_sub(5);
3367
3368        for i in 0..5 {
3369            let diff_dvdi = (result.dvdi[start + i] - expected_dvdi[i]).abs();
3370            let diff_slow = (result.slow_tl[start + i] - expected_slow_tl[i]).abs();
3371            let diff_fast = (result.fast_tl[start + i] - expected_fast_tl[i]).abs();
3372            let diff_center = (result.center_line[start + i] - expected_center[i]).abs();
3373
3374            assert!(
3375                diff_dvdi < 1e-6,
3376                "[{}] DVDI {:?} mismatch at idx {}: got {}, expected {}",
3377                test_name,
3378                kernel,
3379                i,
3380                result.dvdi[start + i],
3381                expected_dvdi[i]
3382            );
3383            assert!(
3384                diff_slow < 1e-6,
3385                "[{}] Slow TL {:?} mismatch at idx {}: got {}, expected {}",
3386                test_name,
3387                kernel,
3388                i,
3389                result.slow_tl[start + i],
3390                expected_slow_tl[i]
3391            );
3392            assert!(
3393                diff_fast < 1e-6,
3394                "[{}] Fast TL {:?} mismatch at idx {}: got {}, expected {}",
3395                test_name,
3396                kernel,
3397                i,
3398                result.fast_tl[start + i],
3399                expected_fast_tl[i]
3400            );
3401            assert!(
3402                diff_center < 1e-6,
3403                "[{}] Center line {:?} mismatch at idx {}: got {}, expected {}",
3404                test_name,
3405                kernel,
3406                i,
3407                result.center_line[start + i],
3408                expected_center[i]
3409            );
3410        }
3411
3412        Ok(())
3413    }
3414
3415    fn check_dvdiqqe_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3416        skip_if_unsupported!(kernel, test_name);
3417        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3418        let candles = read_candles_from_csv(file_path)?;
3419
3420        let params = DvdiqqeParams {
3421            period: None,
3422            smoothing_period: None,
3423            fast_multiplier: None,
3424            slow_multiplier: None,
3425            volume_type: None,
3426            center_type: None,
3427            tick_size: None,
3428        };
3429
3430        let input = DvdiqqeInput::from_candles(&candles, params);
3431        let output = dvdiqqe_with_kernel(&input, kernel)?;
3432        assert_eq!(output.dvdi.len(), candles.close.len());
3433
3434        Ok(())
3435    }
3436
3437    fn check_dvdiqqe_default_candles(
3438        test_name: &str,
3439        kernel: Kernel,
3440    ) -> Result<(), Box<dyn Error>> {
3441        skip_if_unsupported!(kernel, test_name);
3442        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3443        let candles = read_candles_from_csv(file_path)?;
3444
3445        let input = DvdiqqeInput::with_default_candles(&candles);
3446        let output = dvdiqqe_with_kernel(&input, kernel)?;
3447        assert_eq!(output.dvdi.len(), candles.close.len());
3448
3449        Ok(())
3450    }
3451
3452    fn check_dvdiqqe_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3453        skip_if_unsupported!(kernel, test_name);
3454        let data = vec![100.0; 50];
3455        let candles = Candles::new(
3456            (0..50).map(|i| i as i64).collect(),
3457            data.clone(),
3458            data.iter().map(|x| x + 1.0).collect(),
3459            data.iter().map(|x| x - 1.0).collect(),
3460            data.clone(),
3461            vec![1000.0; 50],
3462        );
3463
3464        let params = DvdiqqeParams {
3465            period: Some(0),
3466            ..Default::default()
3467        };
3468
3469        let input = DvdiqqeInput::from_candles(&candles, params);
3470        let res = dvdiqqe_with_kernel(&input, kernel);
3471        assert!(
3472            res.is_err(),
3473            "[{}] DVDIQQE should fail with zero period",
3474            test_name
3475        );
3476
3477        Ok(())
3478    }
3479
3480    fn check_dvdiqqe_period_exceeds_length(
3481        test_name: &str,
3482        kernel: Kernel,
3483    ) -> Result<(), Box<dyn Error>> {
3484        skip_if_unsupported!(kernel, test_name);
3485        let data = vec![100.0; 5];
3486        let candles = Candles::new(
3487            vec![0, 1, 2, 3, 4],
3488            data.clone(),
3489            data.iter().map(|x| x + 1.0).collect(),
3490            data.iter().map(|x| x - 1.0).collect(),
3491            data.clone(),
3492            vec![1000.0; 5],
3493        );
3494
3495        let params = DvdiqqeParams {
3496            period: Some(20),
3497            ..Default::default()
3498        };
3499
3500        let input = DvdiqqeInput::from_candles(&candles, params);
3501        let res = dvdiqqe_with_kernel(&input, kernel);
3502        assert!(
3503            res.is_err(),
3504            "[{}] DVDIQQE should fail with period exceeding length",
3505            test_name
3506        );
3507
3508        Ok(())
3509    }
3510
3511    fn check_dvdiqqe_very_small_dataset(
3512        test_name: &str,
3513        kernel: Kernel,
3514    ) -> Result<(), Box<dyn Error>> {
3515        skip_if_unsupported!(kernel, test_name);
3516        let candles = Candles::new(
3517            vec![0],
3518            vec![100.0],
3519            vec![101.0],
3520            vec![99.0],
3521            vec![100.0],
3522            vec![1000.0],
3523        );
3524
3525        let params = DvdiqqeParams::default();
3526        let input = DvdiqqeInput::from_candles(&candles, params);
3527        let res = dvdiqqe_with_kernel(&input, kernel);
3528        assert!(
3529            res.is_err(),
3530            "[{}] DVDIQQE should fail with insufficient data",
3531            test_name
3532        );
3533
3534        Ok(())
3535    }
3536
3537    fn check_dvdiqqe_empty_input(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3538        skip_if_unsupported!(kernel, test_name);
3539        let candles = Candles::new(vec![], vec![], vec![], vec![], vec![], vec![]);
3540        let input = DvdiqqeInput::from_candles(&candles, DvdiqqeParams::default());
3541        let res = dvdiqqe_with_kernel(&input, kernel);
3542        assert!(
3543            res.is_err(),
3544            "[{}] DVDIQQE should fail with empty input",
3545            test_name
3546        );
3547
3548        Ok(())
3549    }
3550
3551    fn check_dvdiqqe_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3552        skip_if_unsupported!(kernel, test_name);
3553        let nan_data = vec![f64::NAN; 50];
3554        let candles = Candles::new(
3555            (0..50).map(|i| i as i64).collect(),
3556            nan_data.clone(),
3557            nan_data.clone(),
3558            nan_data.clone(),
3559            nan_data.clone(),
3560            nan_data.clone(),
3561        );
3562
3563        let input = DvdiqqeInput::from_candles(&candles, DvdiqqeParams::default());
3564        let res = dvdiqqe_with_kernel(&input, kernel);
3565        assert!(
3566            res.is_err(),
3567            "[{}] DVDIQQE should fail with all NaN input",
3568            test_name
3569        );
3570
3571        Ok(())
3572    }
3573
3574    fn check_dvdiqqe_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3575        skip_if_unsupported!(kernel, test_name);
3576
3577        let mut close = vec![100.0; 50];
3578        close[10] = f64::NAN;
3579        close[11] = f64::NAN;
3580
3581        let candles = Candles::new(
3582            (0..50).map(|i| i as i64).collect(),
3583            close.clone(),
3584            close
3585                .iter()
3586                .map(|x| if x.is_nan() { f64::NAN } else { x + 1.0 })
3587                .collect(),
3588            close
3589                .iter()
3590                .map(|x| if x.is_nan() { f64::NAN } else { x - 1.0 })
3591                .collect(),
3592            close.clone(),
3593            vec![1000.0; 50],
3594        );
3595
3596        let input = DvdiqqeInput::from_candles(&candles, DvdiqqeParams::default());
3597        let res = dvdiqqe_with_kernel(&input, kernel)?;
3598
3599        assert_eq!(res.dvdi.len(), 50);
3600
3601        if res.dvdi.len() > 30 {
3602            assert!(
3603                res.dvdi[30..].iter().any(|x| x.is_finite()),
3604                "[{}] DVDIQQE should recover after NaN values",
3605                test_name
3606            );
3607        }
3608
3609        Ok(())
3610    }
3611
3612    fn check_dvdiqqe_with_tick_volume(
3613        test_name: &str,
3614        kernel: Kernel,
3615    ) -> Result<(), Box<dyn Error>> {
3616        skip_if_unsupported!(kernel, test_name);
3617        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3618        let candles = read_candles_from_csv(file_path)?;
3619
3620        let params = DvdiqqeParams {
3621            volume_type: Some("tick".to_string()),
3622            ..Default::default()
3623        };
3624
3625        let input = DvdiqqeInput::from_candles(&candles, params);
3626        let result = dvdiqqe_with_kernel(&input, kernel)?;
3627
3628        assert_eq!(result.dvdi.len(), candles.close.len());
3629
3630        Ok(())
3631    }
3632
3633    fn check_dvdiqqe_static_center(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3634        skip_if_unsupported!(kernel, test_name);
3635        let data = vec![100.0; 50];
3636        let candles = Candles::new(
3637            (0..50).map(|i| i as i64).collect(),
3638            data.clone(),
3639            data.iter().map(|x| x + 1.0).collect(),
3640            data.iter().map(|x| x - 1.0).collect(),
3641            data.clone(),
3642            vec![1000.0; 50],
3643        );
3644
3645        let params = DvdiqqeParams {
3646            center_type: Some("static".to_string()),
3647            ..Default::default()
3648        };
3649
3650        let input = DvdiqqeInput::from_candles(&candles, params);
3651        let result = dvdiqqe_with_kernel(&input, kernel)?;
3652
3653        let warmup = 25;
3654        for i in warmup..result.center_line.len() {
3655            assert!(
3656                result.center_line[i] == 0.0,
3657                "[{}] Static center line should be 0.0 at index {}, got {}",
3658                test_name,
3659                i,
3660                result.center_line[i]
3661            );
3662        }
3663
3664        Ok(())
3665    }
3666
3667    macro_rules! generate_all_dvdiqqe_tests {
3668        ($($test_fn:ident),* $(,)?) => {
3669            paste::paste! {
3670                $(
3671                    #[test]
3672                    fn [<$test_fn _scalar_f64>]() {
3673                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
3674                    }
3675                )*
3676                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3677                $(
3678                    #[test]
3679                    fn [<$test_fn _avx2_f64>]() {
3680                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
3681                    }
3682                    #[test]
3683                    fn [<$test_fn _avx512_f64>]() {
3684                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
3685                    }
3686                )*
3687            }
3688        }
3689    }
3690
3691    macro_rules! gen_dvdiqqe_batch_tests {
3692        ($fnn:ident) => {
3693            paste::paste!{
3694                #[test] fn [<$fnn _scalar>]() { let _ = $fnn(stringify!([<$fnn _scalar>]), Kernel::ScalarBatch); }
3695                #[cfg(all(feature="nightly-avx", target_arch="x86_64"))]
3696                #[test] fn [<$fnn _avx2>]()   { let _ = $fnn(stringify!([<$fnn _avx2>]),   Kernel::Avx2Batch); }
3697                #[cfg(all(feature="nightly-avx", target_arch="x86_64"))]
3698                #[test] fn [<$fnn _avx512>]() { let _ = $fnn(stringify!([<$fnn _avx512>]), Kernel::Avx512Batch); }
3699                #[test] fn [<$fnn _auto>]()   { let _ = $fnn(stringify!([<$fnn _auto>]),   Kernel::Auto); }
3700            }
3701        }
3702    }
3703
3704    fn check_batch_default_row(test: &str, k: Kernel) -> Result<(), Box<dyn Error>> {
3705        skip_if_unsupported!(k, test);
3706        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3707        let c = read_candles_from_csv(file)?;
3708        let out = DvdiqqeBatchBuilder::new().kernel(k).apply_candles(&c)?;
3709        assert_eq!(out.dvdi_values.len(), out.rows * out.cols);
3710        assert_eq!(out.fast_tl_values.len(), out.rows * out.cols);
3711        assert_eq!(out.slow_tl_values.len(), out.rows * out.cols);
3712        assert_eq!(out.center_values.len(), out.rows * out.cols);
3713        Ok(())
3714    }
3715
3716    fn check_batch_sweep(test: &str, k: Kernel) -> Result<(), Box<dyn Error>> {
3717        skip_if_unsupported!(k, test);
3718        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3719        let c = read_candles_from_csv(file)?;
3720        let out = DvdiqqeBatchBuilder::new()
3721            .kernel(k)
3722            .period_range(13, 16, 1)
3723            .smoothing_range(5, 7, 1)
3724            .fast_range(2.6, 2.6, 0.0)
3725            .slow_range(4.2, 4.2, 0.0)
3726            .apply_candles(&c)?;
3727        let expected = 4 * 3;
3728        assert_eq!(out.rows, expected);
3729        assert_eq!(out.cols, c.close.len());
3730        Ok(())
3731    }
3732
3733    gen_dvdiqqe_batch_tests!(check_batch_default_row);
3734    gen_dvdiqqe_batch_tests!(check_batch_sweep);
3735
3736    fn check_dvdiqqe_batch_default_row_old(
3737        test_name: &str,
3738        kernel: Kernel,
3739    ) -> Result<(), Box<dyn Error>> {
3740        skip_if_unsupported!(kernel, test_name);
3741
3742        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3743        let c = read_candles_from_csv(file)?;
3744
3745        let batch_output = DvdiqqeBatchBuilder::new()
3746            .kernel(kernel)
3747            .period_static(13)
3748            .smoothing_static(6)
3749            .fast_static(2.618)
3750            .slow_static(4.236)
3751            .apply_candles(&c)?;
3752
3753        let def_params = DvdiqqeParams::default();
3754        let batch_values = batch_output
3755            .values_for(&def_params)
3756            .expect("default row missing");
3757
3758        let single_input = DvdiqqeInput::with_default_candles(&c);
3759        let single_output = dvdiqqe_with_kernel(&single_input, kernel)?;
3760
3761        assert_eq!(batch_values.dvdi.len(), single_output.dvdi.len());
3762
3763        for i in 0..batch_values.dvdi.len() {
3764            if batch_values.dvdi[i].is_finite() && single_output.dvdi[i].is_finite() {
3765                assert!(
3766                    (batch_values.dvdi[i] - single_output.dvdi[i]).abs() < 1e-10,
3767                    "[{}] DVDI mismatch at index {}: batch={}, single={}",
3768                    test_name,
3769                    i,
3770                    batch_values.dvdi[i],
3771                    single_output.dvdi[i]
3772                );
3773            }
3774        }
3775
3776        Ok(())
3777    }
3778
3779    fn check_dvdiqqe_streaming(test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
3780        let test_data = vec![
3781            (100.0, 102.0, 99.0, 101.0, 1000.0),
3782            (101.0, 103.0, 100.0, 102.0, 1100.0),
3783            (102.0, 104.0, 101.0, 103.0, 1200.0),
3784            (103.0, 105.0, 102.0, 104.0, 1150.0),
3785            (104.0, 106.0, 103.0, 105.0, 1250.0),
3786        ];
3787
3788        let mut full_data = test_data.clone();
3789        for i in 0..50 {
3790            let base = 105.0 + i as f64 * 0.5;
3791            full_data.push((
3792                base,
3793                base + 2.0,
3794                base - 1.0,
3795                base + 1.0,
3796                1000.0 + (i as f64 * 50.0),
3797            ));
3798        }
3799
3800        let params = DvdiqqeParams::default();
3801        let mut stream = DvdiqqeStream::try_new(params.clone())?;
3802
3803        let mut stream_results = Vec::new();
3804        for (open, high, low, close, volume) in &full_data {
3805            if let Some(output) = stream.update(*open, *high, *low, *close, *volume) {
3806                stream_results.push((
3807                    output.dvdi,
3808                    output.fast_tl,
3809                    output.slow_tl,
3810                    output.center_line,
3811                ));
3812            }
3813        }
3814
3815        assert!(
3816            !stream_results.is_empty(),
3817            "[{}] Stream should produce outputs",
3818            test_name
3819        );
3820
3821        if stream_results.len() > 0 {
3822            let (opens, highs, lows, closes, volumes): (Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>) =
3823                full_data.iter().cloned().unzip_n_tuple();
3824
3825            let batch_input =
3826                DvdiqqeInput::from_slices(&opens, &highs, &lows, &closes, Some(&volumes), params);
3827
3828            if let Ok(batch_output) = dvdiqqe(&batch_input) {
3829                let last_idx = batch_output.dvdi.len() - 1;
3830                let last_stream = stream_results.last().unwrap();
3831
3832                if batch_output.dvdi[last_idx].is_finite() && last_stream.0.is_finite() {
3833                    assert!(
3834                        (batch_output.dvdi[last_idx] - last_stream.0).abs() < 1.0,
3835                        "[{}] Stream DVDI doesn't match batch: stream={}, batch={}",
3836                        test_name,
3837                        last_stream.0,
3838                        batch_output.dvdi[last_idx]
3839                    );
3840                }
3841            }
3842        }
3843
3844        Ok(())
3845    }
3846
3847    fn check_dvdiqqe_batch_sweep(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
3848        skip_if_unsupported!(kernel, test_name);
3849
3850        let n = 100;
3851        let mut opens = vec![100.0; n];
3852        let mut highs = vec![102.0; n];
3853        let mut lows = vec![98.0; n];
3854        let mut closes = vec![100.0; n];
3855        let mut volumes = vec![1000.0; n];
3856
3857        for i in 0..n {
3858            let base = 100.0 + (i as f64 * 0.1);
3859            opens[i] = base;
3860            highs[i] = base + 2.0;
3861            lows[i] = base - 2.0;
3862            closes[i] = base + 0.5;
3863            volumes[i] = 1000.0 + (i as f64 * 10.0);
3864        }
3865
3866        let batch_output = DvdiqqeBatchBuilder::new()
3867            .kernel(kernel)
3868            .period_range(10, 15, 2)
3869            .smoothing_range(4, 8, 2)
3870            .fast_range(2.0, 3.0, 0.5)
3871            .slow_range(4.0, 5.0, 0.5)
3872            .apply_slices(&opens, &highs, &lows, &closes, Some(&volumes))?;
3873
3874        let expected_periods = 3;
3875        let expected_smoothings = 3;
3876        let expected_fasts = 3;
3877        let expected_slows = 3;
3878        let expected_rows =
3879            expected_periods * expected_smoothings * expected_fasts * expected_slows;
3880
3881        assert_eq!(
3882            batch_output.rows, expected_rows,
3883            "[{}] Wrong number of parameter combinations",
3884            test_name
3885        );
3886        assert_eq!(
3887            batch_output.cols, n,
3888            "[{}] Wrong number of data points",
3889            test_name
3890        );
3891
3892        let test_params = DvdiqqeParams {
3893            period: Some(12),
3894            smoothing_period: Some(6),
3895            fast_multiplier: Some(2.5),
3896            slow_multiplier: Some(4.5),
3897            volume_type: None,
3898            center_type: None,
3899            tick_size: None,
3900        };
3901
3902        assert!(
3903            batch_output.values_for(&test_params).is_some(),
3904            "[{}] Should find test parameter combination",
3905            test_name
3906        );
3907
3908        Ok(())
3909    }
3910
3911    trait UnzipN<A, B, C, D, E> {
3912        fn unzip_n_tuple(self) -> (Vec<A>, Vec<B>, Vec<C>, Vec<D>, Vec<E>);
3913    }
3914
3915    impl<A, B, C, D, E, I> UnzipN<A, B, C, D, E> for I
3916    where
3917        I: Iterator<Item = (A, B, C, D, E)>,
3918    {
3919        fn unzip_n_tuple(self) -> (Vec<A>, Vec<B>, Vec<C>, Vec<D>, Vec<E>) {
3920            let mut a_vec = Vec::new();
3921            let mut b_vec = Vec::new();
3922            let mut c_vec = Vec::new();
3923            let mut d_vec = Vec::new();
3924            let mut e_vec = Vec::new();
3925
3926            for (a, b, c, d, e) in self {
3927                a_vec.push(a);
3928                b_vec.push(b);
3929                c_vec.push(c);
3930                d_vec.push(d);
3931                e_vec.push(e);
3932            }
3933
3934            (a_vec, b_vec, c_vec, d_vec, e_vec)
3935        }
3936    }
3937
3938    fn check_dvdiqqe_batch_sweep_old(
3939        test_name: &str,
3940        kernel: Kernel,
3941    ) -> Result<(), Box<dyn Error>> {
3942        skip_if_unsupported!(kernel, test_name);
3943
3944        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3945        let c = read_candles_from_csv(file)?;
3946
3947        let out = DvdiqqeBatchBuilder::new()
3948            .kernel(kernel)
3949            .period_range(13, 16, 1)
3950            .smoothing_range(5, 7, 1)
3951            .fast_range(2.6, 2.6, 0.0)
3952            .slow_range(4.2, 4.2, 0.0)
3953            .apply_candles(&c)?;
3954
3955        let expected = 4 * 3;
3956        assert_eq!(out.rows, expected);
3957        assert_eq!(out.cols, c.close.len());
3958
3959        Ok(())
3960    }
3961
3962    fn check_dvdiqqe_reinput(test: &str, k: Kernel) -> Result<(), Box<dyn Error>> {
3963        skip_if_unsupported!(k, test);
3964        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3965        let c = read_candles_from_csv(file)?;
3966        let out1 = dvdiqqe_with_kernel(&DvdiqqeInput::with_default_candles(&c), k)?;
3967
3968        let i2 = DvdiqqeInput::from_slices(
3969            &c.open,
3970            &c.high,
3971            &c.low,
3972            &out1.dvdi,
3973            Some(&c.volume),
3974            DvdiqqeParams::default(),
3975        );
3976        let out2 = dvdiqqe_with_kernel(&i2, k)?;
3977        assert_eq!(out2.dvdi.len(), out1.dvdi.len());
3978        Ok(())
3979    }
3980
3981    generate_all_dvdiqqe_tests!(
3982        check_dvdiqqe_accuracy,
3983        check_dvdiqqe_partial_params,
3984        check_dvdiqqe_default_candles,
3985        check_dvdiqqe_zero_period,
3986        check_dvdiqqe_period_exceeds_length,
3987        check_dvdiqqe_very_small_dataset,
3988        check_dvdiqqe_empty_input,
3989        check_dvdiqqe_all_nan,
3990        check_dvdiqqe_nan_handling,
3991        check_dvdiqqe_with_tick_volume,
3992        check_dvdiqqe_static_center,
3993        check_dvdiqqe_batch_default_row_old,
3994        check_dvdiqqe_streaming,
3995        check_dvdiqqe_batch_sweep_old,
3996        check_dvdiqqe_reinput
3997    );
3998
3999    #[cfg(debug_assertions)]
4000    #[test]
4001    fn dvdiqqe_no_poison_in_outputs() -> Result<(), Box<dyn Error>> {
4002        use crate::utilities::data_loader::read_candles_from_csv;
4003        let c = read_candles_from_csv("src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv")?;
4004
4005        let out = dvdiqqe_with_kernel(&DvdiqqeInput::with_default_candles(&c), Kernel::Scalar)?;
4006
4007        for &v in out
4008            .dvdi
4009            .iter()
4010            .chain(&out.fast_tl)
4011            .chain(&out.slow_tl)
4012            .chain(&out.center_line)
4013        {
4014            if v.is_nan() {
4015                continue;
4016            }
4017            let b = v.to_bits();
4018            assert_ne!(
4019                b, 0x2222_2222_2222_2222,
4020                "init_matrix_prefixes poison in single output"
4021            );
4022            assert_ne!(
4023                b, 0x3333_3333_3333_3333,
4024                "make_uninit_matrix poison in single output"
4025            );
4026        }
4027
4028        let sweep = DvdiqqeBatchRange::default();
4029        let flat_out = dvdiqqe_batch_with_kernel_flat(
4030            &c.open,
4031            &c.high,
4032            &c.low,
4033            &c.close,
4034            Some(&c.volume),
4035            &sweep,
4036            Kernel::Auto,
4037            "default",
4038            "dynamic",
4039            0.01,
4040        )?;
4041
4042        for (i, &v) in flat_out.values.iter().enumerate() {
4043            if v.is_nan() {
4044                continue;
4045            }
4046            let b = v.to_bits();
4047            assert_ne!(
4048                b, 0x2222_2222_2222_2222,
4049                "init_matrix_prefixes poison at {}",
4050                i
4051            );
4052            assert_ne!(
4053                b, 0x3333_3333_3333_3333,
4054                "make_uninit_matrix poison at {}",
4055                i
4056            );
4057        }
4058
4059        Ok(())
4060    }
4061
4062    #[cfg(feature = "proptest")]
4063    mod proptest_tests {
4064        use super::*;
4065        use proptest::prelude::*;
4066
4067        proptest! {
4068            #[test]
4069            fn test_dvdiqqe_output_length_matches_input(
4070                data in prop::collection::vec(prop::num::f64::NORMAL | prop::num::f64::POSITIVE, 50..200)
4071            ) {
4072                let len = data.len();
4073                let timestamps: Vec<i64> = (0..len as i64).collect();
4074                let mut open = Vec::with_capacity(len);
4075                let mut high = Vec::with_capacity(len);
4076                let mut low = Vec::with_capacity(len);
4077                let mut close = Vec::with_capacity(len);
4078                let mut volume = Vec::with_capacity(len);
4079
4080                for &val in &data {
4081                    open.push(val - 0.5);
4082                    high.push(val + 1.0);
4083                    low.push(val - 1.0);
4084                    close.push(val);
4085                    volume.push(1000.0);
4086                }
4087
4088                let candles = Candles::new(timestamps, open, high, low, close, volume);
4089                let input = DvdiqqeInput::with_default_candles(&candles);
4090
4091                match dvdiqqe(&input) {
4092                    Ok(output) => {
4093                        prop_assert_eq!(output.dvdi.len(), len);
4094                        prop_assert_eq!(output.fast_tl.len(), len);
4095                        prop_assert_eq!(output.slow_tl.len(), len);
4096                        prop_assert_eq!(output.center_line.len(), len);
4097                    }
4098                    Err(DvdiqqeError::NotEnoughValidData { .. }) => {
4099
4100                    }
4101                    Err(e) => {
4102                        prop_assert!(false, "Unexpected error: {:?}", e);
4103                    }
4104                }
4105            }
4106
4107            #[test]
4108            fn test_dvdiqqe_nan_propagation(
4109                valid_data in prop::collection::vec(100.0f64..200.0, 50..100),
4110                nan_positions in prop::collection::vec(0usize..50, 0..10)
4111            ) {
4112                let len = valid_data.len();
4113                let mut data = valid_data.clone();
4114
4115
4116                for &pos in &nan_positions {
4117                    if pos < len {
4118                        data[pos] = f64::NAN;
4119                    }
4120                }
4121
4122                let timestamps: Vec<i64> = (0..len as i64).collect();
4123                let open = data.iter().map(|&v| v - 0.5).collect();
4124                let high = data.iter().map(|&v| v + 1.0).collect();
4125                let low = data.iter().map(|&v| v - 1.0).collect();
4126                let volume = vec![1000.0; len];
4127
4128                let candles = Candles::new(timestamps, open, high, low, data.clone(), volume);
4129                let input = DvdiqqeInput::with_default_candles(&candles);
4130
4131                match dvdiqqe(&input) {
4132                    Ok(output) => {
4133
4134
4135                        prop_assert_eq!(output.dvdi.len(), len);
4136                        prop_assert_eq!(output.fast_tl.len(), len);
4137                        prop_assert_eq!(output.slow_tl.len(), len);
4138                        prop_assert_eq!(output.center_line.len(), len);
4139
4140
4141
4142                        let expected_warmup = 25;
4143                        for i in 0..expected_warmup.min(len) {
4144
4145                            prop_assert!(output.dvdi[i].is_nan() || output.dvdi[i].is_finite(),
4146                                "Position {} should be either NaN (warmup) or finite", i);
4147                        }
4148
4149
4150                        for &v in output.dvdi.iter()
4151                            .chain(&output.fast_tl)
4152                            .chain(&output.slow_tl)
4153                            .chain(&output.center_line) {
4154                            if !v.is_nan() {
4155                                let bits = v.to_bits();
4156                                prop_assert_ne!(bits, 0x2222_2222_2222_2222);
4157                                prop_assert_ne!(bits, 0x3333_3333_3333_3333);
4158                            }
4159                        }
4160                    }
4161                    Err(DvdiqqeError::AllValuesNaN) => {
4162
4163                        prop_assert!(nan_positions.len() >= len / 2);
4164                    }
4165                    Err(_) => {
4166
4167                    }
4168                }
4169            }
4170
4171            #[test]
4172            fn test_dvdiqqe_parameter_bounds(
4173                period in 1usize..50,
4174                smoothing in 1usize..20,
4175                fast_mult in 0.1f64..10.0,
4176                slow_mult in 0.1f64..10.0
4177            ) {
4178                let len = 100;
4179                let data: Vec<f64> = (0..len).map(|i| 100.0 + i as f64).collect();
4180                let timestamps: Vec<i64> = (0..len as i64).collect();
4181                let open = data.iter().map(|&v| v - 0.5).collect();
4182                let high = data.iter().map(|&v| v + 1.0).collect();
4183                let low = data.iter().map(|&v| v - 1.0).collect();
4184                let volume = vec![1000.0; len];
4185
4186                let candles = Candles::new(timestamps, open, high, low, data, volume);
4187                let params = DvdiqqeParams {
4188                    period: Some(period),
4189                    smoothing_period: Some(smoothing),
4190                    fast_multiplier: Some(fast_mult),
4191                    slow_multiplier: Some(slow_mult),
4192                    ..Default::default()
4193                };
4194                let input = DvdiqqeInput::from_candles(&candles, params);
4195
4196                match dvdiqqe(&input) {
4197                    Ok(output) => {
4198
4199                        for i in 30..len {
4200                            prop_assert!(output.dvdi[i].is_finite() || output.dvdi[i].is_nan());
4201                            prop_assert!(output.fast_tl[i].is_finite() || output.fast_tl[i].is_nan());
4202                            prop_assert!(output.slow_tl[i].is_finite() || output.slow_tl[i].is_nan());
4203                        }
4204                    }
4205                    Err(DvdiqqeError::InvalidPeriod { .. }) => {
4206                        prop_assert!(period > len || period == 0);
4207                    }
4208                    Err(_) => {
4209
4210                    }
4211                }
4212            }
4213        }
4214    }
4215}