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}