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(feature = "python")]
8use crate::utilities::kernel_validation::validate_kernel;
9use aligned_vec::{AVec, CACHELINE_ALIGN};
10#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
11use core::arch::x86_64::*;
12#[cfg(feature = "python")]
13use numpy::{IntoPyArray, PyArray1};
14#[cfg(feature = "python")]
15use pyo3::exceptions::PyValueError;
16#[cfg(feature = "python")]
17use pyo3::prelude::*;
18#[cfg(feature = "python")]
19use pyo3::types::PyDict;
20#[cfg(not(target_arch = "wasm32"))]
21use rayon::prelude::*;
22#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
23use serde::{Deserialize, Serialize};
24use std::mem::{ManuallyDrop, MaybeUninit};
25use thiserror::Error;
26#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
27use wasm_bindgen::prelude::*;
28
29use crate::indicators::sma::{sma, SmaData, SmaError, SmaInput, SmaParams};
30
31#[derive(Debug, Clone)]
32pub enum VpciData<'a> {
33 Candles {
34 candles: &'a Candles,
35 close_source: &'a str,
36 volume_source: &'a str,
37 },
38 Slices {
39 close: &'a [f64],
40 volume: &'a [f64],
41 },
42}
43
44#[derive(Debug, Clone)]
45pub struct VpciOutput {
46 pub vpci: Vec<f64>,
47 pub vpcis: Vec<f64>,
48}
49
50#[derive(Debug, Clone)]
51#[cfg_attr(
52 all(target_arch = "wasm32", feature = "wasm"),
53 derive(Serialize, Deserialize)
54)]
55pub struct VpciParams {
56 pub short_range: Option<usize>,
57 pub long_range: Option<usize>,
58}
59
60impl Default for VpciParams {
61 fn default() -> Self {
62 Self {
63 short_range: Some(5),
64 long_range: Some(25),
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
70pub struct VpciInput<'a> {
71 pub data: VpciData<'a>,
72 pub params: VpciParams,
73}
74
75impl<'a> VpciInput<'a> {
76 #[inline]
77 pub fn from_candles(
78 candles: &'a Candles,
79 close_source: &'a str,
80 volume_source: &'a str,
81 params: VpciParams,
82 ) -> Self {
83 Self {
84 data: VpciData::Candles {
85 candles,
86 close_source,
87 volume_source,
88 },
89 params,
90 }
91 }
92
93 #[inline]
94 pub fn from_slices(close: &'a [f64], volume: &'a [f64], params: VpciParams) -> Self {
95 Self {
96 data: VpciData::Slices { close, volume },
97 params,
98 }
99 }
100
101 #[inline]
102 pub fn with_default_candles(candles: &'a Candles) -> Self {
103 Self {
104 data: VpciData::Candles {
105 candles,
106 close_source: "close",
107 volume_source: "volume",
108 },
109 params: VpciParams::default(),
110 }
111 }
112
113 #[inline]
114 pub fn get_short_range(&self) -> usize {
115 self.params.short_range.unwrap_or(5)
116 }
117 #[inline]
118 pub fn get_long_range(&self) -> usize {
119 self.params.long_range.unwrap_or(25)
120 }
121}
122
123#[derive(Copy, Clone, Debug)]
124pub struct VpciBuilder {
125 short_range: Option<usize>,
126 long_range: Option<usize>,
127 kernel: Kernel,
128}
129
130impl Default for VpciBuilder {
131 fn default() -> Self {
132 Self {
133 short_range: None,
134 long_range: None,
135 kernel: Kernel::Auto,
136 }
137 }
138}
139
140impl VpciBuilder {
141 #[inline(always)]
142 pub fn new() -> Self {
143 Self::default()
144 }
145 #[inline(always)]
146 pub fn short_range(mut self, n: usize) -> Self {
147 self.short_range = Some(n);
148 self
149 }
150 #[inline(always)]
151 pub fn long_range(mut self, n: usize) -> Self {
152 self.long_range = Some(n);
153 self
154 }
155 #[inline(always)]
156 pub fn kernel(mut self, k: Kernel) -> Self {
157 self.kernel = k;
158 self
159 }
160 #[inline(always)]
161 pub fn apply(self, c: &Candles) -> Result<VpciOutput, VpciError> {
162 let p = VpciParams {
163 short_range: self.short_range,
164 long_range: self.long_range,
165 };
166 let i = VpciInput::from_candles(c, "close", "volume", p);
167 vpci_with_kernel(&i, self.kernel)
168 }
169 #[inline(always)]
170 pub fn apply_slices(self, close: &[f64], volume: &[f64]) -> Result<VpciOutput, VpciError> {
171 let p = VpciParams {
172 short_range: self.short_range,
173 long_range: self.long_range,
174 };
175 let i = VpciInput::from_slices(close, volume, p);
176 vpci_with_kernel(&i, self.kernel)
177 }
178}
179
180#[derive(Clone, Debug)]
181pub struct VpciStream {
182 short_range: usize,
183 long_range: usize,
184
185 close_buf: Vec<f64>,
186 volume_buf: Vec<f64>,
187 head: usize,
188 count: usize,
189
190 sum_c_long: f64,
191 sum_v_long: f64,
192 sum_cv_long: f64,
193
194 sum_c_short: f64,
195 sum_v_short: f64,
196 sum_cv_short: f64,
197
198 vpci_vol_buf: Vec<f64>,
199 vpci_vol_head: usize,
200 sum_vpci_vol_short: f64,
201
202 inv_long: f64,
203 inv_short: f64,
204}
205
206impl VpciStream {
207 pub fn try_new(params: VpciParams) -> Result<Self, VpciError> {
208 let short_range = params.short_range.unwrap_or(5);
209 let long_range = params.long_range.unwrap_or(25);
210
211 if short_range == 0 || long_range == 0 {
212 return Err(VpciError::InvalidPeriod {
213 period: 0,
214 data_len: 0,
215 });
216 }
217 if short_range > long_range {
218 return Err(VpciError::InvalidPeriod {
219 period: short_range,
220 data_len: long_range,
221 });
222 }
223
224 Ok(Self {
225 short_range,
226 long_range,
227 close_buf: vec![0.0; long_range],
228 volume_buf: vec![0.0; long_range],
229 head: 0,
230 count: 0,
231
232 sum_c_long: 0.0,
233 sum_v_long: 0.0,
234 sum_cv_long: 0.0,
235
236 sum_c_short: 0.0,
237 sum_v_short: 0.0,
238 sum_cv_short: 0.0,
239
240 vpci_vol_buf: vec![0.0; short_range],
241 vpci_vol_head: 0,
242 sum_vpci_vol_short: 0.0,
243
244 inv_long: 1.0 / (long_range as f64),
245 inv_short: 1.0 / (short_range as f64),
246 })
247 }
248
249 #[inline(always)]
250 fn zf(x: f64) -> f64 {
251 if x.is_finite() {
252 x
253 } else {
254 0.0
255 }
256 }
257
258 #[inline(always)]
259 pub fn update(&mut self, close: f64, volume: f64) -> Option<(f64, f64)> {
260 let c_new = Self::zf(close);
261 let v_new = Self::zf(volume);
262 let cv_new = c_new * v_new;
263
264 let i = self.head;
265 let j = (self.head + self.long_range - self.short_range) % self.long_range;
266
267 let c_old_L = Self::zf(self.close_buf[i]);
268 let v_old_L = Self::zf(self.volume_buf[i]);
269 let cv_old_L = c_old_L * v_old_L;
270
271 let c_old_S = Self::zf(self.close_buf[j]);
272 let v_old_S = Self::zf(self.volume_buf[j]);
273 let cv_old_S = c_old_S * v_old_S;
274
275 self.close_buf[i] = close;
276 self.volume_buf[i] = volume;
277
278 self.head = (self.head + 1) % self.long_range;
279 self.count = self.count.saturating_add(1);
280
281 self.sum_c_long += c_new - c_old_L;
282 self.sum_v_long += v_new - v_old_L;
283 self.sum_cv_long += cv_new - cv_old_L;
284
285 self.sum_c_short += c_new - c_old_S;
286 self.sum_v_short += v_new - v_old_S;
287 self.sum_cv_short += cv_new - cv_old_S;
288
289 if self.count < self.long_range {
290 return None;
291 }
292
293 let sv_l = self.sum_v_long;
294 let sc_l = self.sum_c_long;
295 let scv_l = self.sum_cv_long;
296 let sma_l = sc_l * self.inv_long;
297 let vwma_l = if sv_l != 0.0 { scv_l / sv_l } else { f64::NAN };
298 let vpc = vwma_l - sma_l;
299
300 let sv_s = self.sum_v_short;
301 let sc_s = self.sum_c_short;
302 let scv_s = self.sum_cv_short;
303
304 let vpr = if sv_s != 0.0 && sc_s != 0.0 {
305 (scv_s * (self.short_range as f64)) / (sv_s * sc_s)
306 } else {
307 f64::NAN
308 };
309
310 let vm = if sv_l != 0.0 {
311 (sv_s * (self.long_range as f64)) / (sv_l * (self.short_range as f64))
312 } else {
313 f64::NAN
314 };
315
316 let vpci = vpc * vpr * vm;
317
318 let vpci_vol_new = if vpci.is_finite() { vpci * v_new } else { 0.0 };
319 let vpci_vol_old = self.vpci_vol_buf[self.vpci_vol_head];
320 self.sum_vpci_vol_short += vpci_vol_new - vpci_vol_old;
321 self.vpci_vol_buf[self.vpci_vol_head] = vpci_vol_new;
322 self.vpci_vol_head = (self.vpci_vol_head + 1) % self.short_range;
323
324 let denom = sv_s * self.inv_short;
325 let vpcis = if denom != 0.0 && denom.is_finite() {
326 (self.sum_vpci_vol_short * self.inv_short) / denom
327 } else {
328 f64::NAN
329 };
330
331 Some((vpci, vpcis))
332 }
333}
334
335#[derive(Debug, Error)]
336pub enum VpciError {
337 #[error("vpci: Empty input data (All close or volume values are NaN).")]
338 EmptyInputData,
339
340 #[error("vpci: All close or volume values are NaN.")]
341 AllValuesNaN,
342
343 #[error("vpci: Invalid range (Invalid period): period = {period}, data length = {data_len}")]
344 InvalidPeriod { period: usize, data_len: usize },
345
346 #[error("vpci: Not enough valid data: needed = {needed}, valid = {valid}")]
347 NotEnoughValidData { needed: usize, valid: usize },
348
349 #[error("vpci: output length mismatch: expected = {expected}, got = {got}")]
350 OutputLengthMismatch { expected: usize, got: usize },
351
352 #[error("vpci: invalid kernel for batch: {0:?}")]
353 InvalidKernelForBatch(Kernel),
354
355 #[error("vpci: invalid range: start={start}, end={end}, step={step}")]
356 InvalidRange {
357 start: usize,
358 end: usize,
359 step: usize,
360 },
361
362 #[error("vpci: invalid input: {0}")]
363 InvalidInput(String),
364
365 #[error("vpci: SMA error: {0}")]
366 SmaError(#[from] SmaError),
367
368 #[error("vpci: mismatched input lengths: close = {close_len}, volume = {volume_len}")]
369 MismatchedInputLengths { close_len: usize, volume_len: usize },
370
371 #[error("vpci: Mismatched output lengths: vpci_len = {vpci_len}, vpcis_len = {vpcis_len}, expected = {data_len}")]
372 MismatchedOutputLengths {
373 vpci_len: usize,
374 vpcis_len: usize,
375 data_len: usize,
376 },
377
378 #[error("vpci: Kernel not available")]
379 KernelNotAvailable,
380}
381
382#[inline(always)]
383fn first_valid_both(close: &[f64], volume: &[f64]) -> Option<usize> {
384 close
385 .iter()
386 .zip(volume)
387 .position(|(c, v)| !c.is_nan() && !v.is_nan())
388}
389
390#[inline(always)]
391fn ensure_same_len(close: &[f64], volume: &[f64]) -> Result<(), VpciError> {
392 if close.len() != volume.len() {
393 return Err(VpciError::MismatchedInputLengths {
394 close_len: close.len(),
395 volume_len: volume.len(),
396 });
397 }
398 Ok(())
399}
400
401#[inline(always)]
402fn build_prefix_sums(close: &[f64], volume: &[f64]) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
403 let n = close.len();
404 let mut ps_close = vec![0.0; n + 1];
405 let mut ps_vol = vec![0.0; n + 1];
406 let mut ps_cv = vec![0.0; n + 1];
407
408 for i in 0..n {
409 let c = close[i];
410 let v = volume[i];
411
412 let c_val = if c.is_finite() { c } else { 0.0 };
413 let v_val = if v.is_finite() { v } else { 0.0 };
414 ps_close[i + 1] = ps_close[i] + c_val;
415 ps_vol[i + 1] = ps_vol[i] + v_val;
416 ps_cv[i + 1] = ps_cv[i] + c_val * v_val;
417 }
418 (ps_close, ps_vol, ps_cv)
419}
420
421#[inline(always)]
422fn window_sum(ps: &[f64], start: usize, end_inclusive: usize) -> f64 {
423 let a = start;
424 let b = end_inclusive + 1;
425 ps[b] - ps[a]
426}
427
428#[inline(always)]
429fn vpci_prepare<'a>(
430 input: &'a VpciInput,
431 kernel: Kernel,
432) -> Result<(&'a [f64], &'a [f64], usize, usize, usize, Kernel), VpciError> {
433 let (close, volume) = match &input.data {
434 VpciData::Candles {
435 candles,
436 close_source,
437 volume_source,
438 } => (
439 source_type(candles, close_source),
440 source_type(candles, volume_source),
441 ),
442 VpciData::Slices { close, volume } => (*close, *volume),
443 };
444
445 ensure_same_len(close, volume)?;
446
447 let len = close.len();
448 if len == 0 {
449 return Err(VpciError::EmptyInputData);
450 }
451 let first = first_valid_both(close, volume).ok_or(VpciError::AllValuesNaN)?;
452
453 let short = input.get_short_range();
454 let long = input.get_long_range();
455 if short == 0 || long == 0 || short > len || long > len {
456 return Err(VpciError::InvalidPeriod {
457 period: short.max(long),
458 data_len: len,
459 });
460 }
461 if short > long {
462 return Err(VpciError::InvalidPeriod {
463 period: short,
464 data_len: long,
465 });
466 }
467 if (len - first) < long {
468 return Err(VpciError::NotEnoughValidData {
469 needed: long,
470 valid: len - first,
471 });
472 }
473
474 let chosen = match kernel {
475 Kernel::Auto => Kernel::Scalar,
476 k => k,
477 };
478
479 Ok((close, volume, first, short, long, chosen))
480}
481
482#[inline(always)]
483fn vpci_scalar_into_from_psums(
484 close: &[f64],
485 volume: &[f64],
486 first: usize,
487 short: usize,
488 long: usize,
489 ps_close: &[f64],
490 ps_vol: &[f64],
491 ps_cv: &[f64],
492 vpci_out: &mut [f64],
493 vpcis_out: &mut [f64],
494) {
495 debug_assert_eq!(close.len(), volume.len());
496 let n = close.len();
497 let warmup = first + long - 1;
498 if warmup >= n {
499 return;
500 }
501
502 #[inline(always)]
503 fn zf(x: f64) -> f64 {
504 if x.is_finite() {
505 x
506 } else {
507 0.0
508 }
509 }
510
511 let inv_long = 1.0 / (long as f64);
512 let inv_short = 1.0 / (short as f64);
513
514 let mut sum_vpci_vol_short = 0.0;
515
516 unsafe {
517 let pc = ps_close.as_ptr();
518 let pv = ps_vol.as_ptr();
519 let pcv = ps_cv.as_ptr();
520 let vptr = volume.as_ptr();
521 let vpci_ptr = vpci_out.as_mut_ptr();
522 let vpcis_ptr = vpcis_out.as_mut_ptr();
523
524 let mut i = warmup;
525 while i < n {
526 let end = i + 1;
527 let long_start = end.saturating_sub(long);
528 let short_start = end.saturating_sub(short);
529
530 let sc_l = *pc.add(end) - *pc.add(long_start);
531 let sv_l = *pv.add(end) - *pv.add(long_start);
532 let scv_l = *pcv.add(end) - *pcv.add(long_start);
533
534 let sc_s = *pc.add(end) - *pc.add(short_start);
535 let sv_s = *pv.add(end) - *pv.add(short_start);
536 let scv_s = *pcv.add(end) - *pcv.add(short_start);
537
538 let sma_l = sc_l * inv_long;
539 let sma_s = sc_s * inv_short;
540 let sma_v_l = sv_l * inv_long;
541 let sma_v_s = sv_s * inv_short;
542
543 let vwma_l = if sv_l != 0.0 { scv_l / sv_l } else { f64::NAN };
544 let vwma_s = if sv_s != 0.0 { scv_s / sv_s } else { f64::NAN };
545
546 let vpc = vwma_l - sma_l;
547 let vpr = if sma_s != 0.0 {
548 vwma_s / sma_s
549 } else {
550 f64::NAN
551 };
552 let vm = if sma_v_l != 0.0 {
553 sma_v_s / sma_v_l
554 } else {
555 f64::NAN
556 };
557
558 let vpci = vpc * vpr * vm;
559 *vpci_ptr.add(i) = vpci;
560
561 let v_i = *vptr.add(i);
562 sum_vpci_vol_short += zf(vpci) * zf(v_i);
563 if i >= warmup + short {
564 let rm_idx = i - short;
565 let vpci_rm = *vpci_ptr.add(rm_idx);
566 let v_rm = *vptr.add(rm_idx);
567 sum_vpci_vol_short -= zf(vpci_rm) * zf(v_rm);
568 }
569
570 let denom = sma_v_s;
571 *vpcis_ptr.add(i) = if denom != 0.0 && denom.is_finite() {
572 (sum_vpci_vol_short * inv_short) / denom
573 } else {
574 f64::NAN
575 };
576
577 i += 1;
578 }
579 }
580}
581
582#[inline(always)]
583fn vpci_compute_into(
584 close: &[f64],
585 volume: &[f64],
586 first: usize,
587 short: usize,
588 long: usize,
589 kernel: Kernel,
590 vpci_out: &mut [f64],
591 vpcis_out: &mut [f64],
592) {
593 let (ps_c, ps_v, ps_cv) = build_prefix_sums(close, volume);
594 match kernel {
595 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
596 Kernel::Avx512 => unsafe {
597 vpci_avx512_into_from_psums(
598 close, volume, first, short, long, &ps_c, &ps_v, &ps_cv, vpci_out, vpcis_out,
599 );
600 },
601 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
602 Kernel::Avx2 => unsafe {
603 vpci_avx2_into_from_psums(
604 close, volume, first, short, long, &ps_c, &ps_v, &ps_cv, vpci_out, vpcis_out,
605 );
606 },
607 _ => {
608 vpci_scalar_into_from_psums(
609 close, volume, first, short, long, &ps_c, &ps_v, &ps_cv, vpci_out, vpcis_out,
610 );
611 }
612 }
613}
614
615#[inline]
616pub fn vpci(input: &VpciInput) -> Result<VpciOutput, VpciError> {
617 vpci_with_kernel(input, Kernel::Auto)
618}
619
620pub fn vpci_with_kernel(input: &VpciInput, kernel: Kernel) -> Result<VpciOutput, VpciError> {
621 let (close, volume, first, short, long, chosen) = vpci_prepare(input, kernel)?;
622
623 let len = close.len();
624 let warmup = first + long - 1;
625 let mut vpci = alloc_with_nan_prefix(len, warmup);
626 let mut vpcis = alloc_with_nan_prefix(len, warmup);
627
628 vpci_compute_into(
629 close, volume, first, short, long, chosen, &mut vpci, &mut vpcis,
630 );
631
632 Ok(VpciOutput { vpci, vpcis })
633}
634
635#[inline]
636pub fn vpci_into_slice(
637 vpci_dst: &mut [f64],
638 vpcis_dst: &mut [f64],
639 input: &VpciInput,
640 kernel: Kernel,
641) -> Result<(), VpciError> {
642 let (close, volume, first, short, long, chosen) = vpci_prepare(input, kernel)?;
643 if vpci_dst.len() != close.len() || vpcis_dst.len() != close.len() {
644 return Err(VpciError::OutputLengthMismatch {
645 expected: close.len(),
646 got: vpci_dst.len().min(vpcis_dst.len()),
647 });
648 }
649 let warmup = first + long - 1;
650 for i in 0..warmup.min(vpci_dst.len()) {
651 vpci_dst[i] = f64::NAN;
652 vpcis_dst[i] = f64::NAN;
653 }
654 vpci_compute_into(
655 close, volume, first, short, long, chosen, vpci_dst, vpcis_dst,
656 );
657 Ok(())
658}
659
660#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
661#[inline]
662pub fn vpci_into(
663 input: &VpciInput,
664 out_vpci: &mut [f64],
665 out_vpcis: &mut [f64],
666) -> Result<(), VpciError> {
667 vpci_into_slice(out_vpci, out_vpcis, input, Kernel::Auto)
668}
669
670#[inline]
671pub unsafe fn vpci_scalar(
672 close: &[f64],
673 volume: &[f64],
674 short: usize,
675 long: usize,
676) -> Result<VpciOutput, VpciError> {
677 ensure_same_len(close, volume)?;
678 let len = close.len();
679 let first = first_valid_both(close, volume).ok_or(VpciError::AllValuesNaN)?;
680 let warmup = first + long - 1;
681
682 let mut vpci = alloc_with_nan_prefix(len, warmup);
683 let mut vpcis = alloc_with_nan_prefix(len, warmup);
684
685 vpci_compute_into(
686 close,
687 volume,
688 first,
689 short,
690 long,
691 Kernel::Scalar,
692 &mut vpci,
693 &mut vpcis,
694 );
695
696 Ok(VpciOutput { vpci, vpcis })
697}
698
699#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
700#[inline]
701pub unsafe fn vpci_avx2(
702 close: &[f64],
703 volume: &[f64],
704 short: usize,
705 long: usize,
706) -> Result<VpciOutput, VpciError> {
707 ensure_same_len(close, volume)?;
708 let len = close.len();
709 let first = first_valid_both(close, volume).ok_or(VpciError::AllValuesNaN)?;
710 let warmup = first + long - 1;
711
712 let mut vpci = alloc_with_nan_prefix(len, warmup);
713 let mut vpcis = alloc_with_nan_prefix(len, warmup);
714
715 vpci_compute_into(
716 close,
717 volume,
718 first,
719 short,
720 long,
721 Kernel::Avx2,
722 &mut vpci,
723 &mut vpcis,
724 );
725
726 Ok(VpciOutput { vpci, vpcis })
727}
728
729#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
730#[inline]
731pub unsafe fn vpci_avx512(
732 close: &[f64],
733 volume: &[f64],
734 short: usize,
735 long: usize,
736) -> Result<VpciOutput, VpciError> {
737 ensure_same_len(close, volume)?;
738 let len = close.len();
739 let first = first_valid_both(close, volume).ok_or(VpciError::AllValuesNaN)?;
740 let warmup = first + long - 1;
741
742 let mut vpci = alloc_with_nan_prefix(len, warmup);
743 let mut vpcis = alloc_with_nan_prefix(len, warmup);
744
745 vpci_compute_into(
746 close,
747 volume,
748 first,
749 short,
750 long,
751 Kernel::Avx512,
752 &mut vpci,
753 &mut vpcis,
754 );
755
756 Ok(VpciOutput { vpci, vpcis })
757}
758
759#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
760#[inline(always)]
761unsafe fn vpci_avx2_into_from_psums(
762 close: &[f64],
763 volume: &[f64],
764 first: usize,
765 short: usize,
766 long: usize,
767 ps_close: &[f64],
768 ps_vol: &[f64],
769 ps_cv: &[f64],
770 vpci_out: &mut [f64],
771 vpcis_out: &mut [f64],
772) {
773 use core::arch::x86_64::*;
774
775 let n = close.len();
776 let warmup = first + long - 1;
777 if warmup >= n {
778 return;
779 }
780
781 let inv_long = _mm256_set1_pd(1.0 / (long as f64));
782 let inv_short = _mm256_set1_pd(1.0 / (short as f64));
783 let zero = _mm256_set1_pd(0.0);
784 let nan = _mm256_set1_pd(f64::NAN);
785
786 let pc = ps_close.as_ptr();
787 let pv = ps_vol.as_ptr();
788 let pcv = ps_cv.as_ptr();
789 let yptr = vpci_out.as_mut_ptr();
790
791 let mut i = warmup;
792 let step = 4usize;
793 let vec_end = n.saturating_sub(step) + 1;
794
795 while i < vec_end {
796 let end = i + 1;
797
798 let c_end = _mm256_loadu_pd(pc.add(end));
799 let c_l = _mm256_loadu_pd(pc.add(end - long));
800 let v_end = _mm256_loadu_pd(pv.add(end));
801 let v_l = _mm256_loadu_pd(pv.add(end - long));
802 let cv_end = _mm256_loadu_pd(pcv.add(end));
803 let cv_l = _mm256_loadu_pd(pcv.add(end - long));
804
805 let c_s = _mm256_loadu_pd(pc.add(end - short));
806 let v_s = _mm256_loadu_pd(pv.add(end - short));
807 let cv_s = _mm256_loadu_pd(pcv.add(end - short));
808
809 let sc_l = _mm256_sub_pd(c_end, c_l);
810 let sv_l = _mm256_sub_pd(v_end, v_l);
811 let scv_l = _mm256_sub_pd(cv_end, cv_l);
812
813 let sc_s = _mm256_sub_pd(c_end, c_s);
814 let sv_s = _mm256_sub_pd(v_end, v_s);
815 let scv_s = _mm256_sub_pd(cv_end, cv_s);
816
817 let sma_l = _mm256_mul_pd(sc_l, inv_long);
818 let sma_s = _mm256_mul_pd(sc_s, inv_short);
819 let sma_v_l = _mm256_mul_pd(sv_l, inv_long);
820 let sma_v_s = _mm256_mul_pd(sv_s, inv_short);
821
822 let mask_l = _mm256_cmp_pd(sv_l, zero, _CMP_NEQ_OQ);
823 let vwma_l = _mm256_blendv_pd(nan, _mm256_div_pd(scv_l, sv_l), mask_l);
824
825 let mask_s = _mm256_cmp_pd(sv_s, zero, _CMP_NEQ_OQ);
826 let vwma_s = _mm256_blendv_pd(nan, _mm256_div_pd(scv_s, sv_s), mask_s);
827
828 let vpc = _mm256_sub_pd(vwma_l, sma_l);
829 let mask_vpr = _mm256_cmp_pd(sma_s, zero, _CMP_NEQ_OQ);
830 let vpr = _mm256_blendv_pd(nan, _mm256_div_pd(vwma_s, sma_s), mask_vpr);
831 let mask_vm = _mm256_cmp_pd(sma_v_l, zero, _CMP_NEQ_OQ);
832 let vm = _mm256_blendv_pd(nan, _mm256_div_pd(sma_v_s, sma_v_l), mask_vm);
833
834 let vpci = _mm256_mul_pd(_mm256_mul_pd(vpc, vpr), vm);
835 _mm256_storeu_pd(yptr.add(i), vpci);
836 i += step;
837 }
838
839 while i < n {
840 let end = i + 1;
841 let long_start = end - long;
842 let short_start = end - short;
843
844 let sc_l = *pc.add(end) - *pc.add(long_start);
845 let sv_l = *pv.add(end) - *pv.add(long_start);
846 let scv_l = *pcv.add(end) - *pcv.add(long_start);
847 let sc_s = *pc.add(end) - *pc.add(short_start);
848 let sv_s = *pv.add(end) - *pv.add(short_start);
849 let scv_s = *pcv.add(end) - *pcv.add(short_start);
850
851 let sma_l = sc_l * (1.0 / long as f64);
852 let sma_s = sc_s * (1.0 / short as f64);
853 let sma_v_l = sv_l * (1.0 / long as f64);
854 let sma_v_s = sv_s * (1.0 / short as f64);
855
856 let vwma_l = if sv_l != 0.0 { scv_l / sv_l } else { f64::NAN };
857 let vwma_s = if sv_s != 0.0 { scv_s / sv_s } else { f64::NAN };
858
859 let vpc = vwma_l - sma_l;
860 let vpr = if sma_s != 0.0 {
861 vwma_s / sma_s
862 } else {
863 f64::NAN
864 };
865 let vm = if sma_v_l != 0.0 {
866 sma_v_s / sma_v_l
867 } else {
868 f64::NAN
869 };
870 *yptr.add(i) = vpc * vpr * vm;
871 i += 1;
872 }
873
874 #[inline(always)]
875 fn zf(x: f64) -> f64 {
876 if x.is_finite() {
877 x
878 } else {
879 0.0
880 }
881 }
882
883 let inv_short_s = 1.0 / (short as f64);
884 let vptr = volume.as_ptr();
885 let ysp = vpcis_out.as_mut_ptr();
886
887 let mut sum_vpci_vol_short = 0.0;
888 let mut t = warmup;
889 while t < n {
890 let vpci = *yptr.add(t);
891 let vi = *vptr.add(t);
892 sum_vpci_vol_short += zf(vpci) * zf(vi);
893 if t >= warmup + short {
894 let rm = t - short;
895 sum_vpci_vol_short -= zf(*yptr.add(rm)) * zf(*vptr.add(rm));
896 }
897
898 let end = t + 1;
899 let sv_s = *pv.add(end) - *pv.add(end - short);
900 let denom = sv_s * inv_short_s;
901 *ysp.add(t) = if denom != 0.0 && denom.is_finite() {
902 (sum_vpci_vol_short * inv_short_s) / denom
903 } else {
904 f64::NAN
905 };
906 t += 1;
907 }
908}
909
910#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
911#[inline(always)]
912unsafe fn vpci_avx512_into_from_psums(
913 close: &[f64],
914 volume: &[f64],
915 first: usize,
916 short: usize,
917 long: usize,
918 ps_close: &[f64],
919 ps_vol: &[f64],
920 ps_cv: &[f64],
921 vpci_out: &mut [f64],
922 vpcis_out: &mut [f64],
923) {
924 use core::arch::x86_64::*;
925
926 let n = close.len();
927 let warmup = first + long - 1;
928 if warmup >= n {
929 return;
930 }
931
932 let inv_long = _mm512_set1_pd(1.0 / (long as f64));
933 let inv_short = _mm512_set1_pd(1.0 / (short as f64));
934 let zero = _mm512_set1_pd(0.0);
935 let nan = _mm512_set1_pd(f64::NAN);
936
937 let pc = ps_close.as_ptr();
938 let pv = ps_vol.as_ptr();
939 let pcv = ps_cv.as_ptr();
940 let yptr = vpci_out.as_mut_ptr();
941
942 let mut i = warmup;
943 let step = 8usize;
944 let vec_end = n.saturating_sub(step) + 1;
945
946 while i < vec_end {
947 let end = i + 1;
948
949 let c_end = _mm512_loadu_pd(pc.add(end));
950 let c_l = _mm512_loadu_pd(pc.add(end - long));
951 let v_end = _mm512_loadu_pd(pv.add(end));
952 let v_l = _mm512_loadu_pd(pv.add(end - long));
953 let cv_end = _mm512_loadu_pd(pcv.add(end));
954 let cv_l = _mm512_loadu_pd(pcv.add(end - long));
955
956 let c_s = _mm512_loadu_pd(pc.add(end - short));
957 let v_s = _mm512_loadu_pd(pv.add(end - short));
958 let cv_s = _mm512_loadu_pd(pcv.add(end - short));
959
960 let sc_l = _mm512_sub_pd(c_end, c_l);
961 let sv_l = _mm512_sub_pd(v_end, v_l);
962 let scv_l = _mm512_sub_pd(cv_end, cv_l);
963
964 let sc_s = _mm512_sub_pd(c_end, c_s);
965 let sv_s = _mm512_sub_pd(v_end, v_s);
966 let scv_s = _mm512_sub_pd(cv_end, cv_s);
967
968 let sma_l = _mm512_mul_pd(sc_l, inv_long);
969 let sma_s = _mm512_mul_pd(sc_s, inv_short);
970 let sma_v_l = _mm512_mul_pd(sv_l, inv_long);
971 let sma_v_s = _mm512_mul_pd(sv_s, inv_short);
972
973 let mk_l = _mm512_cmp_pd_mask(sv_l, zero, _CMP_NEQ_OQ);
974 let mk_s = _mm512_cmp_pd_mask(sv_s, zero, _CMP_NEQ_OQ);
975 let mk_vr = _mm512_cmp_pd_mask(sma_s, zero, _CMP_NEQ_OQ);
976 let mk_vm = _mm512_cmp_pd_mask(sma_v_l, zero, _CMP_NEQ_OQ);
977
978 let vwma_l = _mm512_mask_div_pd(nan, mk_l, scv_l, sv_l);
979 let vwma_s = _mm512_mask_div_pd(nan, mk_s, scv_s, sv_s);
980
981 let vpc = _mm512_sub_pd(vwma_l, sma_l);
982 let vpr = _mm512_mask_div_pd(nan, mk_vr, vwma_s, sma_s);
983 let vm = _mm512_mask_div_pd(nan, mk_vm, sma_v_s, sma_v_l);
984
985 let vpci = _mm512_mul_pd(_mm512_mul_pd(vpc, vpr), vm);
986 _mm512_storeu_pd(yptr.add(i), vpci);
987 i += step;
988 }
989
990 while i < n {
991 let end = i + 1;
992 let long_start = end - long;
993 let short_start = end - short;
994
995 let sc_l = *pc.add(end) - *pc.add(long_start);
996 let sv_l = *pv.add(end) - *pv.add(long_start);
997 let scv_l = *pcv.add(end) - *pcv.add(long_start);
998 let sc_s = *pc.add(end) - *pc.add(short_start);
999 let sv_s = *pv.add(end) - *pv.add(short_start);
1000 let scv_s = *pcv.add(end) - *pcv.add(short_start);
1001
1002 let sma_l = sc_l * (1.0 / long as f64);
1003 let sma_s = sc_s * (1.0 / short as f64);
1004 let sma_v_l = sv_l * (1.0 / long as f64);
1005 let sma_v_s = sv_s * (1.0 / short as f64);
1006
1007 let vwma_l = if sv_l != 0.0 { scv_l / sv_l } else { f64::NAN };
1008 let vwma_s = if sv_s != 0.0 { scv_s / sv_s } else { f64::NAN };
1009
1010 let vpc = vwma_l - sma_l;
1011 let vpr = if sma_s != 0.0 {
1012 vwma_s / sma_s
1013 } else {
1014 f64::NAN
1015 };
1016 let vm = if sma_v_l != 0.0 {
1017 sma_v_s / sma_v_l
1018 } else {
1019 f64::NAN
1020 };
1021 *yptr.add(i) = vpc * vpr * vm;
1022 i += 1;
1023 }
1024
1025 #[inline(always)]
1026 fn zf(x: f64) -> f64 {
1027 if x.is_finite() {
1028 x
1029 } else {
1030 0.0
1031 }
1032 }
1033
1034 let inv_short_s = 1.0 / (short as f64);
1035 let vptr = volume.as_ptr();
1036 let ysp = vpcis_out.as_mut_ptr();
1037
1038 let mut sum_vpci_vol_short = 0.0;
1039 let mut t = warmup;
1040 while t < n {
1041 let vpci = *yptr.add(t);
1042 let vi = *vptr.add(t);
1043 sum_vpci_vol_short += zf(vpci) * zf(vi);
1044 if t >= warmup + short {
1045 let rm = t - short;
1046 sum_vpci_vol_short -= zf(*yptr.add(rm)) * zf(*vptr.add(rm));
1047 }
1048
1049 let end = t + 1;
1050 let sv_s = *pv.add(end) - *pv.add(end - short);
1051 let denom = sv_s * inv_short_s;
1052 *ysp.add(t) = if denom != 0.0 && denom.is_finite() {
1053 (sum_vpci_vol_short * inv_short_s) / denom
1054 } else {
1055 f64::NAN
1056 };
1057 t += 1;
1058 }
1059}
1060
1061#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1062#[inline]
1063pub unsafe fn vpci_avx512_short(
1064 close: &[f64],
1065 volume: &[f64],
1066 short: usize,
1067 long: usize,
1068) -> Result<VpciOutput, VpciError> {
1069 vpci_avx512(close, volume, short, long)
1070}
1071
1072#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1073#[inline]
1074pub unsafe fn vpci_avx512_long(
1075 close: &[f64],
1076 volume: &[f64],
1077 short: usize,
1078 long: usize,
1079) -> Result<VpciOutput, VpciError> {
1080 vpci_avx512(close, volume, short, long)
1081}
1082
1083#[inline]
1084pub fn vpci_batch_with_kernel(
1085 close: &[f64],
1086 volume: &[f64],
1087 sweep: &VpciBatchRange,
1088 kernel: Kernel,
1089) -> Result<VpciBatchOutput, VpciError> {
1090 let k = match kernel {
1091 Kernel::Auto => match detect_best_batch_kernel() {
1092 Kernel::Avx512Batch => Kernel::Avx2Batch,
1093 other => other,
1094 },
1095 other if other.is_batch() => other,
1096 other => {
1097 return Err(VpciError::InvalidKernelForBatch(other));
1098 }
1099 };
1100 let simd = match k {
1101 Kernel::Avx512Batch => Kernel::Avx512,
1102 Kernel::Avx2Batch => Kernel::Avx2,
1103 Kernel::ScalarBatch => Kernel::Scalar,
1104 _ => unreachable!(),
1105 };
1106 vpci_batch_par_slice(close, volume, sweep, simd)
1107}
1108
1109#[derive(Clone, Debug)]
1110pub struct VpciBatchRange {
1111 pub short_range: (usize, usize, usize),
1112 pub long_range: (usize, usize, usize),
1113}
1114
1115impl Default for VpciBatchRange {
1116 fn default() -> Self {
1117 Self {
1118 short_range: (5, 5, 0),
1119 long_range: (25, 274, 1),
1120 }
1121 }
1122}
1123
1124#[derive(Clone, Debug, Default)]
1125pub struct VpciBatchBuilder {
1126 range: VpciBatchRange,
1127 kernel: Kernel,
1128}
1129
1130impl VpciBatchBuilder {
1131 pub fn new() -> Self {
1132 Self::default()
1133 }
1134 pub fn kernel(mut self, k: Kernel) -> Self {
1135 self.kernel = k;
1136 self
1137 }
1138 pub fn short_range(mut self, start: usize, end: usize, step: usize) -> Self {
1139 self.range.short_range = (start, end, step);
1140 self
1141 }
1142 pub fn long_range(mut self, start: usize, end: usize, step: usize) -> Self {
1143 self.range.long_range = (start, end, step);
1144 self
1145 }
1146 pub fn apply_slices(self, close: &[f64], volume: &[f64]) -> Result<VpciBatchOutput, VpciError> {
1147 vpci_batch_with_kernel(close, volume, &self.range, self.kernel)
1148 }
1149}
1150
1151#[derive(Clone, Debug)]
1152pub struct VpciBatchOutput {
1153 pub vpci: Vec<f64>,
1154 pub vpcis: Vec<f64>,
1155 pub combos: Vec<VpciParams>,
1156 pub rows: usize,
1157 pub cols: usize,
1158}
1159impl VpciBatchOutput {
1160 pub fn row_for_params(&self, p: &VpciParams) -> Option<usize> {
1161 self.combos.iter().position(|c| {
1162 c.short_range.unwrap_or(5) == p.short_range.unwrap_or(5)
1163 && c.long_range.unwrap_or(25) == p.long_range.unwrap_or(25)
1164 })
1165 }
1166 pub fn vpci_for(&self, p: &VpciParams) -> Option<&[f64]> {
1167 self.row_for_params(p).map(|row| {
1168 let start = row * self.cols;
1169 &self.vpci[start..start + self.cols]
1170 })
1171 }
1172 pub fn vpcis_for(&self, p: &VpciParams) -> Option<&[f64]> {
1173 self.row_for_params(p).map(|row| {
1174 let start = row * self.cols;
1175 &self.vpcis[start..start + self.cols]
1176 })
1177 }
1178}
1179
1180#[inline(always)]
1181fn expand_grid(r: &VpciBatchRange) -> Vec<VpciParams> {
1182 fn axis_usize((start, end, step): (usize, usize, usize)) -> Vec<usize> {
1183 if step == 0 || start == end {
1184 return vec![start];
1185 }
1186 let mut out = Vec::new();
1187 if start < end {
1188 let mut v = start;
1189 loop {
1190 out.push(v);
1191 match v.checked_add(step) {
1192 Some(next) if next <= end => v = next,
1193 _ => break,
1194 }
1195 }
1196 } else {
1197 let mut v = start;
1198 loop {
1199 out.push(v);
1200 if v == end {
1201 break;
1202 }
1203 match v.checked_sub(step) {
1204 Some(next) if next >= end => v = next,
1205 _ => break,
1206 }
1207 }
1208 }
1209 out
1210 }
1211
1212 let shorts = axis_usize(r.short_range);
1213 let longs = axis_usize(r.long_range);
1214
1215 let mut out = Vec::with_capacity(shorts.len().saturating_mul(longs.len()));
1216 for &s in &shorts {
1217 for &l in &longs {
1218 out.push(VpciParams {
1219 short_range: Some(s),
1220 long_range: Some(l),
1221 });
1222 }
1223 }
1224 out
1225}
1226
1227#[inline(always)]
1228pub fn vpci_batch_slice(
1229 close: &[f64],
1230 volume: &[f64],
1231 sweep: &VpciBatchRange,
1232 kernel: Kernel,
1233) -> Result<VpciBatchOutput, VpciError> {
1234 vpci_batch_inner(close, volume, sweep, kernel, false)
1235}
1236
1237#[inline(always)]
1238pub fn vpci_batch_par_slice(
1239 close: &[f64],
1240 volume: &[f64],
1241 sweep: &VpciBatchRange,
1242 kernel: Kernel,
1243) -> Result<VpciBatchOutput, VpciError> {
1244 vpci_batch_inner(close, volume, sweep, kernel, true)
1245}
1246
1247#[inline(always)]
1248fn vpci_batch_inner(
1249 close: &[f64],
1250 volume: &[f64],
1251 sweep: &VpciBatchRange,
1252 kern: Kernel,
1253 parallel: bool,
1254) -> Result<VpciBatchOutput, VpciError> {
1255 ensure_same_len(close, volume)?;
1256 let combos = expand_grid(sweep);
1257 let cols = close.len();
1258 let rows = combos.len();
1259 if cols == 0 {
1260 return Err(VpciError::EmptyInputData);
1261 }
1262 if rows == 0 {
1263 let (start, end, step) = sweep.short_range;
1264 return Err(VpciError::InvalidRange { start, end, step });
1265 }
1266
1267 let first = first_valid_both(close, volume).ok_or(VpciError::AllValuesNaN)?;
1268 let warmups: Vec<usize> = combos
1269 .iter()
1270 .map(|p| first + p.long_range.unwrap() - 1)
1271 .collect();
1272
1273 let mut vpci_mu = make_uninit_matrix(rows, cols);
1274 let mut vpcis_mu = make_uninit_matrix(rows, cols);
1275
1276 init_matrix_prefixes(&mut vpci_mu, cols, &warmups);
1277 init_matrix_prefixes(&mut vpcis_mu, cols, &warmups);
1278
1279 let ptr_v = vpci_mu.as_ptr() as *mut f64;
1280 let ptr_s = vpcis_mu.as_ptr() as *mut f64;
1281 let cap_v = vpci_mu.capacity();
1282 let cap_s = vpcis_mu.capacity();
1283
1284 let total_len = rows
1285 .checked_mul(cols)
1286 .ok_or_else(|| VpciError::InvalidInput("rows*cols overflow in vpci_batch_inner".into()))?;
1287 let vpci_slice = unsafe { core::slice::from_raw_parts_mut(ptr_v, total_len) };
1288 let vpcis_slice = unsafe { core::slice::from_raw_parts_mut(ptr_s, total_len) };
1289
1290 let kernel = match kern {
1291 Kernel::Auto => detect_best_batch_kernel(),
1292 k => k,
1293 };
1294 let simd = match kernel {
1295 Kernel::Avx512Batch => Kernel::Avx512,
1296 Kernel::Avx2Batch => Kernel::Avx2,
1297 Kernel::ScalarBatch => Kernel::Scalar,
1298 _ => kernel,
1299 };
1300
1301 let combos = vpci_batch_inner_into(
1302 close,
1303 volume,
1304 sweep,
1305 simd,
1306 parallel,
1307 vpci_slice,
1308 vpcis_slice,
1309 )?;
1310
1311 core::mem::forget(vpci_mu);
1312 core::mem::forget(vpcis_mu);
1313 let vpci_vec = unsafe { Vec::from_raw_parts(ptr_v, total_len, cap_v) };
1314 let vpcis_vec = unsafe { Vec::from_raw_parts(ptr_s, total_len, cap_s) };
1315
1316 Ok(VpciBatchOutput {
1317 vpci: vpci_vec,
1318 vpcis: vpcis_vec,
1319 combos,
1320 rows,
1321 cols,
1322 })
1323}
1324
1325#[inline(always)]
1326fn vpci_batch_inner_into(
1327 close: &[f64],
1328 volume: &[f64],
1329 sweep: &VpciBatchRange,
1330 kernel: Kernel,
1331 parallel: bool,
1332 vpci_out: &mut [f64],
1333 vpcis_out: &mut [f64],
1334) -> Result<Vec<VpciParams>, VpciError> {
1335 ensure_same_len(close, volume)?;
1336 let combos = expand_grid(sweep);
1337 if combos.is_empty() {
1338 let (start, end, step) = sweep.short_range;
1339 return Err(VpciError::InvalidRange { start, end, step });
1340 }
1341 let len = close.len();
1342 let first = first_valid_both(close, volume).ok_or(VpciError::AllValuesNaN)?;
1343 let max_long = combos.iter().map(|c| c.long_range.unwrap()).max().unwrap();
1344 if len - first < max_long {
1345 return Err(VpciError::NotEnoughValidData {
1346 needed: max_long,
1347 valid: len - first,
1348 });
1349 }
1350 let rows = combos.len();
1351 let cols = len;
1352
1353 let (ps_c, ps_v, ps_cv) = build_prefix_sums(close, volume);
1354
1355 for (row, prm) in combos.iter().enumerate() {
1356 let warmup = first + prm.long_range.unwrap() - 1;
1357 let s = row * cols;
1358 for i in 0..warmup.min(cols) {
1359 vpci_out[s + i] = f64::NAN;
1360 vpcis_out[s + i] = f64::NAN;
1361 }
1362 }
1363
1364 if parallel {
1365 #[cfg(not(target_arch = "wasm32"))]
1366 {
1367 use rayon::prelude::*;
1368 vpci_out
1369 .par_chunks_mut(cols)
1370 .zip(vpcis_out.par_chunks_mut(cols))
1371 .enumerate()
1372 .for_each(|(row, (dst_vpci, dst_vpcis))| {
1373 let prm = &combos[row];
1374 let short = prm.short_range.unwrap();
1375 let long = prm.long_range.unwrap();
1376
1377 let use_simd = short <= long;
1378 match (use_simd, kernel) {
1379 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1380 (true, Kernel::Avx512) => unsafe {
1381 vpci_avx512_into_from_psums(
1382 close, volume, first, short, long, &ps_c, &ps_v, &ps_cv, dst_vpci,
1383 dst_vpcis,
1384 );
1385 },
1386 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1387 (true, Kernel::Avx2) => unsafe {
1388 vpci_avx2_into_from_psums(
1389 close, volume, first, short, long, &ps_c, &ps_v, &ps_cv, dst_vpci,
1390 dst_vpcis,
1391 );
1392 },
1393 _ => {
1394 vpci_scalar_into_from_psums(
1395 close, volume, first, short, long, &ps_c, &ps_v, &ps_cv, dst_vpci,
1396 dst_vpcis,
1397 );
1398 }
1399 }
1400 });
1401 }
1402 #[cfg(target_arch = "wasm32")]
1403 {
1404 for row in 0..rows {
1405 let prm = &combos[row];
1406 let short = prm.short_range.unwrap();
1407 let long = prm.long_range.unwrap();
1408
1409 let row_off = row * cols;
1410 let dst_vpci = &mut vpci_out[row_off..row_off + cols];
1411 let dst_vpcis = &mut vpcis_out[row_off..row_off + cols];
1412
1413 vpci_scalar_into_from_psums(
1414 close, volume, first, short, long, &ps_c, &ps_v, &ps_cv, dst_vpci, dst_vpcis,
1415 );
1416 }
1417 }
1418 } else {
1419 for row in 0..rows {
1420 let prm = &combos[row];
1421 let short = prm.short_range.unwrap();
1422 let long = prm.long_range.unwrap();
1423
1424 let row_off = row * cols;
1425 let dst_vpci = &mut vpci_out[row_off..row_off + cols];
1426 let dst_vpcis = &mut vpcis_out[row_off..row_off + cols];
1427
1428 let use_simd = short <= long;
1429 match (use_simd, kernel) {
1430 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1431 (true, Kernel::Avx512) => unsafe {
1432 vpci_avx512_into_from_psums(
1433 close, volume, first, short, long, &ps_c, &ps_v, &ps_cv, dst_vpci,
1434 dst_vpcis,
1435 );
1436 },
1437 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1438 (true, Kernel::Avx2) => unsafe {
1439 vpci_avx2_into_from_psums(
1440 close, volume, first, short, long, &ps_c, &ps_v, &ps_cv, dst_vpci,
1441 dst_vpcis,
1442 );
1443 },
1444 _ => {
1445 vpci_scalar_into_from_psums(
1446 close, volume, first, short, long, &ps_c, &ps_v, &ps_cv, dst_vpci,
1447 dst_vpcis,
1448 );
1449 }
1450 }
1451 }
1452 }
1453
1454 Ok(combos)
1455}
1456
1457#[inline(always)]
1458pub unsafe fn vpci_row_scalar(
1459 close: &[f64],
1460 volume: &[f64],
1461 short: usize,
1462 long: usize,
1463) -> Result<VpciOutput, VpciError> {
1464 vpci_scalar(close, volume, short, long)
1465}
1466
1467#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1468#[inline(always)]
1469pub unsafe fn vpci_row_avx2(
1470 close: &[f64],
1471 volume: &[f64],
1472 short: usize,
1473 long: usize,
1474) -> Result<VpciOutput, VpciError> {
1475 vpci_avx2(close, volume, short, long)
1476}
1477
1478#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1479#[inline(always)]
1480pub unsafe fn vpci_row_avx512(
1481 close: &[f64],
1482 volume: &[f64],
1483 short: usize,
1484 long: usize,
1485) -> Result<VpciOutput, VpciError> {
1486 if long <= 32 {
1487 vpci_row_avx512_short(close, volume, short, long)
1488 } else {
1489 vpci_row_avx512_long(close, volume, short, long)
1490 }
1491}
1492
1493#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1494#[inline(always)]
1495pub unsafe fn vpci_row_avx512_short(
1496 close: &[f64],
1497 volume: &[f64],
1498 short: usize,
1499 long: usize,
1500) -> Result<VpciOutput, VpciError> {
1501 vpci_avx512(close, volume, short, long)
1502}
1503
1504#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1505#[inline(always)]
1506pub unsafe fn vpci_row_avx512_long(
1507 close: &[f64],
1508 volume: &[f64],
1509 short: usize,
1510 long: usize,
1511) -> Result<VpciOutput, VpciError> {
1512 vpci_avx512(close, volume, short, long)
1513}
1514
1515#[inline(always)]
1516pub fn expand_grid_vpci(r: &VpciBatchRange) -> Vec<VpciParams> {
1517 expand_grid(r)
1518}
1519
1520#[cfg(test)]
1521mod tests {
1522 use super::*;
1523 use crate::skip_if_unsupported;
1524 use crate::utilities::data_loader::read_candles_from_csv;
1525
1526 fn check_vpci_partial_params(
1527 test_name: &str,
1528 kernel: Kernel,
1529 ) -> Result<(), Box<dyn std::error::Error>> {
1530 skip_if_unsupported!(kernel, test_name);
1531 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1532 let candles = read_candles_from_csv(file_path)?;
1533 let params = VpciParams {
1534 short_range: Some(3),
1535 long_range: None,
1536 };
1537 let input = VpciInput::from_candles(&candles, "close", "volume", params);
1538 let output = vpci_with_kernel(&input, kernel)?;
1539 assert_eq!(output.vpci.len(), candles.close.len());
1540 assert_eq!(output.vpcis.len(), candles.close.len());
1541 Ok(())
1542 }
1543
1544 fn check_vpci_accuracy(
1545 test_name: &str,
1546 kernel: Kernel,
1547 ) -> Result<(), Box<dyn std::error::Error>> {
1548 skip_if_unsupported!(kernel, test_name);
1549 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1550 let candles = read_candles_from_csv(file_path)?;
1551 let params = VpciParams {
1552 short_range: Some(5),
1553 long_range: Some(25),
1554 };
1555 let input = VpciInput::from_candles(&candles, "close", "volume", params);
1556 let output = vpci_with_kernel(&input, kernel)?;
1557
1558 let vpci_len = output.vpci.len();
1559 let vpcis_len = output.vpcis.len();
1560 assert_eq!(vpci_len, candles.close.len());
1561 assert_eq!(vpcis_len, candles.close.len());
1562
1563 let vpci_last_five = &output.vpci[vpci_len.saturating_sub(5)..];
1564 let vpcis_last_five = &output.vpcis[vpcis_len.saturating_sub(5)..];
1565 let expected_vpci = [
1566 -319.65148214323426,
1567 -133.61700649928346,
1568 -144.76194155503174,
1569 -83.55576212490328,
1570 -169.53504207700533,
1571 ];
1572 let expected_vpcis = [
1573 -1049.2826640115732,
1574 -694.1067814399748,
1575 -519.6960416662324,
1576 -330.9401404636258,
1577 -173.004986803695,
1578 ];
1579 for (i, &val) in vpci_last_five.iter().enumerate() {
1580 let diff = (val - expected_vpci[i]).abs();
1581 assert!(
1582 diff < 5e-2,
1583 "[{}] VPCI mismatch at idx {}: got {}, expected {}",
1584 test_name,
1585 i,
1586 val,
1587 expected_vpci[i]
1588 );
1589 }
1590 for (i, &val) in vpcis_last_five.iter().enumerate() {
1591 let diff = (val - expected_vpcis[i]).abs();
1592 assert!(
1593 diff < 5e-2,
1594 "[{}] VPCIS mismatch at idx {}: got {}, expected {}",
1595 test_name,
1596 i,
1597 val,
1598 expected_vpcis[i]
1599 );
1600 }
1601 Ok(())
1602 }
1603
1604 fn check_vpci_default_candles(
1605 test_name: &str,
1606 kernel: Kernel,
1607 ) -> Result<(), Box<dyn std::error::Error>> {
1608 skip_if_unsupported!(kernel, test_name);
1609 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1610 let candles = read_candles_from_csv(file_path)?;
1611 let input = VpciInput::with_default_candles(&candles);
1612 let output = vpci_with_kernel(&input, kernel)?;
1613 assert_eq!(output.vpci.len(), candles.close.len());
1614 assert_eq!(output.vpcis.len(), candles.close.len());
1615 Ok(())
1616 }
1617
1618 fn check_vpci_slice_input(
1619 test_name: &str,
1620 kernel: Kernel,
1621 ) -> Result<(), Box<dyn std::error::Error>> {
1622 skip_if_unsupported!(kernel, test_name);
1623 let close_data = [10.0, 12.0, 14.0, 13.0, 15.0];
1624 let volume_data = [100.0, 200.0, 300.0, 250.0, 400.0];
1625 let params = VpciParams {
1626 short_range: Some(2),
1627 long_range: Some(3),
1628 };
1629 let input = VpciInput::from_slices(&close_data, &volume_data, params);
1630 let output = vpci_with_kernel(&input, kernel)?;
1631 assert_eq!(output.vpci.len(), close_data.len());
1632 assert_eq!(output.vpcis.len(), close_data.len());
1633 Ok(())
1634 }
1635
1636 #[cfg(debug_assertions)]
1637 fn check_vpci_no_poison(
1638 test_name: &str,
1639 kernel: Kernel,
1640 ) -> Result<(), Box<dyn std::error::Error>> {
1641 skip_if_unsupported!(kernel, test_name);
1642
1643 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1644 let candles = read_candles_from_csv(file_path)?;
1645
1646 let test_params = vec![
1647 VpciParams::default(),
1648 VpciParams {
1649 short_range: Some(2),
1650 long_range: Some(3),
1651 },
1652 VpciParams {
1653 short_range: Some(2),
1654 long_range: Some(10),
1655 },
1656 VpciParams {
1657 short_range: Some(5),
1658 long_range: Some(20),
1659 },
1660 VpciParams {
1661 short_range: Some(10),
1662 long_range: Some(30),
1663 },
1664 VpciParams {
1665 short_range: Some(20),
1666 long_range: Some(50),
1667 },
1668 VpciParams {
1669 short_range: Some(3),
1670 long_range: Some(100),
1671 },
1672 VpciParams {
1673 short_range: Some(50),
1674 long_range: Some(100),
1675 },
1676 VpciParams {
1677 short_range: Some(7),
1678 long_range: Some(21),
1679 },
1680 VpciParams {
1681 short_range: Some(14),
1682 long_range: Some(28),
1683 },
1684 ];
1685
1686 for (param_idx, params) in test_params.iter().enumerate() {
1687 let input = VpciInput::from_candles(&candles, "close", "volume", params.clone());
1688 let output = vpci_with_kernel(&input, kernel)?;
1689
1690 for (i, &val) in output.vpci.iter().enumerate() {
1691 if val.is_nan() {
1692 continue;
1693 }
1694
1695 let bits = val.to_bits();
1696
1697 if bits == 0x11111111_11111111 {
1698 panic!(
1699 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1700 in VPCI with params: short_range={}, long_range={} (param set {})",
1701 test_name,
1702 val,
1703 bits,
1704 i,
1705 params.short_range.unwrap_or(5),
1706 params.long_range.unwrap_or(25),
1707 param_idx
1708 );
1709 }
1710
1711 if bits == 0x22222222_22222222 {
1712 panic!(
1713 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1714 in VPCI with params: short_range={}, long_range={} (param set {})",
1715 test_name,
1716 val,
1717 bits,
1718 i,
1719 params.short_range.unwrap_or(5),
1720 params.long_range.unwrap_or(25),
1721 param_idx
1722 );
1723 }
1724
1725 if bits == 0x33333333_33333333 {
1726 panic!(
1727 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1728 in VPCI with params: short_range={}, long_range={} (param set {})",
1729 test_name,
1730 val,
1731 bits,
1732 i,
1733 params.short_range.unwrap_or(5),
1734 params.long_range.unwrap_or(25),
1735 param_idx
1736 );
1737 }
1738 }
1739
1740 for (i, &val) in output.vpcis.iter().enumerate() {
1741 if val.is_nan() {
1742 continue;
1743 }
1744
1745 let bits = val.to_bits();
1746
1747 if bits == 0x11111111_11111111 {
1748 panic!(
1749 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1750 in VPCIS with params: short_range={}, long_range={} (param set {})",
1751 test_name,
1752 val,
1753 bits,
1754 i,
1755 params.short_range.unwrap_or(5),
1756 params.long_range.unwrap_or(25),
1757 param_idx
1758 );
1759 }
1760
1761 if bits == 0x22222222_22222222 {
1762 panic!(
1763 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1764 in VPCIS with params: short_range={}, long_range={} (param set {})",
1765 test_name,
1766 val,
1767 bits,
1768 i,
1769 params.short_range.unwrap_or(5),
1770 params.long_range.unwrap_or(25),
1771 param_idx
1772 );
1773 }
1774
1775 if bits == 0x33333333_33333333 {
1776 panic!(
1777 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1778 in VPCIS with params: short_range={}, long_range={} (param set {})",
1779 test_name,
1780 val,
1781 bits,
1782 i,
1783 params.short_range.unwrap_or(5),
1784 params.long_range.unwrap_or(25),
1785 param_idx
1786 );
1787 }
1788 }
1789 }
1790
1791 Ok(())
1792 }
1793
1794 #[cfg(not(debug_assertions))]
1795 fn check_vpci_no_poison(
1796 _test_name: &str,
1797 _kernel: Kernel,
1798 ) -> Result<(), Box<dyn std::error::Error>> {
1799 Ok(())
1800 }
1801
1802 #[cfg(feature = "proptest")]
1803 fn calculate_variance(data: &[f64]) -> f64 {
1804 let finite_values: Vec<f64> = data.iter().filter(|v| v.is_finite()).copied().collect();
1805
1806 if finite_values.len() < 2 {
1807 return 0.0;
1808 }
1809
1810 let mean = finite_values.iter().sum::<f64>() / finite_values.len() as f64;
1811 let variance = finite_values
1812 .iter()
1813 .map(|x| (x - mean).powi(2))
1814 .sum::<f64>()
1815 / finite_values.len() as f64;
1816
1817 variance
1818 }
1819
1820 #[cfg(feature = "proptest")]
1821 #[allow(clippy::float_cmp)]
1822 fn check_vpci_property(
1823 test_name: &str,
1824 kernel: Kernel,
1825 ) -> Result<(), Box<dyn std::error::Error>> {
1826 use proptest::prelude::*;
1827 skip_if_unsupported!(kernel, test_name);
1828
1829 let strat = (2usize..=20).prop_flat_map(|short_range| {
1830 ((short_range + 1)..=50).prop_flat_map(move |long_range| {
1831 let min_len = long_range + 10;
1832 (min_len..400).prop_flat_map(move |data_len| {
1833 (
1834 prop::collection::vec(
1835 (100f64..10000f64).prop_filter("finite", |x| x.is_finite()),
1836 data_len,
1837 ),
1838 prop::collection::vec(
1839 (1000f64..1000000f64).prop_filter("finite", |x| x.is_finite()),
1840 data_len,
1841 ),
1842 Just(short_range),
1843 Just(long_range),
1844 )
1845 })
1846 })
1847 });
1848
1849 proptest::test_runner::TestRunner::default()
1850 .run(&strat, |(close, volume, short_range, long_range)| {
1851 let params = VpciParams {
1852 short_range: Some(short_range),
1853 long_range: Some(long_range),
1854 };
1855 let input = VpciInput::from_slices(&close, &volume, params);
1856
1857 let VpciOutput {
1858 vpci: out,
1859 vpcis: out_smooth,
1860 } = vpci_with_kernel(&input, kernel).unwrap();
1861 let VpciOutput {
1862 vpci: ref_out,
1863 vpcis: ref_out_smooth,
1864 } = vpci_with_kernel(&input, Kernel::Scalar).unwrap();
1865
1866 let first_valid = close
1867 .iter()
1868 .zip(volume.iter())
1869 .position(|(c, v)| !c.is_nan() && !v.is_nan())
1870 .unwrap_or(0);
1871
1872 let expected_warmup = first_valid + long_range - 1;
1873
1874 for i in 0..expected_warmup.min(out.len()) {
1875 prop_assert!(
1876 out[i].is_nan(),
1877 "Expected NaN during warmup at index {}, got {}",
1878 i,
1879 out[i]
1880 );
1881 prop_assert!(
1882 out_smooth[i].is_nan(),
1883 "Expected NaN in VPCIS during warmup at index {}, got {}",
1884 i,
1885 out_smooth[i]
1886 );
1887 }
1888
1889 for i in expected_warmup..close.len() {
1890 let y = out[i];
1891 let ys = out_smooth[i];
1892 let r = ref_out[i];
1893 let rs = ref_out_smooth[i];
1894
1895 if !close[i].is_nan() && !volume[i].is_nan() {
1896 prop_assert!(
1897 y.is_finite() || r.is_nan(),
1898 "VPCI should be finite at idx {} after warmup, got {}",
1899 i,
1900 y
1901 );
1902 }
1903
1904 if !y.is_finite() || !r.is_finite() {
1905 prop_assert!(
1906 y.to_bits() == r.to_bits(),
1907 "finite/NaN mismatch in VPCI at idx {}: {} vs {}",
1908 i,
1909 y,
1910 r
1911 );
1912 } else {
1913 let y_bits = y.to_bits();
1914 let r_bits = r.to_bits();
1915 let ulp_diff: u64 = y_bits.abs_diff(r_bits);
1916
1917 prop_assert!(
1918 (y - r).abs() <= 1e-9 || ulp_diff <= 4,
1919 "VPCI mismatch at idx {}: {} vs {} (ULP={})",
1920 i,
1921 y,
1922 r,
1923 ulp_diff
1924 );
1925 }
1926
1927 if !ys.is_finite() || !rs.is_finite() {
1928 prop_assert!(
1929 ys.to_bits() == rs.to_bits(),
1930 "finite/NaN mismatch in VPCIS at idx {}: {} vs {}",
1931 i,
1932 ys,
1933 rs
1934 );
1935 } else {
1936 let ys_bits = ys.to_bits();
1937 let rs_bits = rs.to_bits();
1938 let ulp_diff: u64 = ys_bits.abs_diff(rs_bits);
1939
1940 prop_assert!(
1941 (ys - rs).abs() <= 1e-9 || ulp_diff <= 4,
1942 "VPCIS mismatch at idx {}: {} vs {} (ULP={})",
1943 i,
1944 ys,
1945 rs,
1946 ulp_diff
1947 );
1948 }
1949 }
1950
1951 let prices_constant = close.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-9);
1952
1953 if prices_constant && expected_warmup < close.len() {
1954 for i in expected_warmup..close.len() {
1955 if out[i].is_finite() {
1956 prop_assert!(
1957 out[i].abs() <= 1e-6,
1958 "VPCI should be ~0 when prices are constant, got {} at index {}",
1959 out[i],
1960 i
1961 );
1962 }
1963 }
1964 }
1965
1966 let volumes_constant = volume.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-9);
1967
1968 if volumes_constant && expected_warmup < close.len() {
1969 for i in expected_warmup..close.len() {
1970 if out[i].is_finite() && ref_out[i].is_finite() {
1971 prop_assert!(
1972 (out[i] - ref_out[i]).abs() <= 1e-9,
1973 "VPCI kernels should match exactly with constant volume"
1974 );
1975 }
1976 }
1977 }
1978
1979 if expected_warmup + short_range < close.len() {
1980 for i in (expected_warmup + short_range)..close.len() {
1981 if out[i].is_finite() && volume[i].is_finite() && volume[i] > 0.0 {
1982 if !out_smooth[i].is_finite() {
1983 let vol_window = &volume[i.saturating_sub(short_range - 1)..=i];
1984 let vol_sum: f64 = vol_window.iter().sum();
1985 prop_assert!(
1986 vol_sum.abs() < 1e-10,
1987 "VPCIS should be finite when VPCI is finite and volume > 0 at index {}",
1988 i
1989 );
1990 }
1991 }
1992 }
1993 }
1994
1995 if short_range == long_range && expected_warmup < close.len() {
1996 for i in expected_warmup..close.len().min(expected_warmup + 10) {
1997 if out[i].is_finite() {
1998 prop_assert!(
1999 !out[i].is_nan(),
2000 "VPCI should be valid even when short_range == long_range"
2001 );
2002 }
2003 }
2004 }
2005
2006 let extreme_ratio = long_range as f64 / short_range as f64 > 10.0;
2007 if extreme_ratio && expected_warmup < close.len() {
2008 for i in expected_warmup..close.len().min(expected_warmup + 5) {
2009 prop_assert!(
2010 out[i].is_nan() || out[i].is_finite(),
2011 "VPCI should handle extreme parameter ratios gracefully at index {}",
2012 i
2013 );
2014 }
2015 }
2016
2017 let valid_count = out
2018 .iter()
2019 .skip(expected_warmup)
2020 .filter(|v| v.is_finite())
2021 .count();
2022
2023 let ref_valid_count = ref_out
2024 .iter()
2025 .skip(expected_warmup)
2026 .filter(|v| v.is_finite())
2027 .count();
2028
2029 prop_assert_eq!(
2030 valid_count,
2031 ref_valid_count,
2032 "Valid value count mismatch between kernels"
2033 );
2034
2035 Ok(())
2036 })
2037 .unwrap();
2038
2039 Ok(())
2040 }
2041
2042 macro_rules! generate_all_vpci_tests {
2043 ($($test_fn:ident),*) => {
2044 paste::paste! {
2045 $( #[test] fn [<$test_fn _scalar_f64>]() { let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar); } )*
2046 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2047 $(
2048 #[test] fn [<$test_fn _avx2_f64>]() { let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2); }
2049 #[test] fn [<$test_fn _avx512_f64>]() { let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512); }
2050 )*
2051 }
2052 }
2053 }
2054
2055 generate_all_vpci_tests!(
2056 check_vpci_partial_params,
2057 check_vpci_accuracy,
2058 check_vpci_default_candles,
2059 check_vpci_slice_input,
2060 check_vpci_no_poison
2061 );
2062
2063 #[cfg(feature = "proptest")]
2064 generate_all_vpci_tests!(check_vpci_property);
2065
2066 fn check_batch_default_row(
2067 test: &str,
2068 kernel: Kernel,
2069 ) -> Result<(), Box<dyn std::error::Error>> {
2070 skip_if_unsupported!(kernel, test);
2071
2072 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2073 let c = read_candles_from_csv(file)?;
2074 let close = &c.close;
2075 let volume = &c.volume;
2076
2077 let output = VpciBatchBuilder::new()
2078 .kernel(kernel)
2079 .apply_slices(close, volume)?;
2080
2081 let def = VpciParams::default();
2082 let row = output.vpci_for(&def).expect("default row missing");
2083
2084 assert_eq!(row.len(), close.len());
2085
2086 let expected = [
2087 -319.65148214323426,
2088 -133.61700649928346,
2089 -144.76194155503174,
2090 -83.55576212490328,
2091 -169.53504207700533,
2092 ];
2093 let start = row.len() - 5;
2094 for (i, &v) in row[start..].iter().enumerate() {
2095 assert!(
2096 (v - expected[i]).abs() < 5e-2,
2097 "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
2098 );
2099 }
2100 Ok(())
2101 }
2102
2103 #[cfg(debug_assertions)]
2104 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
2105 skip_if_unsupported!(kernel, test);
2106
2107 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2108 let c = read_candles_from_csv(file)?;
2109 let close = &c.close;
2110 let volume = &c.volume;
2111
2112 let test_configs = vec![
2113 (2, 10, 2, 5, 25, 5),
2114 (5, 15, 5, 20, 40, 10),
2115 (10, 20, 5, 30, 60, 15),
2116 (2, 5, 1, 10, 15, 1),
2117 (20, 30, 2, 40, 60, 5),
2118 (3, 7, 2, 21, 35, 7),
2119 (8, 12, 1, 25, 30, 1),
2120 (2, 50, 10, 10, 100, 20),
2121 ];
2122
2123 for (cfg_idx, &(short_start, short_end, short_step, long_start, long_end, long_step)) in
2124 test_configs.iter().enumerate()
2125 {
2126 let output = VpciBatchBuilder::new()
2127 .kernel(kernel)
2128 .short_range(short_start, short_end, short_step)
2129 .long_range(long_start, long_end, long_step)
2130 .apply_slices(close, volume)?;
2131
2132 for (idx, &val) in output.vpci.iter().enumerate() {
2133 if val.is_nan() {
2134 continue;
2135 }
2136
2137 let bits = val.to_bits();
2138 let row = idx / output.cols;
2139 let col = idx % output.cols;
2140 let combo = &output.combos[row];
2141
2142 if bits == 0x11111111_11111111 {
2143 panic!(
2144 "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
2145 in VPCI at row {} col {} (flat index {}) with params: short_range={}, long_range={}",
2146 test,
2147 cfg_idx,
2148 val,
2149 bits,
2150 row,
2151 col,
2152 idx,
2153 combo.short_range.unwrap_or(5),
2154 combo.long_range.unwrap_or(25)
2155 );
2156 }
2157
2158 if bits == 0x22222222_22222222 {
2159 panic!(
2160 "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
2161 in VPCI at row {} col {} (flat index {}) with params: short_range={}, long_range={}",
2162 test,
2163 cfg_idx,
2164 val,
2165 bits,
2166 row,
2167 col,
2168 idx,
2169 combo.short_range.unwrap_or(5),
2170 combo.long_range.unwrap_or(25)
2171 );
2172 }
2173
2174 if bits == 0x33333333_33333333 {
2175 panic!(
2176 "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
2177 in VPCI at row {} col {} (flat index {}) with params: short_range={}, long_range={}",
2178 test,
2179 cfg_idx,
2180 val,
2181 bits,
2182 row,
2183 col,
2184 idx,
2185 combo.short_range.unwrap_or(5),
2186 combo.long_range.unwrap_or(25)
2187 );
2188 }
2189 }
2190
2191 for (idx, &val) in output.vpcis.iter().enumerate() {
2192 if val.is_nan() {
2193 continue;
2194 }
2195
2196 let bits = val.to_bits();
2197 let row = idx / output.cols;
2198 let col = idx % output.cols;
2199 let combo = &output.combos[row];
2200
2201 if bits == 0x11111111_11111111 {
2202 panic!(
2203 "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
2204 in VPCIS at row {} col {} (flat index {}) with params: short_range={}, long_range={}",
2205 test,
2206 cfg_idx,
2207 val,
2208 bits,
2209 row,
2210 col,
2211 idx,
2212 combo.short_range.unwrap_or(5),
2213 combo.long_range.unwrap_or(25)
2214 );
2215 }
2216
2217 if bits == 0x22222222_22222222 {
2218 panic!(
2219 "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
2220 in VPCIS at row {} col {} (flat index {}) with params: short_range={}, long_range={}",
2221 test,
2222 cfg_idx,
2223 val,
2224 bits,
2225 row,
2226 col,
2227 idx,
2228 combo.short_range.unwrap_or(5),
2229 combo.long_range.unwrap_or(25)
2230 );
2231 }
2232
2233 if bits == 0x33333333_33333333 {
2234 panic!(
2235 "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
2236 in VPCIS at row {} col {} (flat index {}) with params: short_range={}, long_range={}",
2237 test,
2238 cfg_idx,
2239 val,
2240 bits,
2241 row,
2242 col,
2243 idx,
2244 combo.short_range.unwrap_or(5),
2245 combo.long_range.unwrap_or(25)
2246 );
2247 }
2248 }
2249 }
2250
2251 Ok(())
2252 }
2253
2254 #[cfg(not(debug_assertions))]
2255 fn check_batch_no_poison(
2256 _test: &str,
2257 _kernel: Kernel,
2258 ) -> Result<(), Box<dyn std::error::Error>> {
2259 Ok(())
2260 }
2261
2262 macro_rules! gen_batch_tests {
2263 ($fn_name:ident) => {
2264 paste::paste! {
2265 #[test] fn [<$fn_name _scalar>]() {
2266 let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
2267 }
2268 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2269 #[test] fn [<$fn_name _avx2>]() {
2270 let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
2271 }
2272 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2273 #[test] fn [<$fn_name _avx512>]() {
2274 let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
2275 }
2276 #[test] fn [<$fn_name _auto_detect>]() {
2277 let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
2278 }
2279 }
2280 };
2281 }
2282 gen_batch_tests!(check_batch_default_row);
2283 gen_batch_tests!(check_batch_no_poison);
2284}
2285
2286#[cfg(test)]
2287#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
2288mod tests_into {
2289 use super::*;
2290 use crate::utilities::data_loader::read_candles_from_csv;
2291
2292 #[test]
2293 fn test_vpci_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
2294 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2295 let candles = read_candles_from_csv(file_path)?;
2296
2297 let params = VpciParams::default();
2298 let input = VpciInput::from_candles(&candles, "close", "volume", params);
2299
2300 let base = vpci(&input)?;
2301
2302 let n = candles.close.len();
2303 let mut y = vec![0.0f64; n];
2304 let mut ys = vec![0.0f64; n];
2305 vpci_into(&input, &mut y, &mut ys)?;
2306
2307 assert_eq!(base.vpci.len(), y.len());
2308 assert_eq!(base.vpcis.len(), ys.len());
2309
2310 fn eq_or_both_nan(a: f64, b: f64) -> bool {
2311 (a.is_nan() && b.is_nan()) || (a - b).abs() <= 1e-12 || a.to_bits() == b.to_bits()
2312 }
2313
2314 for i in 0..n {
2315 assert!(
2316 eq_or_both_nan(base.vpci[i], y[i]),
2317 "VPCI mismatch at {}: base={}, into={}",
2318 i,
2319 base.vpci[i],
2320 y[i]
2321 );
2322 assert!(
2323 eq_or_both_nan(base.vpcis[i], ys[i]),
2324 "VPCIS mismatch at {}: base={}, into={}",
2325 i,
2326 base.vpcis[i],
2327 ys[i]
2328 );
2329 }
2330
2331 Ok(())
2332 }
2333}
2334
2335#[cfg(feature = "python")]
2336#[pyfunction(name = "vpci")]
2337#[pyo3(signature = (close, volume, short_range, long_range, kernel=None))]
2338pub fn vpci_py<'py>(
2339 py: Python<'py>,
2340 close: numpy::PyReadonlyArray1<'py, f64>,
2341 volume: numpy::PyReadonlyArray1<'py, f64>,
2342 short_range: usize,
2343 long_range: usize,
2344 kernel: Option<&str>,
2345) -> PyResult<(
2346 Bound<'py, numpy::PyArray1<f64>>,
2347 Bound<'py, numpy::PyArray1<f64>>,
2348)> {
2349 use numpy::{IntoPyArray, PyArrayMethods};
2350
2351 let close_slice = close.as_slice()?;
2352 let volume_slice = volume.as_slice()?;
2353
2354 if close_slice.len() != volume_slice.len() {
2355 return Err(PyValueError::new_err(
2356 "Close and volume arrays must have the same length",
2357 ));
2358 }
2359
2360 let kern = validate_kernel(kernel, false)?;
2361 let params = VpciParams {
2362 short_range: Some(short_range),
2363 long_range: Some(long_range),
2364 };
2365 let input = VpciInput::from_slices(close_slice, volume_slice, params);
2366
2367 let (vpci_vec, vpcis_vec) = py
2368 .allow_threads(|| vpci_with_kernel(&input, kern).map(|o| (o.vpci, o.vpcis)))
2369 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2370
2371 Ok((vpci_vec.into_pyarray(py), vpcis_vec.into_pyarray(py)))
2372}
2373
2374#[cfg(feature = "python")]
2375#[pyclass(name = "VpciStream")]
2376pub struct VpciStreamPy {
2377 stream: VpciStream,
2378}
2379
2380#[cfg(feature = "python")]
2381#[pymethods]
2382impl VpciStreamPy {
2383 #[new]
2384 fn new(short_range: usize, long_range: usize) -> PyResult<Self> {
2385 let params = VpciParams {
2386 short_range: Some(short_range),
2387 long_range: Some(long_range),
2388 };
2389 let stream =
2390 VpciStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2391 Ok(VpciStreamPy { stream })
2392 }
2393
2394 fn update(&mut self, close: f64, volume: f64) -> Option<(f64, f64)> {
2395 self.stream.update(close, volume)
2396 }
2397}
2398
2399#[cfg(feature = "python")]
2400#[pyfunction(name = "vpci_batch")]
2401#[pyo3(signature = (close, volume, short_range_tuple, long_range_tuple, kernel=None))]
2402pub fn vpci_batch_py<'py>(
2403 py: Python<'py>,
2404 close: numpy::PyReadonlyArray1<'py, f64>,
2405 volume: numpy::PyReadonlyArray1<'py, f64>,
2406 short_range_tuple: (usize, usize, usize),
2407 long_range_tuple: (usize, usize, usize),
2408 kernel: Option<&str>,
2409) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
2410 use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2411 use pyo3::types::PyDict;
2412
2413 let close_slice = close.as_slice()?;
2414 let volume_slice = volume.as_slice()?;
2415
2416 if close_slice.len() != volume_slice.len() {
2417 return Err(PyValueError::new_err(
2418 "Close and volume arrays must have the same length",
2419 ));
2420 }
2421
2422 let sweep = VpciBatchRange {
2423 short_range: short_range_tuple,
2424 long_range: long_range_tuple,
2425 };
2426
2427 let combos = expand_grid(&sweep);
2428 let rows = combos.len();
2429 let cols = close_slice.len();
2430 if rows == 0 || cols == 0 {
2431 return Err(PyValueError::new_err(
2432 "no parameter combinations or empty input",
2433 ));
2434 }
2435 let total = rows
2436 .checked_mul(cols)
2437 .ok_or_else(|| PyValueError::new_err("rows*cols overflow in vpci_batch_py"))?;
2438
2439 let vpci_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2440 let vpcis_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2441 let vpci_slice = unsafe { vpci_arr.as_slice_mut()? };
2442 let vpcis_slice = unsafe { vpcis_arr.as_slice_mut()? };
2443
2444 let kern = validate_kernel(kernel, true)?;
2445
2446 let combos = py
2447 .allow_threads(|| {
2448 let kernel = match kern {
2449 Kernel::Auto => detect_best_batch_kernel(),
2450 k => k,
2451 };
2452
2453 let simd = match kernel {
2454 Kernel::Avx512Batch => Kernel::Avx512,
2455 Kernel::Avx2Batch => Kernel::Avx2,
2456 Kernel::ScalarBatch => Kernel::Scalar,
2457 _ => kernel,
2458 };
2459
2460 vpci_batch_inner_into(
2461 close_slice,
2462 volume_slice,
2463 &sweep,
2464 simd,
2465 true,
2466 vpci_slice,
2467 vpcis_slice,
2468 )
2469 })
2470 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2471
2472 let dict = PyDict::new(py);
2473 dict.set_item("vpci", vpci_arr.reshape((rows, cols))?)?;
2474 dict.set_item("vpcis", vpcis_arr.reshape((rows, cols))?)?;
2475 dict.set_item(
2476 "short_ranges",
2477 combos
2478 .iter()
2479 .map(|p| p.short_range.unwrap() as u64)
2480 .collect::<Vec<_>>()
2481 .into_pyarray(py),
2482 )?;
2483 dict.set_item(
2484 "long_ranges",
2485 combos
2486 .iter()
2487 .map(|p| p.long_range.unwrap() as u64)
2488 .collect::<Vec<_>>()
2489 .into_pyarray(py),
2490 )?;
2491
2492 Ok(dict)
2493}
2494
2495#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2496#[wasm_bindgen]
2497pub fn vpci_js(
2498 close: &[f64],
2499 volume: &[f64],
2500 short_range: usize,
2501 long_range: usize,
2502) -> Result<JsValue, JsValue> {
2503 let params = VpciParams {
2504 short_range: Some(short_range),
2505 long_range: Some(long_range),
2506 };
2507 let input = VpciInput::from_slices(close, volume, params);
2508
2509 let out = vpci(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
2510 #[derive(Serialize)]
2511 struct Out {
2512 vpci: Vec<f64>,
2513 vpcis: Vec<f64>,
2514 }
2515 serde_wasm_bindgen::to_value(&Out {
2516 vpci: out.vpci,
2517 vpcis: out.vpcis,
2518 })
2519 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2520}
2521
2522#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2523#[wasm_bindgen]
2524pub fn vpci_into(
2525 close_ptr: *const f64,
2526 volume_ptr: *const f64,
2527 vpci_ptr: *mut f64,
2528 vpcis_ptr: *mut f64,
2529 len: usize,
2530 short_range: usize,
2531 long_range: usize,
2532) -> Result<(), JsValue> {
2533 if close_ptr.is_null() || volume_ptr.is_null() || vpci_ptr.is_null() || vpcis_ptr.is_null() {
2534 return Err(JsValue::from_str("null pointer passed to vpci_into"));
2535 }
2536
2537 unsafe {
2538 let close = core::slice::from_raw_parts(close_ptr, len);
2539 let volume = core::slice::from_raw_parts(volume_ptr, len);
2540 let vpci = core::slice::from_raw_parts_mut(vpci_ptr, len);
2541 let vpcis = core::slice::from_raw_parts_mut(vpcis_ptr, len);
2542
2543 let params = VpciParams {
2544 short_range: Some(short_range),
2545 long_range: Some(long_range),
2546 };
2547 let input = VpciInput::from_slices(close, volume, params);
2548
2549 vpci_into_slice(vpci, vpcis, &input, detect_best_kernel())
2550 .map_err(|e| JsValue::from_str(&e.to_string()))
2551 }
2552}
2553
2554#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2555#[wasm_bindgen]
2556pub fn vpci_alloc(len: usize) -> *mut f64 {
2557 let mut vec = Vec::<f64>::with_capacity(len);
2558 let ptr = vec.as_mut_ptr();
2559 std::mem::forget(vec);
2560 ptr
2561}
2562
2563#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2564#[wasm_bindgen]
2565pub fn vpci_free(ptr: *mut f64, len: usize) {
2566 if !ptr.is_null() {
2567 unsafe {
2568 let _ = Vec::from_raw_parts(ptr, len, len);
2569 }
2570 }
2571}
2572
2573#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2574#[derive(Serialize, Deserialize)]
2575pub struct VpciBatchConfig {
2576 pub short_range: (usize, usize, usize),
2577 pub long_range: (usize, usize, usize),
2578}
2579
2580#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2581#[derive(Serialize, Deserialize)]
2582pub struct VpciBatchJsOutput {
2583 pub vpci: Vec<f64>,
2584 pub vpcis: Vec<f64>,
2585 pub combos: Vec<VpciParams>,
2586 pub rows: usize,
2587 pub cols: usize,
2588}
2589
2590#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2591#[wasm_bindgen(js_name = "vpci_batch")]
2592pub fn vpci_batch_unified_js(
2593 close: &[f64],
2594 volume: &[f64],
2595 config: JsValue,
2596) -> Result<JsValue, JsValue> {
2597 let cfg: VpciBatchConfig = serde_wasm_bindgen::from_value(config)
2598 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2599 let sweep = VpciBatchRange {
2600 short_range: cfg.short_range,
2601 long_range: cfg.long_range,
2602 };
2603 let output = vpci_batch_inner(close, volume, &sweep, detect_best_kernel(), false)
2604 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2605 let js_out = VpciBatchJsOutput {
2606 vpci: output.vpci,
2607 vpcis: output.vpcis,
2608 combos: output.combos,
2609 rows: output.rows,
2610 cols: output.cols,
2611 };
2612 serde_wasm_bindgen::to_value(&js_out)
2613 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2614}
2615
2616#[cfg(all(feature = "python", feature = "cuda"))]
2617use crate::cuda::cuda_available;
2618#[cfg(all(feature = "python", feature = "cuda"))]
2619use crate::cuda::vpci_wrapper::CudaVpci;
2620#[cfg(all(feature = "python", feature = "cuda"))]
2621use crate::indicators::moving_averages::alma::DeviceArrayF32Py;
2622
2623#[cfg(all(feature = "python", feature = "cuda"))]
2624#[pyfunction(name = "vpci_cuda_batch_dev")]
2625#[pyo3(signature = (close_f32, volume_f32, short_range_tuple, long_range_tuple, device_id=0))]
2626pub fn vpci_cuda_batch_dev_py<'py>(
2627 py: Python<'py>,
2628 close_f32: numpy::PyReadonlyArray1<'py, f32>,
2629 volume_f32: numpy::PyReadonlyArray1<'py, f32>,
2630 short_range_tuple: (usize, usize, usize),
2631 long_range_tuple: (usize, usize, usize),
2632 device_id: usize,
2633) -> PyResult<Bound<'py, PyDict>> {
2634 use numpy::IntoPyArray;
2635 if !cuda_available() {
2636 return Err(PyValueError::new_err("CUDA not available"));
2637 }
2638 let c = close_f32.as_slice()?;
2639 let v = volume_f32.as_slice()?;
2640 if c.len() != v.len() {
2641 return Err(PyValueError::new_err("length mismatch"));
2642 }
2643 let sweep = VpciBatchRange {
2644 short_range: short_range_tuple,
2645 long_range: long_range_tuple,
2646 };
2647 let (pair, combos, ctx, dev_id_u32) = py.allow_threads(|| {
2648 let cuda = CudaVpci::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2649 let ctx = cuda.context_arc();
2650 let dev_id_u32 = cuda.device_id();
2651 cuda.vpci_batch_dev(c, v, &sweep)
2652 .map(|(pair, combos)| (pair, combos, ctx, dev_id_u32))
2653 .map_err(|e| PyValueError::new_err(e.to_string()))
2654 })?;
2655 let dict = PyDict::new(py);
2656 dict.set_item(
2657 "vpci",
2658 Py::new(
2659 py,
2660 DeviceArrayF32Py {
2661 inner: pair.a,
2662 _ctx: Some(ctx.clone()),
2663 device_id: Some(dev_id_u32),
2664 },
2665 )?,
2666 )?;
2667 dict.set_item(
2668 "vpcis",
2669 Py::new(
2670 py,
2671 DeviceArrayF32Py {
2672 inner: pair.b,
2673 _ctx: Some(ctx),
2674 device_id: Some(dev_id_u32),
2675 },
2676 )?,
2677 )?;
2678 dict.set_item("rows", combos.len())?;
2679 dict.set_item("cols", c.len())?;
2680 dict.set_item(
2681 "short_ranges",
2682 combos
2683 .iter()
2684 .map(|p| p.short_range.unwrap_or(5) as u64)
2685 .collect::<Vec<_>>()
2686 .into_pyarray(py),
2687 )?;
2688 dict.set_item(
2689 "long_ranges",
2690 combos
2691 .iter()
2692 .map(|p| p.long_range.unwrap_or(25) as u64)
2693 .collect::<Vec<_>>()
2694 .into_pyarray(py),
2695 )?;
2696 Ok(dict)
2697}
2698
2699#[cfg(all(feature = "python", feature = "cuda"))]
2700#[pyfunction(name = "vpci_cuda_many_series_one_param_dev")]
2701#[pyo3(signature = (close_tm_f32, volume_tm_f32, short_range, long_range, device_id=0))]
2702pub fn vpci_cuda_many_series_one_param_dev_py<'py>(
2703 py: Python<'py>,
2704 close_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
2705 volume_tm_f32: numpy::PyReadonlyArray2<'py, f32>,
2706 short_range: usize,
2707 long_range: usize,
2708 device_id: usize,
2709) -> PyResult<Bound<'py, PyDict>> {
2710 use numpy::PyUntypedArrayMethods;
2711 if !cuda_available() {
2712 return Err(PyValueError::new_err("CUDA not available"));
2713 }
2714 let shape = close_tm_f32.shape();
2715 if shape.len() != 2 {
2716 return Err(PyValueError::new_err("expected 2D array for close"));
2717 }
2718 if volume_tm_f32.shape() != shape {
2719 return Err(PyValueError::new_err(
2720 "input arrays must share the same shape",
2721 ));
2722 }
2723 let rows = shape[0];
2724 let cols = shape[1];
2725 let c = close_tm_f32.as_slice()?;
2726 let v = volume_tm_f32.as_slice()?;
2727 let params = VpciParams {
2728 short_range: Some(short_range),
2729 long_range: Some(long_range),
2730 };
2731 let (pair, ctx, dev_id_u32) = py.allow_threads(|| {
2732 let cuda = CudaVpci::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2733 let ctx = cuda.context_arc();
2734 let dev_id_u32 = cuda.device_id();
2735 cuda.vpci_many_series_one_param_time_major_dev(c, v, cols, rows, ¶ms)
2736 .map(|pair| (pair, ctx, dev_id_u32))
2737 .map_err(|e| PyValueError::new_err(e.to_string()))
2738 })?;
2739 let dict = PyDict::new(py);
2740 dict.set_item(
2741 "vpci",
2742 Py::new(
2743 py,
2744 DeviceArrayF32Py {
2745 inner: pair.a,
2746 _ctx: Some(ctx.clone()),
2747 device_id: Some(dev_id_u32),
2748 },
2749 )?,
2750 )?;
2751 dict.set_item(
2752 "vpcis",
2753 Py::new(
2754 py,
2755 DeviceArrayF32Py {
2756 inner: pair.b,
2757 _ctx: Some(ctx),
2758 device_id: Some(dev_id_u32),
2759 },
2760 )?,
2761 )?;
2762 dict.set_item("rows", rows)?;
2763 dict.set_item("cols", cols)?;
2764 dict.set_item("short_range", short_range)?;
2765 dict.set_item("long_range", long_range)?;
2766 Ok(dict)
2767}
2768
2769#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2770#[wasm_bindgen]
2771pub fn vpci_batch_into(
2772 close_ptr: *const f64,
2773 volume_ptr: *const f64,
2774 vpci_ptr: *mut f64,
2775 vpcis_ptr: *mut f64,
2776 len: usize,
2777 short_start: usize,
2778 short_end: usize,
2779 short_step: usize,
2780 long_start: usize,
2781 long_end: usize,
2782 long_step: usize,
2783) -> Result<usize, JsValue> {
2784 if close_ptr.is_null() || volume_ptr.is_null() || vpci_ptr.is_null() || vpcis_ptr.is_null() {
2785 return Err(JsValue::from_str("null pointer passed to vpci_batch_into"));
2786 }
2787
2788 unsafe {
2789 let close = std::slice::from_raw_parts(close_ptr, len);
2790 let volume = std::slice::from_raw_parts(volume_ptr, len);
2791
2792 let sweep = VpciBatchRange {
2793 short_range: (short_start, short_end, short_step),
2794 long_range: (long_start, long_end, long_step),
2795 };
2796
2797 let combos = expand_grid_vpci(&sweep);
2798 let rows = combos.len();
2799 if rows == 0 {
2800 return Err(JsValue::from_str(
2801 "no parameter combinations for vpci_batch_into",
2802 ));
2803 }
2804 let total_len = rows
2805 .checked_mul(len)
2806 .ok_or_else(|| JsValue::from_str("rows*len overflow in vpci_batch_into"))?;
2807
2808 let need_temp = close_ptr == vpci_ptr as *const f64
2809 || close_ptr == vpcis_ptr as *const f64
2810 || volume_ptr == vpci_ptr as *const f64
2811 || volume_ptr == vpcis_ptr as *const f64;
2812
2813 if need_temp {
2814 let output = vpci_batch_inner(close, volume, &sweep, detect_best_kernel(), false)
2815 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2816
2817 let vpci_out = std::slice::from_raw_parts_mut(vpci_ptr, total_len);
2818 let vpcis_out = std::slice::from_raw_parts_mut(vpcis_ptr, total_len);
2819 vpci_out.copy_from_slice(&output.vpci);
2820 vpcis_out.copy_from_slice(&output.vpcis);
2821 } else {
2822 let vpci_out = std::slice::from_raw_parts_mut(vpci_ptr, total_len);
2823 let vpcis_out = std::slice::from_raw_parts_mut(vpcis_ptr, total_len);
2824
2825 vpci_batch_inner_into(
2826 close,
2827 volume,
2828 &sweep,
2829 detect_best_kernel(),
2830 false,
2831 vpci_out,
2832 vpcis_out,
2833 )
2834 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2835 }
2836
2837 Ok(rows)
2838 }
2839}
2840
2841#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2842#[wasm_bindgen]
2843#[deprecated(
2844 since = "1.0.0",
2845 note = "For weight reuse patterns, use the fast/unsafe API with persistent buffers or VpciStream"
2846)]
2847pub struct VpciContext {
2848 short_range: usize,
2849 long_range: usize,
2850 kernel: Kernel,
2851}
2852
2853#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2854#[wasm_bindgen]
2855impl VpciContext {
2856 #[wasm_bindgen(constructor)]
2857 pub fn new(short_range: usize, long_range: usize) -> Result<VpciContext, JsValue> {
2858 if short_range == 0 || long_range == 0 || short_range > long_range {
2859 return Err(JsValue::from_str("Invalid range parameters"));
2860 }
2861
2862 Ok(VpciContext {
2863 short_range,
2864 long_range,
2865 kernel: detect_best_kernel(),
2866 })
2867 }
2868
2869 pub fn update_into(
2870 &self,
2871 close_ptr: *const f64,
2872 volume_ptr: *const f64,
2873 vpci_ptr: *mut f64,
2874 vpcis_ptr: *mut f64,
2875 len: usize,
2876 ) -> Result<(), JsValue> {
2877 if close_ptr.is_null() || volume_ptr.is_null() || vpci_ptr.is_null() || vpcis_ptr.is_null()
2878 {
2879 return Err(JsValue::from_str("null pointer passed to update_into"));
2880 }
2881
2882 if len < self.long_range {
2883 return Err(JsValue::from_str("Data length less than long range"));
2884 }
2885
2886 unsafe {
2887 let close = std::slice::from_raw_parts(close_ptr, len);
2888 let volume = std::slice::from_raw_parts(volume_ptr, len);
2889
2890 let params = VpciParams {
2891 short_range: Some(self.short_range),
2892 long_range: Some(self.long_range),
2893 };
2894 let input = VpciInput::from_slices(close, volume, params);
2895
2896 let need_temp = close_ptr == vpci_ptr as *const f64
2897 || close_ptr == vpcis_ptr as *const f64
2898 || volume_ptr == vpci_ptr as *const f64
2899 || volume_ptr == vpcis_ptr as *const f64;
2900
2901 if need_temp {
2902 let mut temp_vpci = vec![0.0; len];
2903 let mut temp_vpcis = vec![0.0; len];
2904 vpci_into_slice(&mut temp_vpci, &mut temp_vpcis, &input, self.kernel)
2905 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2906
2907 let vpci_out = std::slice::from_raw_parts_mut(vpci_ptr, len);
2908 let vpcis_out = std::slice::from_raw_parts_mut(vpcis_ptr, len);
2909 vpci_out.copy_from_slice(&temp_vpci);
2910 vpcis_out.copy_from_slice(&temp_vpcis);
2911 } else {
2912 let vpci_out = std::slice::from_raw_parts_mut(vpci_ptr, len);
2913 let vpcis_out = std::slice::from_raw_parts_mut(vpcis_ptr, len);
2914 vpci_into_slice(vpci_out, vpcis_out, &input, self.kernel)
2915 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2916 }
2917 }
2918
2919 Ok(())
2920 }
2921
2922 pub fn get_warmup_period(&self) -> usize {
2923 self.long_range - 1
2924 }
2925}