Skip to main content

vector_ta/indicators/
vpt.rs

1use crate::utilities::data_loader::{source_type, Candles};
2use crate::utilities::enums::Kernel;
3use crate::utilities::helpers::{
4    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
5    make_uninit_matrix,
6};
7#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
8use core::arch::x86_64::*;
9use std::error::Error;
10use thiserror::Error;
11
12#[cfg(feature = "python")]
13use crate::utilities::kernel_validation::validate_kernel;
14#[cfg(feature = "python")]
15use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
16#[cfg(feature = "python")]
17use pyo3::exceptions::PyValueError;
18#[cfg(feature = "python")]
19use pyo3::prelude::*;
20#[cfg(feature = "python")]
21use pyo3::types::PyDict;
22#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
23use serde::{Deserialize, Serialize};
24#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
25use wasm_bindgen::prelude::*;
26
27#[derive(Debug, Clone)]
28pub enum VptData<'a> {
29    Candles {
30        candles: &'a Candles,
31        source: &'a str,
32    },
33    Slices {
34        price: &'a [f64],
35        volume: &'a [f64],
36    },
37}
38
39#[derive(Debug, Clone)]
40pub struct VptOutput {
41    pub values: Vec<f64>,
42}
43
44#[derive(Debug, Clone, Default)]
45#[cfg_attr(
46    all(target_arch = "wasm32", feature = "wasm"),
47    derive(Serialize, Deserialize)
48)]
49pub struct VptParams;
50
51#[derive(Debug, Clone)]
52pub struct VptInput<'a> {
53    pub data: VptData<'a>,
54    pub params: VptParams,
55}
56
57impl<'a> VptInput<'a> {
58    #[inline]
59    pub fn from_candles(candles: &'a Candles, source: &'a str) -> Self {
60        Self {
61            data: VptData::Candles { candles, source },
62            params: VptParams::default(),
63        }
64    }
65
66    #[inline]
67    pub fn from_slices(price: &'a [f64], volume: &'a [f64]) -> Self {
68        Self {
69            data: VptData::Slices { price, volume },
70            params: VptParams::default(),
71        }
72    }
73
74    #[inline]
75    pub fn with_default_candles(candles: &'a Candles) -> Self {
76        Self {
77            data: VptData::Candles {
78                candles,
79                source: "close",
80            },
81            params: VptParams::default(),
82        }
83    }
84}
85
86#[derive(Copy, Clone, Debug, Default)]
87pub struct VptBuilder {
88    kernel: Kernel,
89}
90
91impl VptBuilder {
92    #[inline(always)]
93    pub fn new() -> Self {
94        Self {
95            kernel: Kernel::Auto,
96        }
97    }
98
99    #[inline(always)]
100    pub fn kernel(mut self, k: Kernel) -> Self {
101        self.kernel = k;
102        self
103    }
104
105    #[inline(always)]
106    pub fn apply(self, c: &Candles) -> Result<VptOutput, VptError> {
107        let i = VptInput::with_default_candles(c);
108        vpt_with_kernel(&i, self.kernel)
109    }
110
111    #[inline(always)]
112    pub fn apply_slices(self, price: &[f64], volume: &[f64]) -> Result<VptOutput, VptError> {
113        let i = VptInput::from_slices(price, volume);
114        vpt_with_kernel(&i, self.kernel)
115    }
116
117    #[inline(always)]
118    pub fn into_stream(self) -> VptStream {
119        VptStream::default()
120    }
121}
122
123#[derive(Debug, Error)]
124pub enum VptError {
125    #[error("vpt: Empty data provided.")]
126    EmptyInputData,
127    #[error("vpt: All values are NaN.")]
128    AllValuesNaN,
129    #[error("vpt: Invalid period: period = {period}, data length = {data_len}")]
130    InvalidPeriod { period: usize, data_len: usize },
131    #[error("vpt: Not enough valid data (needed = {needed}, valid = {valid}).")]
132    NotEnoughValidData { needed: usize, valid: usize },
133    #[error("vpt: Output length mismatch. expected={expected}, got={got}")]
134    OutputLengthMismatch { expected: usize, got: usize },
135    #[error("vpt: Invalid range: start={start}, end={end}, step={step}")]
136    InvalidRange {
137        start: usize,
138        end: usize,
139        step: usize,
140    },
141    #[error("vpt: invalid kernel for batch: {0:?}")]
142    InvalidKernelForBatch(Kernel),
143    #[error("vpt: size overflow computing rows*cols")]
144    SizeOverflow,
145}
146
147#[inline]
148fn vpt_first_valid(price: &[f64], volume: &[f64]) -> Option<usize> {
149    for i in 1..price.len() {
150        let p0 = price[i - 1];
151        let p1 = price[i];
152        let v1 = volume[i];
153        if p0.is_finite() && p0 != 0.0 && p1.is_finite() && v1.is_finite() {
154            return Some(i);
155        }
156    }
157    None
158}
159
160#[inline]
161pub fn vpt(input: &VptInput) -> Result<VptOutput, VptError> {
162    vpt_with_kernel(input, Kernel::Auto)
163}
164
165pub fn vpt_with_kernel(input: &VptInput, kernel: Kernel) -> Result<VptOutput, VptError> {
166    let (price, volume) = match &input.data {
167        VptData::Candles { candles, source } => {
168            let price = source_type(candles, source);
169            let vol = candles
170                .select_candle_field("volume")
171                .map_err(|_| VptError::EmptyInputData)?;
172            (price, vol)
173        }
174        VptData::Slices { price, volume } => (*price, *volume),
175    };
176
177    if price.is_empty() || volume.is_empty() || price.len() != volume.len() {
178        return Err(VptError::EmptyInputData);
179    }
180
181    let valid_count = price
182        .iter()
183        .zip(volume.iter())
184        .filter(|(&p, &v)| !(p.is_nan() || v.is_nan()))
185        .count();
186
187    if valid_count == 0 {
188        return Err(VptError::AllValuesNaN);
189    }
190    if valid_count < 2 {
191        return Err(VptError::NotEnoughValidData {
192            needed: 2,
193            valid: valid_count,
194        });
195    }
196
197    let chosen = match kernel {
198        Kernel::Auto => Kernel::Scalar,
199        other => other,
200    };
201
202    unsafe {
203        match chosen {
204            Kernel::Scalar | Kernel::ScalarBatch => vpt_scalar(price, volume),
205            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
206            Kernel::Avx2 | Kernel::Avx2Batch => vpt_avx2(price, volume),
207            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
208            Kernel::Avx512 | Kernel::Avx512Batch => vpt_avx512(price, volume),
209            _ => unreachable!(),
210        }
211    }
212}
213
214#[inline]
215pub unsafe fn vpt_scalar(price: &[f64], volume: &[f64]) -> Result<VptOutput, VptError> {
216    let n = price.len();
217    if n == 0 || volume.len() != n {
218        return Err(VptError::EmptyInputData);
219    }
220    let valid_count = price
221        .iter()
222        .zip(volume.iter())
223        .filter(|(&p, &v)| !(p.is_nan() || v.is_nan()))
224        .count();
225    if valid_count == 0 {
226        return Err(VptError::AllValuesNaN);
227    }
228    if valid_count < 2 {
229        return Err(VptError::NotEnoughValidData {
230            needed: 2,
231            valid: valid_count,
232        });
233    }
234    let first = vpt_first_valid(price, volume).ok_or(VptError::NotEnoughValidData {
235        needed: 2,
236        valid: valid_count,
237    })?;
238    let mut res = alloc_with_nan_prefix(n, first + 1);
239
240    let p_ptr = price.as_ptr();
241    let v_ptr = volume.as_ptr();
242    let o_ptr = res.as_mut_ptr();
243
244    let mut prev = {
245        let p0 = *p_ptr.add(first - 1);
246        let p1 = *p_ptr.add(first);
247        let v1 = *v_ptr.add(first);
248        if (p0 != p0) || (p0 == 0.0) || (p1 != p1) || (v1 != v1) {
249            f64::NAN
250        } else {
251            v1 * ((p1 - p0) / p0)
252        }
253    };
254
255    let mut i = first + 1;
256    let mut p_prev = *p_ptr.add(i - 1);
257
258    while i + 3 < n {
259        let p1 = *p_ptr.add(i);
260        let v1 = *v_ptr.add(i);
261        let cur0 = if (p_prev != p_prev) || (p_prev == 0.0) || (p1 != p1) || (v1 != v1) {
262            f64::NAN
263        } else {
264            v1 * ((p1 - p_prev) / p_prev)
265        };
266        let val0 = cur0 + prev;
267        *o_ptr.add(i) = val0;
268        prev = val0;
269        p_prev = p1;
270
271        let j1 = i + 1;
272        let p2 = *p_ptr.add(j1);
273        let v2 = *v_ptr.add(j1);
274        let cur1 = if (p_prev != p_prev) || (p_prev == 0.0) || (p2 != p2) || (v2 != v2) {
275            f64::NAN
276        } else {
277            v2 * ((p2 - p_prev) / p_prev)
278        };
279        let val1 = cur1 + prev;
280        *o_ptr.add(j1) = val1;
281        prev = val1;
282        p_prev = p2;
283
284        let j2 = i + 2;
285        let p3 = *p_ptr.add(j2);
286        let v3 = *v_ptr.add(j2);
287        let cur2 = if (p_prev != p_prev) || (p_prev == 0.0) || (p3 != p3) || (v3 != v3) {
288            f64::NAN
289        } else {
290            v3 * ((p3 - p_prev) / p_prev)
291        };
292        let val2 = cur2 + prev;
293        *o_ptr.add(j2) = val2;
294        prev = val2;
295        p_prev = p3;
296
297        let j3 = i + 3;
298        let p4 = *p_ptr.add(j3);
299        let v4 = *v_ptr.add(j3);
300        let cur3 = if (p_prev != p_prev) || (p_prev == 0.0) || (p4 != p4) || (v4 != v4) {
301            f64::NAN
302        } else {
303            v4 * ((p4 - p_prev) / p_prev)
304        };
305        let val3 = cur3 + prev;
306        *o_ptr.add(j3) = val3;
307        prev = val3;
308        p_prev = p4;
309
310        i += 4;
311    }
312
313    while i < n {
314        let p1 = *p_ptr.add(i);
315        let v1 = *v_ptr.add(i);
316        let cur = if (p_prev != p_prev) || (p_prev == 0.0) || (p1 != p1) || (v1 != v1) {
317            f64::NAN
318        } else {
319            v1 * ((p1 - p_prev) / p_prev)
320        };
321        let val = cur + prev;
322        *o_ptr.add(i) = val;
323        prev = val;
324        p_prev = p1;
325        i += 1;
326    }
327
328    Ok(VptOutput { values: res })
329}
330
331#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
332#[inline]
333pub unsafe fn vpt_avx2(price: &[f64], volume: &[f64]) -> Result<VptOutput, VptError> {
334    use core::arch::x86_64::*;
335
336    let n = price.len();
337    if n == 0 || volume.len() != n {
338        return Err(VptError::EmptyInputData);
339    }
340    let valid_count = price
341        .iter()
342        .zip(volume.iter())
343        .filter(|(&p, &v)| !(p.is_nan() || v.is_nan()))
344        .count();
345    if valid_count == 0 {
346        return Err(VptError::AllValuesNaN);
347    }
348    if valid_count < 2 {
349        return Err(VptError::NotEnoughValidData {
350            needed: 2,
351            valid: valid_count,
352        });
353    }
354    let first = vpt_first_valid(price, volume).ok_or(VptError::NotEnoughValidData {
355        needed: 2,
356        valid: valid_count,
357    })?;
358    let mut out = alloc_with_nan_prefix(n, first + 1);
359
360    let p_ptr = price.as_ptr();
361    let v_ptr = volume.as_ptr();
362    let o_ptr = out.as_mut_ptr();
363
364    let mut prev = {
365        let p0 = *p_ptr.add(first - 1);
366        let p1 = *p_ptr.add(first);
367        let v1 = *v_ptr.add(first);
368        if (p0 != p0) || (p0 == 0.0) || (p1 != p1) || (v1 != v1) {
369            f64::NAN
370        } else {
371            v1 * ((p1 - p0) / p0)
372        }
373    };
374
375    let mut i = first + 1;
376    let vzero = _mm256_set1_pd(0.0);
377    let vnan = _mm256_set1_pd(f64::NAN);
378
379    #[inline(always)]
380    unsafe fn prefix4_pd(x: __m256d) -> __m256d {
381        let lo = _mm256_castpd256_pd128(x);
382        let hi = _mm256_extractf128_pd(x, 1);
383        let z = _mm_setzero_pd();
384
385        let tlo = _mm_add_pd(lo, _mm_shuffle_pd(z, lo, 0));
386        let thi = _mm_add_pd(hi, _mm_shuffle_pd(z, hi, 0));
387
388        let last_lo = _mm_unpackhi_pd(tlo, tlo);
389        let thi2 = _mm_add_pd(thi, last_lo);
390
391        _mm256_insertf128_pd(_mm256_castpd128_pd256(tlo), thi2, 1)
392    }
393
394    while i + 3 < n {
395        let p0 = _mm256_loadu_pd(p_ptr.add(i - 1));
396        let p1 = _mm256_loadu_pd(p_ptr.add(i));
397        let vv = _mm256_loadu_pd(v_ptr.add(i));
398
399        let m_nan_p0 = _mm256_cmp_pd(p0, p0, _CMP_UNORD_Q);
400        let m_nan_p1 = _mm256_cmp_pd(p1, p1, _CMP_UNORD_Q);
401        let m_nan_v = _mm256_cmp_pd(vv, vv, _CMP_UNORD_Q);
402        let m_eq0_p0 = _mm256_cmp_pd(p0, vzero, _CMP_EQ_OQ);
403        let invalid = _mm256_or_pd(
404            _mm256_or_pd(m_nan_p0, m_nan_p1),
405            _mm256_or_pd(m_nan_v, m_eq0_p0),
406        );
407
408        let diff = _mm256_sub_pd(p1, p0);
409        let div = _mm256_div_pd(diff, p0);
410        let mul = _mm256_mul_pd(vv, div);
411        let cur = _mm256_blendv_pd(mul, vnan, invalid);
412
413        let ps = prefix4_pd(cur);
414        let cary = _mm256_set1_pd(prev);
415        let outv = _mm256_add_pd(ps, cary);
416
417        _mm256_storeu_pd(o_ptr.add(i), outv);
418
419        let hi128 = _mm256_extractf128_pd(outv, 1);
420        let last_hi = _mm_unpackhi_pd(hi128, hi128);
421        let tmp: [f64; 2] = core::mem::transmute(last_hi);
422        prev = tmp[0];
423
424        i += 4;
425    }
426
427    if i < n {
428        let mut p_prev = *p_ptr.add(i - 1);
429        while i < n {
430            let p1 = *p_ptr.add(i);
431            let v1 = *v_ptr.add(i);
432            let cur = if (p_prev != p_prev) || (p_prev == 0.0) || (p1 != p1) || (v1 != v1) {
433                f64::NAN
434            } else {
435                v1 * ((p1 - p_prev) / p_prev)
436            };
437            let val = cur + prev;
438            *o_ptr.add(i) = val;
439            prev = val;
440            p_prev = p1;
441            i += 1;
442        }
443    }
444
445    Ok(VptOutput { values: out })
446}
447
448#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
449#[inline]
450pub unsafe fn vpt_avx512(price: &[f64], volume: &[f64]) -> Result<VptOutput, VptError> {
451    use core::arch::x86_64::*;
452
453    let n = price.len();
454    if n == 0 || volume.len() != n {
455        return Err(VptError::EmptyInputData);
456    }
457    let valid_count = price
458        .iter()
459        .zip(volume.iter())
460        .filter(|(&p, &v)| !(p.is_nan() || v.is_nan()))
461        .count();
462    if valid_count == 0 {
463        return Err(VptError::AllValuesNaN);
464    }
465    if valid_count < 2 {
466        return Err(VptError::NotEnoughValidData {
467            needed: 2,
468            valid: valid_count,
469        });
470    }
471    let first = vpt_first_valid(price, volume).ok_or(VptError::NotEnoughValidData {
472        needed: 2,
473        valid: valid_count,
474    })?;
475    let mut out = alloc_with_nan_prefix(n, first + 1);
476
477    let p_ptr = price.as_ptr();
478    let v_ptr = volume.as_ptr();
479    let o_ptr = out.as_mut_ptr();
480
481    let mut prev = {
482        let p0 = *p_ptr.add(first - 1);
483        let p1 = *p_ptr.add(first);
484        let v1 = *v_ptr.add(first);
485        if (p0 != p0) || (p0 == 0.0) || (p1 != p1) || (v1 != v1) {
486            f64::NAN
487        } else {
488            v1 * ((p1 - p0) / p0)
489        }
490    };
491
492    let mut i = first + 1;
493
494    #[inline(always)]
495    unsafe fn prefix4_pd(x: __m256d) -> __m256d {
496        use core::arch::x86_64::*;
497        let lo = _mm256_castpd256_pd128(x);
498        let hi = _mm256_extractf128_pd(x, 1);
499        let z = _mm_setzero_pd();
500        let tlo = _mm_add_pd(lo, _mm_shuffle_pd(z, lo, 0));
501        let thi = _mm_add_pd(hi, _mm_shuffle_pd(z, hi, 0));
502        let last_lo = _mm_unpackhi_pd(tlo, tlo);
503        let thi2 = _mm_add_pd(thi, last_lo);
504        _mm256_insertf128_pd(_mm256_castpd128_pd256(tlo), thi2, 1)
505    }
506
507    while i + 7 < n {
508        let p0 = _mm512_loadu_pd(p_ptr.add(i - 1));
509        let p1 = _mm512_loadu_pd(p_ptr.add(i));
510        let vv = _mm512_loadu_pd(v_ptr.add(i));
511
512        let m_nan_p0 = _mm512_cmp_pd_mask(p0, p0, _CMP_UNORD_Q);
513        let m_nan_p1 = _mm512_cmp_pd_mask(p1, p1, _CMP_UNORD_Q);
514        let m_nan_v = _mm512_cmp_pd_mask(vv, vv, _CMP_UNORD_Q);
515        let m_eq0_p0 = _mm512_cmp_pd_mask(p0, _mm512_set1_pd(0.0), _CMP_EQ_OQ);
516        let invalid = m_nan_p0 | m_nan_p1 | m_nan_v | m_eq0_p0;
517
518        let diff = _mm512_sub_pd(p1, p0);
519        let r0 = _mm512_rcp14_pd(p0);
520        let two = _mm512_set1_pd(2.0);
521        let e1 = _mm512_fnmadd_pd(p0, r0, two);
522        let r1 = _mm512_mul_pd(r0, e1);
523        let e2 = _mm512_fnmadd_pd(p0, r1, two);
524        let r2 = _mm512_mul_pd(r1, e2);
525        let div = _mm512_mul_pd(diff, r2);
526        let mul = _mm512_mul_pd(vv, div);
527        let cur = _mm512_mask_mov_pd(mul, invalid, _mm512_set1_pd(f64::NAN));
528
529        let lo256 = _mm512_castpd512_pd256(cur);
530        let hi256 = _mm512_extractf64x4_pd(cur, 1);
531        let lo_ps = prefix4_pd(lo256);
532        let mut hi_ps = prefix4_pd(hi256);
533
534        let lo_hi128 = _mm256_extractf128_pd(lo_ps, 1);
535        let lo_total = {
536            let last_lo = _mm_unpackhi_pd(lo_hi128, lo_hi128);
537            let tmp: [f64; 2] = core::mem::transmute(last_lo);
538            tmp[0]
539        };
540        hi_ps = _mm256_add_pd(hi_ps, _mm256_set1_pd(lo_total));
541
542        let ps512 = _mm512_insertf64x4(_mm512_castpd256_pd512(lo_ps), hi_ps, 1);
543
544        let outv = _mm512_add_pd(ps512, _mm512_set1_pd(prev));
545        _mm512_storeu_pd(o_ptr.add(i), outv);
546
547        let hi2 = _mm512_extractf64x4_pd(outv, 1);
548        let hi128 = _mm256_extractf128_pd(hi2, 1);
549        let last_hi = _mm_unpackhi_pd(hi128, hi128);
550        let tmp: [f64; 2] = core::mem::transmute(last_hi);
551        prev = tmp[0];
552
553        i += 8;
554    }
555
556    while i + 3 < n {
557        use core::arch::x86_64::*;
558        let p0 = _mm256_loadu_pd(p_ptr.add(i - 1));
559        let p1 = _mm256_loadu_pd(p_ptr.add(i));
560        let vv = _mm256_loadu_pd(v_ptr.add(i));
561        let vzero = _mm256_set1_pd(0.0);
562        let vnan = _mm256_set1_pd(f64::NAN);
563
564        let m_nan_p0 = _mm256_cmp_pd(p0, p0, _CMP_UNORD_Q);
565        let m_nan_p1 = _mm256_cmp_pd(p1, p1, _CMP_UNORD_Q);
566        let m_nan_v = _mm256_cmp_pd(vv, vv, _CMP_UNORD_Q);
567        let m_eq0_p0 = _mm256_cmp_pd(p0, vzero, _CMP_EQ_OQ);
568        let invalid = _mm256_or_pd(
569            _mm256_or_pd(m_nan_p0, m_nan_p1),
570            _mm256_or_pd(m_nan_v, m_eq0_p0),
571        );
572
573        let diff = _mm256_sub_pd(p1, p0);
574        let div = _mm256_div_pd(diff, p0);
575        let mul = _mm256_mul_pd(vv, div);
576        let cur = _mm256_blendv_pd(mul, vnan, invalid);
577
578        let ps = {
579            let lo = _mm256_castpd256_pd128(cur);
580            let hi = _mm256_extractf128_pd(cur, 1);
581            let z = _mm_setzero_pd();
582            let tlo = _mm_add_pd(lo, _mm_shuffle_pd(z, lo, 0));
583            let thi = _mm_add_pd(hi, _mm_shuffle_pd(z, hi, 0));
584            let last_lo = _mm_unpackhi_pd(tlo, tlo);
585            let thi2 = _mm_add_pd(thi, last_lo);
586            _mm256_insertf128_pd(_mm256_castpd128_pd256(tlo), thi2, 1)
587        };
588
589        let outv = _mm256_add_pd(ps, _mm256_set1_pd(prev));
590        _mm256_storeu_pd(o_ptr.add(i), outv);
591        let hi128 = _mm256_extractf128_pd(outv, 1);
592        let last_hi = _mm_unpackhi_pd(hi128, hi128);
593        let tmp: [f64; 2] = core::mem::transmute(last_hi);
594        prev = tmp[0];
595        i += 4;
596    }
597
598    if i < n {
599        let mut p_prev = *p_ptr.add(i - 1);
600        while i < n {
601            let p1 = *p_ptr.add(i);
602            let v1 = *v_ptr.add(i);
603            let cur = if (p_prev != p_prev) || (p_prev == 0.0) || (p1 != p1) || (v1 != v1) {
604                f64::NAN
605            } else {
606                v1 * ((p1 - p_prev) / p_prev)
607            };
608            let val = cur + prev;
609            *o_ptr.add(i) = val;
610            prev = val;
611            p_prev = p1;
612            i += 1;
613        }
614    }
615
616    Ok(VptOutput { values: out })
617}
618
619#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
620#[inline]
621pub unsafe fn vpt_avx512_short(price: &[f64], volume: &[f64]) -> Result<VptOutput, VptError> {
622    vpt_avx512(price, volume)
623}
624
625#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
626#[inline]
627pub unsafe fn vpt_avx512_long(price: &[f64], volume: &[f64]) -> Result<VptOutput, VptError> {
628    vpt_avx512(price, volume)
629}
630
631#[inline]
632pub fn vpt_indicator(input: &VptInput) -> Result<VptOutput, VptError> {
633    vpt(input)
634}
635
636#[inline]
637pub fn vpt_indicator_with_kernel(input: &VptInput, kernel: Kernel) -> Result<VptOutput, VptError> {
638    vpt_with_kernel(input, kernel)
639}
640
641#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
642#[inline]
643pub fn vpt_indicator_avx2(input: &VptInput) -> Result<VptOutput, VptError> {
644    unsafe {
645        let (price, volume) = match &input.data {
646            VptData::Candles { candles, source } => {
647                let price = source_type(candles, source);
648                let vol = candles.select_candle_field("volume").unwrap();
649                (price, vol)
650            }
651            VptData::Slices { price, volume } => (*price, *volume),
652        };
653        vpt_avx2(price, volume)
654    }
655}
656
657#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
658#[inline]
659pub fn vpt_indicator_avx512(input: &VptInput) -> Result<VptOutput, VptError> {
660    unsafe {
661        let (price, volume) = match &input.data {
662            VptData::Candles { candles, source } => {
663                let price = source_type(candles, source);
664                let vol = candles.select_candle_field("volume").unwrap();
665                (price, vol)
666            }
667            VptData::Slices { price, volume } => (*price, *volume),
668        };
669        vpt_avx512(price, volume)
670    }
671}
672
673#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
674#[inline]
675pub fn vpt_indicator_avx512_short(input: &VptInput) -> Result<VptOutput, VptError> {
676    unsafe {
677        let (price, volume) = match &input.data {
678            VptData::Candles { candles, source } => {
679                let price = source_type(candles, source);
680                let vol = candles.select_candle_field("volume").unwrap();
681                (price, vol)
682            }
683            VptData::Slices { price, volume } => (*price, *volume),
684        };
685        vpt_avx512_short(price, volume)
686    }
687}
688
689#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
690#[inline]
691pub fn vpt_indicator_avx512_long(input: &VptInput) -> Result<VptOutput, VptError> {
692    unsafe {
693        let (price, volume) = match &input.data {
694            VptData::Candles { candles, source } => {
695                let price = source_type(candles, source);
696                let vol = candles.select_candle_field("volume").unwrap();
697                (price, vol)
698            }
699            VptData::Slices { price, volume } => (*price, *volume),
700        };
701        vpt_avx512_long(price, volume)
702    }
703}
704
705#[inline]
706pub fn vpt_indicator_scalar(input: &VptInput) -> Result<VptOutput, VptError> {
707    unsafe {
708        let (price, volume) = match &input.data {
709            VptData::Candles { candles, source } => {
710                let price = source_type(candles, source);
711                let vol = candles.select_candle_field("volume").unwrap();
712                (price, vol)
713            }
714            VptData::Slices { price, volume } => (*price, *volume),
715        };
716        vpt_scalar(price, volume)
717    }
718}
719
720#[inline]
721pub fn vpt_expand_grid() -> Vec<VptParams> {
722    vec![VptParams::default()]
723}
724
725#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
726pub fn vpt_into(input: &VptInput, out: &mut [f64]) -> Result<(), VptError> {
727    let (price, volume) = match &input.data {
728        VptData::Candles { candles, source } => {
729            let price = source_type(candles, source);
730            let vol = candles
731                .select_candle_field("volume")
732                .map_err(|_| VptError::EmptyInputData)?;
733            (price, vol)
734        }
735        VptData::Slices { price, volume } => (*price, *volume),
736    };
737
738    vpt_into_slice(out, price, volume, Kernel::Auto)
739}
740
741pub fn vpt_into_slice(
742    dst: &mut [f64],
743    price: &[f64],
744    volume: &[f64],
745    kern: Kernel,
746) -> Result<(), VptError> {
747    if price.is_empty() || volume.is_empty() || price.len() != volume.len() {
748        return Err(VptError::EmptyInputData);
749    }
750
751    if dst.len() != price.len() {
752        return Err(VptError::OutputLengthMismatch {
753            expected: price.len(),
754            got: dst.len(),
755        });
756    }
757
758    let valid_count = price
759        .iter()
760        .zip(volume.iter())
761        .filter(|(&p, &v)| !(p.is_nan() || v.is_nan()))
762        .count();
763
764    if valid_count == 0 {
765        return Err(VptError::AllValuesNaN);
766    }
767    if valid_count < 2 {
768        return Err(VptError::NotEnoughValidData {
769            needed: 2,
770            valid: valid_count,
771        });
772    }
773
774    let first = vpt_first_valid(price, volume).ok_or(VptError::NotEnoughValidData {
775        needed: 2,
776        valid: valid_count,
777    })?;
778    unsafe {
779        match kern {
780            Kernel::Scalar | Kernel::ScalarBatch | Kernel::Auto => {
781                vpt_row_scalar_from(price, volume, first + 1, dst)
782            }
783            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
784            Kernel::Avx2 | Kernel::Avx2Batch => vpt_row_avx2_from(price, volume, first + 1, dst),
785            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
786            Kernel::Avx512 | Kernel::Avx512Batch => {
787                vpt_row_avx512_from(price, volume, first + 1, dst)
788            }
789            _ => vpt_row_scalar_from(price, volume, first + 1, dst),
790        }
791    }
792    for v in &mut dst[..=first] {
793        *v = f64::NAN;
794    }
795    Ok(())
796}
797
798pub fn vpt_batch_inner_into(
799    price: &[f64],
800    volume: &[f64],
801    _range: &VptBatchRange,
802    kern: Kernel,
803    _parallel: bool,
804    out: &mut [f64],
805) -> Result<Vec<VptParams>, VptError> {
806    if price.is_empty() || volume.is_empty() || price.len() != volume.len() {
807        return Err(VptError::EmptyInputData);
808    }
809    let combos = vec![VptParams::default()];
810    let cols = price.len();
811    if out.len() != cols {
812        return Err(VptError::OutputLengthMismatch {
813            expected: cols,
814            got: out.len(),
815        });
816    }
817
818    let valid_count = price
819        .iter()
820        .zip(volume.iter())
821        .filter(|(&p, &v)| !(p.is_nan() || v.is_nan()))
822        .count();
823    if valid_count == 0 {
824        return Err(VptError::AllValuesNaN);
825    }
826    if valid_count < 2 {
827        return Err(VptError::NotEnoughValidData {
828            needed: 2,
829            valid: valid_count,
830        });
831    }
832    let first = vpt_first_valid(price, volume).ok_or(VptError::NotEnoughValidData {
833        needed: 2,
834        valid: valid_count,
835    })?;
836
837    unsafe {
838        match kern {
839            Kernel::Scalar | Kernel::ScalarBatch | Kernel::Auto => {
840                vpt_row_scalar_from(price, volume, first + 1, out)
841            }
842            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
843            Kernel::Avx2 | Kernel::Avx2Batch => vpt_row_avx2_from(price, volume, first + 1, out),
844            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
845            Kernel::Avx512 | Kernel::Avx512Batch => {
846                vpt_row_avx512_from(price, volume, first + 1, out)
847            }
848            _ => vpt_row_scalar_from(price, volume, first + 1, out),
849        }
850    }
851    Ok(combos)
852}
853
854#[derive(Clone, Debug, Default)]
855pub struct VptStream {
856    last_price: f64,
857
858    carry_inc: f64,
859
860    cum: f64,
861
862    seeded: bool,
863
864    sticky_nan: bool,
865}
866
867impl VptStream {
868    #[inline(always)]
869    pub fn update(&mut self, price: f64, volume: f64) -> Option<f64> {
870        if !self.seeded {
871            self.last_price = price;
872            self.seeded = true;
873            self.carry_inc = f64::NAN;
874            self.cum = f64::NAN;
875            self.sticky_nan = false;
876            return None;
877        }
878
879        if self.sticky_nan {
880            self.last_price = price;
881            return Some(f64::NAN);
882        }
883
884        if !(self.last_price.is_finite()
885            && self.last_price != 0.0
886            && price.is_finite()
887            && volume.is_finite())
888        {
889            self.sticky_nan = true;
890            self.last_price = price;
891            self.carry_inc = f64::NAN;
892            self.cum = f64::NAN;
893            return Some(f64::NAN);
894        }
895
896        let inv = 1.0 / self.last_price;
897        let scale = volume * inv;
898        let dv = price - self.last_price;
899        self.last_price = price;
900
901        let cur_inc = dv.mul_add(scale, 0.0);
902
903        if self.carry_inc.is_nan() {
904            self.carry_inc = cur_inc;
905            return Some(f64::NAN);
906        }
907
908        let base = if self.cum.is_finite() {
909            self.cum
910        } else {
911            self.carry_inc
912        };
913        let new_cum = base + cur_inc;
914
915        self.carry_inc = cur_inc;
916        self.cum = new_cum;
917        Some(new_cum)
918    }
919
920    #[inline(always)]
921    pub fn reset(&mut self) {
922        *self = Self::default();
923    }
924
925    #[inline(always)]
926    pub fn restart_from(&mut self, price: f64) {
927        self.last_price = price;
928        self.carry_inc = f64::NAN;
929        self.cum = f64::NAN;
930        self.seeded = true;
931        self.sticky_nan = false;
932    }
933}
934
935#[derive(Clone, Debug, Default)]
936pub struct VptBatchRange;
937
938#[derive(Clone, Debug, Default)]
939pub struct VptBatchBuilder {
940    kernel: Kernel,
941}
942
943impl VptBatchBuilder {
944    pub fn new() -> Self {
945        Self {
946            kernel: Kernel::Auto,
947        }
948    }
949
950    pub fn kernel(mut self, k: Kernel) -> Self {
951        self.kernel = k;
952        self
953    }
954
955    pub fn apply_slices(self, price: &[f64], volume: &[f64]) -> Result<VptBatchOutput, VptError> {
956        vpt_batch_with_kernel(price, volume, self.kernel)
957    }
958
959    pub fn with_default_slices(
960        price: &[f64],
961        volume: &[f64],
962        k: Kernel,
963    ) -> Result<VptBatchOutput, VptError> {
964        VptBatchBuilder::new().kernel(k).apply_slices(price, volume)
965    }
966
967    pub fn apply_candles(self, c: &Candles, src: &str) -> Result<VptBatchOutput, VptError> {
968        let price = source_type(c, src);
969        let volume = c
970            .select_candle_field("volume")
971            .map_err(|_| VptError::EmptyInputData)?;
972        self.apply_slices(price, volume)
973    }
974
975    pub fn with_default_candles(c: &Candles) -> Result<VptBatchOutput, VptError> {
976        VptBatchBuilder::new()
977            .kernel(Kernel::Auto)
978            .apply_candles(c, "close")
979    }
980}
981
982pub fn vpt_batch_with_kernel(
983    price: &[f64],
984    volume: &[f64],
985    k: Kernel,
986) -> Result<VptBatchOutput, VptError> {
987    let kernel = match k {
988        Kernel::Auto => detect_best_batch_kernel(),
989        other if other.is_batch() => other,
990        other => return Err(VptError::InvalidKernelForBatch(other)),
991    };
992    vpt_batch_par_slice(price, volume, kernel)
993}
994
995#[derive(Clone, Debug)]
996pub struct VptBatchOutput {
997    pub values: Vec<f64>,
998    pub combos: Vec<VptParams>,
999    pub rows: usize,
1000    pub cols: usize,
1001}
1002
1003impl VptBatchOutput {
1004    pub fn row_for_params(&self, _p: &VptParams) -> Option<usize> {
1005        Some(0)
1006    }
1007
1008    pub fn values_for(&self, _p: &VptParams) -> Option<&[f64]> {
1009        Some(&self.values[..])
1010    }
1011}
1012
1013#[inline(always)]
1014pub fn vpt_batch_slice(
1015    price: &[f64],
1016    volume: &[f64],
1017    kern: Kernel,
1018) -> Result<VptBatchOutput, VptError> {
1019    vpt_batch_inner(price, volume, kern, false)
1020}
1021
1022#[inline(always)]
1023pub fn vpt_batch_par_slice(
1024    price: &[f64],
1025    volume: &[f64],
1026    kern: Kernel,
1027) -> Result<VptBatchOutput, VptError> {
1028    vpt_batch_inner(price, volume, kern, true)
1029}
1030
1031#[inline(always)]
1032fn vpt_batch_inner(
1033    price: &[f64],
1034    volume: &[f64],
1035    kern: Kernel,
1036    _parallel: bool,
1037) -> Result<VptBatchOutput, VptError> {
1038    if price.is_empty() || volume.is_empty() || price.len() != volume.len() {
1039        return Err(VptError::EmptyInputData);
1040    }
1041
1042    let combos = vpt_expand_grid();
1043    let rows = 1usize;
1044    let cols = price.len();
1045
1046    let mut buf_mu = make_uninit_matrix(rows, cols);
1047
1048    let valid_count = price
1049        .iter()
1050        .zip(volume.iter())
1051        .filter(|(&p, &v)| !(p.is_nan() || v.is_nan()))
1052        .count();
1053    if valid_count == 0 {
1054        return Err(VptError::AllValuesNaN);
1055    }
1056    if valid_count < 2 {
1057        return Err(VptError::NotEnoughValidData {
1058            needed: 2,
1059            valid: valid_count,
1060        });
1061    }
1062    let first_valid = vpt_first_valid(price, volume).ok_or(VptError::NotEnoughValidData {
1063        needed: 2,
1064        valid: valid_count,
1065    })?;
1066    let warm = vec![first_valid + 1];
1067    init_matrix_prefixes(&mut buf_mu, cols, &warm);
1068
1069    let mut guard = core::mem::ManuallyDrop::new(buf_mu);
1070    let out: &mut [f64] =
1071        unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
1072
1073    vpt_batch_inner_into(price, volume, &VptBatchRange, kern, _parallel, out)?;
1074
1075    let values = unsafe {
1076        Vec::from_raw_parts(
1077            guard.as_mut_ptr() as *mut f64,
1078            guard.len(),
1079            guard.capacity(),
1080        )
1081    };
1082
1083    Ok(VptBatchOutput {
1084        values,
1085        combos,
1086        rows,
1087        cols,
1088    })
1089}
1090
1091#[inline(always)]
1092pub unsafe fn vpt_row_scalar(price: &[f64], volume: &[f64], out: &mut [f64]) {
1093    let n = price.len();
1094    if let Some(first) = vpt_first_valid(price, volume) {
1095        for i in 0..=first {
1096            out[i] = f64::NAN;
1097        }
1098
1099        vpt_row_scalar_from(price, volume, first + 1, out);
1100    } else {
1101        for i in 0..n {
1102            out[i] = f64::NAN;
1103        }
1104    }
1105}
1106
1107#[inline(always)]
1108pub unsafe fn vpt_row_scalar_from(price: &[f64], volume: &[f64], start_i: usize, out: &mut [f64]) {
1109    let n = price.len();
1110    if start_i >= n {
1111        return;
1112    }
1113
1114    assert!(start_i > 0, "vpt_row_scalar_from requires start_i >= 1");
1115
1116    let p_ptr = price.as_ptr();
1117    let v_ptr = volume.as_ptr();
1118    let o_ptr = out.as_mut_ptr();
1119
1120    let mut prev = if start_i >= 2 {
1121        let k = start_i - 1;
1122        let p0 = *p_ptr.add(k - 1);
1123        let p1 = *p_ptr.add(k);
1124        let v1 = *v_ptr.add(k);
1125        if (p0 != p0) || (p0 == 0.0) || (p1 != p1) || (v1 != v1) {
1126            f64::NAN
1127        } else {
1128            v1 * ((p1 - p0) / p0)
1129        }
1130    } else {
1131        0.0
1132    };
1133
1134    let mut i = start_i;
1135    let mut p_prev = *p_ptr.add(i - 1);
1136
1137    while i + 3 < n {
1138        let p1 = *p_ptr.add(i);
1139        let v1 = *v_ptr.add(i);
1140        let cur0 = if (p_prev != p_prev) || (p_prev == 0.0) || (p1 != p1) || (v1 != v1) {
1141            f64::NAN
1142        } else {
1143            v1 * ((p1 - p_prev) / p_prev)
1144        };
1145        let val0 = cur0 + prev;
1146        *o_ptr.add(i) = val0;
1147        prev = val0;
1148        p_prev = p1;
1149
1150        let j1 = i + 1;
1151        let p2 = *p_ptr.add(j1);
1152        let v2 = *v_ptr.add(j1);
1153        let cur1 = if (p_prev != p_prev) || (p_prev == 0.0) || (p2 != p2) || (v2 != v2) {
1154            f64::NAN
1155        } else {
1156            v2 * ((p2 - p_prev) / p_prev)
1157        };
1158        let val1 = cur1 + prev;
1159        *o_ptr.add(j1) = val1;
1160        prev = val1;
1161        p_prev = p2;
1162
1163        let j2 = i + 2;
1164        let p3 = *p_ptr.add(j2);
1165        let v3 = *v_ptr.add(j2);
1166        let cur2 = if (p_prev != p_prev) || (p_prev == 0.0) || (p3 != p3) || (v3 != v3) {
1167            f64::NAN
1168        } else {
1169            v3 * ((p3 - p_prev) / p_prev)
1170        };
1171        let val2 = cur2 + prev;
1172        *o_ptr.add(j2) = val2;
1173        prev = val2;
1174        p_prev = p3;
1175
1176        let j3 = i + 3;
1177        let p4 = *p_ptr.add(j3);
1178        let v4 = *v_ptr.add(j3);
1179        let cur3 = if (p_prev != p_prev) || (p_prev == 0.0) || (p4 != p4) || (v4 != v4) {
1180            f64::NAN
1181        } else {
1182            v4 * ((p4 - p_prev) / p_prev)
1183        };
1184        let val3 = cur3 + prev;
1185        *o_ptr.add(j3) = val3;
1186        prev = val3;
1187        p_prev = p4;
1188
1189        i += 4;
1190    }
1191
1192    while i < n {
1193        let p1 = *p_ptr.add(i);
1194        let v1 = *v_ptr.add(i);
1195        let cur = if (p_prev != p_prev) || (p_prev == 0.0) || (p1 != p1) || (v1 != v1) {
1196            f64::NAN
1197        } else {
1198            v1 * ((p1 - p_prev) / p_prev)
1199        };
1200        let val = cur + prev;
1201        *o_ptr.add(i) = val;
1202        prev = val;
1203        p_prev = p1;
1204        i += 1;
1205    }
1206}
1207
1208#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1209#[inline(always)]
1210pub unsafe fn vpt_row_avx2(price: &[f64], volume: &[f64], out: &mut [f64]) {
1211    vpt_row_scalar(price, volume, out)
1212}
1213
1214#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1215#[inline(always)]
1216pub unsafe fn vpt_row_avx2_from(price: &[f64], volume: &[f64], start_i: usize, out: &mut [f64]) {
1217    vpt_row_scalar_from(price, volume, start_i, out)
1218}
1219
1220#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1221#[inline(always)]
1222pub unsafe fn vpt_row_avx512(price: &[f64], volume: &[f64], out: &mut [f64]) {
1223    vpt_row_scalar(price, volume, out)
1224}
1225
1226#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1227#[inline(always)]
1228pub unsafe fn vpt_row_avx512_from(price: &[f64], volume: &[f64], start_i: usize, out: &mut [f64]) {
1229    vpt_row_scalar_from(price, volume, start_i, out)
1230}
1231
1232#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1233#[inline(always)]
1234pub unsafe fn vpt_row_avx512_short(price: &[f64], volume: &[f64], out: &mut [f64]) {
1235    vpt_row_scalar(price, volume, out)
1236}
1237
1238#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1239#[inline(always)]
1240pub unsafe fn vpt_row_avx512_long(price: &[f64], volume: &[f64], out: &mut [f64]) {
1241    vpt_row_scalar(price, volume, out)
1242}
1243
1244#[cfg(feature = "python")]
1245#[pyfunction(name = "vpt")]
1246#[pyo3(signature = (price, volume, kernel=None))]
1247pub fn vpt_py<'py>(
1248    py: Python<'py>,
1249    price: PyReadonlyArray1<'py, f64>,
1250    volume: PyReadonlyArray1<'py, f64>,
1251    kernel: Option<&str>,
1252) -> PyResult<Bound<'py, PyArray1<f64>>> {
1253    let price_slice: &[f64];
1254    let volume_slice: &[f64];
1255    let owned_price;
1256    let owned_volume;
1257    price_slice = if let Ok(s) = price.as_slice() {
1258        s
1259    } else {
1260        owned_price = price.to_owned_array();
1261        owned_price.as_slice().unwrap()
1262    };
1263    volume_slice = if let Ok(s) = volume.as_slice() {
1264        s
1265    } else {
1266        owned_volume = volume.to_owned_array();
1267        owned_volume.as_slice().unwrap()
1268    };
1269    let kern = validate_kernel(kernel, false)?;
1270
1271    let input = VptInput::from_slices(price_slice, volume_slice);
1272
1273    let result_vec: Vec<f64> = py
1274        .allow_threads(|| vpt_with_kernel(&input, kern).map(|o| o.values))
1275        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1276
1277    Ok(result_vec.into_pyarray(py))
1278}
1279
1280#[cfg(feature = "python")]
1281#[pyclass(name = "VptStream")]
1282pub struct VptStreamPy {
1283    stream: VptStream,
1284}
1285
1286#[cfg(feature = "python")]
1287#[pymethods]
1288impl VptStreamPy {
1289    #[new]
1290    fn new() -> PyResult<Self> {
1291        Ok(VptStreamPy {
1292            stream: VptStream::default(),
1293        })
1294    }
1295
1296    fn update(&mut self, price: f64, volume: f64) -> Option<f64> {
1297        self.stream.update(price, volume)
1298    }
1299}
1300
1301#[cfg(feature = "python")]
1302#[pyfunction(name = "vpt_batch")]
1303#[pyo3(signature = (price, volume, kernel=None))]
1304pub fn vpt_batch_py<'py>(
1305    py: Python<'py>,
1306    price: PyReadonlyArray1<'py, f64>,
1307    volume: PyReadonlyArray1<'py, f64>,
1308    kernel: Option<&str>,
1309) -> PyResult<Bound<'py, PyDict>> {
1310    let price_slice: &[f64];
1311    let volume_slice: &[f64];
1312    let owned_price;
1313    let owned_volume;
1314    price_slice = if let Ok(s) = price.as_slice() {
1315        s
1316    } else {
1317        owned_price = price.to_owned_array();
1318        owned_price.as_slice().unwrap()
1319    };
1320    volume_slice = if let Ok(s) = volume.as_slice() {
1321        s
1322    } else {
1323        owned_volume = volume.to_owned_array();
1324        owned_volume.as_slice().unwrap()
1325    };
1326    let kern = validate_kernel(kernel, true)?;
1327
1328    if price_slice.is_empty() || volume_slice.is_empty() || price_slice.len() != volume_slice.len()
1329    {
1330        return Err(PyValueError::new_err(VptError::EmptyInputData.to_string()));
1331    }
1332
1333    let rows: usize = 1;
1334    let cols = price_slice.len();
1335
1336    let total = rows
1337        .checked_mul(cols)
1338        .ok_or_else(|| PyValueError::new_err("vpt_batch: size overflow"))?;
1339    let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1340    let slice_out = unsafe { out_arr.as_slice_mut()? };
1341
1342    let _combos = py
1343        .allow_threads(|| {
1344            let kernel = match kern {
1345                Kernel::Auto => detect_best_batch_kernel(),
1346                k => k,
1347            };
1348            let combos = vpt_batch_inner_into(
1349                price_slice,
1350                volume_slice,
1351                &VptBatchRange,
1352                kernel,
1353                true,
1354                slice_out,
1355            )?;
1356            let first_valid =
1357                vpt_first_valid(price_slice, volume_slice).ok_or(VptError::NotEnoughValidData {
1358                    needed: 2,
1359                    valid: 0,
1360                })?;
1361            for v in &mut slice_out[..=first_valid] {
1362                *v = f64::NAN;
1363            }
1364            Ok::<_, VptError>(combos)
1365        })
1366        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1367
1368    let dict = PyDict::new(py);
1369    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1370
1371    dict.set_item("params", Vec::<f64>::new().into_pyarray(py))?;
1372
1373    Ok(dict)
1374}
1375
1376#[cfg(all(feature = "python", feature = "cuda"))]
1377use crate::cuda::cuda_available;
1378#[cfg(all(feature = "python", feature = "cuda"))]
1379use crate::cuda::CudaVpt;
1380#[cfg(all(feature = "python", feature = "cuda"))]
1381use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
1382#[cfg(all(feature = "python", feature = "cuda"))]
1383use cust::context::Context;
1384#[cfg(all(feature = "python", feature = "cuda"))]
1385use cust::memory::DeviceBuffer;
1386#[cfg(all(feature = "python", feature = "cuda"))]
1387use std::sync::Arc;
1388
1389#[cfg(all(feature = "python", feature = "cuda"))]
1390#[pyfunction(name = "vpt_cuda_batch_dev")]
1391#[pyo3(signature = (price, volume, device_id=0))]
1392pub fn vpt_cuda_batch_dev_py(
1393    py: Python<'_>,
1394    price: PyReadonlyArray1<'_, f32>,
1395    volume: PyReadonlyArray1<'_, f32>,
1396    device_id: usize,
1397) -> PyResult<VptDeviceArrayF32Py> {
1398    if !cuda_available() {
1399        return Err(PyValueError::new_err("CUDA not available"));
1400    }
1401    let price_slice = price.as_slice()?;
1402    let volume_slice = volume.as_slice()?;
1403    if price_slice.len() != volume_slice.len() {
1404        return Err(PyValueError::new_err("length mismatch"));
1405    }
1406    let (inner, ctx, dev_id) = py.allow_threads(|| {
1407        let cuda = CudaVpt::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1408        let ctx = cuda.context();
1409        let dev_id = cuda.device_id();
1410        let arr = cuda
1411            .vpt_batch_dev(price_slice, volume_slice)
1412            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1413        Ok::<_, pyo3::PyErr>((arr, ctx, dev_id))
1414    })?;
1415    Ok(VptDeviceArrayF32Py {
1416        buf: Some(inner.buf),
1417        rows: inner.rows,
1418        cols: inner.cols,
1419        _ctx: ctx,
1420        device_id: dev_id,
1421    })
1422}
1423
1424#[cfg(all(feature = "python", feature = "cuda"))]
1425#[pyfunction(name = "vpt_cuda_many_series_one_param_dev")]
1426#[pyo3(signature = (price_tm, volume_tm, cols, rows, device_id=0))]
1427pub fn vpt_cuda_many_series_one_param_dev_py(
1428    py: Python<'_>,
1429    price_tm: PyReadonlyArray1<'_, f32>,
1430    volume_tm: PyReadonlyArray1<'_, f32>,
1431    cols: usize,
1432    rows: usize,
1433    device_id: usize,
1434) -> PyResult<VptDeviceArrayF32Py> {
1435    if !cuda_available() {
1436        return Err(PyValueError::new_err("CUDA not available"));
1437    }
1438    let price_slice = price_tm.as_slice()?;
1439    let volume_slice = volume_tm.as_slice()?;
1440    let (inner, ctx, dev_id) = py.allow_threads(|| {
1441        let cuda = CudaVpt::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1442        let ctx = cuda.context();
1443        let dev_id = cuda.device_id();
1444        let arr = cuda
1445            .vpt_many_series_one_param_time_major_dev(price_slice, volume_slice, cols, rows)
1446            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1447        Ok::<_, pyo3::PyErr>((arr, ctx, dev_id))
1448    })?;
1449    Ok(VptDeviceArrayF32Py {
1450        buf: Some(inner.buf),
1451        rows: inner.rows,
1452        cols: inner.cols,
1453        _ctx: ctx,
1454        device_id: dev_id,
1455    })
1456}
1457
1458#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1459#[wasm_bindgen]
1460pub fn vpt_js(price: &[f64], volume: &[f64]) -> Result<Vec<f64>, JsValue> {
1461    let mut output = vec![0.0; price.len()];
1462
1463    vpt_into_slice(&mut output, price, volume, Kernel::Auto)
1464        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1465
1466    Ok(output)
1467}
1468
1469#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1470#[wasm_bindgen]
1471pub fn vpt_alloc(len: usize) -> *mut f64 {
1472    let mut vec = Vec::<f64>::with_capacity(len);
1473    let ptr = vec.as_mut_ptr();
1474    std::mem::forget(vec);
1475    ptr
1476}
1477
1478#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1479#[wasm_bindgen]
1480pub fn vpt_free(ptr: *mut f64, len: usize) {
1481    if !ptr.is_null() {
1482        unsafe {
1483            let _ = Vec::from_raw_parts(ptr, len, len);
1484        }
1485    }
1486}
1487
1488#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1489#[wasm_bindgen]
1490pub fn vpt_into(
1491    price_ptr: *const f64,
1492    volume_ptr: *const f64,
1493    out_ptr: *mut f64,
1494    len: usize,
1495) -> Result<(), JsValue> {
1496    if price_ptr.is_null() || volume_ptr.is_null() || out_ptr.is_null() {
1497        return Err(JsValue::from_str("Null pointer provided"));
1498    }
1499
1500    unsafe {
1501        let price = std::slice::from_raw_parts(price_ptr, len);
1502        let volume = std::slice::from_raw_parts(volume_ptr, len);
1503
1504        if price_ptr == out_ptr || volume_ptr == out_ptr {
1505            let mut temp = vec![0.0; len];
1506            vpt_into_slice(&mut temp, price, volume, Kernel::Auto)
1507                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1508            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1509            out.copy_from_slice(&temp);
1510        } else {
1511            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1512            vpt_into_slice(out, price, volume, Kernel::Auto)
1513                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1514        }
1515
1516        Ok(())
1517    }
1518}
1519
1520#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1521#[derive(Serialize, Deserialize)]
1522pub struct VptBatchConfig {}
1523
1524#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1525#[derive(Serialize, Deserialize)]
1526pub struct VptBatchJsOutput {
1527    pub values: Vec<f64>,
1528    pub combos: Vec<VptParams>,
1529    pub rows: usize,
1530    pub cols: usize,
1531}
1532
1533#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1534#[wasm_bindgen(js_name = vpt_batch)]
1535pub fn vpt_batch_js(price: &[f64], volume: &[f64], _config: JsValue) -> Result<JsValue, JsValue> {
1536    let output = vpt_batch_with_kernel(price, volume, Kernel::Auto)
1537        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1538
1539    let js_output = VptBatchJsOutput {
1540        values: output.values,
1541        combos: output.combos,
1542        rows: output.rows,
1543        cols: output.cols,
1544    };
1545
1546    serde_wasm_bindgen::to_value(&js_output)
1547        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1548}
1549
1550#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1551#[wasm_bindgen]
1552pub fn vpt_batch_into(
1553    price_ptr: *const f64,
1554    volume_ptr: *const f64,
1555    out_ptr: *mut f64,
1556    len: usize,
1557) -> Result<usize, JsValue> {
1558    if price_ptr.is_null() || volume_ptr.is_null() || out_ptr.is_null() {
1559        return Err(JsValue::from_str("Null pointer provided"));
1560    }
1561
1562    unsafe {
1563        let price = std::slice::from_raw_parts(price_ptr, len);
1564        let volume = std::slice::from_raw_parts(volume_ptr, len);
1565
1566        if price_ptr == out_ptr || volume_ptr == out_ptr {
1567            let mut temp = vec![0.0; len];
1568            vpt_into_slice(&mut temp, price, volume, Kernel::Auto)
1569                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1570            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1571            out.copy_from_slice(&temp);
1572        } else {
1573            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1574            vpt_into_slice(out, price, volume, Kernel::Auto)
1575                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1576        }
1577
1578        Ok(1)
1579    }
1580}
1581
1582#[cfg(all(feature = "python", feature = "cuda"))]
1583#[pyclass(module = "vector_ta", name = "VptDeviceArrayF32", unsendable)]
1584pub struct VptDeviceArrayF32Py {
1585    pub(crate) buf: Option<DeviceBuffer<f32>>,
1586    pub(crate) rows: usize,
1587    pub(crate) cols: usize,
1588    pub(crate) _ctx: Arc<Context>,
1589    pub(crate) device_id: u32,
1590}
1591
1592#[cfg(all(feature = "python", feature = "cuda"))]
1593#[pymethods]
1594impl VptDeviceArrayF32Py {
1595    #[getter]
1596    fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
1597        let d = PyDict::new(py);
1598        d.set_item("shape", (self.rows, self.cols))?;
1599        d.set_item("typestr", "<f4")?;
1600        d.set_item(
1601            "strides",
1602            (
1603                self.cols * std::mem::size_of::<f32>(),
1604                std::mem::size_of::<f32>(),
1605            ),
1606        )?;
1607        let ptr = self
1608            .buf
1609            .as_ref()
1610            .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?
1611            .as_device_ptr()
1612            .as_raw() as usize;
1613        d.set_item("data", (ptr, false))?;
1614
1615        d.set_item("version", 3)?;
1616        Ok(d)
1617    }
1618
1619    fn __dlpack_device__(&self) -> (i32, i32) {
1620        (2, self.device_id as i32)
1621    }
1622
1623    #[pyo3(signature=(stream=None, max_version=None, dl_device=None, copy=None))]
1624    fn __dlpack__<'py>(
1625        &mut self,
1626        py: Python<'py>,
1627        stream: Option<pyo3::PyObject>,
1628        max_version: Option<pyo3::PyObject>,
1629        dl_device: Option<pyo3::PyObject>,
1630        copy: Option<pyo3::PyObject>,
1631    ) -> PyResult<pyo3::PyObject> {
1632        let (kdl, alloc_dev) = self.__dlpack_device__();
1633        if let Some(dev_obj) = dl_device.as_ref() {
1634            if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
1635                if dev_ty != kdl || dev_id != alloc_dev {
1636                    let wants_copy = copy
1637                        .as_ref()
1638                        .and_then(|c| c.extract::<bool>(py).ok())
1639                        .unwrap_or(false);
1640                    if wants_copy {
1641                        return Err(PyValueError::new_err(
1642                            "device copy not implemented for __dlpack__",
1643                        ));
1644                    } else {
1645                        return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
1646                    }
1647                }
1648            }
1649        }
1650        let _ = stream;
1651
1652        let buf = self
1653            .buf
1654            .take()
1655            .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;
1656
1657        let rows = self.rows;
1658        let cols = self.cols;
1659
1660        let max_version_bound = max_version.map(|obj| obj.into_bound(py));
1661
1662        export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
1663    }
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668    use super::*;
1669    use crate::skip_if_unsupported;
1670    use crate::utilities::data_loader::read_candles_from_csv;
1671    #[cfg(feature = "proptest")]
1672    use proptest::prelude::*;
1673
1674    #[test]
1675    fn test_vpt_into_matches_api() -> Result<(), Box<dyn Error>> {
1676        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1677        let candles = read_candles_from_csv(file_path)?;
1678        let input = VptInput::from_candles(&candles, "close");
1679
1680        let baseline = vpt_with_kernel(&input, Kernel::Scalar)?;
1681
1682        let mut out = vec![0.0f64; candles.close.len()];
1683        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1684        vpt_into(&input, &mut out)?;
1685
1686        assert_eq!(baseline.values.len(), out.len());
1687
1688        fn eq_or_both_nan_eps(a: f64, b: f64, eps: f64) -> bool {
1689            (a.is_nan() && b.is_nan()) || (a - b).abs() <= eps
1690        }
1691
1692        for i in 0..out.len() {
1693            assert!(
1694                eq_or_both_nan_eps(baseline.values[i], out[i], 1e-12),
1695                "Mismatch at index {}: baseline={} out={}",
1696                i,
1697                baseline.values[i],
1698                out[i]
1699            );
1700        }
1701
1702        Ok(())
1703    }
1704
1705    fn check_vpt_basic_candles(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1706        skip_if_unsupported!(kernel, test_name);
1707        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1708        let candles = read_candles_from_csv(file_path)?;
1709        let input = VptInput::from_candles(&candles, "close");
1710        let output = vpt_with_kernel(&input, kernel)?;
1711        assert_eq!(output.values.len(), candles.close.len());
1712        Ok(())
1713    }
1714
1715    fn check_vpt_basic_slices(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1716        skip_if_unsupported!(kernel, test_name);
1717        let price = [1.0, 1.1, 1.05, 1.2, 1.3];
1718        let volume = [1000.0, 1100.0, 1200.0, 1300.0, 1400.0];
1719        let input = VptInput::from_slices(&price, &volume);
1720        let output = vpt_with_kernel(&input, kernel)?;
1721        assert_eq!(output.values.len(), price.len());
1722        Ok(())
1723    }
1724
1725    fn check_vpt_not_enough_data(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1726        skip_if_unsupported!(kernel, test_name);
1727        let price = [100.0];
1728        let volume = [500.0];
1729        let input = VptInput::from_slices(&price, &volume);
1730        let result = vpt_with_kernel(&input, kernel);
1731        assert!(result.is_err());
1732        Ok(())
1733    }
1734
1735    fn check_vpt_empty_data(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1736        skip_if_unsupported!(kernel, test_name);
1737        let price: [f64; 0] = [];
1738        let volume: [f64; 0] = [];
1739        let input = VptInput::from_slices(&price, &volume);
1740        let result = vpt_with_kernel(&input, kernel);
1741        assert!(result.is_err());
1742        Ok(())
1743    }
1744
1745    fn check_vpt_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1746        skip_if_unsupported!(kernel, test_name);
1747        let price = [f64::NAN, f64::NAN, f64::NAN];
1748        let volume = [f64::NAN, f64::NAN, f64::NAN];
1749        let input = VptInput::from_slices(&price, &volume);
1750        let result = vpt_with_kernel(&input, kernel);
1751        assert!(result.is_err());
1752        Ok(())
1753    }
1754
1755    fn check_vpt_accuracy_from_csv(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1756        skip_if_unsupported!(kernel, test_name);
1757        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1758        let candles = read_candles_from_csv(file_path)?;
1759        let input = VptInput::from_candles(&candles, "close");
1760        let output = vpt_with_kernel(&input, kernel)?;
1761
1762        let expected_last_five = [
1763            -18292.323972247592,
1764            -18292.510374716476,
1765            -18292.803266539282,
1766            -18292.62919783763,
1767            -18296.152568643138,
1768        ];
1769
1770        assert!(output.values.len() >= 5);
1771        let start_index = output.values.len() - 5;
1772        for (i, &value) in output.values[start_index..].iter().enumerate() {
1773            let expected_value = expected_last_five[i];
1774            assert!(
1775                (value - expected_value).abs() < 1e-9,
1776                "VPT mismatch at final bars, index {}: expected {}, got {}",
1777                i,
1778                expected_value,
1779                value
1780            );
1781        }
1782        Ok(())
1783    }
1784
1785    macro_rules! generate_all_vpt_tests {
1786        ($($test_fn:ident),*) => {
1787            paste::paste! {
1788                $(
1789                    #[test]
1790                    fn [<$test_fn _scalar_f64>]() {
1791                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1792                    }
1793                )*
1794                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1795                $(
1796                    #[test]
1797                    fn [<$test_fn _avx2_f64>]() {
1798                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1799                    }
1800                    #[test]
1801                    fn [<$test_fn _avx512_f64>]() {
1802                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1803                    }
1804                )*
1805            }
1806        }
1807    }
1808
1809    #[cfg(debug_assertions)]
1810    fn check_vpt_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1811        skip_if_unsupported!(kernel, test_name);
1812
1813        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1814        let candles = read_candles_from_csv(file_path)?;
1815
1816        let test_sources = vec!["close", "open", "high", "low"];
1817
1818        for (source_idx, &source) in test_sources.iter().enumerate() {
1819            let input = VptInput::from_candles(&candles, source);
1820            let output = vpt_with_kernel(&input, kernel)?;
1821
1822            for (i, &val) in output.values.iter().enumerate() {
1823                if val.is_nan() {
1824                    continue;
1825                }
1826
1827                let bits = val.to_bits();
1828
1829                if bits == 0x11111111_11111111 {
1830                    panic!(
1831                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1832						 with source: {} (source set {})",
1833                        test_name, val, bits, i, source, source_idx
1834                    );
1835                }
1836
1837                if bits == 0x22222222_22222222 {
1838                    panic!(
1839                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1840						 with source: {} (source set {})",
1841                        test_name, val, bits, i, source, source_idx
1842                    );
1843                }
1844
1845                if bits == 0x33333333_33333333 {
1846                    panic!(
1847                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1848						 with source: {} (source set {})",
1849                        test_name, val, bits, i, source, source_idx
1850                    );
1851                }
1852            }
1853        }
1854
1855        Ok(())
1856    }
1857
1858    #[cfg(not(debug_assertions))]
1859    fn check_vpt_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1860        Ok(())
1861    }
1862
1863    #[cfg(feature = "proptest")]
1864    #[allow(clippy::float_cmp)]
1865    fn check_vpt_property(
1866        test_name: &str,
1867        kernel: Kernel,
1868    ) -> Result<(), Box<dyn std::error::Error>> {
1869        use proptest::prelude::*;
1870        skip_if_unsupported!(kernel, test_name);
1871
1872        let strat = (2usize..=400).prop_flat_map(|len| {
1873            (
1874                prop::collection::vec(
1875                    (0.0f64..1e6f64)
1876                        .prop_filter("finite non-negative price", |x| x.is_finite() && *x >= 0.0),
1877                    len,
1878                ),
1879                prop::collection::vec(
1880                    (0.0f64..1e9f64)
1881                        .prop_filter("finite non-negative volume", |x| x.is_finite() && *x >= 0.0),
1882                    len,
1883                ),
1884            )
1885        });
1886
1887        proptest::test_runner::TestRunner::default().run(&strat, |(price, volume)| {
1888            let input = VptInput::from_slices(&price, &volume);
1889
1890            let VptOutput { values: out } = vpt_with_kernel(&input, kernel)?;
1891
1892            let VptOutput { values: ref_out } = vpt_with_kernel(&input, Kernel::Scalar)?;
1893
1894            prop_assert_eq!(out.len(), price.len(), "Output length mismatch");
1895            prop_assert_eq!(
1896                ref_out.len(),
1897                price.len(),
1898                "Reference output length mismatch"
1899            );
1900
1901            prop_assert!(
1902                out[0].is_nan(),
1903                "First VPT value should be NaN, got {}",
1904                out[0]
1905            );
1906            prop_assert!(
1907                ref_out[0].is_nan(),
1908                "First reference VPT value should be NaN, got {}",
1909                ref_out[0]
1910            );
1911
1912            let mut expected_vpt = vec![f64::NAN; price.len()];
1913            let mut prev_vpt_val = f64::NAN;
1914
1915            for i in 1..price.len() {
1916                let p0 = price[i - 1];
1917                let p1 = price[i];
1918                let v1 = volume[i];
1919
1920                let vpt_val = if p0.is_nan() || p0 == 0.0 || p1.is_nan() || v1.is_nan() {
1921                    f64::NAN
1922                } else {
1923                    v1 * ((p1 - p0) / p0)
1924                };
1925
1926                expected_vpt[i] = if vpt_val.is_nan() || prev_vpt_val.is_nan() {
1927                    f64::NAN
1928                } else {
1929                    vpt_val + prev_vpt_val
1930                };
1931
1932                prev_vpt_val = vpt_val;
1933            }
1934
1935            for i in 0..price.len() {
1936                let y = out[i];
1937                let r = ref_out[i];
1938                let e = expected_vpt[i];
1939
1940                if y.is_nan() && r.is_nan() {
1941                    continue;
1942                } else if !y.is_nan() && !r.is_nan() {
1943                    let diff = (y - r).abs();
1944                    prop_assert!(
1945                        diff < 1e-9,
1946                        "Kernel mismatch at idx {}: {} vs {} (diff: {})",
1947                        i,
1948                        y,
1949                        r,
1950                        diff
1951                    );
1952
1953                    if !e.is_nan() {
1954                        let diff_expected = (y - e).abs();
1955                        prop_assert!(
1956                            diff_expected < 1e-9,
1957                            "Value mismatch at idx {}: got {} expected {} (diff: {})",
1958                            i,
1959                            y,
1960                            e,
1961                            diff_expected
1962                        );
1963                    }
1964                } else {
1965                    prop_assert!(
1966                        false,
1967                        "NaN mismatch at idx {}: kernel={}, scalar={}",
1968                        i,
1969                        y,
1970                        r
1971                    );
1972                }
1973            }
1974
1975            Ok(())
1976        })?;
1977
1978        Ok(())
1979    }
1980
1981    generate_all_vpt_tests!(
1982        check_vpt_basic_candles,
1983        check_vpt_basic_slices,
1984        check_vpt_not_enough_data,
1985        check_vpt_empty_data,
1986        check_vpt_all_nan,
1987        check_vpt_accuracy_from_csv,
1988        check_vpt_no_poison
1989    );
1990
1991    #[cfg(feature = "proptest")]
1992    generate_all_vpt_tests!(check_vpt_property);
1993
1994    #[cfg(debug_assertions)]
1995    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1996        skip_if_unsupported!(kernel, test);
1997
1998        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1999        let c = read_candles_from_csv(file)?;
2000
2001        let test_sources = vec!["close", "open", "high", "low"];
2002
2003        for (src_idx, &source) in test_sources.iter().enumerate() {
2004            let output = VptBatchBuilder::new()
2005                .kernel(kernel)
2006                .apply_candles(&c, source)?;
2007
2008            for (idx, &val) in output.values.iter().enumerate() {
2009                if val.is_nan() {
2010                    continue;
2011                }
2012
2013                let bits = val.to_bits();
2014                let row = idx / output.cols;
2015                let col = idx % output.cols;
2016
2017                if bits == 0x11111111_11111111 {
2018                    panic!(
2019                        "[{}] Source {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
2020						 at row {} col {} (flat index {}) with source: {}",
2021                        test, src_idx, val, bits, row, col, idx, source
2022                    );
2023                }
2024
2025                if bits == 0x22222222_22222222 {
2026                    panic!(
2027                        "[{}] Source {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
2028						 at row {} col {} (flat index {}) with source: {}",
2029                        test, src_idx, val, bits, row, col, idx, source
2030                    );
2031                }
2032
2033                if bits == 0x33333333_33333333 {
2034                    panic!(
2035                        "[{}] Source {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
2036						 at row {} col {} (flat index {}) with source: {}",
2037                        test, src_idx, val, bits, row, col, idx, source
2038                    );
2039                }
2040            }
2041        }
2042
2043        Ok(())
2044    }
2045
2046    #[cfg(not(debug_assertions))]
2047    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2048        Ok(())
2049    }
2050
2051    macro_rules! gen_batch_tests {
2052        ($fn_name:ident) => {
2053            paste::paste! {
2054                #[test] fn [<$fn_name _scalar>]()      {
2055                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
2056                }
2057                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2058                #[test] fn [<$fn_name _avx2>]()        {
2059                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
2060                }
2061                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2062                #[test] fn [<$fn_name _avx512>]()      {
2063                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
2064                }
2065                #[test] fn [<$fn_name _auto_detect>]() {
2066                    let kernel = detect_best_batch_kernel();
2067                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), kernel);
2068                }
2069            }
2070        };
2071    }
2072
2073    gen_batch_tests!(check_batch_no_poison);
2074}