Skip to main content

vector_ta/indicators/
willr.rs

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}