Skip to main content

vector_ta/indicators/
vpci.rs

1use crate::utilities::data_loader::{source_type, Candles};
2use crate::utilities::enums::Kernel;
3use crate::utilities::helpers::{
4    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
5    make_uninit_matrix,
6};
7#[cfg(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, &params)
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}