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}