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 ¢er_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 ¢er_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}