1#[cfg(all(feature = "python", feature = "cuda"))]
2use crate::cuda::moving_averages::DeviceArrayF32;
3#[cfg(all(feature = "python", feature = "cuda"))]
4use crate::cuda::oscillators::CudaWillr;
5use crate::utilities::data_loader::{source_type, Candles};
6#[cfg(all(feature = "python", feature = "cuda"))]
7use crate::utilities::dlpack_cuda::export_f32_cuda_dlpack_2d;
8use crate::utilities::enums::Kernel;
9use crate::utilities::helpers::{
10 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
11 make_uninit_matrix,
12};
13#[cfg(feature = "python")]
14use crate::utilities::kernel_validation::validate_kernel;
15#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
16use core::arch::x86_64::*;
17#[cfg(not(target_arch = "wasm32"))]
18use rayon::prelude::*;
19use std::convert::AsRef;
20use std::error::Error;
21#[cfg(all(feature = "python", feature = "cuda"))]
22use std::sync::Arc;
23use thiserror::Error;
24
25#[derive(Debug, Clone)]
26pub enum WillrData<'a> {
27 Candles {
28 candles: &'a Candles,
29 },
30 Slices {
31 high: &'a [f64],
32 low: &'a [f64],
33 close: &'a [f64],
34 },
35}
36
37#[derive(Debug, Clone)]
38pub struct WillrOutput {
39 pub values: Vec<f64>,
40}
41
42#[derive(Debug, Clone)]
43#[cfg_attr(
44 all(target_arch = "wasm32", feature = "wasm"),
45 derive(serde::Serialize, serde::Deserialize)
46)]
47pub struct WillrParams {
48 pub period: Option<usize>,
49}
50
51impl Default for WillrParams {
52 fn default() -> Self {
53 Self { period: Some(14) }
54 }
55}
56
57#[derive(Debug, Clone)]
58pub struct WillrInput<'a> {
59 pub data: WillrData<'a>,
60 pub params: WillrParams,
61}
62
63impl<'a> WillrInput<'a> {
64 #[inline]
65 pub fn from_candles(candles: &'a Candles, params: WillrParams) -> Self {
66 Self {
67 data: WillrData::Candles { candles },
68 params,
69 }
70 }
71
72 #[inline]
73 pub fn from_slices(
74 high: &'a [f64],
75 low: &'a [f64],
76 close: &'a [f64],
77 params: WillrParams,
78 ) -> Self {
79 Self {
80 data: WillrData::Slices { high, low, close },
81 params,
82 }
83 }
84
85 #[inline]
86 pub fn with_default_candles(candles: &'a Candles) -> Self {
87 Self::from_candles(candles, WillrParams::default())
88 }
89
90 #[inline]
91 pub fn get_period(&self) -> usize {
92 self.params.period.unwrap_or(14)
93 }
94}
95
96#[derive(Copy, Clone, Debug)]
97pub struct WillrBuilder {
98 period: Option<usize>,
99 kernel: Kernel,
100}
101
102impl Default for WillrBuilder {
103 fn default() -> Self {
104 Self {
105 period: None,
106 kernel: Kernel::Auto,
107 }
108 }
109}
110
111impl WillrBuilder {
112 #[inline]
113 pub fn new() -> Self {
114 Self::default()
115 }
116 #[inline]
117 pub fn period(mut self, n: usize) -> Self {
118 self.period = Some(n);
119 self
120 }
121 #[inline]
122 pub fn kernel(mut self, k: Kernel) -> Self {
123 self.kernel = k;
124 self
125 }
126 #[inline]
127 pub fn apply(self, c: &Candles) -> Result<WillrOutput, WillrError> {
128 let p = WillrParams {
129 period: self.period,
130 };
131 let i = WillrInput::from_candles(c, p);
132 willr_with_kernel(&i, self.kernel)
133 }
134 #[inline]
135 pub fn apply_slices(
136 self,
137 high: &[f64],
138 low: &[f64],
139 close: &[f64],
140 ) -> Result<WillrOutput, WillrError> {
141 let p = WillrParams {
142 period: self.period,
143 };
144 let i = WillrInput::from_slices(high, low, close, p);
145 willr_with_kernel(&i, self.kernel)
146 }
147}
148
149#[derive(Debug, Error)]
150pub enum WillrError {
151 #[error("willr: Empty input data or mismatched slices.")]
152 EmptyInputData,
153 #[error("willr: All input values are NaN.")]
154 AllValuesNaN,
155 #[error("willr: Invalid period: period = {period}, data length = {data_len}")]
156 InvalidPeriod { period: usize, data_len: usize },
157 #[error("willr: Not enough valid data: needed = {needed}, valid = {valid}")]
158 NotEnoughValidData { needed: usize, valid: usize },
159 #[error("willr: Output length mismatch: expected = {expected}, got = {got}")]
160 OutputLengthMismatch { expected: usize, got: usize },
161 #[error("willr: Invalid range: start={start}, end={end}, step={step}")]
162 InvalidRange {
163 start: usize,
164 end: usize,
165 step: usize,
166 },
167 #[error("willr: Invalid kernel for batch: {0:?}")]
168 InvalidKernelForBatch(Kernel),
169}
170
171#[inline(always)]
172fn willr_compute_into(
173 high: &[f64],
174 low: &[f64],
175 close: &[f64],
176 period: usize,
177 first_valid: usize,
178 kernel: Kernel,
179 out: &mut [f64],
180) {
181 unsafe {
182 #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
183 {
184 if matches!(kernel, Kernel::Scalar | Kernel::ScalarBatch) {
185 willr_scalar(high, low, close, period, first_valid, out);
186 return;
187 }
188 }
189
190 match kernel {
191 Kernel::Scalar | Kernel::ScalarBatch => {
192 willr_scalar(high, low, close, period, first_valid, out)
193 }
194 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
195 Kernel::Avx2 | Kernel::Avx2Batch => {
196 willr_avx2(high, low, close, period, first_valid, out)
197 }
198 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
199 Kernel::Avx512 | Kernel::Avx512Batch => {
200 willr_avx512(high, low, close, period, first_valid, out)
201 }
202 #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
203 Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
204 willr_scalar(high, low, close, period, first_valid, out)
205 }
206 _ => unreachable!(),
207 }
208 }
209}
210
211#[inline]
212pub fn willr(input: &WillrInput) -> Result<WillrOutput, WillrError> {
213 willr_with_kernel(input, Kernel::Auto)
214}
215
216pub fn willr_with_kernel(input: &WillrInput, kernel: Kernel) -> Result<WillrOutput, WillrError> {
217 let (high, low, close): (&[f64], &[f64], &[f64]) = match &input.data {
218 WillrData::Candles { candles } => (
219 source_type(candles, "high"),
220 source_type(candles, "low"),
221 source_type(candles, "close"),
222 ),
223 WillrData::Slices { high, low, close } => (high, low, close),
224 };
225
226 let len = high.len();
227 if low.len() != len || close.len() != len || len == 0 {
228 return Err(WillrError::EmptyInputData);
229 }
230 let period = input.get_period();
231 if period == 0 || period > len {
232 return Err(WillrError::InvalidPeriod {
233 period,
234 data_len: len,
235 });
236 }
237
238 let first_valid = (0..len)
239 .find(|&i| !(high[i].is_nan() || low[i].is_nan() || close[i].is_nan()))
240 .ok_or(WillrError::AllValuesNaN)?;
241
242 if (len - first_valid) < period {
243 return Err(WillrError::NotEnoughValidData {
244 needed: period,
245 valid: len - first_valid,
246 });
247 }
248
249 let chosen = match kernel {
250 Kernel::Auto => detect_best_kernel(),
251 other => other,
252 };
253
254 let mut out = alloc_with_nan_prefix(len, first_valid + period - 1);
255 willr_compute_into(high, low, close, period, first_valid, chosen, &mut out);
256 Ok(WillrOutput { values: out })
257}
258
259#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
260#[inline]
261pub fn willr_into(dst: &mut [f64], input: &WillrInput) -> Result<(), WillrError> {
262 willr_into_slice(dst, input, Kernel::Auto)
263}
264
265#[inline]
266pub fn willr_into_slice(
267 dst: &mut [f64],
268 input: &WillrInput,
269 kernel: Kernel,
270) -> Result<(), WillrError> {
271 let (high, low, close): (&[f64], &[f64], &[f64]) = match &input.data {
272 WillrData::Candles { candles } => (
273 source_type(candles, "high"),
274 source_type(candles, "low"),
275 source_type(candles, "close"),
276 ),
277 WillrData::Slices { high, low, close } => (high, low, close),
278 };
279
280 let len = high.len();
281 if low.len() != len || close.len() != len || len == 0 {
282 return Err(WillrError::EmptyInputData);
283 }
284
285 if dst.len() != len {
286 return Err(WillrError::OutputLengthMismatch {
287 expected: len,
288 got: dst.len(),
289 });
290 }
291
292 let period = input.get_period();
293 if period == 0 || period > len {
294 return Err(WillrError::InvalidPeriod {
295 period,
296 data_len: len,
297 });
298 }
299
300 let first_valid = (0..len)
301 .find(|&i| !(high[i].is_nan() || low[i].is_nan() || close[i].is_nan()))
302 .ok_or(WillrError::AllValuesNaN)?;
303
304 if (len - first_valid) < period {
305 return Err(WillrError::NotEnoughValidData {
306 needed: period,
307 valid: len - first_valid,
308 });
309 }
310
311 let chosen = match kernel {
312 Kernel::Auto => detect_best_kernel(),
313 other => other,
314 };
315
316 willr_compute_into(high, low, close, period, first_valid, chosen, dst);
317
318 let warmup_end = first_valid + period - 1;
319 for v in &mut dst[..warmup_end] {
320 *v = f64::NAN;
321 }
322
323 Ok(())
324}
325
326#[inline]
327pub fn willr_scalar(
328 high: &[f64],
329 low: &[f64],
330 close: &[f64],
331 period: usize,
332 first_valid: usize,
333 out: &mut [f64],
334) {
335 let n = high.len();
336 if n == 0 {
337 return;
338 }
339 let start_i = first_valid + period - 1;
340 if start_i >= n {
341 return;
342 }
343
344 const DEQUE_SWITCH: usize = 32;
345
346 unsafe {
347 if period <= DEQUE_SWITCH {
348 for i in start_i..n {
349 let c = *close.get_unchecked(i);
350 if c != c {
351 *out.get_unchecked_mut(i) = f64::NAN;
352 continue;
353 }
354
355 let win_start = i + 1 - period;
356 let mut h = f64::NEG_INFINITY;
357 let mut l = f64::INFINITY;
358 let mut any_nan = false;
359
360 let mut j = win_start;
361 let unroll_end = win_start + (period & !3);
362 while j < unroll_end {
363 let h0 = *high.get_unchecked(j);
364 let l0 = *low.get_unchecked(j);
365 let h1 = *high.get_unchecked(j + 1);
366 let l1 = *low.get_unchecked(j + 1);
367 let h2 = *high.get_unchecked(j + 2);
368 let l2 = *low.get_unchecked(j + 2);
369 let h3 = *high.get_unchecked(j + 3);
370 let l3 = *low.get_unchecked(j + 3);
371
372 if (h0 != h0)
373 | (l0 != l0)
374 | (h1 != h1)
375 | (l1 != l1)
376 | (h2 != h2)
377 | (l2 != l2)
378 | (h3 != h3)
379 | (l3 != l3)
380 {
381 any_nan = true;
382 break;
383 }
384
385 if h0 > h {
386 h = h0;
387 }
388 if h1 > h {
389 h = h1;
390 }
391 if h2 > h {
392 h = h2;
393 }
394 if h3 > h {
395 h = h3;
396 }
397
398 if l0 < l {
399 l = l0;
400 }
401 if l1 < l {
402 l = l1;
403 }
404 if l2 < l {
405 l = l2;
406 }
407 if l3 < l {
408 l = l3;
409 }
410
411 j += 4;
412 }
413
414 if !any_nan {
415 while j <= i {
416 let hj = *high.get_unchecked(j);
417 let lj = *low.get_unchecked(j);
418 if (hj != hj) | (lj != lj) {
419 any_nan = true;
420 break;
421 }
422 if hj > h {
423 h = hj;
424 }
425 if lj < l {
426 l = lj;
427 }
428 j += 1;
429 }
430 }
431
432 if any_nan || !(h.is_finite() && l.is_finite()) {
433 *out.get_unchecked_mut(i) = f64::NAN;
434 } else {
435 let denom = h - l;
436 let dst = out.get_unchecked_mut(i);
437 if denom == 0.0 {
438 *dst = 0.0;
439 } else {
440 let ratio = (h - c) / denom;
441 *dst = (-100.0f64).mul_add(ratio, 0.0);
442 }
443 }
444 }
445 return;
446 }
447
448 let cap = period;
449
450 let mut dq_max: Vec<usize> = Vec::with_capacity(cap);
451 dq_max.set_len(cap);
452 let mut dq_min: Vec<usize> = Vec::with_capacity(cap);
453 dq_min.set_len(cap);
454 let mut head_max = 0usize;
455 let mut tail_max = 0usize;
456 let mut len_max = 0usize;
457
458 let mut head_min = 0usize;
459 let mut tail_min = 0usize;
460 let mut len_min = 0usize;
461
462 let mut nan_ring: Vec<u8> = Vec::with_capacity(cap);
463 nan_ring.set_len(cap);
464 let mut ring_pos = 0usize;
465 let mut nan_count: usize = 0;
466
467 let l0 = start_i + 1 - period;
468 let mut j = l0;
469 while j <= start_i {
470 let hj = *high.get_unchecked(j);
471 let lj = *low.get_unchecked(j);
472 let is_nan = ((hj != hj) | (lj != lj)) as u8;
473
474 nan_count += is_nan as usize;
475 *nan_ring.get_unchecked_mut(ring_pos) = is_nan;
476 ring_pos += 1;
477 if ring_pos == cap {
478 ring_pos = 0;
479 }
480
481 if is_nan == 0 {
482 while len_max != 0 {
483 let back_pos = if tail_max == 0 { cap - 1 } else { tail_max - 1 };
484 let back_idx = *dq_max.get_unchecked(back_pos);
485 if *high.get_unchecked(back_idx) <= hj {
486 tail_max = back_pos;
487 len_max -= 1;
488 } else {
489 break;
490 }
491 }
492 *dq_max.get_unchecked_mut(tail_max) = j;
493 tail_max += 1;
494 if tail_max == cap {
495 tail_max = 0;
496 }
497 len_max += 1;
498
499 while len_min != 0 {
500 let back_pos = if tail_min == 0 { cap - 1 } else { tail_min - 1 };
501 let back_idx = *dq_min.get_unchecked(back_pos);
502 if *low.get_unchecked(back_idx) >= lj {
503 tail_min = back_pos;
504 len_min -= 1;
505 } else {
506 break;
507 }
508 }
509 *dq_min.get_unchecked_mut(tail_min) = j;
510 tail_min += 1;
511 if tail_min == cap {
512 tail_min = 0;
513 }
514 len_min += 1;
515 }
516 j += 1;
517 }
518
519 for i in start_i..n {
520 let c = *close.get_unchecked(i);
521
522 if (c != c) || (nan_count != 0) || (len_max == 0) || (len_min == 0) {
523 *out.get_unchecked_mut(i) = f64::NAN;
524 } else {
525 let h_idx = *dq_max.get_unchecked(head_max);
526 let l_idx = *dq_min.get_unchecked(head_min);
527 let h = *high.get_unchecked(h_idx);
528 let l = *low.get_unchecked(l_idx);
529 if !(h.is_finite() && l.is_finite()) {
530 *out.get_unchecked_mut(i) = f64::NAN;
531 } else {
532 let denom = h - l;
533 let dst = out.get_unchecked_mut(i);
534 if denom == 0.0 {
535 *dst = 0.0;
536 } else {
537 let ratio = (h - c) / denom;
538 *dst = (-100.0f64).mul_add(ratio, 0.0);
539 }
540 }
541 }
542
543 let next = i + 1;
544 if next < n {
545 let cutoff = next - period;
546
547 while len_max != 0 {
548 let f_idx = *dq_max.get_unchecked(head_max);
549 if f_idx <= cutoff {
550 head_max += 1;
551 if head_max == cap {
552 head_max = 0;
553 }
554 len_max -= 1;
555 } else {
556 break;
557 }
558 }
559 while len_min != 0 {
560 let f_idx = *dq_min.get_unchecked(head_min);
561 if f_idx <= cutoff {
562 head_min += 1;
563 if head_min == cap {
564 head_min = 0;
565 }
566 len_min -= 1;
567 } else {
568 break;
569 }
570 }
571
572 let hj = *high.get_unchecked(next);
573 let lj = *low.get_unchecked(next);
574 let new_nan = ((hj != hj) | (lj != lj)) as u8;
575 let old_nan = *nan_ring.get_unchecked(ring_pos) as usize;
576 nan_count = nan_count + (new_nan as usize) - old_nan;
577 *nan_ring.get_unchecked_mut(ring_pos) = new_nan;
578 ring_pos += 1;
579 if ring_pos == cap {
580 ring_pos = 0;
581 }
582
583 if new_nan == 0 {
584 while len_max != 0 {
585 let back_pos = if tail_max == 0 { cap - 1 } else { tail_max - 1 };
586 let back_idx = *dq_max.get_unchecked(back_pos);
587 if *high.get_unchecked(back_idx) <= hj {
588 tail_max = back_pos;
589 len_max -= 1;
590 } else {
591 break;
592 }
593 }
594 *dq_max.get_unchecked_mut(tail_max) = next;
595 tail_max += 1;
596 if tail_max == cap {
597 tail_max = 0;
598 }
599 len_max += 1;
600
601 while len_min != 0 {
602 let back_pos = if tail_min == 0 { cap - 1 } else { tail_min - 1 };
603 let back_idx = *dq_min.get_unchecked(back_pos);
604 if *low.get_unchecked(back_idx) >= lj {
605 tail_min = back_pos;
606 len_min -= 1;
607 } else {
608 break;
609 }
610 }
611 *dq_min.get_unchecked_mut(tail_min) = next;
612 tail_min += 1;
613 if tail_min == cap {
614 tail_min = 0;
615 }
616 len_min += 1;
617 }
618 }
619 }
620 }
621}
622
623#[inline(always)]
624fn willr_scalar_naive(
625 high: &[f64],
626 low: &[f64],
627 close: &[f64],
628 period: usize,
629 first_valid: usize,
630 out: &mut [f64],
631) {
632 for i in (first_valid + period - 1)..high.len() {
633 if close[i].is_nan() {
634 out[i] = f64::NAN;
635 continue;
636 }
637 let start = i + 1 - period;
638 let (mut h, mut l) = (f64::NEG_INFINITY, f64::INFINITY);
639 let mut has_nan = false;
640 for j in start..=i {
641 let hj = high[j];
642 let lj = low[j];
643 if hj.is_nan() || lj.is_nan() {
644 has_nan = true;
645 break;
646 }
647 if hj > h {
648 h = hj;
649 }
650 if lj < l {
651 l = lj;
652 }
653 }
654 if has_nan || h.is_infinite() || l.is_infinite() {
655 out[i] = f64::NAN;
656 } else {
657 let denom = h - l;
658 out[i] = if denom == 0.0 {
659 0.0
660 } else {
661 (h - close[i]) / denom * -100.0
662 };
663 }
664 }
665}
666
667#[inline(always)]
668fn willr_scalar_deque(
669 high: &[f64],
670 low: &[f64],
671 close: &[f64],
672 period: usize,
673 first_valid: usize,
674 out: &mut [f64],
675) {
676 let n = high.len();
677 if n == 0 {
678 return;
679 }
680
681 let start_i = first_valid + period - 1;
682 if start_i >= n {
683 return;
684 }
685
686 let cap = period;
687
688 let mut dq_max = vec![0usize; cap];
689 let mut head_max = 0usize;
690 let mut len_max = 0usize;
691
692 let mut dq_min = vec![0usize; cap];
693 let mut head_min = 0usize;
694 let mut len_min = 0usize;
695
696 let mut nan_ring = vec![0u8; cap];
697 let mut ring_pos = 0usize;
698 let mut nan_count: usize = 0;
699
700 let l0 = start_i + 1 - period;
701 for j in l0..=start_i {
702 let hj = high[j];
703 let lj = low[j];
704 let is_nan = (hj.is_nan() || lj.is_nan()) as u8;
705
706 nan_count += is_nan as usize;
707 nan_ring[ring_pos] = is_nan;
708 ring_pos += 1;
709 if ring_pos == cap {
710 ring_pos = 0;
711 }
712
713 if is_nan == 0 {
714 while len_max != 0 {
715 let back_pos = (head_max + len_max - 1) % cap;
716 let back_idx = dq_max[back_pos];
717 if high[back_idx] <= hj {
718 len_max -= 1;
719 } else {
720 break;
721 }
722 }
723 let ins_pos = (head_max + len_max) % cap;
724 dq_max[ins_pos] = j;
725 len_max += 1;
726
727 while len_min != 0 {
728 let back_pos = (head_min + len_min - 1) % cap;
729 let back_idx = dq_min[back_pos];
730 if low[back_idx] >= lj {
731 len_min -= 1;
732 } else {
733 break;
734 }
735 }
736 let ins_pos = (head_min + len_min) % cap;
737 dq_min[ins_pos] = j;
738 len_min += 1;
739 }
740 }
741
742 for i in start_i..n {
743 let c = close[i];
744
745 if c.is_nan() || nan_count != 0 || len_max == 0 || len_min == 0 {
746 out[i] = f64::NAN;
747 } else {
748 let h_idx = dq_max[head_max];
749 let l_idx = dq_min[head_min];
750 let h = high[h_idx];
751 let l = low[l_idx];
752
753 if !(h.is_finite() && l.is_finite()) {
754 out[i] = f64::NAN;
755 } else {
756 let denom = h - l;
757 out[i] = if denom == 0.0 {
758 0.0
759 } else {
760 (h - c) / denom * -100.0
761 };
762 }
763 }
764
765 let next = i + 1;
766 if next < n {
767 let cutoff = next - period;
768
769 while len_max != 0 {
770 let f_idx = dq_max[head_max];
771 if f_idx <= cutoff {
772 head_max += 1;
773 if head_max == cap {
774 head_max = 0;
775 }
776 len_max -= 1;
777 } else {
778 break;
779 }
780 }
781 while len_min != 0 {
782 let f_idx = dq_min[head_min];
783 if f_idx <= cutoff {
784 head_min += 1;
785 if head_min == cap {
786 head_min = 0;
787 }
788 len_min -= 1;
789 } else {
790 break;
791 }
792 }
793
794 let hj = high[next];
795 let lj = low[next];
796 let new_nan = (hj.is_nan() || lj.is_nan()) as u8;
797
798 let old_nan = nan_ring[ring_pos] as usize;
799 nan_count = nan_count + (new_nan as usize) - old_nan;
800 nan_ring[ring_pos] = new_nan;
801 ring_pos += 1;
802 if ring_pos == cap {
803 ring_pos = 0;
804 }
805
806 if new_nan == 0 {
807 while len_max != 0 {
808 let back_pos = (head_max + len_max - 1) % cap;
809 let back_idx = dq_max[back_pos];
810 if high[back_idx] <= hj {
811 len_max -= 1;
812 } else {
813 break;
814 }
815 }
816 let ins_pos = (head_max + len_max) % cap;
817 dq_max[ins_pos] = next;
818 len_max += 1;
819
820 while len_min != 0 {
821 let back_pos = (head_min + len_min - 1) % cap;
822 let back_idx = dq_min[back_pos];
823 if low[back_idx] >= lj {
824 len_min -= 1;
825 } else {
826 break;
827 }
828 }
829 let ins_pos = (head_min + len_min) % cap;
830 dq_min[ins_pos] = next;
831 len_min += 1;
832 }
833 }
834 }
835}
836
837#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
838#[inline]
839pub unsafe fn willr_avx2(
840 high: &[f64],
841 low: &[f64],
842 close: &[f64],
843 period: usize,
844 first_valid: usize,
845 out: &mut [f64],
846) {
847 use core::arch::x86_64::*;
848 let n = high.len();
849 if n == 0 {
850 return;
851 }
852 let start_i = first_valid + period - 1;
853 if start_i >= n {
854 return;
855 }
856
857 const VEC_SWITCH: usize = 64;
858 if period > VEC_SWITCH {
859 willr_scalar(high, low, close, period, first_valid, out);
860 return;
861 }
862
863 for i in start_i..n {
864 let c = *close.get_unchecked(i);
865 if c != c {
866 *out.get_unchecked_mut(i) = f64::NAN;
867 continue;
868 }
869
870 let win_start = i + 1 - period;
871 let mut vmax = _mm256_set1_pd(f64::NEG_INFINITY);
872 let mut vmin = _mm256_set1_pd(f64::INFINITY);
873 let mut any_nan_mask = 0i32;
874
875 let mut j = win_start;
876 let vec_end = win_start + (period & !3);
877 while j < vec_end {
878 let hv = _mm256_loadu_pd(high.as_ptr().add(j));
879 let lv = _mm256_loadu_pd(low.as_ptr().add(j));
880
881 let hnan = _mm256_cmp_pd(hv, hv, _CMP_UNORD_Q);
882 let lnan = _mm256_cmp_pd(lv, lv, _CMP_UNORD_Q);
883 let nmask = _mm256_movemask_pd(_mm256_or_pd(hnan, lnan));
884 any_nan_mask |= nmask;
885
886 vmax = _mm256_max_pd(vmax, hv);
887 vmin = _mm256_min_pd(vmin, lv);
888 j += 4;
889 }
890
891 if any_nan_mask != 0 {
892 *out.get_unchecked_mut(i) = f64::NAN;
893 continue;
894 }
895
896 let vhi_max: __m128d = _mm256_extractf128_pd(vmax, 1);
897 let vlo_max: __m128d = _mm256_castpd256_pd128(vmax);
898 let v128_max = _mm_max_pd(vlo_max, vhi_max);
899 let v64_max_hi = _mm_unpackhi_pd(v128_max, v128_max);
900 let mut h = _mm_cvtsd_f64(_mm_max_sd(v128_max, v64_max_hi));
901
902 let vhi_min: __m128d = _mm256_extractf128_pd(vmin, 1);
903 let vlo_min: __m128d = _mm256_castpd256_pd128(vmin);
904 let v128_min = _mm_min_pd(vlo_min, vhi_min);
905 let v64_min_hi = _mm_unpackhi_pd(v128_min, v128_min);
906 let mut l = _mm_cvtsd_f64(_mm_min_sd(v128_min, v64_min_hi));
907
908 while j <= i {
909 let hj = *high.get_unchecked(j);
910 let lj = *low.get_unchecked(j);
911 if (hj != hj) | (lj != lj) {
912 any_nan_mask = 1;
913 break;
914 }
915 if hj > h {
916 h = hj;
917 }
918 if lj < l {
919 l = lj;
920 }
921 j += 1;
922 }
923
924 if (any_nan_mask != 0) || !(h.is_finite() && l.is_finite()) {
925 *out.get_unchecked_mut(i) = f64::NAN;
926 } else {
927 let denom = h - l;
928 let dst = out.get_unchecked_mut(i);
929 if denom == 0.0 {
930 *dst = 0.0;
931 } else {
932 let ratio = (h - c) / denom;
933 *dst = (-100.0f64).mul_add(ratio, 0.0);
934 }
935 }
936 }
937}
938
939#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
940#[inline]
941pub unsafe fn willr_avx512(
942 high: &[f64],
943 low: &[f64],
944 close: &[f64],
945 period: usize,
946 first_valid: usize,
947 out: &mut [f64],
948) {
949 willr_avx2(high, low, close, period, first_valid, out)
950}
951
952#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
953#[inline]
954pub unsafe fn willr_avx512_short(
955 high: &[f64],
956 low: &[f64],
957 close: &[f64],
958 period: usize,
959 first_valid: usize,
960 out: &mut [f64],
961) {
962 use core::arch::x86_64::*;
963 let n = high.len();
964 if n == 0 {
965 return;
966 }
967 let start_i = first_valid + period - 1;
968 if start_i >= n {
969 return;
970 }
971
972 for i in start_i..n {
973 let c = *close.get_unchecked(i);
974 if c != c {
975 *out.get_unchecked_mut(i) = f64::NAN;
976 continue;
977 }
978
979 let win_start = i + 1 - period;
980 let mut vmax512 = _mm512_set1_pd(f64::NEG_INFINITY);
981 let mut vmin512 = _mm512_set1_pd(f64::INFINITY);
982 let mut any_nan_mask: u16 = 0;
983
984 let mut j = win_start;
985 let vec_end = win_start + (period & !7);
986 while j < vec_end {
987 let hv = _mm512_loadu_pd(high.as_ptr().add(j));
988 let lv = _mm512_loadu_pd(low.as_ptr().add(j));
989
990 let hnan = _mm512_cmp_pd_mask(hv, hv, _CMP_UNORD_Q);
991 let lnan = _mm512_cmp_pd_mask(lv, lv, _CMP_UNORD_Q);
992 any_nan_mask |= (hnan | lnan) as u16;
993
994 vmax512 = _mm512_max_pd(vmax512, hv);
995 vmin512 = _mm512_min_pd(vmin512, lv);
996 j += 8;
997 }
998
999 if any_nan_mask != 0 {
1000 *out.get_unchecked_mut(i) = f64::NAN;
1001 continue;
1002 }
1003
1004 let vmax_lo256 = _mm512_castpd512_pd256(vmax512);
1005 let vmax_hi256 = _mm512_extractf64x4_pd(vmax512, 1);
1006 let vmax256 = _mm256_max_pd(vmax_lo256, vmax_hi256);
1007 let vmax_hi128 = _mm256_extractf128_pd(vmax256, 1);
1008 let vmax_lo128 = _mm256_castpd256_pd128(vmax256);
1009 let vmax128 = _mm_max_pd(vmax_lo128, vmax_hi128);
1010 let vmax64_hi = _mm_unpackhi_pd(vmax128, vmax128);
1011 let mut h = _mm_cvtsd_f64(_mm_max_sd(vmax128, vmax64_hi));
1012
1013 let vmin_lo256 = _mm512_castpd512_pd256(vmin512);
1014 let vmin_hi256 = _mm512_extractf64x4_pd(vmin512, 1);
1015 let vmin256 = _mm256_min_pd(vmin_lo256, vmin_hi256);
1016 let vmin_hi128 = _mm256_extractf128_pd(vmin256, 1);
1017 let vmin_lo128 = _mm256_castpd256_pd128(vmin256);
1018 let vmin128 = _mm_min_pd(vmin_lo128, vmin_hi128);
1019 let vmin64_hi = _mm_unpackhi_pd(vmin128, vmin128);
1020 let mut l = _mm_cvtsd_f64(_mm_min_sd(vmin128, vmin64_hi));
1021
1022 while j <= i {
1023 let hj = *high.get_unchecked(j);
1024 let lj = *low.get_unchecked(j);
1025 if (hj != hj) | (lj != lj) {
1026 any_nan_mask = 1;
1027 break;
1028 }
1029 if hj > h {
1030 h = hj;
1031 }
1032 if lj < l {
1033 l = lj;
1034 }
1035 j += 1;
1036 }
1037
1038 if (any_nan_mask != 0) || !(h.is_finite() && l.is_finite()) {
1039 *out.get_unchecked_mut(i) = f64::NAN;
1040 } else {
1041 let denom = h - l;
1042 let dst = out.get_unchecked_mut(i);
1043 if denom == 0.0 {
1044 *dst = 0.0;
1045 } else {
1046 let ratio = (h - c) / denom;
1047 *dst = (-100.0f64).mul_add(ratio, 0.0);
1048 }
1049 }
1050 }
1051}
1052
1053#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1054#[inline]
1055pub unsafe fn willr_avx512_long(
1056 high: &[f64],
1057 low: &[f64],
1058 close: &[f64],
1059 period: usize,
1060 first_valid: usize,
1061 out: &mut [f64],
1062) {
1063 willr_scalar(high, low, close, period, first_valid, out)
1064}
1065
1066#[derive(Clone, Debug)]
1067pub struct WillrBatchRange {
1068 pub period: (usize, usize, usize),
1069}
1070
1071impl Default for WillrBatchRange {
1072 fn default() -> Self {
1073 Self {
1074 period: (14, 263, 1),
1075 }
1076 }
1077}
1078
1079#[derive(Clone, Debug, Default)]
1080pub struct WillrBatchBuilder {
1081 range: WillrBatchRange,
1082 kernel: Kernel,
1083}
1084
1085impl WillrBatchBuilder {
1086 pub fn new() -> Self {
1087 Self::default()
1088 }
1089 pub fn kernel(mut self, k: Kernel) -> Self {
1090 self.kernel = k;
1091 self
1092 }
1093 #[inline]
1094 pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
1095 self.range.period = (start, end, step);
1096 self
1097 }
1098 #[inline]
1099 pub fn period_static(mut self, p: usize) -> Self {
1100 self.range.period = (p, p, 0);
1101 self
1102 }
1103 pub fn apply_slices(
1104 self,
1105 high: &[f64],
1106 low: &[f64],
1107 close: &[f64],
1108 ) -> Result<WillrBatchOutput, WillrError> {
1109 willr_batch_with_kernel(high, low, close, &self.range, self.kernel)
1110 }
1111 pub fn apply_candles(self, c: &Candles) -> Result<WillrBatchOutput, WillrError> {
1112 let high = source_type(c, "high");
1113 let low = source_type(c, "low");
1114 let close = source_type(c, "close");
1115 self.apply_slices(high, low, close)
1116 }
1117}
1118
1119pub fn willr_batch_with_kernel(
1120 high: &[f64],
1121 low: &[f64],
1122 close: &[f64],
1123 sweep: &WillrBatchRange,
1124 k: Kernel,
1125) -> Result<WillrBatchOutput, WillrError> {
1126 let kernel = match k {
1127 Kernel::Auto => match detect_best_batch_kernel() {
1128 Kernel::Avx512Batch => Kernel::Avx2Batch,
1129 other => other,
1130 },
1131 other if other.is_batch() => other,
1132 other => return Err(WillrError::InvalidKernelForBatch(other)),
1133 };
1134 let simd = match kernel {
1135 Kernel::Avx512Batch => Kernel::Avx512,
1136 Kernel::Avx2Batch => Kernel::Avx2,
1137 Kernel::ScalarBatch => Kernel::Scalar,
1138 _ => unreachable!(),
1139 };
1140 willr_batch_par_slice(high, low, close, sweep, simd)
1141}
1142
1143#[derive(Clone, Debug)]
1144pub struct WillrBatchOutput {
1145 pub values: Vec<f64>,
1146 pub combos: Vec<WillrParams>,
1147 pub rows: usize,
1148 pub cols: usize,
1149}
1150
1151impl WillrBatchOutput {
1152 pub fn row_for_params(&self, p: &WillrParams) -> Option<usize> {
1153 self.combos
1154 .iter()
1155 .position(|c| c.period.unwrap_or(14) == p.period.unwrap_or(14))
1156 }
1157 pub fn values_for(&self, p: &WillrParams) -> Option<&[f64]> {
1158 self.row_for_params(p).map(|row| {
1159 let start = row * self.cols;
1160 &self.values[start..start + self.cols]
1161 })
1162 }
1163}
1164
1165#[inline(always)]
1166fn expand_grid(r: &WillrBatchRange) -> Result<Vec<WillrParams>, WillrError> {
1167 fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, WillrError> {
1168 if step == 0 {
1169 return Ok(vec![start]);
1170 }
1171 if start == end {
1172 return Ok(vec![start]);
1173 }
1174 let mut vals = Vec::new();
1175 if start < end {
1176 let mut v = start;
1177 while v <= end {
1178 vals.push(v);
1179 match v.checked_add(step) {
1180 Some(next) => {
1181 if next == v {
1182 break;
1183 }
1184 v = next;
1185 }
1186 None => break,
1187 }
1188 }
1189 } else {
1190 let mut v = start;
1191 while v >= end {
1192 vals.push(v);
1193 if v == 0 {
1194 break;
1195 }
1196 let next = v.saturating_sub(step);
1197 if next == v {
1198 break;
1199 }
1200 v = next;
1201 if v < end {
1202 break;
1203 }
1204 }
1205 }
1206 if vals.is_empty() {
1207 return Err(WillrError::InvalidRange { start, end, step });
1208 }
1209 Ok(vals)
1210 }
1211
1212 let periods = axis_usize(r.period)?;
1213 Ok(periods
1214 .into_iter()
1215 .map(|p| WillrParams { period: Some(p) })
1216 .collect())
1217}
1218
1219#[derive(Debug)]
1220struct SharedWillrCtx {
1221 log2: Vec<usize>,
1222 st_max: Vec<Vec<f64>>,
1223 st_min: Vec<Vec<f64>>,
1224 nan_psum: Vec<u32>,
1225}
1226
1227impl SharedWillrCtx {
1228 #[inline]
1229 fn build_log2(n: usize) -> Vec<usize> {
1230 let mut lg = vec![0usize; n + 1];
1231 for i in 2..=n {
1232 lg[i] = lg[i >> 1] + 1;
1233 }
1234 lg
1235 }
1236
1237 #[inline]
1238 fn build_sparse(data: &[f64], is_max: bool, log2: &[usize]) -> Vec<Vec<f64>> {
1239 let n = data.len();
1240 if n == 0 {
1241 return vec![Vec::new()];
1242 }
1243 let max_k = log2[n];
1244 let mut st: Vec<Vec<f64>> = Vec::with_capacity(max_k + 1);
1245 st.push(data.to_vec());
1246 let mut k = 1usize;
1247 while k <= max_k {
1248 let window = 1usize << k;
1249 let len = n + 1 - window;
1250 let prev = &st[k - 1];
1251 let offset = 1usize << (k - 1);
1252 let mut row = Vec::with_capacity(len);
1253 for i in 0..len {
1254 let a = prev[i];
1255 let b = prev[i + offset];
1256 row.push(if is_max { a.max(b) } else { a.min(b) });
1257 }
1258 st.push(row);
1259 k += 1;
1260 }
1261 st
1262 }
1263
1264 #[inline]
1265 fn new(high: &[f64], low: &[f64]) -> Self {
1266 debug_assert_eq!(high.len(), low.len());
1267 let n = high.len();
1268 let log2 = Self::build_log2(n);
1269
1270 let mut high_clean = Vec::with_capacity(n);
1271 let mut low_clean = Vec::with_capacity(n);
1272 let mut nan_psum = vec![0u32; n + 1];
1273
1274 for i in 0..n {
1275 let h = high[i];
1276 let l = low[i];
1277 let has_nan = h.is_nan() || l.is_nan();
1278 nan_psum[i + 1] = nan_psum[i] + (has_nan as u32);
1279
1280 high_clean.push(if h.is_nan() { f64::NEG_INFINITY } else { h });
1281 low_clean.push(if l.is_nan() { f64::INFINITY } else { l });
1282 }
1283
1284 let st_max = Self::build_sparse(&high_clean, true, &log2);
1285 let st_min = Self::build_sparse(&low_clean, false, &log2);
1286
1287 Self {
1288 log2,
1289 st_max,
1290 st_min,
1291 nan_psum,
1292 }
1293 }
1294
1295 #[inline]
1296 fn qmax(&self, l: usize, r: usize) -> f64 {
1297 let len = r - l + 1;
1298 let k = self.log2[len];
1299 let offset = 1usize << k;
1300 let a = self.st_max[k][l];
1301 let b = self.st_max[k][r + 1 - offset];
1302 a.max(b)
1303 }
1304
1305 #[inline]
1306 fn qmin(&self, l: usize, r: usize) -> f64 {
1307 let len = r - l + 1;
1308 let k = self.log2[len];
1309 let offset = 1usize << k;
1310 let a = self.st_min[k][l];
1311 let b = self.st_min[k][r + 1 - offset];
1312 a.min(b)
1313 }
1314
1315 #[inline]
1316 fn window_has_nan(&self, l: usize, r: usize) -> bool {
1317 (self.nan_psum[r + 1] - self.nan_psum[l]) != 0
1318 }
1319
1320 #[cfg(feature = "cuda")]
1321 #[inline]
1322 fn flatten(&self) -> (Vec<f64>, Vec<f64>, Vec<usize>) {
1323 let levels = self.st_max.len();
1324 let mut offsets = Vec::with_capacity(levels + 1);
1325 let mut total = 0usize;
1326 for level in &self.st_max {
1327 offsets.push(total);
1328 total += level.len();
1329 }
1330 offsets.push(total);
1331
1332 let mut flat_max = Vec::with_capacity(total);
1333 for level in &self.st_max {
1334 flat_max.extend_from_slice(level);
1335 }
1336
1337 let mut flat_min = Vec::with_capacity(total);
1338 for level in &self.st_min {
1339 flat_min.extend_from_slice(level);
1340 }
1341
1342 (flat_max, flat_min, offsets)
1343 }
1344}
1345
1346#[cfg(feature = "cuda")]
1347#[derive(Debug, Clone)]
1348pub struct WillrGpuTables {
1349 pub log2: Vec<i32>,
1350 pub level_offsets: Vec<i32>,
1351 pub st_max: Vec<f32>,
1352 pub st_min: Vec<f32>,
1353 pub nan_psum: Vec<i32>,
1354}
1355
1356#[cfg(feature = "cuda")]
1357impl WillrGpuTables {
1358 #[inline]
1359 fn from_shared_ctx(ctx: &SharedWillrCtx) -> Self {
1360 let (flat_max_f64, flat_min_f64, offsets_usize) = ctx.flatten();
1361 let log2 = ctx.log2.iter().map(|&v| v as i32).collect();
1362 let level_offsets = offsets_usize.iter().map(|&v| v as i32).collect();
1363 let st_max = flat_max_f64.iter().map(|&v| v as f32).collect();
1364 let st_min = flat_min_f64.iter().map(|&v| v as f32).collect();
1365 let nan_psum = ctx.nan_psum.iter().map(|&v| v as i32).collect();
1366
1367 Self {
1368 log2,
1369 level_offsets,
1370 st_max,
1371 st_min,
1372 nan_psum,
1373 }
1374 }
1375}
1376
1377#[cfg(feature = "cuda")]
1378#[inline]
1379pub fn build_willr_gpu_tables(high: &[f32], low: &[f32]) -> WillrGpuTables {
1380 debug_assert_eq!(high.len(), low.len());
1381 let high_f64: Vec<f64> = high.iter().map(|&v| v as f64).collect();
1382 let low_f64: Vec<f64> = low.iter().map(|&v| v as f64).collect();
1383 let ctx = SharedWillrCtx::new(&high_f64, &low_f64);
1384 WillrGpuTables::from_shared_ctx(&ctx)
1385}
1386
1387#[inline(always)]
1388fn willr_row_shared_core(
1389 close: &[f64],
1390 first_valid: usize,
1391 period: usize,
1392 out: &mut [f64],
1393 ctx: &SharedWillrCtx,
1394) {
1395 let n = close.len();
1396 let start_i = first_valid + period - 1;
1397 if start_i >= n {
1398 return;
1399 }
1400
1401 for i in start_i..n {
1402 let c = close[i];
1403 if c.is_nan() {
1404 out[i] = f64::NAN;
1405 continue;
1406 }
1407
1408 let l_idx = i + 1 - period;
1409 if ctx.window_has_nan(l_idx, i) {
1410 out[i] = f64::NAN;
1411 continue;
1412 }
1413
1414 let h = ctx.qmax(l_idx, i);
1415 let l = ctx.qmin(l_idx, i);
1416
1417 if !h.is_finite() || !l.is_finite() {
1418 out[i] = f64::NAN;
1419 continue;
1420 }
1421
1422 let denom = h - l;
1423 out[i] = if denom == 0.0 {
1424 0.0
1425 } else {
1426 (h - c) / denom * -100.0
1427 };
1428 }
1429}
1430
1431#[inline(always)]
1432fn willr_row_scalar_with_ctx(
1433 close: &[f64],
1434 first_valid: usize,
1435 period: usize,
1436 out: &mut [f64],
1437 ctx: &SharedWillrCtx,
1438) {
1439 willr_row_shared_core(close, first_valid, period, out, ctx);
1440}
1441
1442#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1443#[inline(always)]
1444unsafe fn willr_row_avx2_with_ctx(
1445 close: &[f64],
1446 first_valid: usize,
1447 period: usize,
1448 out: &mut [f64],
1449 ctx: &SharedWillrCtx,
1450) {
1451 willr_row_shared_core(close, first_valid, period, out, ctx);
1452}
1453
1454#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1455#[inline(always)]
1456unsafe fn willr_row_avx512_with_ctx(
1457 close: &[f64],
1458 first_valid: usize,
1459 period: usize,
1460 out: &mut [f64],
1461 ctx: &SharedWillrCtx,
1462) {
1463 willr_row_shared_core(close, first_valid, period, out, ctx);
1464}
1465
1466#[inline(always)]
1467pub fn willr_batch_slice(
1468 high: &[f64],
1469 low: &[f64],
1470 close: &[f64],
1471 sweep: &WillrBatchRange,
1472 kern: Kernel,
1473) -> Result<WillrBatchOutput, WillrError> {
1474 willr_batch_inner(high, low, close, sweep, kern, false)
1475}
1476
1477#[inline(always)]
1478pub fn willr_batch_par_slice(
1479 high: &[f64],
1480 low: &[f64],
1481 close: &[f64],
1482 sweep: &WillrBatchRange,
1483 kern: Kernel,
1484) -> Result<WillrBatchOutput, WillrError> {
1485 willr_batch_inner(high, low, close, sweep, kern, true)
1486}
1487
1488#[inline(always)]
1489fn willr_batch_inner(
1490 high: &[f64],
1491 low: &[f64],
1492 close: &[f64],
1493 sweep: &WillrBatchRange,
1494 kern: Kernel,
1495 parallel: bool,
1496) -> Result<WillrBatchOutput, WillrError> {
1497 let combos = expand_grid(sweep)?;
1498 let len = high.len();
1499 if combos.iter().any(|c| c.period == Some(0)) {
1500 return Err(WillrError::InvalidPeriod {
1501 period: 0,
1502 data_len: len,
1503 });
1504 }
1505 if low.len() != len || close.len() != len || len == 0 {
1506 return Err(WillrError::EmptyInputData);
1507 }
1508
1509 let first_valid = (0..len)
1510 .find(|&i| !(high[i].is_nan() || low[i].is_nan() || close[i].is_nan()))
1511 .ok_or(WillrError::AllValuesNaN)?;
1512 let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
1513 if len - first_valid < max_p {
1514 return Err(WillrError::NotEnoughValidData {
1515 needed: max_p,
1516 valid: len - first_valid,
1517 });
1518 }
1519 let rows = combos.len();
1520 let cols = len;
1521
1522 rows.checked_mul(cols).ok_or(WillrError::InvalidRange {
1523 start: sweep.period.0,
1524 end: sweep.period.1,
1525 step: sweep.period.2,
1526 })?;
1527
1528 let mut buf_mu = make_uninit_matrix(rows, cols);
1529 let warmup_periods: Vec<usize> = combos
1530 .iter()
1531 .map(|c| first_valid + c.period.unwrap() - 1)
1532 .collect();
1533 init_matrix_prefixes(&mut buf_mu, cols, &warmup_periods);
1534
1535 let mut buf_guard = core::mem::ManuallyDrop::new(buf_mu);
1536 let mut values = unsafe {
1537 Vec::from_raw_parts(
1538 buf_guard.as_mut_ptr() as *mut f64,
1539 buf_guard.len(),
1540 buf_guard.capacity(),
1541 )
1542 };
1543
1544 let ctx = SharedWillrCtx::new(high, low);
1545
1546 let do_row = |row: usize, out_row: &mut [f64]| {
1547 let period = combos[row].period.unwrap();
1548 match kern {
1549 Kernel::Scalar => willr_row_scalar_with_ctx(close, first_valid, period, out_row, &ctx),
1550 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1551 Kernel::Avx2 => unsafe {
1552 willr_row_avx2_with_ctx(close, first_valid, period, out_row, &ctx)
1553 },
1554 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1555 Kernel::Avx512 => unsafe {
1556 willr_row_avx512_with_ctx(close, first_valid, period, out_row, &ctx)
1557 },
1558 _ => unreachable!(),
1559 }
1560 };
1561 if parallel {
1562 #[cfg(not(target_arch = "wasm32"))]
1563 {
1564 values
1565 .par_chunks_mut(cols)
1566 .enumerate()
1567 .for_each(|(row, slice)| do_row(row, slice));
1568 }
1569
1570 #[cfg(target_arch = "wasm32")]
1571 {
1572 for (row, slice) in values.chunks_mut(cols).enumerate() {
1573 do_row(row, slice);
1574 }
1575 }
1576 } else {
1577 for (row, slice) in values.chunks_mut(cols).enumerate() {
1578 do_row(row, slice);
1579 }
1580 }
1581 Ok(WillrBatchOutput {
1582 values,
1583 combos,
1584 rows,
1585 cols,
1586 })
1587}
1588
1589#[inline(always)]
1590pub fn willr_row_scalar(
1591 high: &[f64],
1592 low: &[f64],
1593 close: &[f64],
1594 first_valid: usize,
1595 period: usize,
1596 out: &mut [f64],
1597) {
1598 willr_scalar(high, low, close, period, first_valid, out)
1599}
1600
1601#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1602#[inline]
1603pub unsafe fn willr_row_avx2(
1604 high: &[f64],
1605 low: &[f64],
1606 close: &[f64],
1607 first_valid: usize,
1608 period: usize,
1609 out: &mut [f64],
1610) {
1611 willr_scalar(high, low, close, period, first_valid, out)
1612}
1613
1614#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1615#[inline]
1616pub unsafe fn willr_row_avx512(
1617 high: &[f64],
1618 low: &[f64],
1619 close: &[f64],
1620 first_valid: usize,
1621 period: usize,
1622 out: &mut [f64],
1623) {
1624 if period <= 32 {
1625 willr_row_avx512_short(high, low, close, first_valid, period, out);
1626 } else {
1627 willr_row_avx512_long(high, low, close, first_valid, period, out);
1628 }
1629}
1630
1631#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1632#[inline]
1633pub unsafe fn willr_row_avx512_short(
1634 high: &[f64],
1635 low: &[f64],
1636 close: &[f64],
1637 first_valid: usize,
1638 period: usize,
1639 out: &mut [f64],
1640) {
1641 willr_scalar(high, low, close, period, first_valid, out)
1642}
1643
1644#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1645#[inline]
1646pub unsafe fn willr_row_avx512_long(
1647 high: &[f64],
1648 low: &[f64],
1649 close: &[f64],
1650 first_valid: usize,
1651 period: usize,
1652 out: &mut [f64],
1653) {
1654 willr_scalar(high, low, close, period, first_valid, out)
1655}
1656
1657pub struct WillrStream {
1658 period: usize,
1659
1660 high_ring: Vec<f64>,
1661 low_ring: Vec<f64>,
1662
1663 nan_ring: Vec<u8>,
1664 nan_count: usize,
1665
1666 dq_max: Vec<usize>,
1667 dq_min: Vec<usize>,
1668 head_max: usize,
1669 tail_max: usize,
1670 len_max: usize,
1671 head_min: usize,
1672 tail_min: usize,
1673 len_min: usize,
1674
1675 count: usize,
1676}
1677
1678impl WillrStream {
1679 #[inline]
1680 pub fn try_new(params: WillrParams) -> Result<Self, WillrError> {
1681 let period = params.period.unwrap_or(14);
1682 if period == 0 {
1683 return Err(WillrError::InvalidPeriod {
1684 period: 0,
1685 data_len: 0,
1686 });
1687 }
1688 Ok(Self {
1689 period,
1690 high_ring: vec![f64::NAN; period],
1691 low_ring: vec![f64::NAN; period],
1692 nan_ring: vec![0u8; period],
1693 nan_count: 0,
1694
1695 dq_max: vec![0usize; period],
1696 dq_min: vec![0usize; period],
1697 head_max: 0,
1698 tail_max: 0,
1699 len_max: 0,
1700 head_min: 0,
1701 tail_min: 0,
1702 len_min: 0,
1703
1704 count: 0,
1705 })
1706 }
1707
1708 #[inline(always)]
1709 pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
1710 let cap = self.period;
1711 let t = self.count;
1712 let pos = t % cap;
1713
1714 let new_nan = (high.is_nan() || low.is_nan()) as u8;
1715 let old_nan = self.nan_ring[pos] as usize;
1716 self.nan_ring[pos] = new_nan;
1717 self.nan_count = self.nan_count + (new_nan as usize) - old_nan;
1718
1719 self.high_ring[pos] = high;
1720 self.low_ring[pos] = low;
1721
1722 if t + 1 > cap {
1723 let cutoff = t + 1 - cap;
1724
1725 while self.len_max != 0 {
1726 let front_idx = self.dq_max[self.head_max];
1727 if front_idx < cutoff {
1728 self.head_max += 1;
1729 if self.head_max == cap {
1730 self.head_max = 0;
1731 }
1732 self.len_max -= 1;
1733 } else {
1734 break;
1735 }
1736 }
1737
1738 while self.len_min != 0 {
1739 let front_idx = self.dq_min[self.head_min];
1740 if front_idx < cutoff {
1741 self.head_min += 1;
1742 if self.head_min == cap {
1743 self.head_min = 0;
1744 }
1745 self.len_min -= 1;
1746 } else {
1747 break;
1748 }
1749 }
1750 }
1751
1752 if new_nan == 0 {
1753 while self.len_max != 0 {
1754 let back_pos = if self.tail_max == 0 {
1755 cap - 1
1756 } else {
1757 self.tail_max - 1
1758 };
1759 let back_idx = self.dq_max[back_pos];
1760 let back_val = self.high_ring[back_idx % cap];
1761 if back_val <= high {
1762 self.tail_max = back_pos;
1763 self.len_max -= 1;
1764 } else {
1765 break;
1766 }
1767 }
1768 self.dq_max[self.tail_max] = t;
1769 self.tail_max += 1;
1770 if self.tail_max == cap {
1771 self.tail_max = 0;
1772 }
1773 self.len_max += 1;
1774
1775 while self.len_min != 0 {
1776 let back_pos = if self.tail_min == 0 {
1777 cap - 1
1778 } else {
1779 self.tail_min - 1
1780 };
1781 let back_idx = self.dq_min[back_pos];
1782 let back_val = self.low_ring[back_idx % cap];
1783 if back_val >= low {
1784 self.tail_min = back_pos;
1785 self.len_min -= 1;
1786 } else {
1787 break;
1788 }
1789 }
1790 self.dq_min[self.tail_min] = t;
1791 self.tail_min += 1;
1792 if self.tail_min == cap {
1793 self.tail_min = 0;
1794 }
1795 self.len_min += 1;
1796 }
1797
1798 self.count = t + 1;
1799 if self.count < cap {
1800 return None;
1801 }
1802
1803 if close.is_nan() || self.nan_count != 0 || self.len_max == 0 || self.len_min == 0 {
1804 return Some(f64::NAN);
1805 }
1806
1807 let h_idx = self.dq_max[self.head_max];
1808 let l_idx = self.dq_min[self.head_min];
1809 let h = self.high_ring[h_idx % cap];
1810 let l = self.low_ring[l_idx % cap];
1811
1812 if !(h.is_finite() && l.is_finite()) {
1813 return Some(f64::NAN);
1814 }
1815
1816 let denom = h - l;
1817 if denom == 0.0 {
1818 Some(0.0)
1819 } else {
1820 let ratio = (h - close) / denom;
1821 Some((-100.0f64).mul_add(ratio, 0.0))
1822 }
1823 }
1824}
1825
1826#[inline(always)]
1827fn willr_batch_inner_into(
1828 high: &[f64],
1829 low: &[f64],
1830 close: &[f64],
1831 sweep: &WillrBatchRange,
1832 kern: Kernel,
1833 parallel: bool,
1834 out: &mut [f64],
1835) -> Result<Vec<WillrParams>, WillrError> {
1836 let combos = expand_grid(sweep)?;
1837
1838 let len = high.len();
1839 if combos.iter().any(|c| c.period == Some(0)) {
1840 return Err(WillrError::InvalidPeriod {
1841 period: 0,
1842 data_len: len,
1843 });
1844 }
1845 if low.len() != len || close.len() != len || len == 0 {
1846 return Err(WillrError::EmptyInputData);
1847 }
1848
1849 let rows = combos.len();
1850 let cols = len;
1851
1852 let expected = rows.checked_mul(cols).ok_or(WillrError::InvalidRange {
1853 start: sweep.period.0,
1854 end: sweep.period.1,
1855 step: sweep.period.2,
1856 })?;
1857
1858 if out.len() != expected {
1859 return Err(WillrError::OutputLengthMismatch {
1860 expected,
1861 got: out.len(),
1862 });
1863 }
1864
1865 let first_valid = (0..len)
1866 .find(|&i| !(high[i].is_nan() || low[i].is_nan() || close[i].is_nan()))
1867 .ok_or(WillrError::AllValuesNaN)?;
1868
1869 let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
1870 if len - first_valid < max_p {
1871 return Err(WillrError::NotEnoughValidData {
1872 needed: max_p,
1873 valid: len - first_valid,
1874 });
1875 }
1876
1877 let warmup_periods: Vec<usize> = combos
1878 .iter()
1879 .map(|c| first_valid + c.period.unwrap() - 1)
1880 .collect();
1881 let out_uninit = unsafe {
1882 core::slice::from_raw_parts_mut(
1883 out.as_mut_ptr() as *mut core::mem::MaybeUninit<f64>,
1884 out.len(),
1885 )
1886 };
1887 init_matrix_prefixes(out_uninit, cols, &warmup_periods);
1888
1889 let ctx = SharedWillrCtx::new(high, low);
1890
1891 let do_row = |row: usize, out_row: &mut [f64]| {
1892 let period = combos[row].period.unwrap();
1893 match kern {
1894 Kernel::Scalar => willr_row_scalar_with_ctx(close, first_valid, period, out_row, &ctx),
1895 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1896 Kernel::Avx2 => unsafe {
1897 willr_row_avx2_with_ctx(close, first_valid, period, out_row, &ctx)
1898 },
1899 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1900 Kernel::Avx512 => unsafe {
1901 willr_row_avx512_with_ctx(close, first_valid, period, out_row, &ctx)
1902 },
1903 _ => unreachable!(),
1904 }
1905 };
1906
1907 if parallel {
1908 #[cfg(not(target_arch = "wasm32"))]
1909 {
1910 out.par_chunks_mut(cols)
1911 .enumerate()
1912 .for_each(|(row, slice)| do_row(row, slice));
1913 }
1914
1915 #[cfg(target_arch = "wasm32")]
1916 {
1917 for (row, slice) in out.chunks_mut(cols).enumerate() {
1918 do_row(row, slice);
1919 }
1920 }
1921 } else {
1922 for (row, slice) in out.chunks_mut(cols).enumerate() {
1923 do_row(row, slice);
1924 }
1925 }
1926
1927 Ok(combos)
1928}
1929
1930#[cfg(test)]
1931mod tests {
1932 use super::*;
1933 use crate::skip_if_unsupported;
1934 use crate::utilities::data_loader::read_candles_from_csv;
1935 use paste::paste;
1936 #[cfg(feature = "proptest")]
1937 use proptest::prelude::*;
1938
1939 #[test]
1940 fn test_willr_into_matches_api() -> Result<(), Box<dyn Error>> {
1941 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1942 let candles = read_candles_from_csv(file_path)?;
1943
1944 let params = WillrParams::default();
1945 let input = WillrInput::from_candles(&candles, params);
1946
1947 let baseline = willr(&input)?.values;
1948
1949 let mut out = vec![0.0f64; baseline.len()];
1950 #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
1951 {
1952 willr_into(&mut out, &input)?;
1953 }
1954 #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1955 {
1956 willr_into_slice(&mut out, &input, Kernel::Auto)?;
1957 }
1958
1959 assert_eq!(baseline.len(), out.len());
1960 for (a, b) in baseline.iter().zip(out.iter()) {
1961 let eq = (*a == *b) || (a.is_nan() && b.is_nan()) || ((*a - *b).abs() <= 1e-12);
1962 assert!(eq, "mismatch: baseline={} out={}", a, b);
1963 }
1964 Ok(())
1965 }
1966
1967 fn check_willr_partial_params(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1968 skip_if_unsupported!(kernel, test_name);
1969 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1970 let candles = read_candles_from_csv(file_path)?;
1971 let params = WillrParams { period: None };
1972 let input = WillrInput::from_candles(&candles, params);
1973 let output = willr_with_kernel(&input, kernel)?;
1974 assert_eq!(output.values.len(), candles.close.len());
1975 Ok(())
1976 }
1977
1978 fn check_willr_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1979 skip_if_unsupported!(kernel, test_name);
1980 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1981 let candles = read_candles_from_csv(file_path)?;
1982 let params = WillrParams { period: Some(14) };
1983 let input = WillrInput::from_candles(&candles, params);
1984 let output = willr_with_kernel(&input, kernel)?;
1985 let expected_last_five = [
1986 -58.72876391329818,
1987 -61.77504393673111,
1988 -65.93438781487991,
1989 -60.27950310559006,
1990 -65.00449236298293,
1991 ];
1992 let start = output.values.len() - 5;
1993 for (i, &val) in output.values[start..].iter().enumerate() {
1994 assert!(
1995 (val - expected_last_five[i]).abs() < 1e-8,
1996 "[{}] WILLR {:?} mismatch at idx {}: got {}, expected {}",
1997 test_name,
1998 kernel,
1999 i,
2000 val,
2001 expected_last_five[i]
2002 );
2003 }
2004 Ok(())
2005 }
2006
2007 fn check_willr_with_slice_data(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2008 skip_if_unsupported!(kernel, test_name);
2009 let high = [1.0, 2.0, 3.0, 4.0];
2010 let low = [0.5, 1.5, 2.5, 3.5];
2011 let close = [0.75, 1.75, 2.75, 3.75];
2012 let params = WillrParams { period: Some(2) };
2013 let input = WillrInput::from_slices(&high, &low, &close, params);
2014 let output = willr_with_kernel(&input, kernel)?;
2015 assert_eq!(output.values.len(), 4);
2016 Ok(())
2017 }
2018
2019 fn check_willr_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2020 skip_if_unsupported!(kernel, test_name);
2021 let high = [1.0, 2.0];
2022 let low = [0.8, 1.8];
2023 let close = [1.0, 2.0];
2024 let params = WillrParams { period: Some(0) };
2025 let input = WillrInput::from_slices(&high, &low, &close, params);
2026 let res = willr_with_kernel(&input, kernel);
2027 assert!(
2028 res.is_err(),
2029 "[{}] WILLR should fail with zero period",
2030 test_name
2031 );
2032 Ok(())
2033 }
2034
2035 fn check_willr_period_exceeds_length(
2036 test_name: &str,
2037 kernel: Kernel,
2038 ) -> Result<(), Box<dyn Error>> {
2039 skip_if_unsupported!(kernel, test_name);
2040 let high = [1.0, 2.0, 3.0];
2041 let low = [0.5, 1.5, 2.5];
2042 let close = [1.0, 2.0, 3.0];
2043 let params = WillrParams { period: Some(10) };
2044 let input = WillrInput::from_slices(&high, &low, &close, params);
2045 let res = willr_with_kernel(&input, kernel);
2046 assert!(
2047 res.is_err(),
2048 "[{}] WILLR should fail with period exceeding length",
2049 test_name
2050 );
2051 Ok(())
2052 }
2053
2054 fn check_willr_all_nan(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2055 skip_if_unsupported!(kernel, test_name);
2056 let high = [f64::NAN, f64::NAN];
2057 let low = [f64::NAN, f64::NAN];
2058 let close = [f64::NAN, f64::NAN];
2059 let params = WillrParams::default();
2060 let input = WillrInput::from_slices(&high, &low, &close, params);
2061 let res = willr_with_kernel(&input, kernel);
2062 assert!(
2063 res.is_err(),
2064 "[{}] WILLR should fail with all NaN",
2065 test_name
2066 );
2067 Ok(())
2068 }
2069
2070 fn check_willr_not_enough_valid_data(
2071 test_name: &str,
2072 kernel: Kernel,
2073 ) -> Result<(), Box<dyn Error>> {
2074 skip_if_unsupported!(kernel, test_name);
2075 let high = [f64::NAN, 2.0];
2076 let low = [f64::NAN, 1.0];
2077 let close = [f64::NAN, 1.5];
2078 let params = WillrParams { period: Some(3) };
2079 let input = WillrInput::from_slices(&high, &low, &close, params);
2080 let res = willr_with_kernel(&input, kernel);
2081 assert!(
2082 res.is_err(),
2083 "[{}] WILLR should fail with not enough valid data",
2084 test_name
2085 );
2086 Ok(())
2087 }
2088
2089 macro_rules! generate_all_willr_tests {
2090 ($($test_fn:ident),*) => {
2091 paste! {
2092 $(
2093 #[test]
2094 fn [<$test_fn _scalar_f64>]() {
2095 let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
2096 }
2097 )*
2098 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2099 $(
2100 #[test]
2101 fn [<$test_fn _avx2_f64>]() {
2102 let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
2103 }
2104 #[test]
2105 fn [<$test_fn _avx512_f64>]() {
2106 let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
2107 }
2108 )*
2109 }
2110 }
2111 }
2112
2113 #[cfg(debug_assertions)]
2114 fn check_willr_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2115 skip_if_unsupported!(kernel, test_name);
2116
2117 let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2118 let candles = read_candles_from_csv(file_path)?;
2119
2120 let test_params = vec![
2121 WillrParams::default(),
2122 WillrParams { period: Some(2) },
2123 WillrParams { period: Some(5) },
2124 WillrParams { period: Some(10) },
2125 WillrParams { period: Some(20) },
2126 WillrParams { period: Some(30) },
2127 WillrParams { period: Some(50) },
2128 WillrParams { period: Some(100) },
2129 WillrParams { period: Some(200) },
2130 ];
2131
2132 for (param_idx, params) in test_params.iter().enumerate() {
2133 let input = WillrInput::from_candles(&candles, params.clone());
2134 let output = willr_with_kernel(&input, kernel)?;
2135
2136 for (i, &val) in output.values.iter().enumerate() {
2137 if val.is_nan() {
2138 continue;
2139 }
2140
2141 let bits = val.to_bits();
2142
2143 if bits == 0x11111111_11111111 {
2144 panic!(
2145 "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
2146 with params: period={} (param set {})",
2147 test_name,
2148 val,
2149 bits,
2150 i,
2151 params.period.unwrap_or(14),
2152 param_idx
2153 );
2154 }
2155
2156 if bits == 0x22222222_22222222 {
2157 panic!(
2158 "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
2159 with params: period={} (param set {})",
2160 test_name,
2161 val,
2162 bits,
2163 i,
2164 params.period.unwrap_or(14),
2165 param_idx
2166 );
2167 }
2168
2169 if bits == 0x33333333_33333333 {
2170 panic!(
2171 "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
2172 with params: period={} (param set {})",
2173 test_name,
2174 val,
2175 bits,
2176 i,
2177 params.period.unwrap_or(14),
2178 param_idx
2179 );
2180 }
2181 }
2182 }
2183
2184 Ok(())
2185 }
2186
2187 #[cfg(not(debug_assertions))]
2188 fn check_willr_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2189 Ok(())
2190 }
2191
2192 #[cfg(feature = "proptest")]
2193 #[allow(clippy::float_cmp)]
2194 fn check_willr_property(
2195 test_name: &str,
2196 kernel: Kernel,
2197 ) -> Result<(), Box<dyn std::error::Error>> {
2198 use proptest::prelude::*;
2199 skip_if_unsupported!(kernel, test_name);
2200
2201 let strat = (2usize..=50).prop_flat_map(|period| {
2202 (
2203 prop::collection::vec(
2204 (1.0f64..1000.0f64)
2205 .prop_flat_map(|low| (Just(low), 0.0f64..100.0f64, 0.0f64..=1.0f64)),
2206 period..400,
2207 ),
2208 Just(period),
2209 )
2210 });
2211
2212 proptest::test_runner::TestRunner::default().run(&strat, |(price_data, period)| {
2213 let mut high = Vec::with_capacity(price_data.len());
2214 let mut low = Vec::with_capacity(price_data.len());
2215 let mut close = Vec::with_capacity(price_data.len());
2216
2217 for (l, h_offset, c_ratio) in price_data {
2218 let h = l + h_offset;
2219 let c = l + (h - l) * c_ratio;
2220 high.push(h);
2221 low.push(l);
2222 close.push(c);
2223 }
2224
2225 let params = WillrParams {
2226 period: Some(period),
2227 };
2228 let input = WillrInput::from_slices(&high, &low, &close, params.clone());
2229
2230 let WillrOutput { values: out } = willr_with_kernel(&input, kernel).unwrap();
2231
2232 let WillrOutput { values: ref_out } =
2233 willr_with_kernel(&input, Kernel::Scalar).unwrap();
2234
2235 for i in 0..(period - 1) {
2236 prop_assert!(
2237 out[i].is_nan(),
2238 "Warmup period violation at index {}: expected NaN, got {}",
2239 i,
2240 out[i]
2241 );
2242 }
2243
2244 for i in (period - 1)..high.len() {
2245 let y = out[i];
2246 let r = ref_out[i];
2247
2248 if y.is_nan() {
2249 prop_assert!(r.is_nan(), "NaN mismatch at index {}", i);
2250 continue;
2251 }
2252
2253 prop_assert!(
2254 y >= -100.0 - 1e-9 && y <= 0.0 + 1e-9,
2255 "Output bounds violation at index {}: {} not in [-100, 0]",
2256 i,
2257 y
2258 );
2259
2260 let window_start = i + 1 - period;
2261 let window_high = high[window_start..=i]
2262 .iter()
2263 .cloned()
2264 .fold(f64::NEG_INFINITY, f64::max);
2265 let window_low = low[window_start..=i]
2266 .iter()
2267 .cloned()
2268 .fold(f64::INFINITY, f64::min);
2269
2270 if (close[i] - window_high).abs() < 1e-10 {
2271 prop_assert!(
2272 y.abs() < 1e-6,
2273 "When close = highest high, %R should be ~0, got {} at index {}",
2274 y,
2275 i
2276 );
2277 }
2278
2279 if (close[i] - window_low).abs() < 1e-10 {
2280 prop_assert!(
2281 (y + 100.0).abs() < 1e-6,
2282 "When close = lowest low, %R should be ~-100, got {} at index {}",
2283 y,
2284 i
2285 );
2286 }
2287
2288 if period == 1 {
2289 let expected = if high[i] == low[i] {
2290 0.0
2291 } else {
2292 (high[i] - close[i]) / (high[i] - low[i]) * -100.0
2293 };
2294 prop_assert!(
2295 (y - expected).abs() < 1e-9,
2296 "Period=1 mismatch at index {}: expected {}, got {}",
2297 i,
2298 expected,
2299 y
2300 );
2301 }
2302
2303 let window_highs = &high[window_start..=i];
2304 let window_lows = &low[window_start..=i];
2305 let window_closes = &close[window_start..=i];
2306
2307 let all_equal = window_highs.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10)
2308 && window_lows.windows(2).all(|w| (w[0] - w[1]).abs() < 1e-10)
2309 && window_closes
2310 .windows(2)
2311 .all(|w| (w[0] - w[1]).abs() < 1e-10)
2312 && (window_highs[0] - window_lows[0]).abs() < 1e-10;
2313
2314 if all_equal {
2315 prop_assert!(
2316 y.abs() < 1e-6,
2317 "With constant equal prices, %R should be ~0, got {} at index {}",
2318 y,
2319 i
2320 );
2321 }
2322
2323 if !r.is_finite() {
2324 prop_assert!(
2325 y.to_bits() == r.to_bits(),
2326 "NaN/Inf mismatch at index {}: {} vs {}",
2327 i,
2328 y,
2329 r
2330 );
2331 } else {
2332 let ulp_diff: u64 = y.to_bits().abs_diff(r.to_bits());
2333 prop_assert!(
2334 (y - r).abs() <= 1e-9 || ulp_diff <= 4,
2335 "Kernel mismatch at index {}: {} vs {} (diff: {}, ULP: {})",
2336 i,
2337 y,
2338 r,
2339 (y - r).abs(),
2340 ulp_diff
2341 );
2342 }
2343
2344 let range = window_high - window_low;
2345 if range > 1e-10 {
2346 let theoretical_value = (window_high - close[i]) / range * -100.0;
2347
2348 prop_assert!(
2349 (y - theoretical_value).abs() < 1e-6,
2350 "Mathematical formula mismatch at index {}: expected {}, got {}",
2351 i,
2352 theoretical_value,
2353 y
2354 );
2355 }
2356 }
2357
2358 Ok(())
2359 })?;
2360
2361 Ok(())
2362 }
2363
2364 generate_all_willr_tests!(
2365 check_willr_partial_params,
2366 check_willr_accuracy,
2367 check_willr_with_slice_data,
2368 check_willr_zero_period,
2369 check_willr_period_exceeds_length,
2370 check_willr_all_nan,
2371 check_willr_not_enough_valid_data,
2372 check_willr_no_poison
2373 );
2374
2375 #[cfg(feature = "proptest")]
2376 generate_all_willr_tests!(check_willr_property);
2377
2378 fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2379 skip_if_unsupported!(kernel, test);
2380 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2381 let c = read_candles_from_csv(file)?;
2382 let output = WillrBatchBuilder::new().kernel(kernel).apply_candles(&c)?;
2383 let def = WillrParams::default();
2384 let row = output.values_for(&def).expect("default row missing");
2385 assert_eq!(row.len(), c.close.len());
2386 let expected = [
2387 -58.72876391329818,
2388 -61.77504393673111,
2389 -65.93438781487991,
2390 -60.27950310559006,
2391 -65.00449236298293,
2392 ];
2393 let start = row.len() - 5;
2394 for (i, &v) in row[start..].iter().enumerate() {
2395 assert!(
2396 (v - expected[i]).abs() < 1e-8,
2397 "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
2398 );
2399 }
2400 Ok(())
2401 }
2402
2403 macro_rules! gen_batch_tests {
2404 ($fn_name:ident) => {
2405 paste! {
2406 #[test] fn [<$fn_name _scalar>]() {
2407 let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
2408 }
2409 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2410 #[test] fn [<$fn_name _avx2>]() {
2411 let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
2412 }
2413 #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
2414 #[test] fn [<$fn_name _avx512>]() {
2415 let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
2416 }
2417 #[test] fn [<$fn_name _auto_detect>]() {
2418 let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
2419 }
2420 }
2421 };
2422 }
2423 #[cfg(debug_assertions)]
2424 fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
2425 skip_if_unsupported!(kernel, test);
2426
2427 let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2428 let c = read_candles_from_csv(file)?;
2429
2430 let test_configs = vec![
2431 (2, 10, 2),
2432 (5, 25, 5),
2433 (10, 50, 10),
2434 (2, 5, 1),
2435 (14, 14, 0),
2436 (30, 60, 15),
2437 (50, 100, 25),
2438 (100, 200, 50),
2439 ];
2440
2441 for (cfg_idx, &(period_start, period_end, period_step)) in test_configs.iter().enumerate() {
2442 let output = WillrBatchBuilder::new()
2443 .kernel(kernel)
2444 .period_range(period_start, period_end, period_step)
2445 .apply_candles(&c)?;
2446
2447 for (idx, &val) in output.values.iter().enumerate() {
2448 if val.is_nan() {
2449 continue;
2450 }
2451
2452 let bits = val.to_bits();
2453 let row = idx / output.cols;
2454 let col = idx % output.cols;
2455 let combo = &output.combos[row];
2456
2457 if bits == 0x11111111_11111111 {
2458 panic!(
2459 "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
2460 at row {} col {} (flat index {}) with params: period={}",
2461 test,
2462 cfg_idx,
2463 val,
2464 bits,
2465 row,
2466 col,
2467 idx,
2468 combo.period.unwrap_or(14)
2469 );
2470 }
2471
2472 if bits == 0x22222222_22222222 {
2473 panic!(
2474 "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
2475 at row {} col {} (flat index {}) with params: period={}",
2476 test,
2477 cfg_idx,
2478 val,
2479 bits,
2480 row,
2481 col,
2482 idx,
2483 combo.period.unwrap_or(14)
2484 );
2485 }
2486
2487 if bits == 0x33333333_33333333 {
2488 panic!(
2489 "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
2490 at row {} col {} (flat index {}) with params: period={}",
2491 test,
2492 cfg_idx,
2493 val,
2494 bits,
2495 row,
2496 col,
2497 idx,
2498 combo.period.unwrap_or(14)
2499 );
2500 }
2501 }
2502 }
2503
2504 Ok(())
2505 }
2506
2507 #[cfg(not(debug_assertions))]
2508 fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
2509 Ok(())
2510 }
2511
2512 gen_batch_tests!(check_batch_default_row);
2513 gen_batch_tests!(check_batch_no_poison);
2514}
2515
2516#[cfg(all(feature = "python", feature = "cuda"))]
2517use cust::context::Context;
2518#[cfg(feature = "python")]
2519use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
2520#[cfg(feature = "python")]
2521use pyo3::exceptions::PyValueError;
2522#[cfg(feature = "python")]
2523use pyo3::prelude::*;
2524#[cfg(feature = "python")]
2525use pyo3::types::PyDict;
2526
2527#[cfg(feature = "python")]
2528#[pyfunction(name = "willr")]
2529#[pyo3(signature = (high, low, close, period, kernel=None))]
2530pub fn willr_py<'py>(
2531 py: Python<'py>,
2532 high: PyReadonlyArray1<'py, f64>,
2533 low: PyReadonlyArray1<'py, f64>,
2534 close: PyReadonlyArray1<'py, f64>,
2535 period: usize,
2536 kernel: Option<&str>,
2537) -> PyResult<Bound<'py, PyArray1<f64>>> {
2538 let high_slice = high.as_slice()?;
2539 let low_slice = low.as_slice()?;
2540 let close_slice = close.as_slice()?;
2541 let kern = validate_kernel(kernel, false)?;
2542
2543 let params = WillrParams {
2544 period: Some(period),
2545 };
2546 let input = WillrInput::from_slices(high_slice, low_slice, close_slice, params);
2547
2548 let result_vec: Vec<f64> = py
2549 .allow_threads(|| willr_with_kernel(&input, kern).map(|o| o.values))
2550 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2551
2552 Ok(result_vec.into_pyarray(py))
2553}
2554
2555#[cfg(feature = "python")]
2556#[pyclass(name = "WillrStream")]
2557pub struct WillrStreamPy {
2558 stream: WillrStream,
2559}
2560
2561#[cfg(feature = "python")]
2562#[pymethods]
2563impl WillrStreamPy {
2564 #[new]
2565 fn new(period: usize) -> PyResult<Self> {
2566 let params = WillrParams {
2567 period: Some(period),
2568 };
2569 let stream =
2570 WillrStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2571 Ok(WillrStreamPy { stream })
2572 }
2573
2574 fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
2575 self.stream.update(high, low, close)
2576 }
2577}
2578
2579#[cfg(all(feature = "python", feature = "cuda"))]
2580#[pyclass(
2581 module = "ta_indicators.cuda",
2582 name = "WillrDeviceArrayF32",
2583 unsendable
2584)]
2585pub struct WillrDeviceArrayF32Py {
2586 pub(crate) inner: Option<DeviceArrayF32>,
2587 pub(crate) _ctx: Arc<Context>,
2588 pub(crate) device_id: u32,
2589}
2590
2591#[cfg(all(feature = "python", feature = "cuda"))]
2592#[pymethods]
2593impl WillrDeviceArrayF32Py {
2594 #[getter]
2595 fn __cuda_array_interface__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
2596 let inner = self
2597 .inner
2598 .as_ref()
2599 .ok_or_else(|| PyValueError::new_err("buffer already exported via __dlpack__"))?;
2600 let d = PyDict::new(py);
2601 d.set_item("shape", (inner.rows, inner.cols))?;
2602 d.set_item("typestr", "<f4")?;
2603 d.set_item(
2604 "strides",
2605 (
2606 inner.cols * std::mem::size_of::<f32>(),
2607 std::mem::size_of::<f32>(),
2608 ),
2609 )?;
2610 d.set_item("data", (inner.device_ptr() as usize, false))?;
2611
2612 d.set_item("version", 3)?;
2613 Ok(d)
2614 }
2615
2616 fn __dlpack_device__(&self) -> (i32, i32) {
2617 let mut device_ordinal: i32 = self.device_id as i32;
2618 unsafe {
2619 let attr = cust::sys::CUpointer_attribute::CU_POINTER_ATTRIBUTE_DEVICE_ORDINAL;
2620 let mut value = std::mem::MaybeUninit::<i32>::uninit();
2621 let ptr = self
2622 .inner
2623 .as_ref()
2624 .map(|inner| inner.buf.as_device_ptr().as_raw())
2625 .unwrap_or(0);
2626 let err = cust::sys::cuPointerGetAttribute(
2627 value.as_mut_ptr() as *mut std::ffi::c_void,
2628 attr,
2629 ptr,
2630 );
2631 if err == cust::sys::CUresult::CUDA_SUCCESS {
2632 device_ordinal = value.assume_init();
2633 }
2634 }
2635 (2, device_ordinal)
2636 }
2637
2638 #[pyo3(signature=(stream=None, max_version=None, dl_device=None, copy=None))]
2639 fn __dlpack__<'py>(
2640 &mut self,
2641 py: Python<'py>,
2642 stream: Option<pyo3::PyObject>,
2643 max_version: Option<pyo3::PyObject>,
2644 dl_device: Option<pyo3::PyObject>,
2645 copy: Option<pyo3::PyObject>,
2646 ) -> PyResult<pyo3::PyObject> {
2647 let (kdl, alloc_dev) = self.__dlpack_device__();
2648 if let Some(dev_obj) = dl_device.as_ref() {
2649 if let Ok((dev_ty, dev_id)) = dev_obj.extract::<(i32, i32)>(py) {
2650 if dev_ty != kdl || dev_id != alloc_dev {
2651 let wants_copy = copy
2652 .as_ref()
2653 .and_then(|c| c.extract::<bool>(py).ok())
2654 .unwrap_or(false);
2655 if wants_copy {
2656 return Err(PyValueError::new_err(
2657 "device copy not implemented for __dlpack__",
2658 ));
2659 } else {
2660 return Err(PyValueError::new_err("dl_device mismatch for __dlpack__"));
2661 }
2662 }
2663 }
2664 }
2665 let _ = stream;
2666
2667 let inner = self
2668 .inner
2669 .take()
2670 .ok_or_else(|| PyValueError::new_err("__dlpack__ may only be called once"))?;
2671
2672 let rows = inner.rows;
2673 let cols = inner.cols;
2674 let buf = inner.buf;
2675
2676 let max_version_bound = max_version.map(|obj| obj.into_bound(py));
2677
2678 export_f32_cuda_dlpack_2d(py, buf, rows, cols, alloc_dev, max_version_bound)
2679 }
2680}
2681
2682#[cfg(feature = "python")]
2683#[pyfunction(name = "willr_batch")]
2684#[pyo3(signature = (high, low, close, period_range, kernel=None))]
2685pub fn willr_batch_py<'py>(
2686 py: Python<'py>,
2687 high: PyReadonlyArray1<'py, f64>,
2688 low: PyReadonlyArray1<'py, f64>,
2689 close: PyReadonlyArray1<'py, f64>,
2690 period_range: (usize, usize, usize),
2691 kernel: Option<&str>,
2692) -> PyResult<Bound<'py, PyDict>> {
2693 let high_slice = high.as_slice()?;
2694 let low_slice = low.as_slice()?;
2695 let close_slice = close.as_slice()?;
2696
2697 let sweep = WillrBatchRange {
2698 period: period_range,
2699 };
2700
2701 let combos_host = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
2702 let rows = combos_host.len();
2703 let cols = high_slice.len();
2704
2705 let total = rows.checked_mul(cols).ok_or_else(|| {
2706 PyValueError::new_err(format!(
2707 "willr: rows*cols overflow: rows={}, cols={}",
2708 rows, cols
2709 ))
2710 })?;
2711
2712 let out_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
2713 let slice_out = unsafe { out_arr.as_slice_mut()? };
2714
2715 let kern = validate_kernel(kernel, true)?;
2716
2717 let combos = py
2718 .allow_threads(|| {
2719 let kernel = match kern {
2720 Kernel::Auto => match detect_best_batch_kernel() {
2721 Kernel::Avx512Batch => Kernel::Avx2Batch,
2722 other => other,
2723 },
2724 k => k,
2725 };
2726 let simd = match kernel {
2727 Kernel::Avx512Batch => Kernel::Avx512,
2728 Kernel::Avx2Batch => Kernel::Avx2,
2729 Kernel::ScalarBatch => Kernel::Scalar,
2730 _ => unreachable!(),
2731 };
2732 willr_batch_inner_into(
2733 high_slice,
2734 low_slice,
2735 close_slice,
2736 &sweep,
2737 simd,
2738 true,
2739 slice_out,
2740 )
2741 })
2742 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2743
2744 let dict = PyDict::new(py);
2745 dict.set_item("values", out_arr.reshape((rows, cols))?)?;
2746 dict.set_item(
2747 "periods",
2748 combos
2749 .iter()
2750 .map(|p| p.period.unwrap() as u64)
2751 .collect::<Vec<_>>()
2752 .into_pyarray(py),
2753 )?;
2754
2755 dict.set_item("rows", rows)?;
2756 dict.set_item("cols", cols)?;
2757
2758 Ok(dict)
2759}
2760
2761#[cfg(all(feature = "python", feature = "cuda"))]
2762#[pyfunction(name = "willr_cuda_batch_dev")]
2763#[pyo3(signature = (high, low, close, period_range, device_id=0))]
2764pub fn willr_cuda_batch_dev_py(
2765 py: Python<'_>,
2766 high: PyReadonlyArray1<'_, f32>,
2767 low: PyReadonlyArray1<'_, f32>,
2768 close: PyReadonlyArray1<'_, f32>,
2769 period_range: (usize, usize, usize),
2770 device_id: usize,
2771) -> PyResult<WillrDeviceArrayF32Py> {
2772 use crate::cuda::cuda_available;
2773
2774 if !cuda_available() {
2775 return Err(PyValueError::new_err("CUDA not available"));
2776 }
2777
2778 let high_slice = high.as_slice()?;
2779 let low_slice = low.as_slice()?;
2780 let close_slice = close.as_slice()?;
2781
2782 if high_slice.len() != low_slice.len() || high_slice.len() != close_slice.len() {
2783 return Err(PyValueError::new_err("mismatched input lengths"));
2784 }
2785
2786 let sweep = WillrBatchRange {
2787 period: period_range,
2788 };
2789
2790 let (inner, ctx, dev_id_u32) = py.allow_threads(|| {
2791 let cuda = CudaWillr::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2792 let ctx = cuda.context();
2793 let dev_id_u32 = cuda.device_id();
2794 let inner = cuda
2795 .willr_batch_dev(high_slice, low_slice, close_slice, &sweep)
2796 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2797 Ok::<_, pyo3::PyErr>((inner, ctx, dev_id_u32))
2798 })?;
2799
2800 Ok(WillrDeviceArrayF32Py {
2801 inner: Some(inner),
2802 _ctx: ctx,
2803 device_id: dev_id_u32,
2804 })
2805}
2806
2807#[cfg(all(feature = "python", feature = "cuda"))]
2808#[pyfunction(name = "willr_cuda_many_series_one_param_dev")]
2809#[pyo3(signature = (high_tm, low_tm, close_tm, cols, rows, period, device_id=0))]
2810pub fn willr_cuda_many_series_one_param_dev_py(
2811 py: Python<'_>,
2812 high_tm: PyReadonlyArray1<'_, f32>,
2813 low_tm: PyReadonlyArray1<'_, f32>,
2814 close_tm: PyReadonlyArray1<'_, f32>,
2815 cols: usize,
2816 rows: usize,
2817 period: usize,
2818 device_id: usize,
2819) -> PyResult<WillrDeviceArrayF32Py> {
2820 use crate::cuda::cuda_available;
2821
2822 if !cuda_available() {
2823 return Err(PyValueError::new_err("CUDA not available"));
2824 }
2825
2826 let high_slice = high_tm.as_slice()?;
2827 let low_slice = low_tm.as_slice()?;
2828 let close_slice = close_tm.as_slice()?;
2829
2830 let (inner, ctx, dev_id_u32) = py.allow_threads(|| {
2831 let cuda = CudaWillr::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2832 let ctx = cuda.context();
2833 let dev_id_u32 = cuda.device_id();
2834 let inner = cuda
2835 .willr_many_series_one_param_time_major_dev(
2836 high_slice,
2837 low_slice,
2838 close_slice,
2839 cols,
2840 rows,
2841 period,
2842 )
2843 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2844 Ok::<_, pyo3::PyErr>((inner, ctx, dev_id_u32))
2845 })?;
2846
2847 Ok(WillrDeviceArrayF32Py {
2848 inner: Some(inner),
2849 _ctx: ctx,
2850 device_id: dev_id_u32,
2851 })
2852}
2853
2854#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2855use serde::{Deserialize, Serialize};
2856#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2857use wasm_bindgen::prelude::*;
2858
2859#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2860#[wasm_bindgen]
2861pub fn willr_js(
2862 high: &[f64],
2863 low: &[f64],
2864 close: &[f64],
2865 period: usize,
2866) -> Result<Vec<f64>, JsValue> {
2867 if high.len() == 0 || low.len() != high.len() || close.len() != high.len() {
2868 return Err(JsValue::from_str("mismatched input lengths"));
2869 }
2870 let params = WillrParams {
2871 period: Some(period),
2872 };
2873 let input = WillrInput::from_slices(high, low, close, params);
2874 let mut out = vec![0.0; high.len()];
2875 willr_into_slice(&mut out, &input, detect_best_kernel())
2876 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2877 Ok(out)
2878}
2879
2880#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2881#[derive(Serialize, Deserialize)]
2882pub struct WillrBatchConfig {
2883 pub period_range: (usize, usize, usize),
2884}
2885
2886#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2887#[derive(Serialize, Deserialize)]
2888pub struct WillrBatchJsOutput {
2889 pub values: Vec<f64>,
2890 pub combos: Vec<WillrParams>,
2891 pub rows: usize,
2892 pub cols: usize,
2893}
2894
2895#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2896#[wasm_bindgen(js_name = willr_batch)]
2897pub fn willr_batch_unified_js(
2898 high: &[f64],
2899 low: &[f64],
2900 close: &[f64],
2901 config: JsValue,
2902) -> Result<JsValue, JsValue> {
2903 let cfg: WillrBatchConfig = serde_wasm_bindgen::from_value(config)
2904 .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
2905
2906 let sweep = WillrBatchRange {
2907 period: cfg.period_range,
2908 };
2909 let out = willr_batch_inner(high, low, close, &sweep, detect_best_kernel(), false)
2910 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2911
2912 let js_out = WillrBatchJsOutput {
2913 values: out.values,
2914 combos: out.combos,
2915 rows: out.rows,
2916 cols: out.cols,
2917 };
2918 serde_wasm_bindgen::to_value(&js_out)
2919 .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
2920}
2921
2922#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2923#[wasm_bindgen]
2924pub fn willr_alloc(len: usize) -> *mut f64 {
2925 let mut v = Vec::<f64>::with_capacity(len);
2926 let p = v.as_mut_ptr();
2927 core::mem::forget(v);
2928 p
2929}
2930
2931#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2932#[wasm_bindgen]
2933pub fn willr_free(ptr: *mut f64, len: usize) {
2934 unsafe {
2935 let _ = Vec::from_raw_parts(ptr, len, len);
2936 }
2937}
2938
2939#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2940#[wasm_bindgen]
2941pub fn willr_into(
2942 high_ptr: *const f64,
2943 low_ptr: *const f64,
2944 close_ptr: *const f64,
2945 out_ptr: *mut f64,
2946 len: usize,
2947 period: usize,
2948) -> Result<(), JsValue> {
2949 if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
2950 return Err(JsValue::from_str("null pointer passed to willr_into"));
2951 }
2952 unsafe {
2953 if out_ptr == high_ptr as *mut f64
2954 || out_ptr == low_ptr as *mut f64
2955 || out_ptr == close_ptr as *mut f64
2956 {
2957 let mut tmp = vec![0.0f64; len];
2958 {
2959 let high = core::slice::from_raw_parts(high_ptr, len);
2960 let low = core::slice::from_raw_parts(low_ptr, len);
2961 let close = core::slice::from_raw_parts(close_ptr, len);
2962 let params = WillrParams {
2963 period: Some(period),
2964 };
2965 let input = WillrInput::from_slices(high, low, close, params);
2966 willr_into_slice(&mut tmp, &input, detect_best_kernel())
2967 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2968 }
2969 let out = core::slice::from_raw_parts_mut(out_ptr, len);
2970 out.copy_from_slice(&tmp);
2971 Ok(())
2972 } else {
2973 let high = core::slice::from_raw_parts(high_ptr, len);
2974 let low = core::slice::from_raw_parts(low_ptr, len);
2975 let close = core::slice::from_raw_parts(close_ptr, len);
2976 let out = core::slice::from_raw_parts_mut(out_ptr, len);
2977 let params = WillrParams {
2978 period: Some(period),
2979 };
2980 let input = WillrInput::from_slices(high, low, close, params);
2981 willr_into_slice(out, &input, detect_best_kernel())
2982 .map_err(|e| JsValue::from_str(&e.to_string()))
2983 }
2984 }
2985}
2986
2987#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2988#[wasm_bindgen(js_name = willr_batch_into)]
2989pub fn willr_batch_into_js(
2990 high_ptr: *const f64,
2991 low_ptr: *const f64,
2992 close_ptr: *const f64,
2993 out_ptr: *mut f64,
2994 len: usize,
2995 period_start: usize,
2996 period_end: usize,
2997 period_step: usize,
2998) -> Result<usize, JsValue> {
2999 if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
3000 return Err(JsValue::from_str("null pointer passed to willr_batch_into"));
3001 }
3002 unsafe {
3003 let high = core::slice::from_raw_parts(high_ptr, len);
3004 let low = core::slice::from_raw_parts(low_ptr, len);
3005 let close = core::slice::from_raw_parts(close_ptr, len);
3006
3007 let sweep = WillrBatchRange {
3008 period: (period_start, period_end, period_step),
3009 };
3010 let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
3011 let rows = combos.len();
3012 let cols = len;
3013
3014 let total = rows.checked_mul(cols).ok_or_else(|| {
3015 JsValue::from_str(&format!(
3016 "willr: rows*cols overflow: rows={}, cols={}",
3017 rows, cols
3018 ))
3019 })?;
3020
3021 let out = core::slice::from_raw_parts_mut(out_ptr, total);
3022 willr_batch_inner_into(high, low, close, &sweep, detect_best_kernel(), false, out)
3023 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3024 Ok(rows)
3025 }
3026}