1#[cfg(feature = "python")]
2use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
3#[cfg(feature = "python")]
4use pyo3::exceptions::PyValueError;
5#[cfg(feature = "python")]
6use pyo3::prelude::*;
7#[cfg(feature = "python")]
8use pyo3::types::PyDict;
9#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
10use serde::{Deserialize, Serialize};
11#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
12use wasm_bindgen::prelude::*;
13
14use crate::utilities::data_loader::{source_type, Candles};
15use crate::utilities::enums::Kernel;
16use crate::utilities::helpers::{
17 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
18 make_uninit_matrix,
19};
20#[cfg(feature = "python")]
21use crate::utilities::kernel_validation::validate_kernel;
22use aligned_vec::{AVec, CACHELINE_ALIGN};
23#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
24use core::arch::x86_64::*;
25#[cfg(not(target_arch = "wasm32"))]
26use rayon::prelude::*;
27use std::convert::AsRef;
28use thiserror::Error;
29
30impl<'a> AsRef<[f64]> for PviInput<'a> {
31 #[inline(always)]
32 fn as_ref(&self) -> &[f64] {
33 match &self.data {
34 PviData::Slices { close, .. } => close,
35 PviData::Candles {
36 candles,
37 close_source,
38 ..
39 } => source_type(candles, close_source),
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
45pub enum PviData<'a> {
46 Candles {
47 candles: &'a Candles,
48 close_source: &'a str,
49 volume_source: &'a str,
50 },
51 Slices {
52 close: &'a [f64],
53 volume: &'a [f64],
54 },
55}
56
57#[derive(Debug, Clone)]
58pub struct PviOutput {
59 pub values: Vec<f64>,
60}
61
62#[derive(Debug, Clone)]
63#[cfg_attr(
64 all(target_arch = "wasm32", feature = "wasm"),
65 derive(Serialize, Deserialize)
66)]
67pub struct PviParams {
68 pub initial_value: Option<f64>,
69}
70
71impl Default for PviParams {
72 fn default() -> Self {
73 Self {
74 initial_value: Some(1000.0),
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
80pub struct PviInput<'a> {
81 pub data: PviData<'a>,
82 pub params: PviParams,
83}
84
85impl<'a> PviInput<'a> {
86 #[inline]
87 pub fn from_candles(
88 candles: &'a Candles,
89 close_source: &'a str,
90 volume_source: &'a str,
91 params: PviParams,
92 ) -> Self {
93 Self {
94 data: PviData::Candles {
95 candles,
96 close_source,
97 volume_source,
98 },
99 params,
100 }
101 }
102 #[inline]
103 pub fn from_slices(close: &'a [f64], volume: &'a [f64], params: PviParams) -> Self {
104 Self {
105 data: PviData::Slices { close, volume },
106 params,
107 }
108 }
109 #[inline]
110 pub fn with_default_candles(candles: &'a Candles) -> Self {
111 Self::from_candles(candles, "close", "volume", PviParams::default())
112 }
113 #[inline]
114 pub fn get_initial_value(&self) -> f64 {
115 self.params.initial_value.unwrap_or(1000.0)
116 }
117}
118
119#[derive(Copy, Clone, Debug)]
120pub struct PviBuilder {
121 initial_value: Option<f64>,
122 kernel: Kernel,
123}
124
125impl Default for PviBuilder {
126 fn default() -> Self {
127 Self {
128 initial_value: None,
129 kernel: Kernel::Auto,
130 }
131 }
132}
133
134impl PviBuilder {
135 #[inline(always)]
136 pub fn new() -> Self {
137 Self::default()
138 }
139 #[inline(always)]
140 pub fn initial_value(mut self, v: f64) -> Self {
141 self.initial_value = Some(v);
142 self
143 }
144 #[inline(always)]
145 pub fn kernel(mut self, k: Kernel) -> Self {
146 self.kernel = k;
147 self
148 }
149 #[inline(always)]
150 pub fn apply(self, c: &Candles) -> Result<PviOutput, PviError> {
151 let p = PviParams {
152 initial_value: self.initial_value,
153 };
154 let i = PviInput::from_candles(c, "close", "volume", p);
155 pvi_with_kernel(&i, self.kernel)
156 }
157 #[inline(always)]
158 pub fn apply_slice(self, close: &[f64], volume: &[f64]) -> Result<PviOutput, PviError> {
159 let p = PviParams {
160 initial_value: self.initial_value,
161 };
162 let i = PviInput::from_slices(close, volume, p);
163 pvi_with_kernel(&i, self.kernel)
164 }
165 #[inline(always)]
166 pub fn into_stream(self) -> Result<PviStream, PviError> {
167 let p = PviParams {
168 initial_value: self.initial_value,
169 };
170 PviStream::try_new(p)
171 }
172}
173
174#[derive(Debug, Error)]
175pub enum PviError {
176 #[error("pvi: Empty data provided.")]
177 EmptyInputData,
178 #[error("pvi: All values are NaN.")]
179 AllValuesNaN,
180 #[error("pvi: close and volume data have different lengths")]
181 MismatchedLength,
182 #[error("pvi: Not enough valid data: needed at least {needed} valid data points, got {valid}")]
183 NotEnoughValidData { needed: usize, valid: usize },
184 #[error("pvi: output length mismatch: expected {expected}, got {got}")]
185 OutputLengthMismatch { expected: usize, got: usize },
186 #[error("pvi: invalid range: start={start}, end={end}, step={step}")]
187 InvalidRange { start: f64, end: f64, step: f64 },
188 #[error("pvi: invalid kernel for batch: {0:?}")]
189 InvalidKernelForBatch(Kernel),
190 #[error("pvi: invalid input: {0}")]
191 InvalidInput(String),
192}
193
194#[inline]
195pub fn pvi(input: &PviInput) -> Result<PviOutput, PviError> {
196 pvi_with_kernel(input, Kernel::Auto)
197}
198
199pub fn pvi_with_kernel(input: &PviInput, kernel: Kernel) -> Result<PviOutput, PviError> {
200 let (close, volume) = match &input.data {
201 PviData::Candles {
202 candles,
203 close_source,
204 volume_source,
205 } => {
206 let c = source_type(candles, close_source);
207 let v = source_type(candles, volume_source);
208 (c, v)
209 }
210 PviData::Slices { close, volume } => (*close, *volume),
211 };
212
213 if close.is_empty() || volume.is_empty() {
214 return Err(PviError::EmptyInputData);
215 }
216 if close.len() != volume.len() {
217 return Err(PviError::MismatchedLength);
218 }
219 let first_valid_idx = close
220 .iter()
221 .zip(volume.iter())
222 .position(|(&c, &v)| !c.is_nan() && !v.is_nan())
223 .ok_or(PviError::AllValuesNaN)?;
224 let valid = close.len() - first_valid_idx;
225 if valid < 2 {
226 return Err(PviError::NotEnoughValidData { needed: 2, valid });
227 }
228
229 let mut out = alloc_with_nan_prefix(close.len(), first_valid_idx);
230 let chosen = match kernel {
231 Kernel::Auto => match detect_best_kernel() {
232 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
233 Kernel::Avx512 => Kernel::Avx2,
234 other => other,
235 },
236 other => other,
237 };
238 unsafe {
239 match chosen {
240 Kernel::Scalar | Kernel::ScalarBatch => pvi_scalar(
241 close,
242 volume,
243 first_valid_idx,
244 input.get_initial_value(),
245 &mut out,
246 ),
247 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
248 Kernel::Avx2 | Kernel::Avx2Batch => pvi_avx2(
249 close,
250 volume,
251 first_valid_idx,
252 input.get_initial_value(),
253 &mut out,
254 ),
255 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
256 Kernel::Avx512 | Kernel::Avx512Batch => pvi_avx512(
257 close,
258 volume,
259 first_valid_idx,
260 input.get_initial_value(),
261 &mut out,
262 ),
263 _ => unreachable!(),
264 }
265 }
266 Ok(PviOutput { values: out })
267}
268
269#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
270#[inline]
271pub fn pvi_into(input: &PviInput, out: &mut [f64]) -> Result<(), PviError> {
272 let (close, volume) = match &input.data {
273 PviData::Candles {
274 candles,
275 close_source,
276 volume_source,
277 } => {
278 let c = source_type(candles, close_source);
279 let v = source_type(candles, volume_source);
280 (c, v)
281 }
282 PviData::Slices { close, volume } => (*close, *volume),
283 };
284
285 if close.is_empty() || volume.is_empty() {
286 return Err(PviError::EmptyInputData);
287 }
288 if close.len() != volume.len() {
289 return Err(PviError::MismatchedLength);
290 }
291 if out.len() != close.len() {
292 return Err(PviError::OutputLengthMismatch {
293 expected: close.len(),
294 got: out.len(),
295 });
296 }
297
298 let first_valid_idx = close
299 .iter()
300 .zip(volume.iter())
301 .position(|(&c, &v)| !c.is_nan() && !v.is_nan())
302 .ok_or(PviError::AllValuesNaN)?;
303 let valid = close.len() - first_valid_idx;
304 if valid < 2 {
305 return Err(PviError::NotEnoughValidData { needed: 2, valid });
306 }
307
308 let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
309 let warm = first_valid_idx.min(out.len());
310 for v in &mut out[..warm] {
311 *v = qnan;
312 }
313
314 let chosen = match Kernel::Auto {
315 Kernel::Auto => match detect_best_kernel() {
316 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
317 Kernel::Avx512 => Kernel::Avx2,
318 other => other,
319 },
320 other => other,
321 };
322 let initial = input.get_initial_value();
323 unsafe {
324 match chosen {
325 Kernel::Scalar | Kernel::ScalarBatch => {
326 pvi_scalar(close, volume, first_valid_idx, initial, out)
327 }
328 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
329 Kernel::Avx2 | Kernel::Avx2Batch => {
330 pvi_avx2(close, volume, first_valid_idx, initial, out)
331 }
332 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
333 Kernel::Avx512 | Kernel::Avx512Batch => {
334 pvi_avx512(close, volume, first_valid_idx, initial, out)
335 }
336 _ => unreachable!(),
337 }
338 }
339
340 Ok(())
341}
342
343#[inline]
344pub fn pvi_into_slice(dst: &mut [f64], input: &PviInput, kern: Kernel) -> Result<(), PviError> {
345 let (close, volume) = match &input.data {
346 PviData::Candles {
347 candles,
348 close_source,
349 volume_source,
350 } => {
351 let c = source_type(candles, close_source);
352 let v = source_type(candles, volume_source);
353 (c, v)
354 }
355 PviData::Slices { close, volume } => (*close, *volume),
356 };
357
358 if close.is_empty() || volume.is_empty() {
359 return Err(PviError::EmptyInputData);
360 }
361 if close.len() != volume.len() {
362 return Err(PviError::MismatchedLength);
363 }
364 if dst.len() != close.len() {
365 return Err(PviError::OutputLengthMismatch {
366 expected: close.len(),
367 got: dst.len(),
368 });
369 }
370
371 let first_valid_idx = close
372 .iter()
373 .zip(volume.iter())
374 .position(|(&c, &v)| !c.is_nan() && !v.is_nan())
375 .ok_or(PviError::AllValuesNaN)?;
376 let valid = close.len() - first_valid_idx;
377 if valid < 2 {
378 return Err(PviError::NotEnoughValidData { needed: 2, valid });
379 }
380
381 let chosen = match kern {
382 Kernel::Auto => match detect_best_kernel() {
383 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
384 Kernel::Avx512 => Kernel::Avx2,
385 other => other,
386 },
387 other => other,
388 };
389
390 let initial_value = input.get_initial_value();
391
392 unsafe {
393 match chosen {
394 Kernel::Scalar | Kernel::ScalarBatch => {
395 pvi_scalar(close, volume, first_valid_idx, initial_value, dst)
396 }
397 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
398 Kernel::Avx2 | Kernel::Avx2Batch => {
399 pvi_avx2(close, volume, first_valid_idx, initial_value, dst)
400 }
401 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
402 Kernel::Avx512 | Kernel::Avx512Batch => {
403 pvi_avx512(close, volume, first_valid_idx, initial_value, dst)
404 }
405 _ => unreachable!(),
406 }
407 }
408
409 for v in &mut dst[..first_valid_idx] {
410 *v = f64::NAN;
411 }
412
413 Ok(())
414}
415
416#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
417#[inline]
418pub fn pvi_avx512(
419 close: &[f64],
420 volume: &[f64],
421 first_valid: usize,
422 initial: f64,
423 out: &mut [f64],
424) {
425 unsafe {
426 if close.len() <= 32 {
427 pvi_avx512_short(close, volume, first_valid, initial, out)
428 } else {
429 pvi_avx512_long(close, volume, first_valid, initial, out)
430 }
431 }
432}
433
434#[inline]
435pub fn pvi_scalar(
436 close: &[f64],
437 volume: &[f64],
438 first_valid: usize,
439 initial: f64,
440 out: &mut [f64],
441) {
442 debug_assert_eq!(close.len(), volume.len());
443 debug_assert_eq!(close.len(), out.len());
444 let n = close.len();
445 if n == 0 {
446 return;
447 }
448
449 let mut pvi = initial;
450 out[first_valid] = pvi;
451
452 let mut prev_close = close[first_valid];
453 let mut prev_vol = volume[first_valid];
454
455 for i in (first_valid + 1)..n {
456 let c = close[i];
457 let v = volume[i];
458 if c.is_nan() || v.is_nan() || prev_close.is_nan() || prev_vol.is_nan() {
459 out[i] = f64::NAN;
460 if !c.is_nan() && !v.is_nan() {
461 prev_close = c;
462 prev_vol = v;
463 }
464 continue;
465 }
466 if v > prev_vol {
467 let r = (c - prev_close) / prev_close;
468 pvi += r * pvi;
469 }
470 out[i] = pvi;
471 prev_close = c;
472 prev_vol = v;
473 }
474}
475
476#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
477#[inline(always)]
478pub fn pvi_avx2(close: &[f64], volume: &[f64], first_valid: usize, initial: f64, out: &mut [f64]) {
479 debug_assert_eq!(close.len(), volume.len());
480 debug_assert_eq!(close.len(), out.len());
481 let n = close.len();
482 if n == 0 {
483 return;
484 }
485
486 #[inline(always)]
487 fn not_nan(x: f64) -> bool {
488 x == x
489 }
490
491 unsafe {
492 let mut pvi = initial;
493 *out.get_unchecked_mut(first_valid) = pvi;
494
495 let mut prev_close = *close.get_unchecked(first_valid);
496 let mut prev_vol = *volume.get_unchecked(first_valid);
497
498 let cptr = close.as_ptr().add(first_valid + 1);
499 let vptr = volume.as_ptr().add(first_valid + 1);
500 let optr = out.as_mut_ptr().add(first_valid + 1);
501
502 let mut j = 0usize;
503 let rem = n - (first_valid + 1);
504
505 while j + 1 < rem {
506 let c0 = *cptr.add(j);
507 let v0 = *vptr.add(j);
508 if not_nan(c0) && not_nan(v0) && not_nan(prev_close) && not_nan(prev_vol) {
509 if v0 > prev_vol {
510 let r = (c0 - prev_close) / prev_close;
511 pvi = f64::mul_add(r, pvi, pvi);
512 }
513 *optr.add(j) = pvi;
514 prev_close = c0;
515 prev_vol = v0;
516 } else {
517 *optr.add(j) = f64::NAN;
518 if not_nan(c0) && not_nan(v0) {
519 prev_close = c0;
520 prev_vol = v0;
521 }
522 }
523
524 let c1 = *cptr.add(j + 1);
525 let v1 = *vptr.add(j + 1);
526 if not_nan(c1) && not_nan(v1) && not_nan(prev_close) && not_nan(prev_vol) {
527 if v1 > prev_vol {
528 let r = (c1 - prev_close) / prev_close;
529 pvi = f64::mul_add(r, pvi, pvi);
530 }
531 *optr.add(j + 1) = pvi;
532 prev_close = c1;
533 prev_vol = v1;
534 } else {
535 *optr.add(j + 1) = f64::NAN;
536 if not_nan(c1) && not_nan(v1) {
537 prev_close = c1;
538 prev_vol = v1;
539 }
540 }
541
542 j += 2;
543 }
544
545 if j < rem {
546 let c = *cptr.add(j);
547 let v = *vptr.add(j);
548 if not_nan(c) && not_nan(v) && not_nan(prev_close) && not_nan(prev_vol) {
549 if v > prev_vol {
550 let r = (c - prev_close) / prev_close;
551 pvi = f64::mul_add(r, pvi, pvi);
552 }
553 *optr.add(j) = pvi;
554 } else {
555 *optr.add(j) = f64::NAN;
556 }
557 }
558 }
559}
560
561#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
562#[inline(always)]
563pub fn pvi_avx512_short(
564 close: &[f64],
565 volume: &[f64],
566 first_valid: usize,
567 initial: f64,
568 out: &mut [f64],
569) {
570 pvi_avx2(close, volume, first_valid, initial, out)
571}
572
573#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
574#[inline(always)]
575pub fn pvi_avx512_long(
576 close: &[f64],
577 volume: &[f64],
578 first_valid: usize,
579 initial: f64,
580 out: &mut [f64],
581) {
582 pvi_avx2(close, volume, first_valid, initial, out)
583}
584
585#[derive(Debug, Clone)]
586pub struct PviStream {
587 initial_value: f64,
588 last_close: f64,
589 last_volume: f64,
590 curr: f64,
591 state: StreamState,
592}
593
594#[derive(Clone, Copy, Debug, PartialEq, Eq)]
595enum StreamState {
596 Init,
597 Valid,
598}
599
600impl PviStream {
601 #[inline]
602 pub fn try_new(params: PviParams) -> Result<Self, PviError> {
603 let initial = params.initial_value.unwrap_or(1000.0);
604 Ok(Self {
605 initial_value: initial,
606 last_close: f64::NAN,
607 last_volume: f64::NAN,
608 curr: initial,
609 state: StreamState::Init,
610 })
611 }
612
613 #[inline(always)]
614 pub fn update(&mut self, close: f64, volume: f64) -> Option<f64> {
615 if let StreamState::Init = self.state {
616 return self.init_or_none(close, volume);
617 }
618
619 if close.is_nan() | volume.is_nan() {
620 return self.cold_invalid(close, volume);
621 }
622
623 if volume > self.last_volume {
624 let prev = self.last_close;
625 let r = (close - prev) / prev;
626 self.curr += r * self.curr;
627 }
628
629 self.last_close = close;
630 self.last_volume = volume;
631
632 Some(self.curr)
633 }
634
635 #[inline(always)]
636 fn init_or_none(&mut self, close: f64, volume: f64) -> Option<f64> {
637 if close.is_nan() || volume.is_nan() {
638 return None;
639 }
640 self.last_close = close;
641 self.last_volume = volume;
642 self.curr = self.initial_value;
643 self.state = StreamState::Valid;
644 Some(self.curr)
645 }
646
647 #[cold]
648 #[inline(never)]
649 fn cold_invalid(&mut self, _close: f64, _volume: f64) -> Option<f64> {
650 None
651 }
652
653 #[inline(always)]
654 pub fn update_unchecked_finite(&mut self, close: f64, volume: f64) -> f64 {
655 debug_assert!(self.state == StreamState::Valid);
656 if volume > self.last_volume {
657 let prev = self.last_close;
658 let r = (close - prev) / prev;
659 self.curr += r * self.curr;
660 }
661 self.last_close = close;
662 self.last_volume = volume;
663 self.curr
664 }
665}
666
667#[derive(Clone, Debug)]
668pub struct PviBatchRange {
669 pub initial_value: (f64, f64, f64),
670}
671
672impl Default for PviBatchRange {
673 fn default() -> Self {
674 Self {
675 initial_value: (1000.0, 1249.0, 1.0),
676 }
677 }
678}
679
680#[derive(Clone, Debug, Default)]
681pub struct PviBatchBuilder {
682 range: PviBatchRange,
683 kernel: Kernel,
684}
685
686impl PviBatchBuilder {
687 pub fn new() -> Self {
688 Self::default()
689 }
690 pub fn kernel(mut self, k: Kernel) -> Self {
691 self.kernel = k;
692 self
693 }
694 #[inline]
695 pub fn initial_value_range(mut self, start: f64, end: f64, step: f64) -> Self {
696 self.range.initial_value = (start, end, step);
697 self
698 }
699 #[inline]
700 pub fn initial_value_static(mut self, v: f64) -> Self {
701 self.range.initial_value = (v, v, 0.0);
702 self
703 }
704 pub fn apply_slices(self, close: &[f64], volume: &[f64]) -> Result<PviBatchOutput, PviError> {
705 pvi_batch_with_kernel(close, volume, &self.range, self.kernel)
706 }
707 pub fn with_default_slices(
708 close: &[f64],
709 volume: &[f64],
710 k: Kernel,
711 ) -> Result<PviBatchOutput, PviError> {
712 PviBatchBuilder::new().kernel(k).apply_slices(close, volume)
713 }
714 pub fn apply_candles(
715 self,
716 c: &Candles,
717 close_src: &str,
718 vol_src: &str,
719 ) -> Result<PviBatchOutput, PviError> {
720 let close = source_type(c, close_src);
721 let vol = source_type(c, vol_src);
722 self.apply_slices(close, vol)
723 }
724 pub fn with_default_candles(c: &Candles) -> Result<PviBatchOutput, PviError> {
725 PviBatchBuilder::new()
726 .kernel(Kernel::Auto)
727 .apply_candles(c, "close", "volume")
728 }
729}
730
731pub fn pvi_batch_with_kernel(
732 close: &[f64],
733 volume: &[f64],
734 sweep: &PviBatchRange,
735 k: Kernel,
736) -> Result<PviBatchOutput, PviError> {
737 let kernel = match k {
738 Kernel::Auto => detect_best_batch_kernel(),
739 other if other.is_batch() => other,
740 other => return Err(PviError::InvalidKernelForBatch(other)),
741 };
742 let simd = match kernel {
743 Kernel::Avx512Batch => Kernel::Avx512,
744 Kernel::Avx2Batch => Kernel::Avx2,
745 Kernel::ScalarBatch => Kernel::Scalar,
746 _ => unreachable!(),
747 };
748 pvi_batch_par_slice(close, volume, sweep, simd)
749}
750
751#[derive(Clone, Debug)]
752pub struct PviBatchOutput {
753 pub values: Vec<f64>,
754 pub combos: Vec<PviParams>,
755 pub rows: usize,
756 pub cols: usize,
757}
758impl PviBatchOutput {
759 pub fn row_for_params(&self, p: &PviParams) -> Option<usize> {
760 self.combos.iter().position(|c| {
761 (c.initial_value.unwrap_or(1000.0) - p.initial_value.unwrap_or(1000.0)).abs() < 1e-12
762 })
763 }
764 pub fn values_for(&self, p: &PviParams) -> Option<&[f64]> {
765 self.row_for_params(p).map(|row| {
766 let start = row * self.cols;
767 &self.values[start..start + self.cols]
768 })
769 }
770}
771
772#[inline(always)]
773fn expand_grid(r: &PviBatchRange) -> Result<Vec<PviParams>, PviError> {
774 fn axis_f64((start, end, step): (f64, f64, f64)) -> Result<Vec<f64>, PviError> {
775 if step.abs() < 1e-12 || (start - end).abs() < 1e-12 {
776 return Ok(vec![start]);
777 }
778 let mut vals = Vec::new();
779 if start <= end {
780 let mut x = start;
781 loop {
782 vals.push(x);
783 if x >= end {
784 break;
785 }
786 let next = x + step;
787 if !next.is_finite() || next == x {
788 break;
789 }
790 x = next;
791 if x > end + 1e-12 {
792 break;
793 }
794 }
795 } else {
796 let mut x = start;
797 loop {
798 vals.push(x);
799 if x <= end {
800 break;
801 }
802 let next = x - step.abs();
803 if !next.is_finite() || next == x {
804 break;
805 }
806 x = next;
807 if x < end - 1e-12 {
808 break;
809 }
810 }
811 }
812 if vals.is_empty() {
813 return Err(PviError::InvalidRange { start, end, step });
814 }
815 Ok(vals)
816 }
817
818 let initials = axis_f64(r.initial_value)?;
819 let mut out = Vec::with_capacity(initials.len());
820 for &v in &initials {
821 out.push(PviParams {
822 initial_value: Some(v),
823 });
824 }
825 if out.is_empty() {
826 return Err(PviError::InvalidRange {
827 start: r.initial_value.0,
828 end: r.initial_value.1,
829 step: r.initial_value.2,
830 });
831 }
832 Ok(out)
833}
834
835#[inline(always)]
836pub fn pvi_batch_slice(
837 close: &[f64],
838 volume: &[f64],
839 sweep: &PviBatchRange,
840 kern: Kernel,
841) -> Result<PviBatchOutput, PviError> {
842 pvi_batch_inner(close, volume, sweep, kern, false)
843}
844
845#[inline(always)]
846pub fn pvi_batch_par_slice(
847 close: &[f64],
848 volume: &[f64],
849 sweep: &PviBatchRange,
850 kern: Kernel,
851) -> Result<PviBatchOutput, PviError> {
852 pvi_batch_inner(close, volume, sweep, kern, true)
853}
854
855#[inline(always)]
856fn pvi_batch_inner(
857 close: &[f64],
858 volume: &[f64],
859 sweep: &PviBatchRange,
860 kern: Kernel,
861 parallel: bool,
862) -> Result<PviBatchOutput, PviError> {
863 let combos = expand_grid(sweep)?;
864 if close.is_empty() || volume.is_empty() {
865 return Err(PviError::EmptyInputData);
866 }
867 if close.len() != volume.len() {
868 return Err(PviError::MismatchedLength);
869 }
870
871 let first_valid_idx = close
872 .iter()
873 .zip(volume.iter())
874 .position(|(&c, &v)| !c.is_nan() && !v.is_nan())
875 .ok_or(PviError::AllValuesNaN)?;
876 let valid = close.len() - first_valid_idx;
877 if valid < 2 {
878 return Err(PviError::NotEnoughValidData { needed: 2, valid });
879 }
880
881 let rows = combos.len();
882 let cols = close.len();
883
884 let mut buf_mu = make_uninit_matrix(rows, cols);
885
886 if rows <= 32 {
887 let mut warm = [0usize; 32];
888 for i in 0..rows {
889 warm[i] = first_valid_idx;
890 }
891 init_matrix_prefixes(&mut buf_mu, cols, &warm[..rows]);
892 } else {
893 let warm = vec![first_valid_idx; rows];
894 init_matrix_prefixes(&mut buf_mu, cols, &warm);
895 }
896
897 let mut guard = core::mem::ManuallyDrop::new(buf_mu);
898
899 let out_f: &mut [f64] =
900 unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
901 pvi_batch_inner_into(close, volume, sweep, kern, parallel, out_f)?;
902
903 let values = unsafe {
904 Vec::from_raw_parts(
905 guard.as_mut_ptr() as *mut f64,
906 guard.len(),
907 guard.capacity(),
908 )
909 };
910
911 Ok(PviBatchOutput {
912 values,
913 combos,
914 rows,
915 cols,
916 })
917}
918
919#[inline(always)]
920fn pvi_batch_inner_into(
921 close: &[f64],
922 volume: &[f64],
923 sweep: &PviBatchRange,
924 kern: Kernel,
925 parallel: bool,
926 out: &mut [f64],
927) -> Result<Vec<PviParams>, PviError> {
928 let combos = expand_grid(sweep)?;
929 if close.is_empty() || volume.is_empty() {
930 return Err(PviError::EmptyInputData);
931 }
932 if close.len() != volume.len() {
933 return Err(PviError::MismatchedLength);
934 }
935
936 let first_valid_idx = close
937 .iter()
938 .zip(volume.iter())
939 .position(|(&c, &v)| !c.is_nan() && !v.is_nan())
940 .ok_or(PviError::AllValuesNaN)?;
941 let valid = close.len() - first_valid_idx;
942 if valid < 2 {
943 return Err(PviError::NotEnoughValidData { needed: 2, valid });
944 }
945
946 let rows = combos.len();
947 let cols = close.len();
948 let expected = rows
949 .checked_mul(cols)
950 .ok_or_else(|| PviError::InvalidInput("rows*cols overflow".into()))?;
951 if out.len() != expected {
952 return Err(PviError::OutputLengthMismatch {
953 expected,
954 got: out.len(),
955 });
956 }
957
958 let out_mu: &mut [core::mem::MaybeUninit<f64>] = unsafe {
959 core::slice::from_raw_parts_mut(
960 out.as_mut_ptr() as *mut core::mem::MaybeUninit<f64>,
961 out.len(),
962 )
963 };
964
965 let mut scale = vec![f64::NAN; cols];
966 scale[first_valid_idx] = 1.0;
967
968 #[inline(always)]
969 fn not_nan(x: f64) -> bool {
970 x == x
971 }
972
973 unsafe {
974 let mut prev_close = *close.get_unchecked(first_valid_idx);
975 let mut prev_vol = *volume.get_unchecked(first_valid_idx);
976 let mut accum = 1.0f64;
977
978 let cptr = close.as_ptr();
979 let vptr = volume.as_ptr();
980 let mut i = first_valid_idx + 1;
981 while i < cols {
982 let c = *cptr.add(i);
983 let v = *vptr.add(i);
984 if not_nan(c) && not_nan(v) && not_nan(prev_close) && not_nan(prev_vol) {
985 if v > prev_vol {
986 let r = (c - prev_close) / prev_close;
987 accum = f64::mul_add(r, accum, accum);
988 }
989 scale[i] = accum;
990 prev_close = c;
991 prev_vol = v;
992 } else {
993 scale[i] = f64::NAN;
994 if not_nan(c) && not_nan(v) {
995 prev_close = c;
996 prev_vol = v;
997 }
998 }
999 i += 1;
1000 }
1001 }
1002
1003 let do_row = |row: usize, dst_row_mu: &mut [core::mem::MaybeUninit<f64>]| unsafe {
1004 let iv = combos[row].initial_value.unwrap_or(1000.0);
1005
1006 let dst_row: &mut [f64] =
1007 core::slice::from_raw_parts_mut(dst_row_mu.as_mut_ptr() as *mut f64, dst_row_mu.len());
1008
1009 *dst_row.get_unchecked_mut(first_valid_idx) = iv;
1010
1011 let mut j = first_valid_idx + 1;
1012 while j < cols {
1013 let s = *scale.get_unchecked(j);
1014 if s == s {
1015 *dst_row.get_unchecked_mut(j) = iv * s;
1016 } else {
1017 *dst_row.get_unchecked_mut(j) = f64::NAN;
1018 }
1019 j += 1;
1020 }
1021 };
1022
1023 if parallel {
1024 #[cfg(not(target_arch = "wasm32"))]
1025 {
1026 use rayon::prelude::*;
1027 out_mu
1028 .par_chunks_mut(cols)
1029 .enumerate()
1030 .for_each(|(r, row)| do_row(r, row));
1031 }
1032 #[cfg(target_arch = "wasm32")]
1033 for (r, row) in out_mu.chunks_mut(cols).enumerate() {
1034 do_row(r, row);
1035 }
1036 } else {
1037 for (r, row) in out_mu.chunks_mut(cols).enumerate() {
1038 do_row(r, row);
1039 }
1040 }
1041
1042 Ok(combos)
1043}
1044
1045#[inline(always)]
1046unsafe fn pvi_row_scalar(
1047 close: &[f64],
1048 volume: &[f64],
1049 first: usize,
1050 initial: f64,
1051 out: &mut [f64],
1052) {
1053 pvi_scalar(close, volume, first, initial, out)
1054}
1055
1056#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1057#[inline(always)]
1058unsafe fn pvi_row_avx2(close: &[f64], volume: &[f64], first: usize, initial: f64, out: &mut [f64]) {
1059 pvi_scalar(close, volume, first, initial, out)
1060}
1061
1062#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1063#[inline(always)]
1064unsafe fn pvi_row_avx512(
1065 close: &[f64],
1066 volume: &[f64],
1067 first: usize,
1068 initial: f64,
1069 out: &mut [f64],
1070) {
1071 if close.len() <= 32 {
1072 pvi_row_avx512_short(close, volume, first, initial, out);
1073 } else {
1074 pvi_row_avx512_long(close, volume, first, initial, out);
1075 }
1076}
1077
1078#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1079#[inline(always)]
1080unsafe fn pvi_row_avx512_short(
1081 close: &[f64],
1082 volume: &[f64],
1083 first: usize,
1084 initial: f64,
1085 out: &mut [f64],
1086) {
1087 pvi_scalar(close, volume, first, initial, out)
1088}
1089
1090#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1091#[inline(always)]
1092unsafe fn pvi_row_avx512_long(
1093 close: &[f64],
1094 volume: &[f64],
1095 first: usize,
1096 initial: f64,
1097 out: &mut [f64],
1098) {
1099 pvi_scalar(close, volume, first, initial, out)
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104 use super::*;
1105 use crate::skip_if_unsupported;
1106 use crate::utilities::data_loader::read_candles_from_csv;
1107
1108 fn check_pvi_partial_params(
1109 test_name: &str,
1110 kernel: Kernel,
1111 ) -> Result<(), Box<dyn std::error::Error>> {
1112 skip_if_unsupported!(kernel, test_name);
1113 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1114 let candles = read_candles_from_csv(file_path)?;
1115 let default_params = PviParams {
1116 initial_value: None,
1117 };
1118 let input = PviInput::from_candles(&candles, "close", "volume", default_params);
1119 let output = pvi_with_kernel(&input, kernel)?;
1120 assert_eq!(output.values.len(), candles.close.len());
1121 Ok(())
1122 }
1123
1124 fn check_pvi_accuracy(
1125 test_name: &str,
1126 kernel: Kernel,
1127 ) -> Result<(), Box<dyn std::error::Error>> {
1128 skip_if_unsupported!(kernel, test_name);
1129 let close_data = [100.0, 102.0, 101.0, 103.0, 103.0, 105.0];
1130 let volume_data = [500.0, 600.0, 500.0, 700.0, 680.0, 900.0];
1131 let params = PviParams {
1132 initial_value: Some(1000.0),
1133 };
1134 let input = PviInput::from_slices(&close_data, &volume_data, params);
1135 let output = pvi_with_kernel(&input, kernel)?;
1136 assert_eq!(output.values.len(), close_data.len());
1137 assert!((output.values[0] - 1000.0).abs() < 1e-6);
1138 Ok(())
1139 }
1140
1141 fn check_pvi_default_candles(
1142 test_name: &str,
1143 kernel: Kernel,
1144 ) -> Result<(), Box<dyn std::error::Error>> {
1145 skip_if_unsupported!(kernel, test_name);
1146 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1147 let candles = read_candles_from_csv(file_path)?;
1148 let input = PviInput::with_default_candles(&candles);
1149 let output = pvi_with_kernel(&input, kernel)?;
1150 assert_eq!(output.values.len(), candles.close.len());
1151 Ok(())
1152 }
1153
1154 fn check_pvi_empty_data(
1155 test_name: &str,
1156 kernel: Kernel,
1157 ) -> Result<(), Box<dyn std::error::Error>> {
1158 skip_if_unsupported!(kernel, test_name);
1159 let close_data = [];
1160 let volume_data = [];
1161 let params = PviParams::default();
1162 let input = PviInput::from_slices(&close_data, &volume_data, params);
1163 let result = pvi_with_kernel(&input, kernel);
1164 assert!(result.is_err());
1165 Ok(())
1166 }
1167
1168 fn check_pvi_mismatched_length(
1169 test_name: &str,
1170 kernel: Kernel,
1171 ) -> Result<(), Box<dyn std::error::Error>> {
1172 skip_if_unsupported!(kernel, test_name);
1173 let close_data = [100.0, 101.0];
1174 let volume_data = [500.0];
1175 let params = PviParams::default();
1176 let input = PviInput::from_slices(&close_data, &volume_data, params);
1177 let result = pvi_with_kernel(&input, kernel);
1178 assert!(result.is_err());
1179 Ok(())
1180 }
1181
1182 fn check_pvi_all_values_nan(
1183 test_name: &str,
1184 kernel: Kernel,
1185 ) -> Result<(), Box<dyn std::error::Error>> {
1186 skip_if_unsupported!(kernel, test_name);
1187 let close_data = [f64::NAN, f64::NAN, f64::NAN];
1188 let volume_data = [f64::NAN, f64::NAN, f64::NAN];
1189 let params = PviParams::default();
1190 let input = PviInput::from_slices(&close_data, &volume_data, params);
1191 let result = pvi_with_kernel(&input, kernel);
1192 assert!(result.is_err());
1193 Ok(())
1194 }
1195
1196 fn check_pvi_not_enough_valid_data(
1197 test_name: &str,
1198 kernel: Kernel,
1199 ) -> Result<(), Box<dyn std::error::Error>> {
1200 skip_if_unsupported!(kernel, test_name);
1201 let close_data = [f64::NAN, 100.0];
1202 let volume_data = [f64::NAN, 500.0];
1203 let params = PviParams::default();
1204 let input = PviInput::from_slices(&close_data, &volume_data, params);
1205 let result = pvi_with_kernel(&input, kernel);
1206 assert!(result.is_err());
1207 Ok(())
1208 }
1209
1210 fn check_pvi_streaming(
1211 test_name: &str,
1212 kernel: Kernel,
1213 ) -> Result<(), Box<dyn std::error::Error>> {
1214 skip_if_unsupported!(kernel, test_name);
1215 let close_data = [100.0, 102.0, 101.0, 103.0, 103.0, 105.0];
1216 let volume_data = [500.0, 600.0, 500.0, 700.0, 680.0, 900.0];
1217 let params = PviParams {
1218 initial_value: Some(1000.0),
1219 };
1220 let input = PviInput::from_slices(&close_data, &volume_data, params.clone());
1221 let batch_output = pvi_with_kernel(&input, kernel)?.values;
1222
1223 let mut stream = PviStream::try_new(params)?;
1224 let mut stream_values = Vec::with_capacity(close_data.len());
1225 for (&close, &vol) in close_data.iter().zip(volume_data.iter()) {
1226 match stream.update(close, vol) {
1227 Some(val) => stream_values.push(val),
1228 None => stream_values.push(f64::NAN),
1229 }
1230 }
1231 assert_eq!(batch_output.len(), stream_values.len());
1232 for (i, (&b, &s)) in batch_output.iter().zip(stream_values.iter()).enumerate() {
1233 if b.is_nan() && s.is_nan() {
1234 continue;
1235 }
1236 let diff = (b - s).abs();
1237 assert!(
1238 diff < 1e-9,
1239 "[{}] PVI streaming mismatch at idx {}: batch={}, stream={}, diff={}",
1240 test_name,
1241 i,
1242 b,
1243 s,
1244 diff
1245 );
1246 }
1247 Ok(())
1248 }
1249
1250 macro_rules! generate_all_pvi_tests {
1251 ($($test_fn:ident),*) => {
1252 paste::paste! {
1253 $(
1254 #[test]
1255 fn [<$test_fn _scalar_f64>]() {
1256 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1257 }
1258 )*
1259 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1260 $(
1261 #[test]
1262 fn [<$test_fn _avx2_f64>]() {
1263 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1264 }
1265 #[test]
1266 fn [<$test_fn _avx512_f64>]() {
1267 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1268 }
1269 )*
1270 }
1271 }
1272 }
1273
1274 #[cfg(debug_assertions)]
1275 fn check_pvi_no_poison(
1276 test_name: &str,
1277 kernel: Kernel,
1278 ) -> Result<(), Box<dyn std::error::Error>> {
1279 skip_if_unsupported!(kernel, test_name);
1280
1281 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1282 let candles = read_candles_from_csv(file_path)?;
1283
1284 let test_params = vec![
1285 PviParams::default(),
1286 PviParams {
1287 initial_value: Some(100.0),
1288 },
1289 PviParams {
1290 initial_value: Some(500.0),
1291 },
1292 PviParams {
1293 initial_value: Some(5000.0),
1294 },
1295 PviParams {
1296 initial_value: Some(10000.0),
1297 },
1298 PviParams {
1299 initial_value: Some(0.0),
1300 },
1301 PviParams {
1302 initial_value: Some(1.0),
1303 },
1304 PviParams {
1305 initial_value: Some(-1000.0),
1306 },
1307 PviParams {
1308 initial_value: Some(999999.0),
1309 },
1310 PviParams {
1311 initial_value: None,
1312 },
1313 ];
1314
1315 for (param_idx, params) in test_params.iter().enumerate() {
1316 let input = PviInput::from_candles(&candles, "close", "volume", params.clone());
1317 let output = pvi_with_kernel(&input, kernel)?;
1318
1319 for (i, &val) in output.values.iter().enumerate() {
1320 if val.is_nan() {
1321 continue;
1322 }
1323
1324 let bits = val.to_bits();
1325
1326 if bits == 0x11111111_11111111 {
1327 panic!(
1328 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1329 with params: initial_value={:?} (param set {})",
1330 test_name, val, bits, i, params.initial_value, param_idx
1331 );
1332 }
1333
1334 if bits == 0x22222222_22222222 {
1335 panic!(
1336 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1337 with params: initial_value={:?} (param set {})",
1338 test_name, val, bits, i, params.initial_value, param_idx
1339 );
1340 }
1341
1342 if bits == 0x33333333_33333333 {
1343 panic!(
1344 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1345 with params: initial_value={:?} (param set {})",
1346 test_name, val, bits, i, params.initial_value, param_idx
1347 );
1348 }
1349 }
1350 }
1351
1352 Ok(())
1353 }
1354
1355 #[cfg(not(debug_assertions))]
1356 fn check_pvi_no_poison(
1357 _test_name: &str,
1358 _kernel: Kernel,
1359 ) -> Result<(), Box<dyn std::error::Error>> {
1360 Ok(())
1361 }
1362
1363 #[test]
1364 fn test_pvi_into_matches_api() {
1365 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1366 let candles = read_candles_from_csv(file_path).expect("load candles");
1367
1368 let input = PviInput::from_candles(&candles, "close", "volume", PviParams::default());
1369
1370 let baseline = pvi(&input).expect("pvi baseline").values;
1371
1372 let mut into_out = vec![0.0f64; baseline.len()];
1373 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1374 {
1375 pvi_into(&input, &mut into_out).expect("pvi_into");
1376 }
1377 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1378 {
1379 pvi_into_slice(&mut into_out, &input, Kernel::Auto).expect("pvi_into_slice");
1380 }
1381
1382 assert_eq!(baseline.len(), into_out.len());
1383
1384 fn eq_or_both_nan(a: f64, b: f64) -> bool {
1385 (a.is_nan() && b.is_nan()) || (a == b) || ((a - b).abs() <= 1e-12)
1386 }
1387 for i in 0..baseline.len() {
1388 assert!(
1389 eq_or_both_nan(baseline[i], into_out[i]),
1390 "Mismatch at index {}: got {}, expected {}",
1391 i,
1392 into_out[i],
1393 baseline[i]
1394 );
1395 }
1396 }
1397
1398 #[cfg(feature = "proptest")]
1399 #[allow(clippy::float_cmp)]
1400 fn check_pvi_property(
1401 test_name: &str,
1402 kernel: Kernel,
1403 ) -> Result<(), Box<dyn std::error::Error>> {
1404 use proptest::prelude::*;
1405 skip_if_unsupported!(kernel, test_name);
1406
1407 let strat = (
1408 prop::collection::vec(
1409 (-1e6f64..1e6f64).prop_filter("finite close", |x| x.is_finite() && x.abs() > 1e-10),
1410 10..400,
1411 ),
1412 prop::collection::vec(
1413 (0f64..1e6f64).prop_filter("finite volume", |x| x.is_finite()),
1414 10..400,
1415 ),
1416 100f64..10000f64,
1417 )
1418 .prop_filter("same length", |(close, volume, _)| {
1419 close.len() == volume.len()
1420 });
1421
1422 proptest::test_runner::TestRunner::default().run(
1423 &strat,
1424 |(close_data, volume_data, initial_value)| {
1425 let params = PviParams {
1426 initial_value: Some(initial_value),
1427 };
1428 let input = PviInput::from_slices(&close_data, &volume_data, params);
1429
1430 let output = match pvi_with_kernel(&input, kernel) {
1431 Ok(o) => o,
1432 Err(_) => return Ok(()),
1433 };
1434 let out = &output.values;
1435
1436 let scalar_output = match pvi_with_kernel(&input, Kernel::Scalar) {
1437 Ok(o) => o,
1438 Err(_) => return Ok(()),
1439 };
1440 let ref_out = &scalar_output.values;
1441
1442 let first_valid_idx = close_data
1443 .iter()
1444 .zip(volume_data.iter())
1445 .position(|(&c, &v)| !c.is_nan() && !v.is_nan());
1446
1447 if let Some(first_idx) = first_valid_idx {
1448 if !out[first_idx].is_nan() {
1449 prop_assert!(
1450 (out[first_idx] - initial_value).abs() < 1e-9,
1451 "First valid PVI value {} should equal initial_value {} at index {}",
1452 out[first_idx],
1453 initial_value,
1454 first_idx
1455 );
1456 }
1457
1458 for i in (first_idx + 1)..close_data.len() {
1459 if !out[i].is_nan() && i > 0 && !out[i - 1].is_nan() {
1460 if !volume_data[i].is_nan() && !volume_data[i - 1].is_nan() {
1461 if volume_data[i] <= volume_data[i - 1] {
1462 prop_assert!(
1463 (out[i] - out[i - 1]).abs() < 1e-9,
1464 "PVI should remain constant when volume doesn't increase: {} != {} at index {}",
1465 out[i], out[i - 1], i
1466 );
1467 }
1468 }
1469 }
1470 }
1471
1472 for i in (first_idx + 1)..close_data.len() {
1473 if !out[i].is_nan() && i > 0 && !out[i - 1].is_nan() {
1474 if !volume_data[i].is_nan() && !volume_data[i - 1].is_nan() {
1475 let volume_increased = volume_data[i] > volume_data[i - 1];
1476 let pvi_changed = (out[i] - out[i - 1]).abs() > 1e-9;
1477
1478 if pvi_changed {
1479 prop_assert!(
1480 volume_increased,
1481 "PVI changed without volume increase at index {}: vol[{}]={} <= vol[{}]={}",
1482 i, i, volume_data[i], i - 1, volume_data[i - 1]
1483 );
1484 }
1485 }
1486 }
1487 }
1488
1489 for i in (first_idx + 1)..close_data.len() {
1490 if !out[i].is_nan()
1491 && i > 0
1492 && !out[i - 1].is_nan()
1493 && !close_data[i].is_nan()
1494 && !close_data[i - 1].is_nan()
1495 && !volume_data[i].is_nan()
1496 && !volume_data[i - 1].is_nan()
1497 {
1498 if volume_data[i] > volume_data[i - 1]
1499 && close_data[i - 1].abs() > 1e-10
1500 {
1501 let expected_change = ((close_data[i] - close_data[i - 1])
1502 / close_data[i - 1])
1503 * out[i - 1];
1504 let expected_pvi = out[i - 1] + expected_change;
1505 prop_assert!(
1506 (out[i] - expected_pvi).abs() < 1e-9,
1507 "PVI calculation error at index {}: expected {} but got {}",
1508 i,
1509 expected_pvi,
1510 out[i]
1511 );
1512 }
1513 }
1514 }
1515
1516 for i in 0..out.len() {
1517 if out[i].is_nan() && ref_out[i].is_nan() {
1518 continue;
1519 }
1520 prop_assert!(
1521 (out[i] - ref_out[i]).abs() < 1e-9,
1522 "Kernel mismatch at index {}: {} ({:?}) vs {} (Scalar)",
1523 i,
1524 out[i],
1525 kernel,
1526 ref_out[i]
1527 );
1528 }
1529
1530 for (i, &val) in out.iter().enumerate() {
1531 if !val.is_nan() {
1532 let bits = val.to_bits();
1533 prop_assert!(
1534 bits != 0x11111111_11111111
1535 && bits != 0x22222222_22222222
1536 && bits != 0x33333333_33333333,
1537 "Found poison value {} (0x{:016X}) at index {}",
1538 val,
1539 bits,
1540 i
1541 );
1542 }
1543 }
1544
1545 if volume_data.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10) {
1546 for &val in out.iter().skip(first_idx) {
1547 if !val.is_nan() {
1548 prop_assert!(
1549 (val - initial_value).abs() < 1e-9,
1550 "PVI should remain at initial_value {} with constant volume, but got {}",
1551 initial_value, val
1552 );
1553 }
1554 }
1555 }
1556
1557 let is_monotonic_increasing = volume_data
1558 .windows(2)
1559 .all(|w| !w[0].is_nan() && !w[1].is_nan() && w[1] > w[0]);
1560
1561 if is_monotonic_increasing && close_data.len() > first_idx + 2 {
1562 let mut last_valid_pvi = out[first_idx];
1563 for i in (first_idx + 1)..out.len() {
1564 if !out[i].is_nan()
1565 && !close_data[i].is_nan()
1566 && !close_data[i - 1].is_nan()
1567 {
1568 if (close_data[i] - close_data[i - 1]).abs() > 1e-10 {
1569 prop_assert!(
1570 (out[i] - last_valid_pvi).abs() > 1e-10,
1571 "PVI should change with monotonic increasing volume and price change at index {}",
1572 i
1573 );
1574 }
1575 last_valid_pvi = out[i];
1576 }
1577 }
1578 }
1579
1580 for i in (first_idx + 1)..close_data.len() {
1581 if !volume_data[i].is_nan()
1582 && !volume_data[i - 1].is_nan()
1583 && volume_data[i] > volume_data[i - 1]
1584 && !close_data[i].is_nan()
1585 && !close_data[i - 1].is_nan()
1586 && close_data[i - 1].abs() > 1e-10
1587 {
1588 if !out[i].is_nan() && i > 0 && !out[i - 1].is_nan() {
1589 let expected_change = ((close_data[i] - close_data[i - 1])
1590 / close_data[i - 1])
1591 * out[i - 1];
1592 let expected_pvi = out[i - 1] + expected_change;
1593
1594 prop_assert!(
1595 (out[i] - expected_pvi).abs() < 1e-9 || out[i].is_infinite(),
1596 "PVI calculation should be correct or handle extreme values at index {}",
1597 i
1598 );
1599 }
1600 }
1601 }
1602
1603 for (i, &val) in out.iter().enumerate() {
1604 if !val.is_nan() {
1605 prop_assert!(
1606 val.is_finite(),
1607 "PVI should be finite, but got {} at index {}",
1608 val,
1609 i
1610 );
1611
1612 if initial_value > 0.0
1613 && val.is_finite()
1614 && val.abs() < initial_value * 100.0
1615 {
1616 prop_assert!(
1617 val >= 0.0 || close_data[..i].iter().any(|&c| c < 0.0),
1618 "PVI unexpectedly negative ({}) with positive initial value {} at index {}",
1619 val, initial_value, i
1620 );
1621 }
1622 }
1623 }
1624 }
1625
1626 Ok(())
1627 },
1628 )?;
1629
1630 Ok(())
1631 }
1632
1633 #[cfg(feature = "proptest")]
1634 generate_all_pvi_tests!(
1635 check_pvi_partial_params,
1636 check_pvi_accuracy,
1637 check_pvi_default_candles,
1638 check_pvi_empty_data,
1639 check_pvi_mismatched_length,
1640 check_pvi_all_values_nan,
1641 check_pvi_not_enough_valid_data,
1642 check_pvi_streaming,
1643 check_pvi_no_poison,
1644 check_pvi_property
1645 );
1646
1647 #[cfg(not(feature = "proptest"))]
1648 generate_all_pvi_tests!(
1649 check_pvi_partial_params,
1650 check_pvi_accuracy,
1651 check_pvi_default_candles,
1652 check_pvi_empty_data,
1653 check_pvi_mismatched_length,
1654 check_pvi_all_values_nan,
1655 check_pvi_not_enough_valid_data,
1656 check_pvi_streaming,
1657 check_pvi_no_poison
1658 );
1659
1660 fn check_batch_default_row(
1661 test: &str,
1662 kernel: Kernel,
1663 ) -> Result<(), Box<dyn std::error::Error>> {
1664 skip_if_unsupported!(kernel, test);
1665 let close_data = [100.0, 102.0, 101.0, 103.0, 103.0, 105.0];
1666 let volume_data = [500.0, 600.0, 500.0, 700.0, 680.0, 900.0];
1667 let output = PviBatchBuilder::new()
1668 .kernel(kernel)
1669 .apply_slices(&close_data, &volume_data)?;
1670 let def = PviParams::default();
1671 let row = output.values_for(&def).expect("default row missing");
1672 assert_eq!(row.len(), close_data.len());
1673 Ok(())
1674 }
1675
1676 macro_rules! gen_batch_tests {
1677 ($fn_name:ident) => {
1678 paste::paste! {
1679 #[test] fn [<$fn_name _scalar>]() {
1680 let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1681 }
1682 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1683 #[test] fn [<$fn_name _avx2>]() {
1684 let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1685 }
1686 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1687 #[test] fn [<$fn_name _avx512>]() {
1688 let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1689 }
1690 #[test] fn [<$fn_name _auto_detect>]() {
1691 let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
1692 }
1693 }
1694 };
1695 }
1696 #[cfg(debug_assertions)]
1697 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
1698 skip_if_unsupported!(kernel, test);
1699
1700 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1701 let c = read_candles_from_csv(file)?;
1702
1703 let test_configs = vec![
1704 (100.0, 500.0, 100.0),
1705 (1000.0, 5000.0, 1000.0),
1706 (10000.0, 50000.0, 10000.0),
1707 (900.0, 1100.0, 50.0),
1708 (0.0, 100.0, 25.0),
1709 (-1000.0, 1000.0, 500.0),
1710 (1.0, 10.0, 1.0),
1711 (999999.0, 1000001.0, 1.0),
1712 ];
1713
1714 for (cfg_idx, &(start, end, step)) in test_configs.iter().enumerate() {
1715 let output = PviBatchBuilder::new()
1716 .kernel(kernel)
1717 .initial_value_range(start, end, step)
1718 .apply_candles(&c, "close", "volume")?;
1719
1720 for (idx, &val) in output.values.iter().enumerate() {
1721 if val.is_nan() {
1722 continue;
1723 }
1724
1725 let bits = val.to_bits();
1726 let row = idx / output.cols;
1727 let col = idx % output.cols;
1728 let combo = &output.combos[row];
1729
1730 if bits == 0x11111111_11111111 {
1731 panic!(
1732 "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1733 at row {} col {} (flat index {}) with params: initial_value={}",
1734 test,
1735 cfg_idx,
1736 val,
1737 bits,
1738 row,
1739 col,
1740 idx,
1741 combo.initial_value.unwrap_or(1000.0)
1742 );
1743 }
1744
1745 if bits == 0x22222222_22222222 {
1746 panic!(
1747 "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
1748 at row {} col {} (flat index {}) with params: initial_value={}",
1749 test,
1750 cfg_idx,
1751 val,
1752 bits,
1753 row,
1754 col,
1755 idx,
1756 combo.initial_value.unwrap_or(1000.0)
1757 );
1758 }
1759
1760 if bits == 0x33333333_33333333 {
1761 panic!(
1762 "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
1763 at row {} col {} (flat index {}) with params: initial_value={}",
1764 test,
1765 cfg_idx,
1766 val,
1767 bits,
1768 row,
1769 col,
1770 idx,
1771 combo.initial_value.unwrap_or(1000.0)
1772 );
1773 }
1774 }
1775 }
1776
1777 Ok(())
1778 }
1779
1780 #[cfg(not(debug_assertions))]
1781 fn check_batch_no_poison(
1782 _test: &str,
1783 _kernel: Kernel,
1784 ) -> Result<(), Box<dyn std::error::Error>> {
1785 Ok(())
1786 }
1787
1788 gen_batch_tests!(check_batch_default_row);
1789 gen_batch_tests!(check_batch_no_poison);
1790}
1791
1792#[cfg(feature = "python")]
1793#[pyfunction(name = "pvi")]
1794#[pyo3(signature = (close, volume, initial_value=None, kernel=None))]
1795pub fn pvi_py<'py>(
1796 py: Python<'py>,
1797 close: PyReadonlyArray1<'py, f64>,
1798 volume: PyReadonlyArray1<'py, f64>,
1799 initial_value: Option<f64>,
1800 kernel: Option<&str>,
1801) -> PyResult<Bound<'py, PyArray1<f64>>> {
1802 use numpy::{IntoPyArray, PyArrayMethods};
1803
1804 let close_slice = close.as_slice()?;
1805 let volume_slice = volume.as_slice()?;
1806 let kern = validate_kernel(kernel, false)?;
1807
1808 let params = PviParams { initial_value };
1809 let input = PviInput::from_slices(close_slice, volume_slice, params);
1810
1811 let result_vec: Vec<f64> = py
1812 .allow_threads(|| pvi_with_kernel(&input, kern).map(|o| o.values))
1813 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1814
1815 Ok(result_vec.into_pyarray(py))
1816}
1817
1818#[cfg(feature = "python")]
1819#[pyfunction(name = "pvi_batch")]
1820#[pyo3(signature = (close, volume, initial_value_range, kernel=None))]
1821pub fn pvi_batch_py<'py>(
1822 py: Python<'py>,
1823 close: PyReadonlyArray1<'py, f64>,
1824 volume: PyReadonlyArray1<'py, f64>,
1825 initial_value_range: (f64, f64, f64),
1826 kernel: Option<&str>,
1827) -> PyResult<Bound<'py, PyDict>> {
1828 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1829
1830 let close_slice = close.as_slice()?;
1831 let volume_slice = volume.as_slice()?;
1832
1833 let sweep = PviBatchRange {
1834 initial_value: initial_value_range,
1835 };
1836
1837 let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1838 let rows = combos.len();
1839 let cols = close_slice.len();
1840
1841 let out_arr = unsafe { PyArray1::<f64>::new(py, [rows * cols], false) };
1842 let slice_out = unsafe { out_arr.as_slice_mut()? };
1843
1844 let kern = validate_kernel(kernel, true)?;
1845
1846 let combos = py
1847 .allow_threads(|| {
1848 let kernel = match kern {
1849 Kernel::Auto => detect_best_batch_kernel(),
1850 k => k,
1851 };
1852 let simd = match kernel {
1853 Kernel::Avx512Batch => Kernel::Avx512,
1854 Kernel::Avx2Batch => Kernel::Avx2,
1855 Kernel::ScalarBatch => Kernel::Scalar,
1856 _ => unreachable!(),
1857 };
1858 pvi_batch_inner_into(close_slice, volume_slice, &sweep, simd, true, slice_out)
1859 })
1860 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1861
1862 let dict = PyDict::new(py);
1863 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1864 dict.set_item(
1865 "initial_values",
1866 combos
1867 .iter()
1868 .map(|p| p.initial_value.unwrap_or(1000.0))
1869 .collect::<Vec<_>>()
1870 .into_pyarray(py),
1871 )?;
1872
1873 Ok(dict)
1874}
1875
1876#[cfg(feature = "python")]
1877#[pyclass(name = "PviStream")]
1878pub struct PviStreamPy {
1879 stream: PviStream,
1880}
1881
1882#[cfg(feature = "python")]
1883#[pymethods]
1884impl PviStreamPy {
1885 #[new]
1886 #[pyo3(signature = (initial_value=None))]
1887 fn new(initial_value: Option<f64>) -> PyResult<Self> {
1888 let params = PviParams { initial_value };
1889 let stream =
1890 PviStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
1891 Ok(PviStreamPy { stream })
1892 }
1893
1894 fn update(&mut self, close: f64, volume: f64) -> Option<f64> {
1895 self.stream.update(close, volume)
1896 }
1897}
1898
1899#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1900#[wasm_bindgen]
1901pub fn pvi_js(close: &[f64], volume: &[f64], initial_value: f64) -> Result<Vec<f64>, JsValue> {
1902 let params = PviParams {
1903 initial_value: Some(initial_value),
1904 };
1905 let input = PviInput::from_slices(close, volume, params);
1906
1907 let mut output = vec![0.0; close.len()];
1908 pvi_into_slice(&mut output, &input, Kernel::Auto)
1909 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1910
1911 Ok(output)
1912}
1913
1914#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1915#[wasm_bindgen]
1916pub fn pvi_into(
1917 close_ptr: *const f64,
1918 volume_ptr: *const f64,
1919 out_ptr: *mut f64,
1920 len: usize,
1921 initial_value: f64,
1922) -> Result<(), JsValue> {
1923 if close_ptr.is_null() || volume_ptr.is_null() || out_ptr.is_null() {
1924 return Err(JsValue::from_str("Null pointer provided"));
1925 }
1926
1927 unsafe {
1928 let close = std::slice::from_raw_parts(close_ptr, len);
1929 let volume = std::slice::from_raw_parts(volume_ptr, len);
1930
1931 let params = PviParams {
1932 initial_value: Some(initial_value),
1933 };
1934 let input = PviInput::from_slices(close, volume, params);
1935
1936 if close_ptr == out_ptr || volume_ptr == out_ptr {
1937 let mut temp = vec![0.0; len];
1938 pvi_into_slice(&mut temp, &input, Kernel::Auto)
1939 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1940 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1941 out.copy_from_slice(&temp);
1942 } else {
1943 let out = std::slice::from_raw_parts_mut(out_ptr, len);
1944 pvi_into_slice(out, &input, Kernel::Auto)
1945 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1946 }
1947 Ok(())
1948 }
1949}
1950
1951#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1952#[wasm_bindgen]
1953pub fn pvi_alloc(len: usize) -> *mut f64 {
1954 let mut vec = Vec::<f64>::with_capacity(len);
1955 let ptr = vec.as_mut_ptr();
1956 std::mem::forget(vec);
1957 ptr
1958}
1959
1960#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1961#[wasm_bindgen]
1962pub fn pvi_free(ptr: *mut f64, len: usize) {
1963 if !ptr.is_null() {
1964 unsafe {
1965 let _ = Vec::from_raw_parts(ptr, len, len);
1966 }
1967 }
1968}
1969
1970#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1971#[derive(Serialize, Deserialize)]
1972pub struct PviBatchConfig {
1973 pub initial_value_range: (f64, f64, f64),
1974}
1975
1976#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1977#[derive(Serialize, Deserialize)]
1978pub struct PviBatchJsOutput {
1979 pub values: Vec<f64>,
1980 pub combos: Vec<PviParams>,
1981 pub rows: usize,
1982 pub cols: usize,
1983}
1984
1985#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1986#[wasm_bindgen(js_name = pvi_batch)]
1987pub fn pvi_batch_js(close: &[f64], volume: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1988 let config: PviBatchConfig = serde_wasm_bindgen::from_value(config)
1989 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
1990
1991 let sweep = PviBatchRange {
1992 initial_value: config.initial_value_range,
1993 };
1994
1995 let output = pvi_batch_with_kernel(close, volume, &sweep, Kernel::Auto)
1996 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1997
1998 let js_output = PviBatchJsOutput {
1999 values: output.values,
2000 combos: output.combos,
2001 rows: output.rows,
2002 cols: output.cols,
2003 };
2004
2005 serde_wasm_bindgen::to_value(&js_output)
2006 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2007}
2008
2009#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2010#[wasm_bindgen]
2011pub fn pvi_batch_into(
2012 close_ptr: *const f64,
2013 volume_ptr: *const f64,
2014 out_ptr: *mut f64,
2015 len: usize,
2016 initial_value_start: f64,
2017 initial_value_end: f64,
2018 initial_value_step: f64,
2019) -> Result<usize, JsValue> {
2020 if close_ptr.is_null() || volume_ptr.is_null() || out_ptr.is_null() {
2021 return Err(JsValue::from_str("null pointer passed to pvi_batch_into"));
2022 }
2023
2024 unsafe {
2025 let close = std::slice::from_raw_parts(close_ptr, len);
2026 let volume = std::slice::from_raw_parts(volume_ptr, len);
2027
2028 let sweep = PviBatchRange {
2029 initial_value: (initial_value_start, initial_value_end, initial_value_step),
2030 };
2031
2032 let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
2033 let rows = combos.len();
2034 let total = rows
2035 .checked_mul(len)
2036 .ok_or_else(|| JsValue::from_str("pvi_batch_into: rows*len overflow"))?;
2037 let out = std::slice::from_raw_parts_mut(out_ptr, total);
2038
2039 let kernel = detect_best_batch_kernel();
2040 let simd = match kernel {
2041 Kernel::Avx512Batch => Kernel::Avx512,
2042 Kernel::Avx2Batch => Kernel::Avx2,
2043 Kernel::ScalarBatch => Kernel::Scalar,
2044 _ => unreachable!(),
2045 };
2046
2047 pvi_batch_inner_into(close, volume, &sweep, simd, true, out)
2048 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2049
2050 Ok(rows)
2051 }
2052}
2053
2054#[cfg(all(feature = "python", feature = "cuda"))]
2055use crate::cuda::cuda_available;
2056#[cfg(all(feature = "python", feature = "cuda"))]
2057use crate::cuda::moving_averages::DeviceArrayF32;
2058#[cfg(all(feature = "python", feature = "cuda"))]
2059use crate::cuda::CudaPvi;
2060#[cfg(all(feature = "python", feature = "cuda"))]
2061use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
2062#[cfg(all(feature = "python", feature = "cuda"))]
2063use cust::context::Context;
2064#[cfg(all(feature = "python", feature = "cuda"))]
2065use std::sync::Arc;
2066
2067#[cfg(all(feature = "python", feature = "cuda"))]
2068#[pyclass(module = "ta_indicators.cuda", name = "PviDeviceArrayF32", unsendable)]
2069pub struct PviDeviceArrayF32Py {
2070 pub(crate) inner: Option<DeviceArrayF32>,
2071 pub(crate) _ctx: Arc<Context>,
2072 pub(crate) device_id: u32,
2073}
2074
2075#[cfg(all(feature = "python", feature = "cuda"))]
2076#[pymethods]
2077impl PviDeviceArrayF32Py {
2078 #[getter]
2079 fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
2080 let inner = self
2081 .inner
2082 .as_ref()
2083 .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
2084 let d = PyDict::new(py);
2085 let itemsize = std::mem::size_of::<f32>();
2086 d.set_item("shape", (inner.rows, inner.cols))?;
2087 d.set_item("typestr", "<f4")?;
2088 d.set_item("strides", (inner.cols * itemsize, itemsize))?;
2089 d.set_item("data", (inner.device_ptr() as usize, false))?;
2090
2091 d.set_item("version", 3)?;
2092 Ok(d)
2093 }
2094
2095 fn __dlpack_device__(&self) -> (i32, i32) {
2096 (2, self.device_id as i32)
2097 }
2098
2099 #[pyo3(signature=(stream=None, max_version=None, dl_device=None, copy=None))]
2100 fn __dlpack__<'py>(
2101 &mut self,
2102 py: Python<'py>,
2103 stream: Option<PyObject>,
2104 max_version: Option<PyObject>,
2105 dl_device: Option<PyObject>,
2106 copy: Option<PyObject>,
2107 ) -> PyResult<PyObject> {
2108 if let Some(s_obj) = stream.as_ref() {
2109 if let Ok(s) = s_obj.extract::<usize>(py) {
2110 if s == 0 {
2111 return Err(PyValueError::new_err(
2112 "__dlpack__ stream=0 is invalid for CUDA",
2113 ));
2114 }
2115 }
2116 }
2117
2118 let (kdl, alloc_dev) = self.__dlpack_device__();
2119 if let Some(dev_obj) = dl_device.as_ref() {
2120 if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
2121 if dev_ty != kdl || dev_id != alloc_dev {
2122 return Err(PyValueError::new_err(
2123 "dl_device mismatch; cross-device copy not supported for PviDeviceArrayF32",
2124 ));
2125 }
2126 }
2127 }
2128
2129 if copy.as_ref().and_then(|c| c.extract::<bool>(py).ok()) == Some(true) {
2130 return Err(PyValueError::new_err(
2131 "copy=True not supported for PviDeviceArrayF32",
2132 ));
2133 }
2134
2135 let _ = stream;
2136
2137 let inner = self
2138 .inner
2139 .take()
2140 .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;
2141
2142 let rows = inner.rows;
2143 let cols = inner.cols;
2144 let buf = inner.buf;
2145
2146 let max_version_bound = max_version.map(|obj| obj.into_bound(py));
2147
2148 export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
2149 }
2150}
2151
2152#[cfg(all(feature = "python", feature = "cuda"))]
2153impl PviDeviceArrayF32Py {
2154 pub fn new_from_rust(inner: DeviceArrayF32, ctx_guard: Arc<Context>, device_id: u32) -> Self {
2155 Self {
2156 inner: Some(inner),
2157 _ctx: ctx_guard,
2158 device_id,
2159 }
2160 }
2161}
2162
2163#[cfg(all(feature = "python", feature = "cuda"))]
2164#[pyfunction(name = "pvi_cuda_batch_dev")]
2165#[pyo3(signature = (close, volume, initial_values, device_id=0))]
2166pub fn pvi_cuda_batch_dev_py(
2167 py: Python<'_>,
2168 close: PyReadonlyArray1<'_, f32>,
2169 volume: PyReadonlyArray1<'_, f32>,
2170 initial_values: PyReadonlyArray1<'_, f32>,
2171 device_id: usize,
2172) -> PyResult<PviDeviceArrayF32Py> {
2173 if !cuda_available() {
2174 return Err(PyValueError::new_err("CUDA not available"));
2175 }
2176 let close_slice = close.as_slice()?;
2177 let volume_slice = volume.as_slice()?;
2178 let inits_slice = initial_values.as_slice()?;
2179 if close_slice.len() != volume_slice.len() {
2180 return Err(PyValueError::new_err("mismatched input lengths"));
2181 }
2182 if inits_slice.is_empty() {
2183 return Err(PyValueError::new_err("initial_values must be non-empty"));
2184 }
2185 let (inner, ctx, dev_id) = py.allow_threads(|| {
2186 let cuda = CudaPvi::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2187 let ctx = cuda.context_arc();
2188 let dev_id = cuda.device_id();
2189 let arr = cuda
2190 .pvi_batch_dev(close_slice, volume_slice, inits_slice)
2191 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2192 Ok::<_, pyo3::PyErr>((arr, ctx, dev_id))
2193 })?;
2194 Ok(PviDeviceArrayF32Py::new_from_rust(inner, ctx, dev_id))
2195}
2196
2197#[cfg(all(feature = "python", feature = "cuda"))]
2198#[pyfunction(name = "pvi_cuda_many_series_one_param_dev")]
2199#[pyo3(signature = (close_tm, volume_tm, cols, rows, initial_value, device_id=0))]
2200pub fn pvi_cuda_many_series_one_param_dev_py(
2201 py: Python<'_>,
2202 close_tm: PyReadonlyArray1<'_, f32>,
2203 volume_tm: PyReadonlyArray1<'_, f32>,
2204 cols: usize,
2205 rows: usize,
2206 initial_value: f32,
2207 device_id: usize,
2208) -> PyResult<PviDeviceArrayF32Py> {
2209 if !cuda_available() {
2210 return Err(PyValueError::new_err("CUDA not available"));
2211 }
2212 let close_slice = close_tm.as_slice()?;
2213 let volume_slice = volume_tm.as_slice()?;
2214 let (inner, ctx, dev_id) = py.allow_threads(|| {
2215 let cuda = CudaPvi::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2216 let ctx = cuda.context_arc();
2217 let dev_id = cuda.device_id();
2218 let arr = cuda
2219 .pvi_many_series_one_param_time_major_dev(
2220 close_slice,
2221 volume_slice,
2222 cols,
2223 rows,
2224 initial_value,
2225 )
2226 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2227 Ok::<_, pyo3::PyErr>((arr, ctx, dev_id))
2228 })?;
2229 Ok(PviDeviceArrayF32Py::new_from_rust(inner, ctx, dev_id))
2230}