Skip to main content

vector_ta/indicators/
wclprice.rs

1#[cfg(all(feature = "python", feature = "cuda"))]
2use crate::cuda::{cuda_available, CudaWclprice};
3#[cfg(all(feature = "python", feature = "cuda"))]
4use crate::utilities::dlpack_cuda::DeviceArrayF32Py;
5#[cfg(feature = "python")]
6use numpy::{IntoPyArray, PyArray1};
7#[cfg(feature = "python")]
8use pyo3::exceptions::PyValueError;
9#[cfg(feature = "python")]
10use pyo3::prelude::*;
11#[cfg(feature = "python")]
12use pyo3::types::PyDict;
13
14#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
15use serde::{Deserialize, Serialize};
16#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
17use wasm_bindgen::prelude::*;
18
19use crate::utilities::data_loader::Candles;
20use crate::utilities::enums::Kernel;
21use crate::utilities::helpers::{
22    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
23    make_uninit_matrix,
24};
25#[cfg(feature = "python")]
26use crate::utilities::kernel_validation::validate_kernel;
27use std::error::Error;
28use thiserror::Error;
29
30#[derive(Debug, Clone)]
31pub enum WclpriceData<'a> {
32    Candles {
33        candles: &'a Candles,
34    },
35    Slices {
36        high: &'a [f64],
37        low: &'a [f64],
38        close: &'a [f64],
39    },
40}
41
42#[derive(Debug, Clone)]
43pub struct WclpriceOutput {
44    pub values: Vec<f64>,
45}
46
47#[derive(Debug, Clone)]
48#[cfg_attr(
49    all(target_arch = "wasm32", feature = "wasm"),
50    derive(serde::Serialize, serde::Deserialize)
51)]
52pub struct WclpriceParams;
53
54impl Default for WclpriceParams {
55    fn default() -> Self {
56        Self
57    }
58}
59
60#[derive(Debug, Clone)]
61pub struct WclpriceInput<'a> {
62    pub data: WclpriceData<'a>,
63    pub params: WclpriceParams,
64}
65
66impl<'a> WclpriceInput<'a> {
67    #[inline]
68    pub fn from_candles(candles: &'a Candles) -> Self {
69        Self {
70            data: WclpriceData::Candles { candles },
71            params: WclpriceParams::default(),
72        }
73    }
74    #[inline]
75    pub fn from_slices(high: &'a [f64], low: &'a [f64], close: &'a [f64]) -> Self {
76        Self {
77            data: WclpriceData::Slices { high, low, close },
78            params: WclpriceParams::default(),
79        }
80    }
81    #[inline]
82    pub fn with_default_candles(candles: &'a Candles) -> Self {
83        Self::from_candles(candles)
84    }
85}
86
87#[derive(Copy, Clone, Debug)]
88pub struct WclpriceBuilder {
89    kernel: Kernel,
90}
91impl Default for WclpriceBuilder {
92    fn default() -> Self {
93        Self {
94            kernel: Kernel::Auto,
95        }
96    }
97}
98impl WclpriceBuilder {
99    #[inline]
100    pub fn new() -> Self {
101        Self::default()
102    }
103    #[inline]
104    pub fn kernel(mut self, k: Kernel) -> Self {
105        self.kernel = k;
106        self
107    }
108    #[inline]
109    pub fn apply(self, candles: &Candles) -> Result<WclpriceOutput, WclpriceError> {
110        let i = WclpriceInput::from_candles(candles);
111        wclprice_with_kernel(&i, self.kernel)
112    }
113    #[inline]
114    pub fn apply_slices(
115        self,
116        high: &[f64],
117        low: &[f64],
118        close: &[f64],
119    ) -> Result<WclpriceOutput, WclpriceError> {
120        let i = WclpriceInput::from_slices(high, low, close);
121        wclprice_with_kernel(&i, self.kernel)
122    }
123    #[inline]
124    pub fn into_stream(self) -> WclpriceStream {
125        WclpriceStream::default()
126    }
127}
128
129#[derive(Debug, Error)]
130pub enum WclpriceError {
131    #[error("wclprice: empty input")]
132    EmptyInputData,
133    #[error("wclprice: all values are NaN")]
134    AllValuesNaN,
135    #[error("wclprice: invalid period: period = {period}, data length = {data_len}")]
136    InvalidPeriod { period: usize, data_len: usize },
137    #[error("wclprice: not enough valid data: needed = {needed}, valid = {valid}")]
138    NotEnoughValidData { needed: usize, valid: usize },
139    #[error("wclprice: output length mismatch: expected = {expected}, got = {got}")]
140    OutputLengthMismatch { expected: usize, got: usize },
141    #[error("wclprice: invalid range: start = {start}, end = {end}, step = {step}")]
142    InvalidRange {
143        start: usize,
144        end: usize,
145        step: usize,
146    },
147    #[error("wclprice: invalid kernel for batch mode: {0:?}")]
148    InvalidKernelForBatch(Kernel),
149    #[error("wclprice: missing candle field '{field}'")]
150    MissingField { field: &'static str },
151}
152
153#[inline(always)]
154fn wclprice_prepare<'a>(
155    input: &'a WclpriceInput<'a>,
156    kernel: Kernel,
157) -> Result<(&'a [f64], &'a [f64], &'a [f64], usize, usize, Kernel), WclpriceError> {
158    let (high, low, close) = match &input.data {
159        WclpriceData::Candles { candles } => {
160            let h = candles
161                .select_candle_field("high")
162                .map_err(|_| WclpriceError::MissingField { field: "high" })?;
163            let l = candles
164                .select_candle_field("low")
165                .map_err(|_| WclpriceError::MissingField { field: "low" })?;
166            let c = candles
167                .select_candle_field("close")
168                .map_err(|_| WclpriceError::MissingField { field: "close" })?;
169            (h, l, c)
170        }
171        WclpriceData::Slices { high, low, close } => (*high, *low, *close),
172    };
173
174    if high.is_empty() || low.is_empty() || close.is_empty() {
175        return Err(WclpriceError::EmptyInputData);
176    }
177    let lh = high.len();
178    let ll = low.len();
179    let lc = close.len();
180    let len = lh.min(ll).min(lc);
181
182    let first = (0..len)
183        .find(|&i| !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan())
184        .ok_or(WclpriceError::AllValuesNaN)?;
185
186    let chosen = match kernel {
187        Kernel::Auto => Kernel::Scalar,
188        Kernel::Avx2Batch => Kernel::Avx2,
189        Kernel::Avx512Batch => Kernel::Avx512,
190        Kernel::ScalarBatch => Kernel::Scalar,
191        k => k,
192    };
193    Ok((high, low, close, len, first, chosen))
194}
195
196#[inline]
197pub fn wclprice(input: &WclpriceInput) -> Result<WclpriceOutput, WclpriceError> {
198    wclprice_with_kernel(input, Kernel::Auto)
199}
200
201pub fn wclprice_with_kernel(
202    input: &WclpriceInput,
203    kernel: Kernel,
204) -> Result<WclpriceOutput, WclpriceError> {
205    let (high, low, close, len, first, chosen) = wclprice_prepare(input, kernel)?;
206    let mut out = alloc_with_nan_prefix(len, first);
207    unsafe {
208        match chosen {
209            Kernel::Scalar | Kernel::ScalarBatch => {
210                wclprice_scalar(high, low, close, first, &mut out)
211            }
212            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
213            Kernel::Avx2 | Kernel::Avx2Batch => wclprice_avx2(high, low, close, first, &mut out),
214            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
215            Kernel::Avx512 | Kernel::Avx512Batch => {
216                wclprice_avx512(high, low, close, first, &mut out)
217            }
218            _ => wclprice_scalar(high, low, close, first, &mut out),
219        }
220    }
221    Ok(WclpriceOutput { values: out })
222}
223
224#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
225#[inline]
226
227pub fn wclprice_into(input: &WclpriceInput, out: &mut [f64]) -> Result<(), WclpriceError> {
228    wclprice_into_slice(out, input, Kernel::Auto)
229}
230
231#[inline]
232pub fn wclprice_into_slice(
233    dst: &mut [f64],
234    input: &WclpriceInput,
235    kern: Kernel,
236) -> Result<(), WclpriceError> {
237    let (high, low, close, len, first, chosen) = wclprice_prepare(input, kern)?;
238    if dst.len() != len {
239        return Err(WclpriceError::OutputLengthMismatch {
240            expected: len,
241            got: dst.len(),
242        });
243    }
244
245    if first > 0 {
246        dst[..first].fill(f64::NAN);
247    }
248    unsafe {
249        match chosen {
250            Kernel::Scalar | Kernel::ScalarBatch => wclprice_scalar(high, low, close, first, dst),
251            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
252            Kernel::Avx2 | Kernel::Avx2Batch => wclprice_avx2(high, low, close, first, dst),
253            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
254            Kernel::Avx512 | Kernel::Avx512Batch => wclprice_avx512(high, low, close, first, dst),
255            _ => wclprice_scalar(high, low, close, first, dst),
256        }
257    }
258    Ok(())
259}
260
261#[inline]
262pub fn wclprice_scalar(
263    high: &[f64],
264    low: &[f64],
265    close: &[f64],
266    first_valid: usize,
267    out: &mut [f64],
268) {
269    let len = high.len().min(low.len()).min(close.len());
270    debug_assert_eq!(out.len(), len);
271
272    const HALF: f64 = 0.5;
273    const QUARTER: f64 = 0.25;
274
275    let mut i = first_valid;
276    let end = len;
277    while i + 8 <= end {
278        let h0 = high[i + 0];
279        let l0 = low[i + 0];
280        let c0 = close[i + 0];
281        out[i + 0] = c0.mul_add(HALF, (h0 + l0) * QUARTER);
282
283        let h1 = high[i + 1];
284        let l1 = low[i + 1];
285        let c1 = close[i + 1];
286        out[i + 1] = c1.mul_add(HALF, (h1 + l1) * QUARTER);
287
288        let h2 = high[i + 2];
289        let l2 = low[i + 2];
290        let c2 = close[i + 2];
291        out[i + 2] = c2.mul_add(HALF, (h2 + l2) * QUARTER);
292
293        let h3 = high[i + 3];
294        let l3 = low[i + 3];
295        let c3 = close[i + 3];
296        out[i + 3] = c3.mul_add(HALF, (h3 + l3) * QUARTER);
297
298        let h4 = high[i + 4];
299        let l4 = low[i + 4];
300        let c4 = close[i + 4];
301        out[i + 4] = c4.mul_add(HALF, (h4 + l4) * QUARTER);
302
303        let h5 = high[i + 5];
304        let l5 = low[i + 5];
305        let c5 = close[i + 5];
306        out[i + 5] = c5.mul_add(HALF, (h5 + l5) * QUARTER);
307
308        let h6 = high[i + 6];
309        let l6 = low[i + 6];
310        let c6 = close[i + 6];
311        out[i + 6] = c6.mul_add(HALF, (h6 + l6) * QUARTER);
312
313        let h7 = high[i + 7];
314        let l7 = low[i + 7];
315        let c7 = close[i + 7];
316        out[i + 7] = c7.mul_add(HALF, (h7 + l7) * QUARTER);
317
318        i += 8;
319    }
320    while i + 4 <= end {
321        let h0 = high[i];
322        let l0 = low[i];
323        let c0 = close[i];
324        out[i] = c0.mul_add(HALF, (h0 + l0) * QUARTER);
325
326        let h1 = high[i + 1];
327        let l1 = low[i + 1];
328        let c1 = close[i + 1];
329        out[i + 1] = c1.mul_add(HALF, (h1 + l1) * QUARTER);
330
331        let h2 = high[i + 2];
332        let l2 = low[i + 2];
333        let c2 = close[i + 2];
334        out[i + 2] = c2.mul_add(HALF, (h2 + l2) * QUARTER);
335
336        let h3 = high[i + 3];
337        let l3 = low[i + 3];
338        let c3 = close[i + 3];
339        out[i + 3] = c3.mul_add(HALF, (h3 + l3) * QUARTER);
340
341        i += 4;
342    }
343    while i < end {
344        let h = high[i];
345        let l = low[i];
346        let c = close[i];
347        out[i] = c.mul_add(HALF, (h + l) * QUARTER);
348        i += 1;
349    }
350}
351
352#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
353#[inline]
354#[target_feature(enable = "avx2,fma")]
355pub unsafe fn wclprice_avx2(
356    high: &[f64],
357    low: &[f64],
358    close: &[f64],
359    first_valid: usize,
360    out: &mut [f64],
361) {
362    use core::arch::x86_64::*;
363
364    let len = high.len().min(low.len()).min(close.len());
365    debug_assert_eq!(out.len(), len);
366
367    let mut i = first_valid;
368    let end = len;
369
370    let vhalf = _mm256_set1_pd(0.5);
371    let vquart = _mm256_set1_pd(0.25);
372
373    const STEP: usize = 4;
374
375    while i + 2 * STEP <= end {
376        let h0 = _mm256_loadu_pd(high.as_ptr().add(i));
377        let l0 = _mm256_loadu_pd(low.as_ptr().add(i));
378        let c0 = _mm256_loadu_pd(close.as_ptr().add(i));
379        let hl0 = _mm256_add_pd(h0, l0);
380        let t0 = _mm256_mul_pd(hl0, vquart);
381
382        let h1 = _mm256_loadu_pd(high.as_ptr().add(i + STEP));
383        let l1 = _mm256_loadu_pd(low.as_ptr().add(i + STEP));
384        let c1 = _mm256_loadu_pd(close.as_ptr().add(i + STEP));
385        let hl1 = _mm256_add_pd(h1, l1);
386        let t1 = _mm256_mul_pd(hl1, vquart);
387
388        let y0 = _mm256_fmadd_pd(c0, vhalf, t0);
389        let y1 = _mm256_fmadd_pd(c1, vhalf, t1);
390
391        _mm256_storeu_pd(out.as_mut_ptr().add(i), y0);
392        _mm256_storeu_pd(out.as_mut_ptr().add(i + STEP), y1);
393
394        i += 2 * STEP;
395    }
396
397    while i + STEP <= end {
398        let h = _mm256_loadu_pd(high.as_ptr().add(i));
399        let l = _mm256_loadu_pd(low.as_ptr().add(i));
400        let c = _mm256_loadu_pd(close.as_ptr().add(i));
401        let hl = _mm256_add_pd(h, l);
402        let t = _mm256_mul_pd(hl, vquart);
403        let y = _mm256_fmadd_pd(c, vhalf, t);
404        _mm256_storeu_pd(out.as_mut_ptr().add(i), y);
405        i += STEP;
406    }
407    while i < end {
408        let h = *high.get_unchecked(i);
409        let l = *low.get_unchecked(i);
410        let c = *close.get_unchecked(i);
411        *out.get_unchecked_mut(i) = c.mul_add(0.5, (h + l) * 0.25);
412        i += 1;
413    }
414}
415
416#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
417#[inline]
418#[target_feature(enable = "avx512f,fma")]
419pub unsafe fn wclprice_avx512(
420    high: &[f64],
421    low: &[f64],
422    close: &[f64],
423    first_valid: usize,
424    out: &mut [f64],
425) {
426    use core::arch::x86_64::*;
427
428    let len = high.len().min(low.len()).min(close.len());
429    debug_assert_eq!(out.len(), len);
430
431    let mut i = first_valid;
432    let end = len;
433
434    let vhalf = _mm512_set1_pd(0.5);
435    let vquart = _mm512_set1_pd(0.25);
436
437    const STEP: usize = 8;
438
439    while i + 2 * STEP <= end {
440        let h0 = _mm512_loadu_pd(high.as_ptr().add(i));
441        let l0 = _mm512_loadu_pd(low.as_ptr().add(i));
442        let c0 = _mm512_loadu_pd(close.as_ptr().add(i));
443        let hl0 = _mm512_add_pd(h0, l0);
444        let t0 = _mm512_mul_pd(hl0, vquart);
445
446        let h1 = _mm512_loadu_pd(high.as_ptr().add(i + STEP));
447        let l1 = _mm512_loadu_pd(low.as_ptr().add(i + STEP));
448        let c1 = _mm512_loadu_pd(close.as_ptr().add(i + STEP));
449        let hl1 = _mm512_add_pd(h1, l1);
450        let t1 = _mm512_mul_pd(hl1, vquart);
451
452        let y0 = _mm512_fmadd_pd(c0, vhalf, t0);
453        let y1 = _mm512_fmadd_pd(c1, vhalf, t1);
454
455        _mm512_storeu_pd(out.as_mut_ptr().add(i), y0);
456        _mm512_storeu_pd(out.as_mut_ptr().add(i + STEP), y1);
457
458        i += 2 * STEP;
459    }
460
461    while i + STEP <= end {
462        let h = _mm512_loadu_pd(high.as_ptr().add(i));
463        let l = _mm512_loadu_pd(low.as_ptr().add(i));
464        let c = _mm512_loadu_pd(close.as_ptr().add(i));
465        let hl = _mm512_add_pd(h, l);
466        let t = _mm512_mul_pd(hl, vquart);
467        let y = _mm512_fmadd_pd(c, vhalf, t);
468        _mm512_storeu_pd(out.as_mut_ptr().add(i), y);
469        i += STEP;
470    }
471    while i < end {
472        let h = *high.get_unchecked(i);
473        let l = *low.get_unchecked(i);
474        let c = *close.get_unchecked(i);
475        *out.get_unchecked_mut(i) = c.mul_add(0.5, (h + l) * 0.25);
476        i += 1;
477    }
478}
479
480#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
481#[inline]
482pub fn wclprice_avx512_short(
483    high: &[f64],
484    low: &[f64],
485    close: &[f64],
486    first_valid: usize,
487    out: &mut [f64],
488) {
489    wclprice_scalar(high, low, close, first_valid, out)
490}
491
492#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
493#[inline]
494pub fn wclprice_avx512_long(
495    high: &[f64],
496    low: &[f64],
497    close: &[f64],
498    first_valid: usize,
499    out: &mut [f64],
500) {
501    wclprice_scalar(high, low, close, first_valid, out)
502}
503
504#[inline]
505pub fn wclprice_row_scalar(
506    high: &[f64],
507    low: &[f64],
508    close: &[f64],
509    first_valid: usize,
510    out: &mut [f64],
511) {
512    wclprice_scalar(high, low, close, first_valid, out)
513}
514
515#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
516#[inline]
517pub fn wclprice_row_avx2(
518    high: &[f64],
519    low: &[f64],
520    close: &[f64],
521    first_valid: usize,
522    out: &mut [f64],
523) {
524    unsafe { wclprice_avx2(high, low, close, first_valid, out) }
525}
526
527#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
528#[inline]
529pub fn wclprice_row_avx512(
530    high: &[f64],
531    low: &[f64],
532    close: &[f64],
533    first_valid: usize,
534    out: &mut [f64],
535) {
536    unsafe { wclprice_avx512(high, low, close, first_valid, out) }
537}
538
539#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
540#[inline]
541pub fn wclprice_row_avx512_short(
542    high: &[f64],
543    low: &[f64],
544    close: &[f64],
545    first_valid: usize,
546    out: &mut [f64],
547) {
548    wclprice_avx512_short(high, low, close, first_valid, out)
549}
550
551#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
552#[inline]
553pub fn wclprice_row_avx512_long(
554    high: &[f64],
555    low: &[f64],
556    close: &[f64],
557    first_valid: usize,
558    out: &mut [f64],
559) {
560    wclprice_avx512_long(high, low, close, first_valid, out)
561}
562
563#[derive(Clone, Debug)]
564pub struct WclpriceBatchRange;
565
566impl Default for WclpriceBatchRange {
567    fn default() -> Self {
568        Self
569    }
570}
571
572#[derive(Clone, Debug, Default)]
573pub struct WclpriceBatchBuilder {
574    kernel: Kernel,
575}
576impl WclpriceBatchBuilder {
577    pub fn new() -> Self {
578        Self::default()
579    }
580    pub fn kernel(mut self, k: Kernel) -> Self {
581        self.kernel = k;
582        self
583    }
584    pub fn apply_slices(
585        self,
586        high: &[f64],
587        low: &[f64],
588        close: &[f64],
589    ) -> Result<WclpriceBatchOutput, WclpriceError> {
590        wclprice_batch_with_kernel(high, low, close, self.kernel)
591    }
592    pub fn apply_candles(self, c: &Candles) -> Result<WclpriceBatchOutput, WclpriceError> {
593        let h = c
594            .select_candle_field("high")
595            .map_err(|_| WclpriceError::MissingField { field: "high" })?;
596        let l = c
597            .select_candle_field("low")
598            .map_err(|_| WclpriceError::MissingField { field: "low" })?;
599        let cl = c
600            .select_candle_field("close")
601            .map_err(|_| WclpriceError::MissingField { field: "close" })?;
602        self.apply_slices(h, l, cl)
603    }
604    pub fn with_default_candles(c: &Candles) -> Result<WclpriceBatchOutput, WclpriceError> {
605        WclpriceBatchBuilder::new().apply_candles(c)
606    }
607}
608
609pub fn wclprice_batch_with_kernel(
610    high: &[f64],
611    low: &[f64],
612    close: &[f64],
613    k: Kernel,
614) -> Result<WclpriceBatchOutput, WclpriceError> {
615    let kernel = match k {
616        Kernel::Auto => detect_best_batch_kernel(),
617        other if other.is_batch() => other,
618        other => return Err(WclpriceError::InvalidKernelForBatch(other)),
619    };
620    wclprice_batch_par_slice(high, low, close, kernel)
621}
622
623#[derive(Clone, Debug)]
624pub struct WclpriceBatchOutput {
625    pub values: Vec<f64>,
626    pub combos: Vec<WclpriceParams>,
627    pub rows: usize,
628    pub cols: usize,
629}
630impl WclpriceBatchOutput {
631    pub fn values_for(&self, _params: &WclpriceParams) -> Option<&[f64]> {
632        if self.rows == 1 {
633            Some(&self.values[..self.cols])
634        } else {
635            None
636        }
637    }
638}
639
640#[inline(always)]
641pub fn wclprice_batch_slice(
642    high: &[f64],
643    low: &[f64],
644    close: &[f64],
645    kern: Kernel,
646) -> Result<WclpriceBatchOutput, WclpriceError> {
647    wclprice_batch_inner(high, low, close, kern, false)
648}
649#[inline(always)]
650pub fn wclprice_batch_par_slice(
651    high: &[f64],
652    low: &[f64],
653    close: &[f64],
654    kern: Kernel,
655) -> Result<WclpriceBatchOutput, WclpriceError> {
656    wclprice_batch_inner(high, low, close, kern, true)
657}
658#[inline(always)]
659pub fn wclprice_batch_inner(
660    high: &[f64],
661    low: &[f64],
662    close: &[f64],
663    kern: Kernel,
664    _parallel: bool,
665) -> Result<WclpriceBatchOutput, WclpriceError> {
666    if high.is_empty() || low.is_empty() || close.is_empty() {
667        return Err(WclpriceError::EmptyInputData);
668    }
669    let len = high.len().min(low.len()).min(close.len());
670    let first = (0..len)
671        .find(|&i| !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan())
672        .ok_or(WclpriceError::AllValuesNaN)?;
673
674    let mut buf_mu = make_uninit_matrix(1, len);
675    init_matrix_prefixes(&mut buf_mu, len, &[first]);
676
677    let mut guard = core::mem::ManuallyDrop::new(buf_mu);
678    let out_slice: &mut [f64] =
679        unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
680
681    let simd = match kern {
682        Kernel::Auto => detect_best_batch_kernel(),
683        k => k,
684    };
685    let map = match simd {
686        Kernel::Avx512Batch => Kernel::Avx512,
687        Kernel::Avx2Batch => Kernel::Avx2,
688        Kernel::ScalarBatch => Kernel::Scalar,
689        other => other,
690    };
691
692    wclprice_batch_inner_into(high, low, close, map, _parallel, out_slice)?;
693
694    let values = unsafe {
695        Vec::from_raw_parts(
696            guard.as_mut_ptr() as *mut f64,
697            guard.len(),
698            guard.capacity(),
699        )
700    };
701
702    Ok(WclpriceBatchOutput {
703        values,
704        combos: vec![WclpriceParams],
705        rows: 1,
706        cols: len,
707    })
708}
709
710#[inline(always)]
711fn expand_grid(_r: &WclpriceBatchRange) -> Vec<WclpriceParams> {
712    vec![WclpriceParams]
713}
714
715#[inline(always)]
716fn wclprice_batch_inner_into(
717    high: &[f64],
718    low: &[f64],
719    close: &[f64],
720    kern: Kernel,
721    _parallel: bool,
722    out: &mut [f64],
723) -> Result<Vec<WclpriceParams>, WclpriceError> {
724    if high.is_empty() || low.is_empty() || close.is_empty() {
725        return Err(WclpriceError::EmptyInputData);
726    }
727    let len = high.len().min(low.len()).min(close.len());
728    if out.len() < len {
729        return Err(WclpriceError::OutputLengthMismatch {
730            expected: len,
731            got: out.len(),
732        });
733    }
734    let first = (0..len)
735        .find(|&i| !high[i].is_nan() && !low[i].is_nan() && !close[i].is_nan())
736        .ok_or(WclpriceError::AllValuesNaN)?;
737
738    if first > 0 {
739        out[..first].fill(f64::NAN);
740    }
741
742    unsafe {
743        match kern {
744            Kernel::Scalar => wclprice_row_scalar(high, low, close, first, out),
745            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
746            Kernel::Avx2 => wclprice_row_avx2(high, low, close, first, out),
747            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
748            Kernel::Avx512 => wclprice_row_avx512(high, low, close, first, out),
749            _ => wclprice_row_scalar(high, low, close, first, out),
750        }
751    }
752
753    Ok(vec![WclpriceParams])
754}
755
756#[derive(Debug, Clone)]
757pub struct WclpriceStream;
758impl Default for WclpriceStream {
759    fn default() -> Self {
760        Self
761    }
762}
763impl WclpriceStream {
764    #[inline(always)]
765    pub fn update(&mut self, h: f64, l: f64, c: f64) -> Option<f64> {
766        if h.is_nan() | l.is_nan() | c.is_nan() {
767            return None;
768        }
769
770        Some(c.mul_add(0.5, (h + l) * 0.25))
771    }
772}
773
774#[cfg(feature = "python")]
775#[pyfunction(name = "wclprice")]
776#[pyo3(signature = (high, low, close, kernel=None))]
777pub fn wclprice_py<'py>(
778    py: Python<'py>,
779    high: numpy::PyReadonlyArray1<'py, f64>,
780    low: numpy::PyReadonlyArray1<'py, f64>,
781    close: numpy::PyReadonlyArray1<'py, f64>,
782    kernel: Option<&str>,
783) -> PyResult<Bound<'py, numpy::PyArray1<f64>>> {
784    use numpy::{PyArray1, PyArrayMethods};
785    let hs = high.as_slice()?;
786    let ls = low.as_slice()?;
787    let cs = close.as_slice()?;
788    let len = hs.len().min(ls.len()).min(cs.len());
789    let out = unsafe { PyArray1::<f64>::new(py, [len], false) };
790    let out_slice = unsafe { out.as_slice_mut()? };
791    let input = WclpriceInput::from_slices(hs, ls, cs);
792    let kern = validate_kernel(kernel, false)?;
793    py.allow_threads(|| wclprice_into_slice(out_slice, &input, kern))
794        .map_err(|e| PyValueError::new_err(e.to_string()))?;
795    Ok(out)
796}
797
798#[cfg(feature = "python")]
799#[pyclass(name = "WclpriceStream")]
800pub struct WclpriceStreamPy {
801    stream: WclpriceStream,
802}
803
804#[cfg(feature = "python")]
805#[pymethods]
806impl WclpriceStreamPy {
807    #[new]
808    fn new() -> PyResult<Self> {
809        Ok(WclpriceStreamPy {
810            stream: WclpriceStream::default(),
811        })
812    }
813
814    fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
815        self.stream.update(high, low, close)
816    }
817}
818
819#[cfg(feature = "python")]
820#[pyfunction(name = "wclprice_batch")]
821#[pyo3(signature = (high, low, close, kernel=None))]
822pub fn wclprice_batch_py<'py>(
823    py: Python<'py>,
824    high: numpy::PyReadonlyArray1<'py, f64>,
825    low: numpy::PyReadonlyArray1<'py, f64>,
826    close: numpy::PyReadonlyArray1<'py, f64>,
827    kernel: Option<&str>,
828) -> PyResult<Bound<'py, pyo3::types::PyDict>> {
829    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
830    use pyo3::types::PyDict;
831
832    let hs = high.as_slice()?;
833    let ls = low.as_slice()?;
834    let cs = close.as_slice()?;
835
836    let rows = 1usize;
837    let cols = hs.len().min(ls.len()).min(cs.len());
838
839    let size = rows
840        .checked_mul(cols)
841        .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
842    let out_arr = unsafe { PyArray1::<f64>::new(py, [size], false) };
843    let out_slice = unsafe { out_arr.as_slice_mut()? };
844
845    let kern = validate_kernel(kernel, true)?;
846    py.allow_threads(|| {
847        let batch_kernel = match kern {
848            Kernel::Auto => detect_best_batch_kernel(),
849            k => k,
850        };
851        let simd = match batch_kernel {
852            Kernel::Avx512Batch => Kernel::Avx512,
853            Kernel::Avx2Batch => Kernel::Avx2,
854            Kernel::ScalarBatch => Kernel::Scalar,
855            other => other,
856        };
857        wclprice_batch_inner_into(hs, ls, cs, simd, true, out_slice)
858    })
859    .map_err(|e| PyValueError::new_err(e.to_string()))?;
860
861    let dict = PyDict::new(py);
862    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
863
864    dict.set_item("periods", vec![0u64].into_pyarray(py))?;
865    dict.set_item("offsets", vec![0.0f64].into_pyarray(py))?;
866    dict.set_item("sigmas", vec![0.0f64].into_pyarray(py))?;
867    Ok(dict)
868}
869
870#[cfg(all(feature = "python", feature = "cuda"))]
871#[pyfunction(name = "wclprice_cuda_dev")]
872#[pyo3(signature = (high, low, close, device_id=0))]
873pub fn wclprice_cuda_dev_py(
874    py: Python<'_>,
875    high: numpy::PyReadonlyArray1<'_, f32>,
876    low: numpy::PyReadonlyArray1<'_, f32>,
877    close: numpy::PyReadonlyArray1<'_, f32>,
878    device_id: usize,
879) -> PyResult<DeviceArrayF32Py> {
880    if !cuda_available() {
881        return Err(PyValueError::new_err("CUDA not available"));
882    }
883
884    let hs = high.as_slice()?;
885    let ls = low.as_slice()?;
886    let cs = close.as_slice()?;
887
888    let (inner, ctx, dev_id) = py.allow_threads(|| {
889        let cuda =
890            CudaWclprice::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
891        let ctx = cuda.context_arc();
892        let dev_id = cuda.device_id();
893        cuda.wclprice_batch_dev(hs, ls, cs, &WclpriceBatchRange)
894            .map(|inner| (inner, ctx, dev_id))
895            .map_err(|e| PyValueError::new_err(e.to_string()))
896    })?;
897
898    Ok(DeviceArrayF32Py {
899        inner,
900        _ctx: Some(ctx),
901        device_id: Some(dev_id),
902    })
903}
904
905#[cfg(all(feature = "python", feature = "cuda"))]
906#[pyfunction(name = "wclprice_cuda_batch_dev")]
907#[pyo3(signature = (high_f32, low_f32, close_f32, device_id=0))]
908pub fn wclprice_cuda_batch_dev_py(
909    py: Python<'_>,
910    high_f32: numpy::PyReadonlyArray1<'_, f32>,
911    low_f32: numpy::PyReadonlyArray1<'_, f32>,
912    close_f32: numpy::PyReadonlyArray1<'_, f32>,
913    device_id: usize,
914) -> PyResult<DeviceArrayF32Py> {
915    if !cuda_available() {
916        return Err(PyValueError::new_err("CUDA not available"));
917    }
918    let hs = high_f32.as_slice()?;
919    let ls = low_f32.as_slice()?;
920    let cs = close_f32.as_slice()?;
921    let (inner, ctx, dev_id) = py.allow_threads(|| {
922        let cuda =
923            CudaWclprice::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
924        let ctx = cuda.context_arc();
925        let dev_id = cuda.device_id();
926        cuda.wclprice_batch_dev(hs, ls, cs, &WclpriceBatchRange)
927            .map(|inner| (inner, ctx, dev_id))
928            .map_err(|e| PyValueError::new_err(e.to_string()))
929    })?;
930    Ok(DeviceArrayF32Py {
931        inner,
932        _ctx: Some(ctx),
933        device_id: Some(dev_id),
934    })
935}
936
937#[cfg(all(feature = "python", feature = "cuda"))]
938#[pyfunction(name = "wclprice_cuda_many_series_one_param_dev")]
939#[pyo3(signature = (high_tm_f32, low_tm_f32, close_tm_f32, device_id=0))]
940pub fn wclprice_cuda_many_series_one_param_dev_py(
941    py: Python<'_>,
942    high_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
943    low_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
944    close_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
945    device_id: usize,
946) -> PyResult<DeviceArrayF32Py> {
947    use numpy::PyUntypedArrayMethods;
948    if !cuda_available() {
949        return Err(PyValueError::new_err("CUDA not available"));
950    }
951    let h_shape = high_tm_f32.shape();
952    if h_shape != low_tm_f32.shape() || h_shape != close_tm_f32.shape() {
953        return Err(PyValueError::new_err(
954            "high/low/close matrices must share shape",
955        ));
956    }
957    let rows = h_shape[0];
958    let cols = h_shape[1];
959    let hs = high_tm_f32.as_slice()?;
960    let ls = low_tm_f32.as_slice()?;
961    let cs = close_tm_f32.as_slice()?;
962    let (inner, ctx, dev_id) = py.allow_threads(|| {
963        let cuda =
964            CudaWclprice::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
965        let ctx = cuda.context_arc();
966        let dev_id = cuda.device_id();
967        cuda.wclprice_many_series_one_param_time_major_dev(hs, ls, cs, cols, rows)
968            .map(|inner| (inner, ctx, dev_id))
969            .map_err(|e| PyValueError::new_err(e.to_string()))
970    })?;
971    Ok(DeviceArrayF32Py {
972        inner,
973        _ctx: Some(ctx),
974        device_id: Some(dev_id),
975    })
976}
977
978#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
979#[wasm_bindgen]
980pub fn wclprice_js(high: &[f64], low: &[f64], close: &[f64]) -> Result<Vec<f64>, JsValue> {
981    if high.is_empty() || low.is_empty() || close.is_empty() {
982        return Err(JsValue::from_str("wclprice: Empty data provided"));
983    }
984
985    let input = WclpriceInput::from_slices(high, low, close);
986    let mut output = vec![0.0; high.len().min(low.len()).min(close.len())];
987
988    wclprice_into_slice(&mut output, &input, detect_best_kernel())
989        .map_err(|e| JsValue::from_str(&e.to_string()))?;
990
991    Ok(output)
992}
993
994#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
995#[wasm_bindgen]
996pub fn wclprice_alloc(len: usize) -> *mut f64 {
997    let mut vec = Vec::<f64>::with_capacity(len);
998    let ptr = vec.as_mut_ptr();
999    std::mem::forget(vec);
1000    ptr
1001}
1002
1003#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1004#[wasm_bindgen]
1005pub fn wclprice_free(ptr: *mut f64, len: usize) {
1006    if !ptr.is_null() {
1007        unsafe {
1008            let _ = Vec::from_raw_parts(ptr, len, len);
1009        }
1010    }
1011}
1012
1013#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1014#[wasm_bindgen]
1015pub fn wclprice_into(
1016    high_ptr: *const f64,
1017    low_ptr: *const f64,
1018    close_ptr: *const f64,
1019    out_ptr: *mut f64,
1020    len: usize,
1021) -> Result<(), JsValue> {
1022    if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
1023        return Err(JsValue::from_str("Null pointer provided"));
1024    }
1025
1026    unsafe {
1027        let high = std::slice::from_raw_parts(high_ptr, len);
1028        let low = std::slice::from_raw_parts(low_ptr, len);
1029        let close = std::slice::from_raw_parts(close_ptr, len);
1030
1031        let input = WclpriceInput::from_slices(high, low, close);
1032
1033        if high_ptr == out_ptr || low_ptr == out_ptr || close_ptr == out_ptr {
1034            let mut temp = vec![0.0; len];
1035            wclprice_into_slice(&mut temp, &input, detect_best_kernel())
1036                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1037            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1038            out.copy_from_slice(&temp);
1039        } else {
1040            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1041            wclprice_into_slice(out, &input, detect_best_kernel())
1042                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1043        }
1044
1045        Ok(())
1046    }
1047}
1048
1049#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1050#[derive(Serialize, Deserialize)]
1051pub struct WclpriceBatchConfig {}
1052
1053#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1054#[derive(Serialize, Deserialize)]
1055pub struct WclpriceBatchJsOutput {
1056    pub values: Vec<f64>,
1057    pub combos: Vec<WclpriceParams>,
1058    pub rows: usize,
1059    pub cols: usize,
1060}
1061
1062#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1063#[wasm_bindgen(js_name = wclprice_batch)]
1064pub fn wclprice_batch_unified_js(
1065    high: &[f64],
1066    low: &[f64],
1067    close: &[f64],
1068    cfg: JsValue,
1069) -> Result<JsValue, JsValue> {
1070    let _cfg: WclpriceBatchConfig =
1071        serde_wasm_bindgen::from_value(cfg).unwrap_or(WclpriceBatchConfig {});
1072    let out = wclprice_batch_inner(high, low, close, detect_best_kernel(), false)
1073        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1074    let js = WclpriceBatchJsOutput {
1075        values: out.values,
1076        combos: out.combos,
1077        rows: out.rows,
1078        cols: out.cols,
1079    };
1080    serde_wasm_bindgen::to_value(&js)
1081        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1082}
1083
1084#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1085#[wasm_bindgen]
1086pub fn wclprice_batch_into(
1087    high_ptr: *const f64,
1088    low_ptr: *const f64,
1089    close_ptr: *const f64,
1090    out_ptr: *mut f64,
1091    len: usize,
1092) -> Result<usize, JsValue> {
1093    if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
1094        return Err(JsValue::from_str("Null pointer provided"));
1095    }
1096
1097    unsafe {
1098        let high = std::slice::from_raw_parts(high_ptr, len);
1099        let low = std::slice::from_raw_parts(low_ptr, len);
1100        let close = std::slice::from_raw_parts(close_ptr, len);
1101
1102        let rows = 1;
1103
1104        if high_ptr == out_ptr || low_ptr == out_ptr || close_ptr == out_ptr {
1105            let mut temp = vec![0.0; len];
1106            wclprice_batch_inner_into(high, low, close, detect_best_kernel(), false, &mut temp)
1107                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1108            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1109            out.copy_from_slice(&temp);
1110        } else {
1111            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1112            wclprice_batch_inner_into(high, low, close, detect_best_kernel(), false, out)
1113                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1114        }
1115
1116        Ok(rows)
1117    }
1118}
1119
1120#[cfg(test)]
1121mod tests {
1122    use super::*;
1123    use crate::skip_if_unsupported;
1124    use crate::utilities::data_loader::read_candles_from_csv;
1125    #[cfg(feature = "proptest")]
1126    use proptest::prelude::*;
1127
1128    #[test]
1129    fn test_wclprice_into_matches_api() -> Result<(), Box<dyn Error>> {
1130        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1131        let candles = read_candles_from_csv(file)?;
1132
1133        let input = WclpriceInput::from_candles(&candles);
1134
1135        let WclpriceOutput { values: expected } = wclprice(&input)?;
1136
1137        let mut out = vec![0.0f64; expected.len()];
1138        wclprice_into(&input, &mut out)?;
1139
1140        assert_eq!(out.len(), expected.len());
1141        for i in 0..expected.len() {
1142            let a = expected[i];
1143            let b = out[i];
1144            let equal = (a.is_nan() && b.is_nan()) || (a == b);
1145            assert!(equal, "mismatch at {}: expected={}, got={}", i, a, b);
1146        }
1147        Ok(())
1148    }
1149
1150    fn check_wclprice_slices(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1151        skip_if_unsupported!(kernel, test);
1152        let high = vec![59230.0, 59220.0, 59077.0, 59160.0, 58717.0];
1153        let low = vec![59222.0, 59211.0, 59077.0, 59143.0, 58708.0];
1154        let close = vec![59225.0, 59210.0, 59080.0, 59150.0, 58710.0];
1155        let input = WclpriceInput::from_slices(&high, &low, &close);
1156        let output = wclprice_with_kernel(&input, kernel)?;
1157        let expected = vec![59225.5, 59212.75, 59078.5, 59150.75, 58711.25];
1158        for (i, &v) in output.values.iter().enumerate() {
1159            assert!(
1160                (v - expected[i]).abs() < 1e-2,
1161                "[{test}] mismatch at {i}: {v} vs {expected:?}"
1162            );
1163        }
1164        Ok(())
1165    }
1166    fn check_wclprice_candles(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1167        skip_if_unsupported!(kernel, test);
1168        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1169        let candles = read_candles_from_csv(file)?;
1170        let input = WclpriceInput::from_candles(&candles);
1171        let output = wclprice_with_kernel(&input, kernel)?;
1172        assert_eq!(output.values.len(), candles.close.len());
1173        Ok(())
1174    }
1175    fn check_wclprice_empty_data(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1176        skip_if_unsupported!(kernel, test);
1177        let high: [f64; 0] = [];
1178        let low: [f64; 0] = [];
1179        let close: [f64; 0] = [];
1180        let input = WclpriceInput::from_slices(&high, &low, &close);
1181        let res = wclprice_with_kernel(&input, kernel);
1182        assert!(res.is_err(), "[{}] should fail with empty data", test);
1183        Ok(())
1184    }
1185    fn check_wclprice_all_nan(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1186        skip_if_unsupported!(kernel, test);
1187        let high = vec![f64::NAN, f64::NAN];
1188        let low = vec![f64::NAN, f64::NAN];
1189        let close = vec![f64::NAN, f64::NAN];
1190        let input = WclpriceInput::from_slices(&high, &low, &close);
1191        let res = wclprice_with_kernel(&input, kernel);
1192        assert!(res.is_err(), "[{}] should fail with all NaN", test);
1193        Ok(())
1194    }
1195    fn check_wclprice_partial_nan(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1196        skip_if_unsupported!(kernel, test);
1197        let high = vec![f64::NAN, 59000.0];
1198        let low = vec![f64::NAN, 58950.0];
1199        let close = vec![f64::NAN, 58975.0];
1200        let input = WclpriceInput::from_slices(&high, &low, &close);
1201        let output = wclprice_with_kernel(&input, kernel)?;
1202        assert!(output.values[0].is_nan());
1203        assert!((output.values[1] - (59000.0 + 58950.0 + 2.0 * 58975.0) / 4.0).abs() < 1e-8);
1204        Ok(())
1205    }
1206
1207    fn check_wclprice_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1208        skip_if_unsupported!(kernel, test_name);
1209        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1210        let candles = read_candles_from_csv(file_path)?;
1211
1212        let input = WclpriceInput::from_candles(&candles);
1213        let result = wclprice_with_kernel(&input, kernel)?;
1214
1215        let expected_last_five = [59225.5, 59212.75, 59078.5, 59150.75, 58711.25];
1216
1217        let start = result.values.len().saturating_sub(5);
1218        for (i, &val) in result.values[start..].iter().enumerate() {
1219            let diff = (val - expected_last_five[i]).abs();
1220            assert!(
1221                diff < 1e-8,
1222                "[{}] WCLPRICE {:?} mismatch at idx {}: got {}, expected {}",
1223                test_name,
1224                kernel,
1225                i,
1226                val,
1227                expected_last_five[i]
1228            );
1229        }
1230        Ok(())
1231    }
1232
1233    #[cfg(debug_assertions)]
1234    fn check_wclprice_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1235        skip_if_unsupported!(kernel, test_name);
1236
1237        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1238        let candles = read_candles_from_csv(file_path)?;
1239
1240        let input = WclpriceInput::from_candles(&candles);
1241        let output = wclprice_with_kernel(&input, kernel)?;
1242
1243        for (i, &val) in output.values.iter().enumerate() {
1244            if val.is_nan() {
1245                continue;
1246            }
1247
1248            let bits = val.to_bits();
1249
1250            if bits == 0x11111111_11111111 {
1251                panic!(
1252                    "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1253					 in WCLPRICE with candle data",
1254                    test_name, val, bits, i
1255                );
1256            }
1257
1258            if bits == 0x22222222_22222222 {
1259                panic!(
1260                    "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1261					 in WCLPRICE with candle data",
1262                    test_name, val, bits, i
1263                );
1264            }
1265
1266            if bits == 0x33333333_33333333 {
1267                panic!(
1268                    "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1269					 in WCLPRICE with candle data",
1270                    test_name, val, bits, i
1271                );
1272            }
1273        }
1274
1275        let test_patterns = vec![
1276            (
1277                "small dataset",
1278                vec![100.0, 101.0, 102.0],
1279                vec![99.0, 100.0, 101.0],
1280                vec![100.5, 101.5, 102.5],
1281            ),
1282            (
1283                "large values",
1284                vec![59000.0, 60000.0, 61000.0],
1285                vec![58900.0, 59900.0, 60900.0],
1286                vec![58950.0, 59950.0, 60950.0],
1287            ),
1288            (
1289                "with leading NaN",
1290                vec![f64::NAN, 100.0, 101.0],
1291                vec![f64::NAN, 99.0, 100.0],
1292                vec![f64::NAN, 100.5, 101.5],
1293            ),
1294            (
1295                "with trailing NaN",
1296                vec![100.0, 101.0, f64::NAN],
1297                vec![99.0, 100.0, f64::NAN],
1298                vec![100.5, 101.5, f64::NAN],
1299            ),
1300            (
1301                "mixed NaN pattern",
1302                vec![100.0, f64::NAN, 101.0, f64::NAN, 102.0],
1303                vec![99.0, f64::NAN, 100.0, f64::NAN, 101.0],
1304                vec![100.5, f64::NAN, 101.5, f64::NAN, 102.5],
1305            ),
1306            (
1307                "single valid value",
1308                vec![f64::NAN, f64::NAN, 100.0],
1309                vec![f64::NAN, f64::NAN, 99.0],
1310                vec![f64::NAN, f64::NAN, 100.5],
1311            ),
1312            (
1313                "extreme values",
1314                vec![1e-10, 1e10, 1e-10],
1315                vec![1e-10, 1e10, 1e-10],
1316                vec![1e-10, 1e10, 1e-10],
1317            ),
1318            (
1319                "zero values",
1320                vec![0.0, 1.0, 0.0],
1321                vec![0.0, 0.0, 0.0],
1322                vec![0.0, 0.5, 0.0],
1323            ),
1324            (
1325                "negative values",
1326                vec![-100.0, -50.0, -25.0],
1327                vec![-101.0, -51.0, -26.0],
1328                vec![-100.5, -50.5, -25.5],
1329            ),
1330            (
1331                "large dataset",
1332                (0..1000).map(|i| 100.0 + i as f64).collect(),
1333                (0..1000).map(|i| 99.0 + i as f64).collect(),
1334                (0..1000).map(|i| 100.5 + i as f64).collect(),
1335            ),
1336        ];
1337
1338        for (pattern_idx, (desc, high, low, close)) in test_patterns.iter().enumerate() {
1339            let input = WclpriceInput::from_slices(high, low, close);
1340            let output = wclprice_with_kernel(&input, kernel)?;
1341
1342            for (i, &val) in output.values.iter().enumerate() {
1343                if val.is_nan() {
1344                    continue;
1345                }
1346
1347                let bits = val.to_bits();
1348
1349                if bits == 0x11111111_11111111 {
1350                    panic!(
1351                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1352						 in WCLPRICE with pattern '{}' (pattern {})",
1353                        test_name, val, bits, i, desc, pattern_idx
1354                    );
1355                }
1356
1357                if bits == 0x22222222_22222222 {
1358                    panic!(
1359                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1360						 in WCLPRICE with pattern '{}' (pattern {})",
1361                        test_name, val, bits, i, desc, pattern_idx
1362                    );
1363                }
1364
1365                if bits == 0x33333333_33333333 {
1366                    panic!(
1367                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1368						 in WCLPRICE with pattern '{}' (pattern {})",
1369                        test_name, val, bits, i, desc, pattern_idx
1370                    );
1371                }
1372            }
1373        }
1374
1375        Ok(())
1376    }
1377
1378    #[cfg(not(debug_assertions))]
1379    fn check_wclprice_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1380        Ok(())
1381    }
1382
1383    #[cfg(feature = "proptest")]
1384    #[allow(clippy::float_cmp)]
1385    fn check_wclprice_property(
1386        test_name: &str,
1387        kernel: Kernel,
1388    ) -> Result<(), Box<dyn std::error::Error>> {
1389        skip_if_unsupported!(kernel, test_name);
1390
1391        let strat = (2usize..=400).prop_flat_map(|len| {
1392            prop::collection::vec(
1393                (0.0f64..1e6f64)
1394                    .prop_filter("finite non-negative price", |x| x.is_finite() && *x >= 0.0)
1395                    .prop_flat_map(|low| {
1396                        (0.0f64..10000.0f64)
1397                            .prop_filter("finite diff", |x| x.is_finite())
1398                            .prop_flat_map(move |high_diff| {
1399                                let high = low + high_diff;
1400                                (
1401                                    Just(low),
1402                                    Just(high),
1403                                    (low..=high).prop_filter("finite close", |x| x.is_finite()),
1404                                )
1405                            })
1406                    }),
1407                len,
1408            )
1409        });
1410
1411        proptest::test_runner::TestRunner::default()
1412            .run(&strat, |price_data| {
1413                let mut high = Vec::with_capacity(price_data.len());
1414                let mut low = Vec::with_capacity(price_data.len());
1415                let mut close = Vec::with_capacity(price_data.len());
1416
1417                for (l, h, c) in price_data.iter() {
1418                    low.push(*l);
1419                    high.push(*h);
1420                    close.push(*c);
1421                }
1422
1423                let input = WclpriceInput::from_slices(&high, &low, &close);
1424                let WclpriceOutput { values: out } = wclprice_with_kernel(&input, kernel)?;
1425
1426                let WclpriceOutput { values: ref_out } =
1427                    wclprice_with_kernel(&input, Kernel::Scalar)?;
1428
1429                for i in 0..price_data.len() {
1430                    let h = high[i];
1431                    let l = low[i];
1432                    let c = close[i];
1433                    let y = out[i];
1434                    let r = ref_out[i];
1435
1436                    if h.is_finite() && l.is_finite() && c.is_finite() {
1437                        let expected = (h + l + 2.0 * c) / 4.0;
1438                        prop_assert!(
1439                            (y - expected).abs() <= 1e-9,
1440                            "Formula mismatch at idx {}: got {} expected {} (h={}, l={}, c={})",
1441                            i,
1442                            y,
1443                            expected,
1444                            h,
1445                            l,
1446                            c
1447                        );
1448                    } else {
1449                        prop_assert!(
1450                            y.is_nan(),
1451                            "Expected NaN at idx {} when input has non-finite values, got {}",
1452                            i,
1453                            y
1454                        );
1455                    }
1456
1457                    if h.is_finite() && l.is_finite() && c.is_finite() {
1458                        let min_val = h.min(l).min(c);
1459                        let max_val = h.max(l).max(c);
1460                        prop_assert!(
1461                            y >= min_val - 1e-9 && y <= max_val + 1e-9,
1462                            "Output {} at idx {} outside bounds [{}, {}]",
1463                            y,
1464                            i,
1465                            min_val,
1466                            max_val
1467                        );
1468                    }
1469
1470                    let y_bits = y.to_bits();
1471                    let r_bits = r.to_bits();
1472
1473                    if !y.is_finite() || !r.is_finite() {
1474                        prop_assert!(
1475                            y_bits == r_bits,
1476                            "NaN/infinite mismatch at idx {}: {} vs {} (bits: {:016x} vs {:016x})",
1477                            i,
1478                            y,
1479                            r,
1480                            y_bits,
1481                            r_bits
1482                        );
1483                    } else {
1484                        let ulp_diff: u64 = y_bits.abs_diff(r_bits);
1485                        prop_assert!(
1486                            (y - r).abs() <= 1e-9 || ulp_diff <= 4,
1487                            "Kernel mismatch at idx {}: {} vs {} (ULP={})",
1488                            i,
1489                            y,
1490                            r,
1491                            ulp_diff
1492                        );
1493                    }
1494
1495                    if (h - l).abs() < f64::EPSILON && (h - c).abs() < f64::EPSILON {
1496                        prop_assert!(
1497                            (y - h).abs() <= 1e-9,
1498                            "When all prices equal {}, WCLPRICE should be {}, got {}",
1499                            h,
1500                            h,
1501                            y
1502                        );
1503                    }
1504                }
1505
1506                Ok(())
1507            })
1508            .unwrap();
1509
1510        Ok(())
1511    }
1512
1513    macro_rules! generate_all_wclprice_tests {
1514        ($($test_fn:ident),*) => {
1515            paste::paste! {
1516                $( #[test] fn [<$test_fn _scalar_f64>]() { let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar); } )*
1517                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1518                $( #[test] fn [<$test_fn _avx2_f64>]() { let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2); }
1519                   #[test] fn [<$test_fn _avx512_f64>]() { let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512); } )*
1520            }
1521        }
1522    }
1523    generate_all_wclprice_tests!(
1524        check_wclprice_slices,
1525        check_wclprice_candles,
1526        check_wclprice_empty_data,
1527        check_wclprice_all_nan,
1528        check_wclprice_partial_nan,
1529        check_wclprice_accuracy,
1530        check_wclprice_no_poison
1531    );
1532
1533    #[cfg(feature = "proptest")]
1534    generate_all_wclprice_tests!(check_wclprice_property);
1535    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1536        skip_if_unsupported!(kernel, test);
1537        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1538        let c = read_candles_from_csv(file)?;
1539        let output = WclpriceBatchBuilder::new()
1540            .kernel(kernel)
1541            .apply_candles(&c)?;
1542        let row = output
1543            .values_for(&WclpriceParams)
1544            .expect("default row missing");
1545        assert_eq!(row.len(), c.close.len());
1546        Ok(())
1547    }
1548
1549    #[cfg(debug_assertions)]
1550    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1551        skip_if_unsupported!(kernel, test);
1552
1553        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1554        let c = read_candles_from_csv(file)?;
1555
1556        let output = WclpriceBatchBuilder::new()
1557            .kernel(kernel)
1558            .apply_candles(&c)?;
1559
1560        assert_eq!(output.rows, 1);
1561        assert_eq!(output.cols, c.close.len());
1562
1563        for (idx, &val) in output.values.iter().enumerate() {
1564            if val.is_nan() {
1565                continue;
1566            }
1567
1568            let bits = val.to_bits();
1569
1570            if bits == 0x11111111_11111111 {
1571                panic!(
1572                    "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1573					 in WCLPRICE batch at index {} (candle data)",
1574                    test, val, bits, idx
1575                );
1576            }
1577
1578            if bits == 0x22222222_22222222 {
1579                panic!(
1580                    "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) \
1581					 in WCLPRICE batch at index {} (candle data)",
1582                    test, val, bits, idx
1583                );
1584            }
1585
1586            if bits == 0x33333333_33333333 {
1587                panic!(
1588                    "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) \
1589					 in WCLPRICE batch at index {} (candle data)",
1590                    test, val, bits, idx
1591                );
1592            }
1593        }
1594
1595        let test_configs = vec![
1596            (
1597                "small data",
1598                vec![100.0, 101.0, 102.0],
1599                vec![99.0, 100.0, 101.0],
1600                vec![100.5, 101.5, 102.5],
1601            ),
1602            (
1603                "medium data",
1604                (0..100).map(|i| 100.0 + i as f64).collect(),
1605                (0..100).map(|i| 99.0 + i as f64).collect(),
1606                (0..100).map(|i| 100.5 + i as f64).collect(),
1607            ),
1608            (
1609                "large data",
1610                (0..5000).map(|i| 100.0 + (i as f64 * 0.1)).collect(),
1611                (0..5000).map(|i| 99.0 + (i as f64 * 0.1)).collect(),
1612                (0..5000).map(|i| 100.5 + (i as f64 * 0.1)).collect(),
1613            ),
1614            (
1615                "with NaN prefix",
1616                [
1617                    vec![f64::NAN; 10],
1618                    (0..90).map(|i| 100.0 + i as f64).collect(),
1619                ]
1620                .concat(),
1621                [
1622                    vec![f64::NAN; 10],
1623                    (0..90).map(|i| 99.0 + i as f64).collect(),
1624                ]
1625                .concat(),
1626                [
1627                    vec![f64::NAN; 10],
1628                    (0..90).map(|i| 100.5 + i as f64).collect(),
1629                ]
1630                .concat(),
1631            ),
1632            (
1633                "sparse NaN pattern",
1634                (0..50)
1635                    .map(|i| {
1636                        if i % 5 == 0 {
1637                            f64::NAN
1638                        } else {
1639                            100.0 + i as f64
1640                        }
1641                    })
1642                    .collect(),
1643                (0..50)
1644                    .map(|i| {
1645                        if i % 5 == 0 {
1646                            f64::NAN
1647                        } else {
1648                            99.0 + i as f64
1649                        }
1650                    })
1651                    .collect(),
1652                (0..50)
1653                    .map(|i| {
1654                        if i % 5 == 0 {
1655                            f64::NAN
1656                        } else {
1657                            100.5 + i as f64
1658                        }
1659                    })
1660                    .collect(),
1661            ),
1662            (
1663                "extreme values",
1664                vec![1e-100, 1e100, 1e-50, 1e50],
1665                vec![1e-100, 1e100, 1e-50, 1e50],
1666                vec![1e-100, 1e100, 1e-50, 1e50],
1667            ),
1668        ];
1669
1670        for (cfg_idx, (desc, high, low, close)) in test_configs.iter().enumerate() {
1671            let output = WclpriceBatchBuilder::new()
1672                .kernel(kernel)
1673                .apply_slices(high, low, close)?;
1674
1675            assert_eq!(
1676                output.rows, 1,
1677                "[{}] Config {}: Expected 1 row for WCLPRICE",
1678                test, cfg_idx
1679            );
1680            assert_eq!(
1681                output.cols,
1682                high.len(),
1683                "[{}] Config {}: Cols mismatch",
1684                test,
1685                cfg_idx
1686            );
1687
1688            for (idx, &val) in output.values.iter().enumerate() {
1689                if val.is_nan() {
1690                    continue;
1691                }
1692
1693                let bits = val.to_bits();
1694
1695                if bits == 0x11111111_11111111 {
1696                    panic!(
1697						"[{}] Config {} ({}): Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1698						 in WCLPRICE batch at index {}",
1699						test, cfg_idx, desc, val, bits, idx
1700					);
1701                }
1702
1703                if bits == 0x22222222_22222222 {
1704                    panic!(
1705						"[{}] Config {} ({}): Found init_matrix_prefixes poison value {} (0x{:016X}) \
1706						 in WCLPRICE batch at index {}",
1707						test, cfg_idx, desc, val, bits, idx
1708					);
1709                }
1710
1711                if bits == 0x33333333_33333333 {
1712                    panic!(
1713						"[{}] Config {} ({}): Found make_uninit_matrix poison value {} (0x{:016X}) \
1714						 in WCLPRICE batch at index {}",
1715						test, cfg_idx, desc, val, bits, idx
1716					);
1717                }
1718            }
1719        }
1720
1721        Ok(())
1722    }
1723
1724    #[cfg(not(debug_assertions))]
1725    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1726        Ok(())
1727    }
1728
1729    macro_rules! gen_batch_tests {
1730        ($fn_name:ident) => {
1731            paste::paste! {
1732                #[test] fn [<$fn_name _scalar>]() { let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch); }
1733                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1734                #[test] fn [<$fn_name _avx2>]() { let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch); }
1735                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1736                #[test] fn [<$fn_name _avx512>]() { let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch); }
1737                #[test] fn [<$fn_name _auto_detect>]() { let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto); }
1738            }
1739        }
1740    }
1741    gen_batch_tests!(check_batch_default_row);
1742    gen_batch_tests!(check_batch_no_poison);
1743}