Skip to main content

vector_ta/indicators/
pivot.rs

1use crate::utilities::data_loader::{source_type, Candles};
2use crate::utilities::enums::Kernel;
3use crate::utilities::helpers::{
4    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
5    make_uninit_matrix,
6};
7use aligned_vec::{AVec, CACHELINE_ALIGN};
8#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
9use core::arch::x86_64::*;
10#[cfg(not(target_arch = "wasm32"))]
11use rayon::prelude::*;
12use std::convert::AsRef;
13use thiserror::Error;
14
15#[cfg(feature = "python")]
16use crate::utilities::kernel_validation::validate_kernel;
17#[cfg(feature = "python")]
18use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
19#[cfg(feature = "python")]
20use pyo3::exceptions::PyValueError;
21#[cfg(feature = "python")]
22use pyo3::prelude::*;
23#[cfg(feature = "python")]
24use pyo3::types::PyDict;
25
26#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
27use serde::{Deserialize, Serialize};
28#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
29use wasm_bindgen::prelude::*;
30
31const N_LEVELS: usize = 9;
32
33#[inline(always)]
34fn first_valid_ohlc(high: &[f64], low: &[f64], close: &[f64]) -> Option<usize> {
35    let len = high.len();
36    for i in 0..len {
37        if !(high[i].is_nan() || low[i].is_nan() || close[i].is_nan()) {
38            return Some(i);
39        }
40    }
41    None
42}
43
44#[inline(always)]
45fn pivot_compute_into(
46    high: &[f64],
47    low: &[f64],
48    close: &[f64],
49    open: &[f64],
50    mode: usize,
51    first: usize,
52    k: Kernel,
53    r4: &mut [f64],
54    r3: &mut [f64],
55    r2: &mut [f64],
56    r1: &mut [f64],
57    pp: &mut [f64],
58    s1: &mut [f64],
59    s2: &mut [f64],
60    s3: &mut [f64],
61    s4: &mut [f64],
62) {
63    unsafe {
64        match k {
65            Kernel::Scalar | Kernel::ScalarBatch => pivot_scalar(
66                high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
67            ),
68            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
69            Kernel::Avx2 | Kernel::Avx2Batch => pivot_avx2(
70                high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
71            ),
72            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
73            Kernel::Avx512 | Kernel::Avx512Batch => pivot_avx512(
74                high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
75            ),
76            _ => unreachable!(),
77        }
78    }
79}
80
81#[derive(Debug, Clone)]
82pub enum PivotData<'a> {
83    Candles {
84        candles: &'a Candles,
85    },
86    Slices {
87        high: &'a [f64],
88        low: &'a [f64],
89        close: &'a [f64],
90        open: &'a [f64],
91    },
92}
93
94#[derive(Debug, Clone)]
95pub struct PivotParams {
96    pub mode: Option<usize>,
97}
98impl Default for PivotParams {
99    fn default() -> Self {
100        Self { mode: Some(3) }
101    }
102}
103
104#[derive(Debug, Clone)]
105pub struct PivotInput<'a> {
106    pub data: PivotData<'a>,
107    pub params: PivotParams,
108}
109impl<'a> PivotInput<'a> {
110    #[inline]
111    pub fn from_candles(candles: &'a Candles, params: PivotParams) -> Self {
112        Self {
113            data: PivotData::Candles { candles },
114            params,
115        }
116    }
117    #[inline]
118    pub fn from_slices(
119        high: &'a [f64],
120        low: &'a [f64],
121        close: &'a [f64],
122        open: &'a [f64],
123        params: PivotParams,
124    ) -> Self {
125        Self {
126            data: PivotData::Slices {
127                high,
128                low,
129                close,
130                open,
131            },
132            params,
133        }
134    }
135    #[inline]
136    pub fn with_default_candles(candles: &'a Candles) -> Self {
137        Self::from_candles(candles, PivotParams::default())
138    }
139    #[inline]
140    pub fn get_mode(&self) -> usize {
141        self.params
142            .mode
143            .unwrap_or_else(|| PivotParams::default().mode.unwrap())
144    }
145}
146impl<'a> AsRef<PivotData<'a>> for PivotInput<'a> {
147    fn as_ref(&self) -> &PivotData<'a> {
148        &self.data
149    }
150}
151
152#[derive(Debug, Clone)]
153pub struct PivotOutput {
154    pub r4: Vec<f64>,
155    pub r3: Vec<f64>,
156    pub r2: Vec<f64>,
157    pub r1: Vec<f64>,
158    pub pp: Vec<f64>,
159    pub s1: Vec<f64>,
160    pub s2: Vec<f64>,
161    pub s3: Vec<f64>,
162    pub s4: Vec<f64>,
163}
164
165#[derive(Debug, Error)]
166pub enum PivotError {
167    #[error("pivot: One or more required fields is empty.")]
168    EmptyData,
169    #[error("pivot: All values are NaN.")]
170    AllValuesNaN,
171    #[error("pivot: Not enough valid data after the first valid index.")]
172    NotEnoughValidData,
173    #[error("pivot: Output slice length mismatch (expected {expected}, got {got}).")]
174    OutputLengthMismatch { expected: usize, got: usize },
175    #[error("pivot: Invalid range: start={start}, end={end}, step={step}.")]
176    InvalidRange {
177        start: usize,
178        end: usize,
179        step: usize,
180    },
181    #[error("pivot: Invalid kernel for batch path: {0:?}.")]
182    InvalidKernelForBatch(Kernel),
183}
184
185#[derive(Copy, Clone, Debug)]
186pub struct PivotBuilder {
187    mode: Option<usize>,
188    kernel: Kernel,
189}
190impl Default for PivotBuilder {
191    fn default() -> Self {
192        Self {
193            mode: None,
194            kernel: Kernel::Auto,
195        }
196    }
197}
198impl PivotBuilder {
199    #[inline(always)]
200    pub fn new() -> Self {
201        Self::default()
202    }
203    #[inline(always)]
204    pub fn mode(mut self, mode: usize) -> Self {
205        self.mode = Some(mode);
206        self
207    }
208    #[inline(always)]
209    pub fn kernel(mut self, k: Kernel) -> Self {
210        self.kernel = k;
211        self
212    }
213    #[inline(always)]
214    pub fn apply(self, candles: &Candles) -> Result<PivotOutput, PivotError> {
215        let params = PivotParams { mode: self.mode };
216        let input = PivotInput::from_candles(candles, params);
217        pivot_with_kernel(&input, self.kernel)
218    }
219    #[inline(always)]
220    pub fn apply_slices(
221        self,
222        high: &[f64],
223        low: &[f64],
224        close: &[f64],
225        open: &[f64],
226    ) -> Result<PivotOutput, PivotError> {
227        let params = PivotParams { mode: self.mode };
228        let input = PivotInput::from_slices(high, low, close, open, params);
229        pivot_with_kernel(&input, self.kernel)
230    }
231}
232
233#[inline]
234pub fn pivot(input: &PivotInput) -> Result<PivotOutput, PivotError> {
235    pivot_with_kernel(input, Kernel::Auto)
236}
237
238pub fn pivot_with_kernel(input: &PivotInput, kernel: Kernel) -> Result<PivotOutput, PivotError> {
239    let (high, low, close, open) = match &input.data {
240        PivotData::Candles { candles } => {
241            let high = source_type(candles, "high");
242            let low = source_type(candles, "low");
243            let close = source_type(candles, "close");
244            let open = source_type(candles, "open");
245            (high, low, close, open)
246        }
247        PivotData::Slices {
248            high,
249            low,
250            close,
251            open,
252        } => (*high, *low, *close, *open),
253    };
254    let len = high.len();
255    if high.is_empty() || low.is_empty() || close.is_empty() {
256        return Err(PivotError::EmptyData);
257    }
258    if low.len() != len || close.len() != len || open.len() != len {
259        return Err(PivotError::EmptyData);
260    }
261    let mode = input.get_mode();
262
263    let mut first_valid_idx = None;
264    for i in 0..len {
265        let h = high[i];
266        let l = low[i];
267        let c = close[i];
268        if !(h.is_nan() || l.is_nan() || c.is_nan()) {
269            first_valid_idx = Some(i);
270            break;
271        }
272    }
273    let first_valid_idx = match first_valid_idx {
274        Some(idx) => idx,
275        None => return Err(PivotError::AllValuesNaN),
276    };
277    if first_valid_idx >= len {
278        return Err(PivotError::NotEnoughValidData);
279    }
280
281    let mut r4 = alloc_with_nan_prefix(len, first_valid_idx);
282    let mut r3 = alloc_with_nan_prefix(len, first_valid_idx);
283    let mut r2 = alloc_with_nan_prefix(len, first_valid_idx);
284    let mut r1 = alloc_with_nan_prefix(len, first_valid_idx);
285    let mut pp = alloc_with_nan_prefix(len, first_valid_idx);
286    let mut s1 = alloc_with_nan_prefix(len, first_valid_idx);
287    let mut s2 = alloc_with_nan_prefix(len, first_valid_idx);
288    let mut s3 = alloc_with_nan_prefix(len, first_valid_idx);
289    let mut s4 = alloc_with_nan_prefix(len, first_valid_idx);
290
291    let chosen = match kernel {
292        Kernel::Auto => detect_best_kernel(),
293        other => other,
294    };
295    pivot_compute_into(
296        high,
297        low,
298        close,
299        open,
300        mode,
301        first_valid_idx,
302        chosen,
303        &mut r4,
304        &mut r3,
305        &mut r2,
306        &mut r1,
307        &mut pp,
308        &mut s1,
309        &mut s2,
310        &mut s3,
311        &mut s4,
312    );
313    Ok(PivotOutput {
314        r4,
315        r3,
316        r2,
317        r1,
318        pp,
319        s1,
320        s2,
321        s3,
322        s4,
323    })
324}
325
326#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
327#[inline]
328pub fn pivot_into(
329    input: &PivotInput,
330    r4: &mut [f64],
331    r3: &mut [f64],
332    r2: &mut [f64],
333    r1: &mut [f64],
334    pp: &mut [f64],
335    s1: &mut [f64],
336    s2: &mut [f64],
337    s3: &mut [f64],
338    s4: &mut [f64],
339) -> Result<(), PivotError> {
340    let (high, low, close, open) = match &input.data {
341        PivotData::Candles { candles } => {
342            let high = source_type(candles, "high");
343            let low = source_type(candles, "low");
344            let close = source_type(candles, "close");
345            let open = source_type(candles, "open");
346            (high, low, close, open)
347        }
348        PivotData::Slices {
349            high,
350            low,
351            close,
352            open,
353        } => (*high, *low, *close, *open),
354    };
355
356    let len = high.len();
357    if high.is_empty() || low.is_empty() || close.is_empty() {
358        return Err(PivotError::EmptyData);
359    }
360    if low.len() != len || close.len() != len || open.len() != len {
361        return Err(PivotError::EmptyData);
362    }
363    let expected = len;
364    let first_mismatch = [
365        r4.len(),
366        r3.len(),
367        r2.len(),
368        r1.len(),
369        pp.len(),
370        s1.len(),
371        s2.len(),
372        s3.len(),
373        s4.len(),
374    ]
375    .into_iter()
376    .find(|&got| got != expected);
377    if let Some(got) = first_mismatch {
378        return Err(PivotError::OutputLengthMismatch { expected, got });
379    }
380
381    let mode = input.get_mode();
382
383    let first_valid_idx = first_valid_ohlc(high, low, close).ok_or(PivotError::AllValuesNaN)?;
384    if first_valid_idx >= len {
385        return Err(PivotError::NotEnoughValidData);
386    }
387
388    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
389    for i in 0..first_valid_idx {
390        r4[i] = qnan;
391        r3[i] = qnan;
392        r2[i] = qnan;
393        r1[i] = qnan;
394        pp[i] = qnan;
395        s1[i] = qnan;
396        s2[i] = qnan;
397        s3[i] = qnan;
398        s4[i] = qnan;
399    }
400
401    let chosen = detect_best_kernel();
402    pivot_compute_into(
403        high,
404        low,
405        close,
406        open,
407        mode,
408        first_valid_idx,
409        chosen,
410        r4,
411        r3,
412        r2,
413        r1,
414        pp,
415        s1,
416        s2,
417        s3,
418        s4,
419    );
420
421    Ok(())
422}
423
424#[inline]
425pub fn pivot_into_slices(
426    r4: &mut [f64],
427    r3: &mut [f64],
428    r2: &mut [f64],
429    r1: &mut [f64],
430    pp: &mut [f64],
431    s1: &mut [f64],
432    s2: &mut [f64],
433    s3: &mut [f64],
434    s4: &mut [f64],
435    input: &PivotInput,
436    kern: Kernel,
437) -> Result<(), PivotError> {
438    let (high, low, close, open) = match &input.data {
439        PivotData::Candles { candles } => {
440            let high = source_type(candles, "high");
441            let low = source_type(candles, "low");
442            let close = source_type(candles, "close");
443            let open = source_type(candles, "open");
444            (high, low, close, open)
445        }
446        PivotData::Slices {
447            high,
448            low,
449            close,
450            open,
451        } => (*high, *low, *close, *open),
452    };
453
454    let len = high.len();
455    if high.is_empty() || low.is_empty() || close.is_empty() {
456        return Err(PivotError::EmptyData);
457    }
458    if low.len() != len || close.len() != len || open.len() != len {
459        return Err(PivotError::EmptyData);
460    }
461    let expected = len;
462    let first_mismatch = [
463        r4.len(),
464        r3.len(),
465        r2.len(),
466        r1.len(),
467        pp.len(),
468        s1.len(),
469        s2.len(),
470        s3.len(),
471        s4.len(),
472    ]
473    .into_iter()
474    .find(|&got| got != expected);
475    if let Some(got) = first_mismatch {
476        return Err(PivotError::OutputLengthMismatch { expected, got });
477    }
478
479    let mode = input.get_mode();
480
481    let mut first_valid_idx = None;
482    for i in 0..len {
483        let h = high[i];
484        let l = low[i];
485        let c = close[i];
486        if !(h.is_nan() || l.is_nan() || c.is_nan()) {
487            first_valid_idx = Some(i);
488            break;
489        }
490    }
491    let first_valid_idx = match first_valid_idx {
492        Some(idx) => idx,
493        None => return Err(PivotError::AllValuesNaN),
494    };
495    if first_valid_idx >= len {
496        return Err(PivotError::NotEnoughValidData);
497    }
498
499    let chosen = match kern {
500        Kernel::Auto => detect_best_kernel(),
501        other => other,
502    };
503
504    pivot_compute_into(
505        high,
506        low,
507        close,
508        open,
509        mode,
510        first_valid_idx,
511        chosen,
512        r4,
513        r3,
514        r2,
515        r1,
516        pp,
517        s1,
518        s2,
519        s3,
520        s4,
521    );
522
523    for i in 0..first_valid_idx {
524        r4[i] = f64::NAN;
525        r3[i] = f64::NAN;
526        r2[i] = f64::NAN;
527        r1[i] = f64::NAN;
528        pp[i] = f64::NAN;
529        s1[i] = f64::NAN;
530        s2[i] = f64::NAN;
531        s3[i] = f64::NAN;
532        s4[i] = f64::NAN;
533    }
534
535    Ok(())
536}
537
538#[inline]
539pub unsafe fn pivot_scalar(
540    high: &[f64],
541    low: &[f64],
542    close: &[f64],
543    open: &[f64],
544    mode: usize,
545    first: usize,
546    r4: &mut [f64],
547    r3: &mut [f64],
548    r2: &mut [f64],
549    r1: &mut [f64],
550    pp: &mut [f64],
551    s1: &mut [f64],
552    s2: &mut [f64],
553    s3: &mut [f64],
554    s4: &mut [f64],
555) {
556    let len = high.len();
557    if first >= len {
558        return;
559    }
560
561    let nan = f64::NAN;
562
563    match mode {
564        0 => {
565            for i in first..len {
566                let h = high[i];
567                let l = low[i];
568                let c = close[i];
569                if h.is_nan() || l.is_nan() || c.is_nan() {
570                    r4[i] = nan;
571                    r3[i] = nan;
572                    r2[i] = nan;
573                    r1[i] = nan;
574                    pp[i] = nan;
575                    s1[i] = nan;
576                    s2[i] = nan;
577                    s3[i] = nan;
578                    s4[i] = nan;
579                    continue;
580                }
581                let d = h - l;
582                let p = (h + l + c) * (1.0 / 3.0);
583                let t2 = p + p;
584                pp[i] = p;
585                r1[i] = t2 - l;
586                r2[i] = p + d;
587                s1[i] = t2 - h;
588                s2[i] = p - d;
589                r3[i] = nan;
590                r4[i] = nan;
591                s3[i] = nan;
592                s4[i] = nan;
593            }
594        }
595
596        1 => {
597            for i in first..len {
598                let h = high[i];
599                let l = low[i];
600                let c = close[i];
601                if h.is_nan() || l.is_nan() || c.is_nan() {
602                    r4[i] = nan;
603                    r3[i] = nan;
604                    r2[i] = nan;
605                    r1[i] = nan;
606                    pp[i] = nan;
607                    s1[i] = nan;
608                    s2[i] = nan;
609                    s3[i] = nan;
610                    s4[i] = nan;
611                    continue;
612                }
613                let d = h - l;
614                let p = (h + l + c) * (1.0 / 3.0);
615                let d38 = d * 0.382_f64;
616                let d62 = d * 0.618_f64;
617                pp[i] = p;
618                r1[i] = p + d38;
619                r2[i] = p + d62;
620                r3[i] = p + d;
621                s1[i] = p - d38;
622                s2[i] = p - d62;
623                s3[i] = p - d;
624                r4[i] = nan;
625                s4[i] = nan;
626            }
627        }
628
629        2 => {
630            for i in first..len {
631                let h = high[i];
632                let l = low[i];
633                let c = close[i];
634                let o = open[i];
635                if h.is_nan() || l.is_nan() || c.is_nan() {
636                    r4[i] = nan;
637                    r3[i] = nan;
638                    r2[i] = nan;
639                    r1[i] = nan;
640                    pp[i] = nan;
641                    s1[i] = nan;
642                    s2[i] = nan;
643                    s3[i] = nan;
644                    s4[i] = nan;
645                    continue;
646                }
647                let p = if c < o {
648                    (h + (l + l) + c) * 0.25
649                } else if c > o {
650                    ((h + h) + l + c) * 0.25
651                } else {
652                    (h + l + (c + c)) * 0.25
653                };
654                pp[i] = p;
655                let num = if c < o {
656                    (h + (l + l) + c) * 0.5
657                } else if c > o {
658                    ((h + h) + l + c) * 0.5
659                } else {
660                    (h + l + (c + c)) * 0.5
661                };
662                r1[i] = num - l;
663                s1[i] = num - h;
664                r2[i] = nan;
665                r3[i] = nan;
666                r4[i] = nan;
667                s2[i] = nan;
668                s3[i] = nan;
669                s4[i] = nan;
670            }
671        }
672
673        3 => {
674            const C1: f64 = 0.0916_f64;
675            const C2: f64 = 0.183_f64;
676            const C3: f64 = 0.275_f64;
677            const C4: f64 = 0.55_f64;
678            for i in first..len {
679                let h = high[i];
680                let l = low[i];
681                let c = close[i];
682                if h.is_nan() || l.is_nan() || c.is_nan() {
683                    r4[i] = nan;
684                    r3[i] = nan;
685                    r2[i] = nan;
686                    r1[i] = nan;
687                    pp[i] = nan;
688                    s1[i] = nan;
689                    s2[i] = nan;
690                    s3[i] = nan;
691                    s4[i] = nan;
692                    continue;
693                }
694                let d = h - l;
695                let p = (h + l + c) * (1.0 / 3.0);
696                pp[i] = p;
697                let d1 = d * C1;
698                let d2 = d * C2;
699                let d3 = d * C3;
700                let d4 = d * C4;
701                r1[i] = d1 + c;
702                r2[i] = d2 + c;
703                r3[i] = d3 + c;
704                r4[i] = d4 + c;
705                s1[i] = c - d1;
706                s2[i] = c - d2;
707                s3[i] = c - d3;
708                s4[i] = c - d4;
709            }
710        }
711
712        4 => {
713            for i in first..len {
714                let h = high[i];
715                let l = low[i];
716                let c = close[i];
717                let o = open[i];
718                if h.is_nan() || l.is_nan() || c.is_nan() {
719                    r4[i] = nan;
720                    r3[i] = nan;
721                    r2[i] = nan;
722                    r1[i] = nan;
723                    pp[i] = nan;
724                    s1[i] = nan;
725                    s2[i] = nan;
726                    s3[i] = nan;
727                    s4[i] = nan;
728                    continue;
729                }
730                let d = h - l;
731                let p = (h + l + (o + o)) * 0.25;
732                pp[i] = p;
733                let t2p = p + p;
734                let t2l = l + l;
735                let t2h = h + h;
736                let r3v = (t2p - t2l) + h;
737                r3[i] = r3v;
738                r4[i] = r3v + d;
739                r2[i] = p + d;
740                r1[i] = t2p - l;
741                s1[i] = t2p - h;
742                s2[i] = p - d;
743                let s3v = (l + t2p) - t2h;
744                s3[i] = s3v;
745                s4[i] = s3v - d;
746            }
747        }
748
749        _ => {}
750    }
751}
752
753#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
754#[inline]
755#[target_feature(enable = "avx2,fma")]
756pub unsafe fn pivot_avx2(
757    high: &[f64],
758    low: &[f64],
759    close: &[f64],
760    open: &[f64],
761    mode: usize,
762    first: usize,
763    r4: &mut [f64],
764    r3: &mut [f64],
765    r2: &mut [f64],
766    r1: &mut [f64],
767    pp: &mut [f64],
768    s1: &mut [f64],
769    s2: &mut [f64],
770    s3: &mut [f64],
771    s4: &mut [f64],
772) {
773    use core::arch::x86_64::*;
774
775    let len = high.len();
776    if first >= len {
777        return;
778    }
779
780    let hp = high.as_ptr();
781    let lp = low.as_ptr();
782    let cp = close.as_ptr();
783    let op = open.as_ptr();
784
785    let r4p = r4.as_mut_ptr();
786    let r3p = r3.as_mut_ptr();
787    let r2p = r2.as_mut_ptr();
788    let r1p = r1.as_mut_ptr();
789    let ppp = pp.as_mut_ptr();
790    let s1p = s1.as_mut_ptr();
791    let s2p = s2.as_mut_ptr();
792    let s3p = s3.as_mut_ptr();
793    let s4p = s4.as_mut_ptr();
794
795    let v_nan = _mm256_set1_pd(f64::NAN);
796    let v_third = _mm256_set1_pd(1.0 / 3.0);
797    let v_quart = _mm256_set1_pd(0.25);
798    let v_half = _mm256_set1_pd(0.5);
799    let v_one = _mm256_set1_pd(1.0);
800    let v_c0916 = _mm256_set1_pd(0.0916);
801    let v_c0183 = _mm256_set1_pd(0.183);
802    let v_c0275 = _mm256_set1_pd(0.275);
803    let v_c0550 = _mm256_set1_pd(0.55);
804    let v_c0382 = _mm256_set1_pd(0.382);
805    let v_c0618 = _mm256_set1_pd(0.618);
806    let v_neg1 = _mm256_set1_pd(-1.0);
807    let v_n0382 = _mm256_set1_pd(-0.382);
808    let v_n0618 = _mm256_set1_pd(-0.618);
809
810    let mut i = first;
811    let end4 = first + ((len - first) & !3);
812
813    #[inline(always)]
814    unsafe fn valid_mask_avx2(h: __m256d, l: __m256d, c: __m256d) -> __m256d {
815        let ord_h = _mm256_cmp_pd(h, h, _CMP_ORD_Q);
816        let ord_l = _mm256_cmp_pd(l, l, _CMP_ORD_Q);
817        let ord_c = _mm256_cmp_pd(c, c, _CMP_ORD_Q);
818        _mm256_and_pd(_mm256_and_pd(ord_h, ord_l), ord_c)
819    }
820
821    #[inline(always)]
822    unsafe fn blendv(a: __m256d, b: __m256d, mask: __m256d) -> __m256d {
823        _mm256_blendv_pd(a, b, mask)
824    }
825
826    match mode {
827        0 => {
828            while i < end4 {
829                let h = _mm256_loadu_pd(hp.add(i));
830                let l = _mm256_loadu_pd(lp.add(i));
831                let c = _mm256_loadu_pd(cp.add(i));
832                let vld = valid_mask_avx2(h, l, c);
833
834                let p = _mm256_mul_pd(_mm256_add_pd(_mm256_add_pd(h, l), c), v_third);
835                let d = _mm256_sub_pd(h, l);
836                let t2 = _mm256_add_pd(p, p);
837
838                let r1v = _mm256_sub_pd(t2, l);
839                let r2v = _mm256_fmadd_pd(d, v_one, p);
840                let s1v = _mm256_sub_pd(t2, h);
841                let s2v = _mm256_fmadd_pd(d, v_neg1, p);
842
843                _mm256_storeu_pd(ppp.add(i), blendv(v_nan, p, vld));
844                _mm256_storeu_pd(r1p.add(i), blendv(v_nan, r1v, vld));
845                _mm256_storeu_pd(r2p.add(i), blendv(v_nan, r2v, vld));
846                _mm256_storeu_pd(s1p.add(i), blendv(v_nan, s1v, vld));
847                _mm256_storeu_pd(s2p.add(i), blendv(v_nan, s2v, vld));
848                _mm256_storeu_pd(r3p.add(i), v_nan);
849                _mm256_storeu_pd(r4p.add(i), v_nan);
850                _mm256_storeu_pd(s3p.add(i), v_nan);
851                _mm256_storeu_pd(s4p.add(i), v_nan);
852
853                i += 4;
854            }
855        }
856        1 => {
857            while i < end4 {
858                let h = _mm256_loadu_pd(hp.add(i));
859                let l = _mm256_loadu_pd(lp.add(i));
860                let c = _mm256_loadu_pd(cp.add(i));
861                let vld = valid_mask_avx2(h, l, c);
862
863                let p = _mm256_mul_pd(_mm256_add_pd(_mm256_add_pd(h, l), c), v_third);
864                let d = _mm256_sub_pd(h, l);
865                let r1v = _mm256_fmadd_pd(d, v_c0382, p);
866                let r2v = _mm256_fmadd_pd(d, v_c0618, p);
867                let r3v = _mm256_fmadd_pd(d, v_one, p);
868                let s1v = _mm256_fmadd_pd(d, v_n0382, p);
869                let s2v = _mm256_fmadd_pd(d, v_n0618, p);
870                let s3v = _mm256_fmadd_pd(d, v_neg1, p);
871
872                _mm256_storeu_pd(ppp.add(i), blendv(v_nan, p, vld));
873                _mm256_storeu_pd(r1p.add(i), blendv(v_nan, r1v, vld));
874                _mm256_storeu_pd(r2p.add(i), blendv(v_nan, r2v, vld));
875                _mm256_storeu_pd(r3p.add(i), blendv(v_nan, r3v, vld));
876                _mm256_storeu_pd(s1p.add(i), blendv(v_nan, s1v, vld));
877                _mm256_storeu_pd(s2p.add(i), blendv(v_nan, s2v, vld));
878                _mm256_storeu_pd(s3p.add(i), blendv(v_nan, s3v, vld));
879                _mm256_storeu_pd(r4p.add(i), v_nan);
880                _mm256_storeu_pd(s4p.add(i), v_nan);
881
882                i += 4;
883            }
884        }
885        2 => {
886            while i < end4 {
887                let h = _mm256_loadu_pd(hp.add(i));
888                let l = _mm256_loadu_pd(lp.add(i));
889                let c = _mm256_loadu_pd(cp.add(i));
890                let o = _mm256_loadu_pd(op.add(i));
891                let vld = valid_mask_avx2(h, l, c);
892
893                let mlt = _mm256_cmp_pd(c, o, _CMP_LT_OQ);
894                let mgt = _mm256_cmp_pd(c, o, _CMP_GT_OQ);
895
896                let p_lt = _mm256_mul_pd(
897                    _mm256_add_pd(_mm256_add_pd(h, _mm256_add_pd(l, l)), c),
898                    v_quart,
899                );
900                let p_gt = _mm256_mul_pd(
901                    _mm256_add_pd(_mm256_add_pd(_mm256_add_pd(h, h), l), c),
902                    v_quart,
903                );
904                let p_eq = _mm256_mul_pd(
905                    _mm256_add_pd(_mm256_add_pd(h, l), _mm256_add_pd(c, c)),
906                    v_quart,
907                );
908
909                let mut p = blendv(p_eq, p_gt, mgt);
910                p = blendv(p, p_lt, mlt);
911                _mm256_storeu_pd(ppp.add(i), blendv(v_nan, p, vld));
912
913                let n_lt = _mm256_mul_pd(
914                    _mm256_add_pd(_mm256_add_pd(h, _mm256_add_pd(l, l)), c),
915                    v_half,
916                );
917                let n_gt = _mm256_mul_pd(
918                    _mm256_add_pd(_mm256_add_pd(_mm256_add_pd(h, h), l), c),
919                    v_half,
920                );
921                let n_eq = _mm256_mul_pd(
922                    _mm256_add_pd(_mm256_add_pd(h, l), _mm256_add_pd(c, c)),
923                    v_half,
924                );
925
926                let mut n = blendv(n_eq, n_gt, mgt);
927                n = blendv(n, n_lt, mlt);
928
929                let r1v = _mm256_sub_pd(n, l);
930                let s1v = _mm256_sub_pd(n, h);
931
932                _mm256_storeu_pd(r1p.add(i), blendv(v_nan, r1v, vld));
933                _mm256_storeu_pd(s1p.add(i), blendv(v_nan, s1v, vld));
934                _mm256_storeu_pd(r2p.add(i), v_nan);
935                _mm256_storeu_pd(r3p.add(i), v_nan);
936                _mm256_storeu_pd(r4p.add(i), v_nan);
937                _mm256_storeu_pd(s2p.add(i), v_nan);
938                _mm256_storeu_pd(s3p.add(i), v_nan);
939                _mm256_storeu_pd(s4p.add(i), v_nan);
940
941                i += 4;
942            }
943        }
944        3 => {
945            while i < end4 {
946                let h = _mm256_loadu_pd(hp.add(i));
947                let l = _mm256_loadu_pd(lp.add(i));
948                let c = _mm256_loadu_pd(cp.add(i));
949                let vld = valid_mask_avx2(h, l, c);
950
951                let p = _mm256_mul_pd(_mm256_add_pd(_mm256_add_pd(h, l), c), v_third);
952                _mm256_storeu_pd(ppp.add(i), blendv(v_nan, p, vld));
953
954                let d = _mm256_sub_pd(h, l);
955                let d1 = _mm256_mul_pd(d, v_c0916);
956                let d2 = _mm256_mul_pd(d, v_c0183);
957                let d3 = _mm256_mul_pd(d, v_c0275);
958                let d4 = _mm256_mul_pd(d, v_c0550);
959
960                let r1v = _mm256_fmadd_pd(d, v_c0916, c);
961                let r2v = _mm256_fmadd_pd(d, v_c0183, c);
962                let r3v = _mm256_fmadd_pd(d, v_c0275, c);
963                let r4v = _mm256_fmadd_pd(d, v_c0550, c);
964
965                let s1v = _mm256_fmadd_pd(d, _mm256_sub_pd(_mm256_setzero_pd(), v_c0916), c);
966                let s2v = _mm256_fmadd_pd(d, _mm256_sub_pd(_mm256_setzero_pd(), v_c0183), c);
967                let s3v = _mm256_fmadd_pd(d, _mm256_sub_pd(_mm256_setzero_pd(), v_c0275), c);
968                let s4v = _mm256_fmadd_pd(d, _mm256_sub_pd(_mm256_setzero_pd(), v_c0550), c);
969
970                _mm256_storeu_pd(r1p.add(i), blendv(v_nan, r1v, vld));
971                _mm256_storeu_pd(r2p.add(i), blendv(v_nan, r2v, vld));
972                _mm256_storeu_pd(r3p.add(i), blendv(v_nan, r3v, vld));
973                _mm256_storeu_pd(r4p.add(i), blendv(v_nan, r4v, vld));
974                _mm256_storeu_pd(s1p.add(i), blendv(v_nan, s1v, vld));
975                _mm256_storeu_pd(s2p.add(i), blendv(v_nan, s2v, vld));
976                _mm256_storeu_pd(s3p.add(i), blendv(v_nan, s3v, vld));
977                _mm256_storeu_pd(s4p.add(i), blendv(v_nan, s4v, vld));
978
979                i += 4;
980            }
981        }
982        4 => {
983            while i < end4 {
984                let h = _mm256_loadu_pd(hp.add(i));
985                let l = _mm256_loadu_pd(lp.add(i));
986                let c = _mm256_loadu_pd(cp.add(i));
987                let o = _mm256_loadu_pd(op.add(i));
988                let vld = valid_mask_avx2(h, l, c);
989
990                let p = _mm256_mul_pd(
991                    _mm256_add_pd(_mm256_add_pd(h, l), _mm256_add_pd(o, o)),
992                    v_quart,
993                );
994                let t2p = _mm256_add_pd(p, p);
995                let t2l = _mm256_add_pd(l, l);
996                let t2h = _mm256_add_pd(h, h);
997                let d = _mm256_sub_pd(h, l);
998
999                let r3v = _mm256_add_pd(_mm256_sub_pd(t2p, t2l), h);
1000                let r4v = _mm256_fmadd_pd(d, v_one, r3v);
1001                let r2v = _mm256_fmadd_pd(d, v_one, p);
1002                let r1v = _mm256_sub_pd(t2p, l);
1003
1004                let s1v = _mm256_sub_pd(t2p, h);
1005                let s2v = _mm256_fmadd_pd(d, v_neg1, p);
1006                let s3v = _mm256_sub_pd(_mm256_add_pd(l, t2p), t2h);
1007                let s4v = _mm256_fmadd_pd(d, v_neg1, s3v);
1008
1009                _mm256_storeu_pd(ppp.add(i), blendv(v_nan, p, vld));
1010                _mm256_storeu_pd(r1p.add(i), blendv(v_nan, r1v, vld));
1011                _mm256_storeu_pd(r2p.add(i), blendv(v_nan, r2v, vld));
1012                _mm256_storeu_pd(r3p.add(i), blendv(v_nan, r3v, vld));
1013                _mm256_storeu_pd(r4p.add(i), blendv(v_nan, r4v, vld));
1014                _mm256_storeu_pd(s1p.add(i), blendv(v_nan, s1v, vld));
1015                _mm256_storeu_pd(s2p.add(i), blendv(v_nan, s2v, vld));
1016                _mm256_storeu_pd(s3p.add(i), blendv(v_nan, s3v, vld));
1017                _mm256_storeu_pd(s4p.add(i), blendv(v_nan, s4v, vld));
1018
1019                i += 4;
1020            }
1021        }
1022        _ => {}
1023    }
1024
1025    if i < len {
1026        pivot_scalar(
1027            high, low, close, open, mode, i, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1028        );
1029    }
1030}
1031
1032#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1033#[inline]
1034pub unsafe fn pivot_avx512(
1035    high: &[f64],
1036    low: &[f64],
1037    close: &[f64],
1038    open: &[f64],
1039    mode: usize,
1040    first: usize,
1041    r4: &mut [f64],
1042    r3: &mut [f64],
1043    r2: &mut [f64],
1044    r1: &mut [f64],
1045    pp: &mut [f64],
1046    s1: &mut [f64],
1047    s2: &mut [f64],
1048    s3: &mut [f64],
1049    s4: &mut [f64],
1050) {
1051    if high.len() <= 32 {
1052        pivot_avx512_short(
1053            high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1054        )
1055    } else {
1056        pivot_avx512_long(
1057            high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1058        )
1059    }
1060}
1061
1062#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1063#[inline]
1064pub unsafe fn pivot_avx512_short(
1065    high: &[f64],
1066    low: &[f64],
1067    close: &[f64],
1068    open: &[f64],
1069    mode: usize,
1070    first: usize,
1071    r4: &mut [f64],
1072    r3: &mut [f64],
1073    r2: &mut [f64],
1074    r1: &mut [f64],
1075    pp: &mut [f64],
1076    s1: &mut [f64],
1077    s2: &mut [f64],
1078    s3: &mut [f64],
1079    s4: &mut [f64],
1080) {
1081    pivot_avx512_long(
1082        high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1083    )
1084}
1085#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1086#[inline]
1087#[target_feature(enable = "avx512f,fma")]
1088pub unsafe fn pivot_avx512_long(
1089    high: &[f64],
1090    low: &[f64],
1091    close: &[f64],
1092    open: &[f64],
1093    mode: usize,
1094    first: usize,
1095    r4: &mut [f64],
1096    r3: &mut [f64],
1097    r2: &mut [f64],
1098    r1: &mut [f64],
1099    pp: &mut [f64],
1100    s1: &mut [f64],
1101    s2: &mut [f64],
1102    s3: &mut [f64],
1103    s4: &mut [f64],
1104) {
1105    use core::arch::x86_64::*;
1106
1107    let len = high.len();
1108    if first >= len {
1109        return;
1110    }
1111
1112    let hp = high.as_ptr();
1113    let lp = low.as_ptr();
1114    let cp = close.as_ptr();
1115    let op = open.as_ptr();
1116
1117    let r4p = r4.as_mut_ptr();
1118    let r3p = r3.as_mut_ptr();
1119    let r2p = r2.as_mut_ptr();
1120    let r1p = r1.as_mut_ptr();
1121    let ppp = pp.as_mut_ptr();
1122    let s1p = s1.as_mut_ptr();
1123    let s2p = s2.as_mut_ptr();
1124    let s3p = s3.as_mut_ptr();
1125    let s4p = s4.as_mut_ptr();
1126
1127    let v_nan = _mm512_set1_pd(f64::NAN);
1128    let v_third = _mm512_set1_pd(1.0 / 3.0);
1129    let v_quart = _mm512_set1_pd(0.25);
1130    let v_half = _mm512_set1_pd(0.5);
1131    let v_one = _mm512_set1_pd(1.0);
1132    let v_c0916 = _mm512_set1_pd(0.0916);
1133    let v_c0183 = _mm512_set1_pd(0.183);
1134    let v_c0275 = _mm512_set1_pd(0.275);
1135    let v_c0550 = _mm512_set1_pd(0.55);
1136    let v_c0382 = _mm512_set1_pd(0.382);
1137    let v_c0618 = _mm512_set1_pd(0.618);
1138    let v_neg1 = _mm512_set1_pd(-1.0);
1139    let v_n0382 = _mm512_set1_pd(-0.382);
1140    let v_n0618 = _mm512_set1_pd(-0.618);
1141
1142    let mut i = first;
1143    let step = 8;
1144
1145    #[inline(always)]
1146    unsafe fn valid_mask_avx512(h: __m512d, l: __m512d, c: __m512d) -> u8 {
1147        let mh = _mm512_cmp_pd_mask(h, h, _CMP_ORD_Q);
1148        let ml = _mm512_cmp_pd_mask(l, l, _CMP_ORD_Q);
1149        let mc = _mm512_cmp_pd_mask(c, c, _CMP_ORD_Q);
1150        mh & ml & mc
1151    }
1152
1153    match mode {
1154        0 => {
1155            while i + step <= len {
1156                let h = _mm512_loadu_pd(hp.add(i));
1157                let l = _mm512_loadu_pd(lp.add(i));
1158                let c = _mm512_loadu_pd(cp.add(i));
1159                let mk = valid_mask_avx512(h, l, c);
1160
1161                let p = _mm512_mul_pd(_mm512_add_pd(_mm512_add_pd(h, l), c), v_third);
1162                let d = _mm512_sub_pd(h, l);
1163                let t2 = _mm512_add_pd(p, p);
1164
1165                let r1v = _mm512_sub_pd(t2, l);
1166                let r2v = _mm512_fmadd_pd(d, v_one, p);
1167                let s1v = _mm512_sub_pd(t2, h);
1168                let s2v = _mm512_fmadd_pd(d, v_neg1, p);
1169
1170                _mm512_storeu_pd(ppp.add(i), _mm512_mask_blend_pd(mk, v_nan, p));
1171                _mm512_storeu_pd(r1p.add(i), _mm512_mask_blend_pd(mk, v_nan, r1v));
1172                _mm512_storeu_pd(r2p.add(i), _mm512_mask_blend_pd(mk, v_nan, r2v));
1173                _mm512_storeu_pd(s1p.add(i), _mm512_mask_blend_pd(mk, v_nan, s1v));
1174                _mm512_storeu_pd(s2p.add(i), _mm512_mask_blend_pd(mk, v_nan, s2v));
1175                _mm512_storeu_pd(r3p.add(i), v_nan);
1176                _mm512_storeu_pd(r4p.add(i), v_nan);
1177                _mm512_storeu_pd(s3p.add(i), v_nan);
1178                _mm512_storeu_pd(s4p.add(i), v_nan);
1179
1180                i += step;
1181            }
1182        }
1183        1 => {
1184            while i + step <= len {
1185                let h = _mm512_loadu_pd(hp.add(i));
1186                let l = _mm512_loadu_pd(lp.add(i));
1187                let c = _mm512_loadu_pd(cp.add(i));
1188                let mk = valid_mask_avx512(h, l, c);
1189
1190                let p = _mm512_mul_pd(_mm512_add_pd(_mm512_add_pd(h, l), c), v_third);
1191                let d = _mm512_sub_pd(h, l);
1192                let r1v = _mm512_fmadd_pd(d, v_c0382, p);
1193                let r2v = _mm512_fmadd_pd(d, v_c0618, p);
1194                let r3v = _mm512_fmadd_pd(d, v_one, p);
1195                let s1v = _mm512_fmadd_pd(d, v_n0382, p);
1196                let s2v = _mm512_fmadd_pd(d, v_n0618, p);
1197                let s3v = _mm512_fmadd_pd(d, v_neg1, p);
1198
1199                _mm512_storeu_pd(ppp.add(i), _mm512_mask_blend_pd(mk, v_nan, p));
1200                _mm512_storeu_pd(r1p.add(i), _mm512_mask_blend_pd(mk, v_nan, r1v));
1201                _mm512_storeu_pd(r2p.add(i), _mm512_mask_blend_pd(mk, v_nan, r2v));
1202                _mm512_storeu_pd(r3p.add(i), _mm512_mask_blend_pd(mk, v_nan, r3v));
1203                _mm512_storeu_pd(s1p.add(i), _mm512_mask_blend_pd(mk, v_nan, s1v));
1204                _mm512_storeu_pd(s2p.add(i), _mm512_mask_blend_pd(mk, v_nan, s2v));
1205                _mm512_storeu_pd(s3p.add(i), _mm512_mask_blend_pd(mk, v_nan, s3v));
1206                _mm512_storeu_pd(r4p.add(i), v_nan);
1207                _mm512_storeu_pd(s4p.add(i), v_nan);
1208
1209                i += step;
1210            }
1211        }
1212        2 => {
1213            while i + step <= len {
1214                let h = _mm512_loadu_pd(hp.add(i));
1215                let l = _mm512_loadu_pd(lp.add(i));
1216                let c = _mm512_loadu_pd(cp.add(i));
1217                let o = _mm512_loadu_pd(op.add(i));
1218                let mk = valid_mask_avx512(h, l, c);
1219
1220                let mlt = _mm512_cmp_pd_mask(c, o, _CMP_LT_OQ);
1221                let mgt = _mm512_cmp_pd_mask(c, o, _CMP_GT_OQ);
1222                let meq = (!mlt) & (!mgt);
1223
1224                let p_lt = _mm512_mul_pd(
1225                    _mm512_add_pd(_mm512_add_pd(h, _mm512_add_pd(l, l)), c),
1226                    v_quart,
1227                );
1228                let p_gt = _mm512_mul_pd(
1229                    _mm512_add_pd(_mm512_add_pd(_mm512_add_pd(h, h), l), c),
1230                    v_quart,
1231                );
1232                let p_eq = _mm512_mul_pd(
1233                    _mm512_add_pd(_mm512_add_pd(h, l), _mm512_add_pd(c, c)),
1234                    v_quart,
1235                );
1236
1237                let mut p = p_eq;
1238                p = _mm512_mask_blend_pd(mgt, p, p_gt);
1239                p = _mm512_mask_blend_pd(mlt, p, p_lt);
1240                _mm512_storeu_pd(ppp.add(i), _mm512_mask_blend_pd(mk, v_nan, p));
1241
1242                let n_lt = _mm512_mul_pd(
1243                    _mm512_add_pd(_mm512_add_pd(h, _mm512_add_pd(l, l)), c),
1244                    v_half,
1245                );
1246                let n_gt = _mm512_mul_pd(
1247                    _mm512_add_pd(_mm512_add_pd(_mm512_add_pd(h, h), l), c),
1248                    v_half,
1249                );
1250                let n_eq = _mm512_mul_pd(
1251                    _mm512_add_pd(_mm512_add_pd(h, l), _mm512_add_pd(c, c)),
1252                    v_half,
1253                );
1254
1255                let mut n = n_eq;
1256                n = _mm512_mask_blend_pd(mgt, n, n_gt);
1257                n = _mm512_mask_blend_pd(mlt, n, n_lt);
1258
1259                let r1v = _mm512_sub_pd(n, l);
1260                let s1v = _mm512_sub_pd(n, h);
1261
1262                _mm512_storeu_pd(r1p.add(i), _mm512_mask_blend_pd(mk, v_nan, r1v));
1263                _mm512_storeu_pd(s1p.add(i), _mm512_mask_blend_pd(mk, v_nan, s1v));
1264                _mm512_storeu_pd(r2p.add(i), v_nan);
1265                _mm512_storeu_pd(r3p.add(i), v_nan);
1266                _mm512_storeu_pd(r4p.add(i), v_nan);
1267                _mm512_storeu_pd(s2p.add(i), v_nan);
1268                _mm512_storeu_pd(s3p.add(i), v_nan);
1269                _mm512_storeu_pd(s4p.add(i), v_nan);
1270
1271                i += step;
1272            }
1273        }
1274        3 => {
1275            while i + step <= len {
1276                let h = _mm512_loadu_pd(hp.add(i));
1277                let l = _mm512_loadu_pd(lp.add(i));
1278                let c = _mm512_loadu_pd(cp.add(i));
1279                let mk = valid_mask_avx512(h, l, c);
1280
1281                let p = _mm512_mul_pd(_mm512_add_pd(_mm512_add_pd(h, l), c), v_third);
1282                _mm512_storeu_pd(ppp.add(i), _mm512_mask_blend_pd(mk, v_nan, p));
1283
1284                let d = _mm512_sub_pd(h, l);
1285                let d1 = _mm512_mul_pd(d, v_c0916);
1286                let d2 = _mm512_mul_pd(d, v_c0183);
1287                let d3 = _mm512_mul_pd(d, v_c0275);
1288                let d4 = _mm512_mul_pd(d, v_c0550);
1289
1290                let r1v = _mm512_fmadd_pd(d, v_c0916, c);
1291                let r2v = _mm512_fmadd_pd(d, v_c0183, c);
1292                let r3v = _mm512_fmadd_pd(d, v_c0275, c);
1293                let r4v = _mm512_fmadd_pd(d, v_c0550, c);
1294
1295                let s1v = _mm512_fmadd_pd(d, _mm512_sub_pd(_mm512_setzero_pd(), v_c0916), c);
1296                let s2v = _mm512_fmadd_pd(d, _mm512_sub_pd(_mm512_setzero_pd(), v_c0183), c);
1297                let s3v = _mm512_fmadd_pd(d, _mm512_sub_pd(_mm512_setzero_pd(), v_c0275), c);
1298                let s4v = _mm512_fmadd_pd(d, _mm512_sub_pd(_mm512_setzero_pd(), v_c0550), c);
1299
1300                _mm512_storeu_pd(r1p.add(i), _mm512_mask_blend_pd(mk, v_nan, r1v));
1301                _mm512_storeu_pd(r2p.add(i), _mm512_mask_blend_pd(mk, v_nan, r2v));
1302                _mm512_storeu_pd(r3p.add(i), _mm512_mask_blend_pd(mk, v_nan, r3v));
1303                _mm512_storeu_pd(r4p.add(i), _mm512_mask_blend_pd(mk, v_nan, r4v));
1304                _mm512_storeu_pd(s1p.add(i), _mm512_mask_blend_pd(mk, v_nan, s1v));
1305                _mm512_storeu_pd(s2p.add(i), _mm512_mask_blend_pd(mk, v_nan, s2v));
1306                _mm512_storeu_pd(s3p.add(i), _mm512_mask_blend_pd(mk, v_nan, s3v));
1307                _mm512_storeu_pd(s4p.add(i), _mm512_mask_blend_pd(mk, v_nan, s4v));
1308
1309                i += step;
1310            }
1311        }
1312        4 => {
1313            while i + step <= len {
1314                let h = _mm512_loadu_pd(hp.add(i));
1315                let l = _mm512_loadu_pd(lp.add(i));
1316                let c = _mm512_loadu_pd(cp.add(i));
1317                let o = _mm512_loadu_pd(op.add(i));
1318                let mk = valid_mask_avx512(h, l, c);
1319
1320                let p = _mm512_mul_pd(
1321                    _mm512_add_pd(_mm512_add_pd(h, l), _mm512_add_pd(o, o)),
1322                    v_quart,
1323                );
1324                let t2p = _mm512_add_pd(p, p);
1325                let t2l = _mm512_add_pd(l, l);
1326                let t2h = _mm512_add_pd(h, h);
1327                let d = _mm512_sub_pd(h, l);
1328
1329                let r3v = _mm512_add_pd(_mm512_sub_pd(t2p, t2l), h);
1330                let r4v = _mm512_fmadd_pd(d, v_one, r3v);
1331                let r2v = _mm512_fmadd_pd(d, v_one, p);
1332                let r1v = _mm512_sub_pd(t2p, l);
1333
1334                let s1v = _mm512_sub_pd(t2p, h);
1335                let s2v = _mm512_fmadd_pd(d, v_neg1, p);
1336                let s3v = _mm512_sub_pd(_mm512_add_pd(l, t2p), t2h);
1337                let s4v = _mm512_fmadd_pd(d, v_neg1, s3v);
1338
1339                _mm512_storeu_pd(ppp.add(i), _mm512_mask_blend_pd(mk, v_nan, p));
1340                _mm512_storeu_pd(r1p.add(i), _mm512_mask_blend_pd(mk, v_nan, r1v));
1341                _mm512_storeu_pd(r2p.add(i), _mm512_mask_blend_pd(mk, v_nan, r2v));
1342                _mm512_storeu_pd(r3p.add(i), _mm512_mask_blend_pd(mk, v_nan, r3v));
1343                _mm512_storeu_pd(r4p.add(i), _mm512_mask_blend_pd(mk, v_nan, r4v));
1344                _mm512_storeu_pd(s1p.add(i), _mm512_mask_blend_pd(mk, v_nan, s1v));
1345                _mm512_storeu_pd(s2p.add(i), _mm512_mask_blend_pd(mk, v_nan, s2v));
1346                _mm512_storeu_pd(s3p.add(i), _mm512_mask_blend_pd(mk, v_nan, s3v));
1347                _mm512_storeu_pd(s4p.add(i), _mm512_mask_blend_pd(mk, v_nan, s4v));
1348
1349                i += step;
1350            }
1351        }
1352        _ => {}
1353    }
1354
1355    if i < len {
1356        pivot_scalar(
1357            high, low, close, open, mode, i, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1358        );
1359    }
1360}
1361
1362#[inline(always)]
1363pub unsafe fn pivot_row_scalar(
1364    high: &[f64],
1365    low: &[f64],
1366    close: &[f64],
1367    open: &[f64],
1368    mode: usize,
1369    first: usize,
1370    r4: &mut [f64],
1371    r3: &mut [f64],
1372    r2: &mut [f64],
1373    r1: &mut [f64],
1374    pp: &mut [f64],
1375    s1: &mut [f64],
1376    s2: &mut [f64],
1377    s3: &mut [f64],
1378    s4: &mut [f64],
1379) {
1380    pivot_scalar(
1381        high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1382    )
1383}
1384#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1385#[inline(always)]
1386pub unsafe fn pivot_row_avx2(
1387    high: &[f64],
1388    low: &[f64],
1389    close: &[f64],
1390    open: &[f64],
1391    mode: usize,
1392    first: usize,
1393    r4: &mut [f64],
1394    r3: &mut [f64],
1395    r2: &mut [f64],
1396    r1: &mut [f64],
1397    pp: &mut [f64],
1398    s1: &mut [f64],
1399    s2: &mut [f64],
1400    s3: &mut [f64],
1401    s4: &mut [f64],
1402) {
1403    pivot_avx2(
1404        high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1405    )
1406}
1407#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1408#[inline(always)]
1409pub unsafe fn pivot_row_avx512(
1410    high: &[f64],
1411    low: &[f64],
1412    close: &[f64],
1413    open: &[f64],
1414    mode: usize,
1415    first: usize,
1416    r4: &mut [f64],
1417    r3: &mut [f64],
1418    r2: &mut [f64],
1419    r1: &mut [f64],
1420    pp: &mut [f64],
1421    s1: &mut [f64],
1422    s2: &mut [f64],
1423    s3: &mut [f64],
1424    s4: &mut [f64],
1425) {
1426    pivot_avx512(
1427        high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1428    )
1429}
1430#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1431#[inline(always)]
1432pub unsafe fn pivot_row_avx512_short(
1433    high: &[f64],
1434    low: &[f64],
1435    close: &[f64],
1436    open: &[f64],
1437    mode: usize,
1438    first: usize,
1439    r4: &mut [f64],
1440    r3: &mut [f64],
1441    r2: &mut [f64],
1442    r1: &mut [f64],
1443    pp: &mut [f64],
1444    s1: &mut [f64],
1445    s2: &mut [f64],
1446    s3: &mut [f64],
1447    s4: &mut [f64],
1448) {
1449    pivot_avx512_short(
1450        high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1451    )
1452}
1453#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1454#[inline(always)]
1455pub unsafe fn pivot_row_avx512_long(
1456    high: &[f64],
1457    low: &[f64],
1458    close: &[f64],
1459    open: &[f64],
1460    mode: usize,
1461    first: usize,
1462    r4: &mut [f64],
1463    r3: &mut [f64],
1464    r2: &mut [f64],
1465    r1: &mut [f64],
1466    pp: &mut [f64],
1467    s1: &mut [f64],
1468    s2: &mut [f64],
1469    s3: &mut [f64],
1470    s4: &mut [f64],
1471) {
1472    pivot_avx512_long(
1473        high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1474    )
1475}
1476
1477#[inline(always)]
1478unsafe fn pivot_rows_scalar_into(
1479    high: &[f64],
1480    low: &[f64],
1481    close: &[f64],
1482    open: &[f64],
1483    mode: usize,
1484    first: usize,
1485    r4: &mut [f64],
1486    r3: &mut [f64],
1487    r2: &mut [f64],
1488    r1: &mut [f64],
1489    pp: &mut [f64],
1490    s1: &mut [f64],
1491    s2: &mut [f64],
1492    s3: &mut [f64],
1493    s4: &mut [f64],
1494) {
1495    pivot_scalar(
1496        high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1497    )
1498}
1499
1500#[inline(always)]
1501fn pivot_batch_inner_into(
1502    high: &[f64],
1503    low: &[f64],
1504    close: &[f64],
1505    open: &[f64],
1506    sweep: &PivotBatchRange,
1507    kern: Kernel,
1508    parallel: bool,
1509    out: &mut [f64],
1510) -> Result<Vec<PivotParams>, PivotError> {
1511    let combos = expand_grid(sweep)?;
1512    if combos.is_empty() {
1513        let (start, end, step) = sweep.mode;
1514        return Err(PivotError::InvalidRange { start, end, step });
1515    }
1516    let cols = high.len();
1517    if cols == 0 || low.len() != cols || close.len() != cols || open.len() != cols {
1518        return Err(PivotError::EmptyData);
1519    }
1520
1521    let first = first_valid_ohlc(high, low, close).ok_or(PivotError::AllValuesNaN)?;
1522    if first >= cols {
1523        return Err(PivotError::NotEnoughValidData);
1524    }
1525
1526    let rows = combos
1527        .len()
1528        .checked_mul(N_LEVELS)
1529        .ok_or(PivotError::InvalidRange {
1530            start: combos.len(),
1531            end: N_LEVELS,
1532            step: 0,
1533        })?;
1534    let expected_len = rows.checked_mul(cols).ok_or(PivotError::InvalidRange {
1535        start: rows,
1536        end: cols,
1537        step: 0,
1538    })?;
1539    if out.len() != expected_len {
1540        return Err(PivotError::OutputLengthMismatch {
1541            expected: expected_len,
1542            got: out.len(),
1543        });
1544    }
1545
1546    let warm: Vec<usize> = vec![first; rows];
1547
1548    let out_mu = unsafe {
1549        let mu = std::slice::from_raw_parts_mut(
1550            out.as_mut_ptr() as *mut std::mem::MaybeUninit<f64>,
1551            out.len(),
1552        );
1553        init_matrix_prefixes(mu, cols, &warm);
1554        mu
1555    };
1556
1557    let chosen = match kern {
1558        Kernel::Auto => detect_best_batch_kernel(),
1559        k => k,
1560    };
1561
1562    if parallel {
1563        #[cfg(not(target_arch = "wasm32"))]
1564        {
1565            use rayon::prelude::*;
1566            use std::sync::atomic::{AtomicPtr, Ordering};
1567
1568            let out_ptr = AtomicPtr::new(out.as_mut_ptr());
1569            let out_len = out.len();
1570
1571            (0..combos.len()).into_par_iter().for_each(|ci| {
1572                let mode = combos[ci].mode.unwrap_or(3);
1573
1574                let base = ci * N_LEVELS * cols;
1575                unsafe {
1576                    let ptr = out_ptr.load(Ordering::Relaxed);
1577                    let mu = std::slice::from_raw_parts_mut(
1578                        ptr as *mut std::mem::MaybeUninit<f64>,
1579                        out_len,
1580                    );
1581                    let mut rows_mu = mu[base..base + N_LEVELS * cols].chunks_mut(cols);
1582                    let mut cast = |mu: &mut [std::mem::MaybeUninit<f64>]| {
1583                        std::slice::from_raw_parts_mut(mu.as_mut_ptr() as *mut f64, mu.len())
1584                    };
1585                    let r4_mu = rows_mu.next().unwrap();
1586                    let r3_mu = rows_mu.next().unwrap();
1587                    let r2_mu = rows_mu.next().unwrap();
1588                    let r1_mu = rows_mu.next().unwrap();
1589                    let pp_mu = rows_mu.next().unwrap();
1590                    let s1_mu = rows_mu.next().unwrap();
1591                    let s2_mu = rows_mu.next().unwrap();
1592                    let s3_mu = rows_mu.next().unwrap();
1593                    let s4_mu = rows_mu.next().unwrap();
1594
1595                    let r4 = cast(r4_mu);
1596                    let r3 = cast(r3_mu);
1597                    let r2 = cast(r2_mu);
1598                    let r1 = cast(r1_mu);
1599                    let pp = cast(pp_mu);
1600                    let s1 = cast(s1_mu);
1601                    let s2 = cast(s2_mu);
1602                    let s3 = cast(s3_mu);
1603                    let s4 = cast(s4_mu);
1604
1605                    match chosen {
1606                        Kernel::Scalar | Kernel::ScalarBatch => pivot_rows_scalar_into(
1607                            high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1608                        ),
1609                        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1610                        Kernel::Avx2 | Kernel::Avx2Batch => pivot_row_avx2(
1611                            high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1612                        ),
1613                        #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1614                        Kernel::Avx512 | Kernel::Avx512Batch => pivot_row_avx512(
1615                            high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1616                        ),
1617                        _ => unreachable!(),
1618                    }
1619                }
1620            });
1621        }
1622        #[cfg(target_arch = "wasm32")]
1623        {
1624            let mut row_chunks = out_mu.chunks_mut(cols);
1625            for p in &combos {
1626                let mode = p.mode.unwrap_or(3);
1627                unsafe {
1628                    let r4_mu = row_chunks.next().unwrap();
1629                    let r3_mu = row_chunks.next().unwrap();
1630                    let r2_mu = row_chunks.next().unwrap();
1631                    let r1_mu = row_chunks.next().unwrap();
1632                    let pp_mu = row_chunks.next().unwrap();
1633                    let s1_mu = row_chunks.next().unwrap();
1634                    let s2_mu = row_chunks.next().unwrap();
1635                    let s3_mu = row_chunks.next().unwrap();
1636                    let s4_mu = row_chunks.next().unwrap();
1637
1638                    let mut cast = |mu: &mut [std::mem::MaybeUninit<f64>]| {
1639                        std::slice::from_raw_parts_mut(mu.as_mut_ptr() as *mut f64, mu.len())
1640                    };
1641                    let (r4, r3, r2, r1, pp, s1, s2, s3, s4) = (
1642                        cast(r4_mu),
1643                        cast(r3_mu),
1644                        cast(r2_mu),
1645                        cast(r1_mu),
1646                        cast(pp_mu),
1647                        cast(s1_mu),
1648                        cast(s2_mu),
1649                        cast(s3_mu),
1650                        cast(s4_mu),
1651                    );
1652
1653                    pivot_rows_scalar_into(
1654                        high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1655                    );
1656                }
1657            }
1658        }
1659    } else {
1660        let mut row_chunks = out_mu.chunks_mut(cols);
1661        for p in &combos {
1662            let mode = p.mode.unwrap_or(3);
1663            unsafe {
1664                let r4_mu = row_chunks.next().unwrap();
1665                let r3_mu = row_chunks.next().unwrap();
1666                let r2_mu = row_chunks.next().unwrap();
1667                let r1_mu = row_chunks.next().unwrap();
1668                let pp_mu = row_chunks.next().unwrap();
1669                let s1_mu = row_chunks.next().unwrap();
1670                let s2_mu = row_chunks.next().unwrap();
1671                let s3_mu = row_chunks.next().unwrap();
1672                let s4_mu = row_chunks.next().unwrap();
1673
1674                let mut cast = |mu: &mut [std::mem::MaybeUninit<f64>]| {
1675                    std::slice::from_raw_parts_mut(mu.as_mut_ptr() as *mut f64, mu.len())
1676                };
1677                let (r4, r3, r2, r1, pp, s1, s2, s3, s4) = (
1678                    cast(r4_mu),
1679                    cast(r3_mu),
1680                    cast(r2_mu),
1681                    cast(r1_mu),
1682                    cast(pp_mu),
1683                    cast(s1_mu),
1684                    cast(s2_mu),
1685                    cast(s3_mu),
1686                    cast(s4_mu),
1687                );
1688
1689                pivot_rows_scalar_into(
1690                    high, low, close, open, mode, first, r4, r3, r2, r1, pp, s1, s2, s3, s4,
1691                );
1692            }
1693        }
1694    }
1695    Ok(combos)
1696}
1697
1698#[derive(Clone, Debug)]
1699pub struct PivotBatchRange {
1700    pub mode: (usize, usize, usize),
1701}
1702impl Default for PivotBatchRange {
1703    fn default() -> Self {
1704        Self { mode: (3, 3, 1) }
1705    }
1706}
1707#[derive(Clone, Debug, Default)]
1708pub struct PivotBatchBuilder {
1709    range: PivotBatchRange,
1710    kernel: Kernel,
1711}
1712impl PivotBatchBuilder {
1713    pub fn new() -> Self {
1714        Self::default()
1715    }
1716    pub fn kernel(mut self, k: Kernel) -> Self {
1717        self.kernel = k;
1718        self
1719    }
1720    #[inline]
1721    pub fn mode_range(mut self, start: usize, end: usize, step: usize) -> Self {
1722        self.range.mode = (start, end, step);
1723        self
1724    }
1725    #[inline]
1726    pub fn mode_static(mut self, m: usize) -> Self {
1727        self.range.mode = (m, m, 1);
1728        self
1729    }
1730    pub fn apply_slice(
1731        self,
1732        high: &[f64],
1733        low: &[f64],
1734        close: &[f64],
1735        open: &[f64],
1736    ) -> Result<PivotBatchOutput, PivotError> {
1737        pivot_batch_with_kernel(high, low, close, open, &self.range, self.kernel)
1738    }
1739    pub fn apply_candles(self, candles: &Candles) -> Result<PivotBatchOutput, PivotError> {
1740        let high = source_type(candles, "high");
1741        let low = source_type(candles, "low");
1742        let close = source_type(candles, "close");
1743        let open = source_type(candles, "open");
1744        self.apply_slice(high, low, close, open)
1745    }
1746    pub fn with_default_candles(candles: &Candles) -> Result<PivotBatchOutput, PivotError> {
1747        PivotBatchBuilder::new()
1748            .kernel(Kernel::Auto)
1749            .apply_candles(candles)
1750    }
1751}
1752
1753pub fn pivot_batch_with_kernel(
1754    high: &[f64],
1755    low: &[f64],
1756    close: &[f64],
1757    open: &[f64],
1758    sweep: &PivotBatchRange,
1759    k: Kernel,
1760) -> Result<PivotBatchOutput, PivotError> {
1761    let kernel = match k {
1762        Kernel::Auto => detect_best_batch_kernel(),
1763        other if other.is_batch() => other,
1764        _ => return Err(PivotError::InvalidKernelForBatch(k)),
1765    };
1766    pivot_batch_inner(high, low, close, open, sweep, kernel)
1767}
1768
1769#[derive(Clone, Debug)]
1770pub struct PivotBatchOutput {
1771    pub levels: Vec<[Vec<f64>; 9]>,
1772    pub combos: Vec<PivotParams>,
1773    pub rows: usize,
1774    pub cols: usize,
1775}
1776
1777#[derive(Clone, Debug)]
1778pub struct PivotBatchFlatOutput {
1779    pub values: Vec<f64>,
1780    pub combos: Vec<PivotParams>,
1781    pub rows: usize,
1782    pub cols: usize,
1783}
1784
1785pub fn pivot_batch_flat_with_kernel(
1786    high: &[f64],
1787    low: &[f64],
1788    close: &[f64],
1789    open: &[f64],
1790    sweep: &PivotBatchRange,
1791    k: Kernel,
1792) -> Result<PivotBatchFlatOutput, PivotError> {
1793    let kernel = match k {
1794        Kernel::Auto => detect_best_batch_kernel(),
1795        other if other.is_batch() => other,
1796        _ => return Err(PivotError::InvalidKernelForBatch(k)),
1797    };
1798    let combos = expand_grid(sweep)?;
1799    if combos.is_empty() {
1800        let (start, end, step) = sweep.mode;
1801        return Err(PivotError::InvalidRange { start, end, step });
1802    }
1803    let cols = high.len();
1804    let rows = combos
1805        .len()
1806        .checked_mul(N_LEVELS)
1807        .ok_or(PivotError::InvalidRange {
1808            start: combos.len(),
1809            end: N_LEVELS,
1810            step: 0,
1811        })?;
1812    let _ = rows.checked_mul(cols).ok_or(PivotError::InvalidRange {
1813        start: rows,
1814        end: cols,
1815        step: 0,
1816    })?;
1817
1818    let mut buf_mu = make_uninit_matrix(rows, cols);
1819    let warm: Vec<usize> =
1820        vec![first_valid_ohlc(high, low, close).ok_or(PivotError::AllValuesNaN)?; rows];
1821    init_matrix_prefixes(&mut buf_mu, cols, &warm);
1822
1823    let mut guard = core::mem::ManuallyDrop::new(buf_mu);
1824    let out: &mut [f64] =
1825        unsafe { core::slice::from_raw_parts_mut(guard.as_mut_ptr() as *mut f64, guard.len()) };
1826
1827    pivot_batch_inner_into(high, low, close, open, sweep, kernel, true, out)?;
1828
1829    let values = unsafe {
1830        Vec::from_raw_parts(
1831            guard.as_mut_ptr() as *mut f64,
1832            guard.len(),
1833            guard.capacity(),
1834        )
1835    };
1836    Ok(PivotBatchFlatOutput {
1837        values,
1838        combos,
1839        rows,
1840        cols,
1841    })
1842}
1843
1844fn expand_grid(r: &PivotBatchRange) -> Result<Vec<PivotParams>, PivotError> {
1845    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, PivotError> {
1846        if step == 0 || start == end {
1847            return Ok(vec![start]);
1848        }
1849        let mut vals = Vec::new();
1850        if start < end {
1851            let mut cur = start;
1852            while cur <= end {
1853                vals.push(cur);
1854                cur = cur
1855                    .checked_add(step)
1856                    .ok_or(PivotError::InvalidRange { start, end, step })?;
1857            }
1858        } else {
1859            let mut cur = start;
1860            while cur >= end {
1861                vals.push(cur);
1862                cur = cur
1863                    .checked_sub(step)
1864                    .ok_or(PivotError::InvalidRange { start, end, step })?;
1865                if cur == 0 && end > 0 {
1866                    break;
1867                }
1868                if let Some(&last) = vals.last() {
1869                    if last == cur {
1870                        break;
1871                    }
1872                }
1873            }
1874            if let Some(&last) = vals.last() {
1875                if last < end {
1876                    vals.pop();
1877                }
1878            }
1879        }
1880        if vals.is_empty() {
1881            return Err(PivotError::InvalidRange { start, end, step });
1882        }
1883        Ok(vals)
1884    }
1885
1886    let modes = axis_usize(r.mode)?;
1887    let mut v = Vec::with_capacity(modes.len());
1888    for m in modes {
1889        v.push(PivotParams { mode: Some(m) });
1890    }
1891    Ok(v)
1892}
1893fn pivot_batch_inner(
1894    high: &[f64],
1895    low: &[f64],
1896    close: &[f64],
1897    open: &[f64],
1898    sweep: &PivotBatchRange,
1899    kernel: Kernel,
1900) -> Result<PivotBatchOutput, PivotError> {
1901    let combos = expand_grid(sweep)?;
1902    if combos.is_empty() {
1903        let (start, end, step) = sweep.mode;
1904        return Err(PivotError::InvalidRange { start, end, step });
1905    }
1906    let len = high.len();
1907    let mut levels = Vec::with_capacity(combos.len());
1908    for p in &combos {
1909        let mode = p.mode.unwrap_or(3);
1910        let mut first = None;
1911        for i in 0..len {
1912            if !(high[i].is_nan() || low[i].is_nan() || close[i].is_nan()) {
1913                first = Some(i);
1914                break;
1915            }
1916        }
1917        let first = first.unwrap_or(len);
1918
1919        let mut r4 = alloc_with_nan_prefix(len, first);
1920        let mut r3 = alloc_with_nan_prefix(len, first);
1921        let mut r2 = alloc_with_nan_prefix(len, first);
1922        let mut r1 = alloc_with_nan_prefix(len, first);
1923        let mut pp = alloc_with_nan_prefix(len, first);
1924        let mut s1 = alloc_with_nan_prefix(len, first);
1925        let mut s2 = alloc_with_nan_prefix(len, first);
1926        let mut s3 = alloc_with_nan_prefix(len, first);
1927        let mut s4 = alloc_with_nan_prefix(len, first);
1928        unsafe {
1929            match kernel {
1930                Kernel::Scalar | Kernel::ScalarBatch => pivot_row_scalar(
1931                    high, low, close, open, mode, first, &mut r4, &mut r3, &mut r2, &mut r1,
1932                    &mut pp, &mut s1, &mut s2, &mut s3, &mut s4,
1933                ),
1934                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1935                Kernel::Avx2 | Kernel::Avx2Batch => pivot_row_avx2(
1936                    high, low, close, open, mode, first, &mut r4, &mut r3, &mut r2, &mut r1,
1937                    &mut pp, &mut s1, &mut s2, &mut s3, &mut s4,
1938                ),
1939                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1940                Kernel::Avx512 | Kernel::Avx512Batch => pivot_row_avx512(
1941                    high, low, close, open, mode, first, &mut r4, &mut r3, &mut r2, &mut r1,
1942                    &mut pp, &mut s1, &mut s2, &mut s3, &mut s4,
1943                ),
1944                _ => unreachable!(),
1945            }
1946        }
1947        levels.push([r4, r3, r2, r1, pp, s1, s2, s3, s4]);
1948    }
1949    let rows = combos.len();
1950    let cols = high.len();
1951    Ok(PivotBatchOutput {
1952        levels,
1953        combos,
1954        rows,
1955        cols,
1956    })
1957}
1958
1959pub struct PivotStream {
1960    mode: usize,
1961}
1962
1963impl PivotStream {
1964    pub fn new(mode: usize) -> Self {
1965        Self { mode }
1966    }
1967
1968    pub fn try_new(params: PivotParams) -> Result<Self, PivotError> {
1969        let mode = params.mode.unwrap_or(3);
1970        if mode > 4 {
1971            return Err(PivotError::EmptyData);
1972        }
1973        Ok(Self { mode })
1974    }
1975
1976    #[inline(always)]
1977    pub fn update(
1978        &mut self,
1979        high: f64,
1980        low: f64,
1981        close: f64,
1982        open: f64,
1983    ) -> Option<(f64, f64, f64, f64, f64, f64, f64, f64, f64)> {
1984        const C1: f64 = 0.0916;
1985        const C2: f64 = 0.183;
1986        const C3: f64 = 0.275;
1987        const C4: f64 = 0.55;
1988
1989        const INV3: f64 = 1.0 / 3.0;
1990        const INV4: f64 = 0.25;
1991        const INV2: f64 = 0.5;
1992
1993        match self.mode {
1994            0 => {
1995                if high.is_nan() || low.is_nan() || close.is_nan() {
1996                    return None;
1997                }
1998                let d = high - low;
1999                let p = (high + low + close) * INV3;
2000                let t2 = p + p;
2001
2002                let r1 = t2 - low;
2003                let r2 = d.mul_add(1.0, p);
2004                let s1 = t2 - high;
2005                let s2 = (-d).mul_add(1.0, p);
2006
2007                Some((f64::NAN, f64::NAN, r2, r1, p, s1, s2, f64::NAN, f64::NAN))
2008            }
2009
2010            1 => {
2011                if high.is_nan() || low.is_nan() || close.is_nan() {
2012                    return None;
2013                }
2014                let d = high - low;
2015                let p = (high + low + close) * INV3;
2016
2017                let r1 = d.mul_add(0.382, p);
2018                let r2 = d.mul_add(0.618, p);
2019                let r3 = d.mul_add(1.000, p);
2020                let s1 = d.mul_add(-0.382, p);
2021                let s2 = d.mul_add(-0.618, p);
2022                let s3 = d.mul_add(-1.000, p);
2023
2024                Some((f64::NAN, r3, r2, r1, p, s1, s2, s3, f64::NAN))
2025            }
2026
2027            2 => {
2028                if high.is_nan() || low.is_nan() || close.is_nan() || open.is_nan() {
2029                    return None;
2030                }
2031
2032                let x_lt = high + low + low + close;
2033                let x_gt = high + high + low + close;
2034                let x_eq = high + low + close + close;
2035
2036                let lt: f64 = if close < open { 1.0 } else { 0.0 };
2037                let gt: f64 = if close > open { 1.0 } else { 0.0 };
2038                let eq: f64 = 1.0 - lt - gt;
2039
2040                let x = lt.mul_add(x_lt, gt.mul_add(x_gt, eq * x_eq));
2041                let pp = x * INV4;
2042                let half = x * INV2;
2043
2044                let r1 = half - low;
2045                let s1 = half - high;
2046
2047                Some((
2048                    f64::NAN,
2049                    f64::NAN,
2050                    f64::NAN,
2051                    r1,
2052                    pp,
2053                    s1,
2054                    f64::NAN,
2055                    f64::NAN,
2056                    f64::NAN,
2057                ))
2058            }
2059
2060            3 => {
2061                if high.is_nan() || low.is_nan() || close.is_nan() {
2062                    return None;
2063                }
2064                let d = high - low;
2065                let p = (high + low + close) * INV3;
2066
2067                let r1 = d.mul_add(C1, close);
2068                let r2 = d.mul_add(C2, close);
2069                let r3 = d.mul_add(C3, close);
2070                let r4 = d.mul_add(C4, close);
2071                let s1 = (-C1).mul_add(d, close);
2072                let s2 = (-C2).mul_add(d, close);
2073                let s3 = (-C3).mul_add(d, close);
2074                let s4 = (-C4).mul_add(d, close);
2075
2076                Some((r4, r3, r2, r1, p, s1, s2, s3, s4))
2077            }
2078
2079            4 => {
2080                if high.is_nan() || low.is_nan() || close.is_nan() || open.is_nan() {
2081                    return None;
2082                }
2083                let d = high - low;
2084                let p = (high + low + open + open) * INV4;
2085                let t2 = p + p;
2086
2087                let r1 = t2 - low;
2088                let r2 = d.mul_add(1.0, p);
2089                let r3 = high + (p - low) * 2.0;
2090                let r4 = (high - low).mul_add(1.0, r3);
2091
2092                let s1 = t2 - high;
2093                let s2 = (-d).mul_add(1.0, p);
2094                let s3 = low - (high - p) * 2.0;
2095                let s4 = (-(high - low)).mul_add(1.0, s3);
2096
2097                Some((r4, r3, r2, r1, p, s1, s2, s3, s4))
2098            }
2099
2100            _ => None,
2101        }
2102    }
2103}
2104
2105#[cfg(feature = "python")]
2106#[pyfunction(name = "pivot")]
2107#[pyo3(signature = (high, low, close, open, mode=3, kernel=None))]
2108pub fn pivot_py<'py>(
2109    py: Python<'py>,
2110    high: PyReadonlyArray1<'py, f64>,
2111    low: PyReadonlyArray1<'py, f64>,
2112    close: PyReadonlyArray1<'py, f64>,
2113    open: PyReadonlyArray1<'py, f64>,
2114    mode: usize,
2115    kernel: Option<&str>,
2116) -> PyResult<(
2117    Bound<'py, PyArray1<f64>>,
2118    Bound<'py, PyArray1<f64>>,
2119    Bound<'py, PyArray1<f64>>,
2120    Bound<'py, PyArray1<f64>>,
2121    Bound<'py, PyArray1<f64>>,
2122    Bound<'py, PyArray1<f64>>,
2123    Bound<'py, PyArray1<f64>>,
2124    Bound<'py, PyArray1<f64>>,
2125    Bound<'py, PyArray1<f64>>,
2126)> {
2127    use numpy::{IntoPyArray, PyArrayMethods};
2128
2129    let high_slice = high.as_slice()?;
2130    let low_slice = low.as_slice()?;
2131    let close_slice = close.as_slice()?;
2132    let open_slice = open.as_slice()?;
2133
2134    let kern = validate_kernel(kernel, false)?;
2135
2136    let params = PivotParams { mode: Some(mode) };
2137    let input = PivotInput::from_slices(high_slice, low_slice, close_slice, open_slice, params);
2138
2139    let result = py
2140        .allow_threads(|| pivot_with_kernel(&input, kern))
2141        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2142
2143    Ok((
2144        result.r4.into_pyarray(py),
2145        result.r3.into_pyarray(py),
2146        result.r2.into_pyarray(py),
2147        result.r1.into_pyarray(py),
2148        result.pp.into_pyarray(py),
2149        result.s1.into_pyarray(py),
2150        result.s2.into_pyarray(py),
2151        result.s3.into_pyarray(py),
2152        result.s4.into_pyarray(py),
2153    ))
2154}
2155
2156#[cfg(all(feature = "python", feature = "cuda"))]
2157use crate::cuda::pivot_wrapper::CudaPivot;
2158#[cfg(all(feature = "python", feature = "cuda"))]
2159use crate::indicators::moving_averages::alma::{make_device_array_py, DeviceArrayF32Py};
2160#[cfg(all(feature = "python", feature = "cuda"))]
2161#[pyfunction(name = "pivot_cuda_batch_dev")]
2162#[pyo3(signature = (high_f32, low_f32, close_f32, open_f32, mode_range, device_id=0))]
2163pub fn pivot_cuda_batch_dev_py<'py>(
2164    py: Python<'py>,
2165    high_f32: numpy::PyReadonlyArray1<'py, f32>,
2166    low_f32: numpy::PyReadonlyArray1<'py, f32>,
2167    close_f32: numpy::PyReadonlyArray1<'py, f32>,
2168    open_f32: numpy::PyReadonlyArray1<'py, f32>,
2169    mode_range: (usize, usize, usize),
2170    device_id: usize,
2171) -> PyResult<(DeviceArrayF32Py, Bound<'py, PyDict>)> {
2172    use crate::cuda::cuda_available;
2173    if !cuda_available() {
2174        return Err(PyValueError::new_err("CUDA not available"));
2175    }
2176    let h = high_f32.as_slice()?;
2177    let l = low_f32.as_slice()?;
2178    let c = close_f32.as_slice()?;
2179    let o = open_f32.as_slice()?;
2180    let sweep = PivotBatchRange { mode: mode_range };
2181    let (inner, combos) = py.allow_threads(|| {
2182        let cuda = CudaPivot::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2183        cuda.pivot_batch_dev(h, l, c, o, &sweep)
2184            .map_err(|e| PyValueError::new_err(e.to_string()))
2185    })?;
2186    let dict = PyDict::new(py);
2187    dict.set_item(
2188        "modes",
2189        combos
2190            .iter()
2191            .map(|p| p.mode.unwrap() as u64)
2192            .collect::<Vec<_>>()
2193            .into_pyarray(py),
2194    )?;
2195    let handle = make_device_array_py(device_id, inner)?;
2196    Ok((handle, dict))
2197}
2198
2199#[cfg(all(feature = "python", feature = "cuda"))]
2200#[pyfunction(name = "pivot_cuda_many_series_one_param_dev")]
2201#[pyo3(signature = (high_tm_f32, low_tm_f32, close_tm_f32, open_tm_f32, cols, rows, mode, device_id=0))]
2202pub fn pivot_cuda_many_series_one_param_dev_py(
2203    py: Python<'_>,
2204    high_tm_f32: numpy::PyReadonlyArray1<'_, f32>,
2205    low_tm_f32: numpy::PyReadonlyArray1<'_, f32>,
2206    close_tm_f32: numpy::PyReadonlyArray1<'_, f32>,
2207    open_tm_f32: numpy::PyReadonlyArray1<'_, f32>,
2208    cols: usize,
2209    rows: usize,
2210    mode: usize,
2211    device_id: usize,
2212) -> PyResult<DeviceArrayF32Py> {
2213    use crate::cuda::cuda_available;
2214    if !cuda_available() {
2215        return Err(PyValueError::new_err("CUDA not available"));
2216    }
2217    let h = high_tm_f32.as_slice()?;
2218    let l = low_tm_f32.as_slice()?;
2219    let c = close_tm_f32.as_slice()?;
2220    let o = open_tm_f32.as_slice()?;
2221    let inner = py.allow_threads(|| {
2222        let cuda = CudaPivot::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
2223        cuda.pivot_many_series_one_param_time_major_dev(h, l, c, o, cols, rows, mode)
2224            .map_err(|e| PyValueError::new_err(e.to_string()))
2225    })?;
2226    let handle = make_device_array_py(device_id, inner)?;
2227    Ok(handle)
2228}
2229
2230#[cfg(feature = "python")]
2231#[pyclass(name = "PivotStream")]
2232pub struct PivotStreamPy {
2233    inner: PivotStream,
2234}
2235
2236#[cfg(feature = "python")]
2237#[pymethods]
2238impl PivotStreamPy {
2239    #[new]
2240    fn new(mode: Option<usize>) -> PyResult<Self> {
2241        let params = PivotParams { mode };
2242        let inner =
2243            PivotStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
2244        Ok(PivotStreamPy { inner })
2245    }
2246
2247    fn update(
2248        &mut self,
2249        high: f64,
2250        low: f64,
2251        close: f64,
2252        open: f64,
2253    ) -> Option<(f64, f64, f64, f64, f64, f64, f64, f64, f64)> {
2254        self.inner.update(high, low, close, open)
2255    }
2256}
2257
2258#[cfg(feature = "python")]
2259#[pyfunction(name = "pivot_batch")]
2260#[pyo3(signature = (high, low, close, open, mode_range, kernel=None))]
2261pub fn pivot_batch_py<'py>(
2262    py: Python<'py>,
2263    high: PyReadonlyArray1<'py, f64>,
2264    low: PyReadonlyArray1<'py, f64>,
2265    close: PyReadonlyArray1<'py, f64>,
2266    open: PyReadonlyArray1<'py, f64>,
2267    mode_range: (usize, usize, usize),
2268    kernel: Option<&str>,
2269) -> PyResult<Bound<'py, PyDict>> {
2270    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
2271
2272    let (h, l, c, o) = (
2273        high.as_slice()?,
2274        low.as_slice()?,
2275        close.as_slice()?,
2276        open.as_slice()?,
2277    );
2278    let sweep = PivotBatchRange { mode: mode_range };
2279    let kern = validate_kernel(kernel, true)?;
2280
2281    let flat = py
2282        .allow_threads(|| pivot_batch_flat_with_kernel(h, l, c, o, &sweep, kern))
2283        .map_err(|e| PyValueError::new_err(e.to_string()))?;
2284
2285    let combos = flat.combos.len();
2286    let cols = flat.cols;
2287    let vals = flat.values;
2288
2289    let arr = unsafe { PyArray1::<f64>::new(py, [vals.len()], false) };
2290    unsafe {
2291        arr.as_slice_mut()?.copy_from_slice(&vals);
2292    }
2293
2294    let dict = PyDict::new(py);
2295    let names = ["r4", "r3", "r2", "r1", "pp", "s1", "s2", "s3", "s4"];
2296
2297    for (li, name) in names.iter().enumerate() {
2298        let level_arr = unsafe { PyArray1::<f64>::new(py, [combos * cols], false) };
2299        let level_slice = unsafe { level_arr.as_slice_mut()? };
2300
2301        for combo_idx in 0..combos {
2302            let base_idx = combo_idx * N_LEVELS * cols;
2303            let level_base = li * cols;
2304            for col_idx in 0..cols {
2305                level_slice[combo_idx * cols + col_idx] = vals[base_idx + level_base + col_idx];
2306            }
2307        }
2308
2309        dict.set_item(*name, level_arr.reshape((combos, cols))?)?;
2310    }
2311
2312    dict.set_item(
2313        "modes",
2314        flat.combos
2315            .iter()
2316            .map(|p| p.mode.unwrap() as u64)
2317            .collect::<Vec<_>>()
2318            .into_pyarray(py),
2319    )?;
2320    dict.set_item("rows_per_level", combos)?;
2321    dict.set_item("cols", cols)?;
2322    Ok(dict)
2323}
2324
2325#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2326#[wasm_bindgen]
2327pub fn pivot_js(
2328    high: &[f64],
2329    low: &[f64],
2330    close: &[f64],
2331    open: &[f64],
2332    mode: usize,
2333) -> Result<Vec<f64>, JsValue> {
2334    let len = high.len();
2335    if low.len() != len || close.len() != len || open.len() != len {
2336        return Err(JsValue::from_str(
2337            "pivot: Input arrays must have the same length",
2338        ));
2339    }
2340
2341    let params = PivotParams { mode: Some(mode) };
2342    let input = PivotInput::from_slices(high, low, close, open, params);
2343    let out =
2344        pivot_with_kernel(&input, Kernel::Auto).map_err(|e| JsValue::from_str(&e.to_string()))?;
2345    let cols = high.len();
2346    let mut values = Vec::with_capacity(N_LEVELS * cols);
2347    values.extend_from_slice(&out.r4);
2348    values.extend_from_slice(&out.r3);
2349    values.extend_from_slice(&out.r2);
2350    values.extend_from_slice(&out.r1);
2351    values.extend_from_slice(&out.pp);
2352    values.extend_from_slice(&out.s1);
2353    values.extend_from_slice(&out.s2);
2354    values.extend_from_slice(&out.s3);
2355    values.extend_from_slice(&out.s4);
2356    Ok(values)
2357}
2358
2359#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2360#[wasm_bindgen]
2361pub fn pivot_into(
2362    high_ptr: *const f64,
2363    low_ptr: *const f64,
2364    close_ptr: *const f64,
2365    open_ptr: *const f64,
2366    r4_ptr: *mut f64,
2367    r3_ptr: *mut f64,
2368    r2_ptr: *mut f64,
2369    r1_ptr: *mut f64,
2370    pp_ptr: *mut f64,
2371    s1_ptr: *mut f64,
2372    s2_ptr: *mut f64,
2373    s3_ptr: *mut f64,
2374    s4_ptr: *mut f64,
2375    len: usize,
2376    mode: usize,
2377) -> Result<(), JsValue> {
2378    if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || open_ptr.is_null() {
2379        return Err(JsValue::from_str("Null input pointer provided"));
2380    }
2381
2382    if r4_ptr.is_null()
2383        || r3_ptr.is_null()
2384        || r2_ptr.is_null()
2385        || r1_ptr.is_null()
2386        || pp_ptr.is_null()
2387        || s1_ptr.is_null()
2388        || s2_ptr.is_null()
2389        || s3_ptr.is_null()
2390        || s4_ptr.is_null()
2391    {
2392        return Err(JsValue::from_str("Null output pointer provided"));
2393    }
2394
2395    unsafe {
2396        let high = std::slice::from_raw_parts(high_ptr, len);
2397        let low = std::slice::from_raw_parts(low_ptr, len);
2398        let close = std::slice::from_raw_parts(close_ptr, len);
2399        let open = std::slice::from_raw_parts(open_ptr, len);
2400
2401        let params = PivotParams { mode: Some(mode) };
2402        let input = PivotInput::from_slices(high, low, close, open, params);
2403
2404        let input_ptrs = [
2405            high_ptr as *const u8,
2406            low_ptr as *const u8,
2407            close_ptr as *const u8,
2408            open_ptr as *const u8,
2409        ];
2410        let output_ptrs = [
2411            r4_ptr as *const u8,
2412            r3_ptr as *const u8,
2413            r2_ptr as *const u8,
2414            r1_ptr as *const u8,
2415            pp_ptr as *const u8,
2416            s1_ptr as *const u8,
2417            s2_ptr as *const u8,
2418            s3_ptr as *const u8,
2419            s4_ptr as *const u8,
2420        ];
2421
2422        let has_aliasing = input_ptrs
2423            .iter()
2424            .any(|&inp| output_ptrs.iter().any(|&out| inp == out));
2425
2426        if has_aliasing {
2427            let mut temp = vec![0.0; len * 9];
2428
2429            let (r4_temp, rest) = temp.split_at_mut(len);
2430            let (r3_temp, rest) = rest.split_at_mut(len);
2431            let (r2_temp, rest) = rest.split_at_mut(len);
2432            let (r1_temp, rest) = rest.split_at_mut(len);
2433            let (pp_temp, rest) = rest.split_at_mut(len);
2434            let (s1_temp, rest) = rest.split_at_mut(len);
2435            let (s2_temp, rest) = rest.split_at_mut(len);
2436            let (s3_temp, s4_temp) = rest.split_at_mut(len);
2437
2438            pivot_into_slices(
2439                r4_temp,
2440                r3_temp,
2441                r2_temp,
2442                r1_temp,
2443                pp_temp,
2444                s1_temp,
2445                s2_temp,
2446                s3_temp,
2447                s4_temp,
2448                &input,
2449                Kernel::Auto,
2450            )
2451            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2452
2453            let r4_out = std::slice::from_raw_parts_mut(r4_ptr, len);
2454            let r3_out = std::slice::from_raw_parts_mut(r3_ptr, len);
2455            let r2_out = std::slice::from_raw_parts_mut(r2_ptr, len);
2456            let r1_out = std::slice::from_raw_parts_mut(r1_ptr, len);
2457            let pp_out = std::slice::from_raw_parts_mut(pp_ptr, len);
2458            let s1_out = std::slice::from_raw_parts_mut(s1_ptr, len);
2459            let s2_out = std::slice::from_raw_parts_mut(s2_ptr, len);
2460            let s3_out = std::slice::from_raw_parts_mut(s3_ptr, len);
2461            let s4_out = std::slice::from_raw_parts_mut(s4_ptr, len);
2462
2463            r4_out.copy_from_slice(r4_temp);
2464            r3_out.copy_from_slice(r3_temp);
2465            r2_out.copy_from_slice(r2_temp);
2466            r1_out.copy_from_slice(r1_temp);
2467            pp_out.copy_from_slice(pp_temp);
2468            s1_out.copy_from_slice(s1_temp);
2469            s2_out.copy_from_slice(s2_temp);
2470            s3_out.copy_from_slice(s3_temp);
2471            s4_out.copy_from_slice(s4_temp);
2472        } else {
2473            let r4_out = std::slice::from_raw_parts_mut(r4_ptr, len);
2474            let r3_out = std::slice::from_raw_parts_mut(r3_ptr, len);
2475            let r2_out = std::slice::from_raw_parts_mut(r2_ptr, len);
2476            let r1_out = std::slice::from_raw_parts_mut(r1_ptr, len);
2477            let pp_out = std::slice::from_raw_parts_mut(pp_ptr, len);
2478            let s1_out = std::slice::from_raw_parts_mut(s1_ptr, len);
2479            let s2_out = std::slice::from_raw_parts_mut(s2_ptr, len);
2480            let s3_out = std::slice::from_raw_parts_mut(s3_ptr, len);
2481            let s4_out = std::slice::from_raw_parts_mut(s4_ptr, len);
2482
2483            pivot_into_slices(
2484                r4_out,
2485                r3_out,
2486                r2_out,
2487                r1_out,
2488                pp_out,
2489                s1_out,
2490                s2_out,
2491                s3_out,
2492                s4_out,
2493                &input,
2494                Kernel::Auto,
2495            )
2496            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2497        }
2498
2499        Ok(())
2500    }
2501}
2502
2503#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2504#[wasm_bindgen]
2505pub fn pivot_alloc(len: usize) -> *mut f64 {
2506    let mut vec = Vec::<f64>::with_capacity(len);
2507    let ptr = vec.as_mut_ptr();
2508    std::mem::forget(vec);
2509    ptr
2510}
2511
2512#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2513#[wasm_bindgen]
2514pub fn pivot_free(ptr: *mut f64, len: usize) {
2515    if !ptr.is_null() {
2516        unsafe {
2517            let _ = Vec::from_raw_parts(ptr, len, len);
2518        }
2519    }
2520}
2521
2522#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2523#[derive(Serialize, Deserialize)]
2524pub struct PivotBatchConfig {
2525    pub mode_range: (usize, usize, usize),
2526}
2527
2528#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2529#[derive(Serialize, Deserialize)]
2530pub struct PivotBatchFlatJsOutput {
2531    pub values: Vec<f64>,
2532    pub modes: Vec<usize>,
2533    pub rows: usize,
2534    pub cols: usize,
2535}
2536
2537#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
2538#[wasm_bindgen(js_name = pivot_batch)]
2539pub fn pivot_batch_js(
2540    high: &[f64],
2541    low: &[f64],
2542    close: &[f64],
2543    open: &[f64],
2544    config: JsValue,
2545) -> Result<JsValue, JsValue> {
2546    let cfg: PivotBatchConfig =
2547        serde_wasm_bindgen::from_value(config).map_err(|e| JsValue::from_str(&e.to_string()))?;
2548    let sweep = PivotBatchRange {
2549        mode: cfg.mode_range,
2550    };
2551    let flat = pivot_batch_flat_with_kernel(high, low, close, open, &sweep, Kernel::Auto)
2552        .map_err(|e| JsValue::from_str(&e.to_string()))?;
2553    let modes = flat.combos.iter().map(|p| p.mode.unwrap()).collect();
2554    let out = PivotBatchFlatJsOutput {
2555        values: flat.values,
2556        modes,
2557        rows: flat.rows,
2558        cols: flat.cols,
2559    };
2560    serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
2561}
2562
2563#[cfg(test)]
2564mod tests {
2565    use super::*;
2566    use crate::skip_if_unsupported;
2567    use crate::utilities::data_loader::read_candles_from_csv;
2568    use crate::utilities::enums::Kernel;
2569    use paste::paste;
2570
2571    fn check_pivot_default_mode_camarilla(
2572        test_name: &str,
2573        kernel: Kernel,
2574    ) -> Result<(), Box<dyn std::error::Error>> {
2575        skip_if_unsupported!(kernel, test_name);
2576        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2577        let candles = read_candles_from_csv(file_path)?;
2578
2579        let params = PivotParams { mode: None };
2580        let input = PivotInput::from_candles(&candles, params);
2581        let result = pivot_with_kernel(&input, kernel)?;
2582
2583        assert_eq!(result.r4.len(), candles.close.len());
2584        assert_eq!(result.r3.len(), candles.close.len());
2585        assert_eq!(result.r2.len(), candles.close.len());
2586        assert_eq!(result.r1.len(), candles.close.len());
2587        assert_eq!(result.pp.len(), candles.close.len());
2588        assert_eq!(result.s1.len(), candles.close.len());
2589        assert_eq!(result.s2.len(), candles.close.len());
2590        assert_eq!(result.s3.len(), candles.close.len());
2591        assert_eq!(result.s4.len(), candles.close.len());
2592
2593        let last_five_r4 = &result.r4[result.r4.len().saturating_sub(5)..];
2594        let expected_r4 = [59466.5, 59357.55, 59243.6, 59334.85, 59170.35];
2595        for (i, &val) in last_five_r4.iter().enumerate() {
2596            let exp = expected_r4[i];
2597            assert!(
2598                (val - exp).abs() < 1e-1,
2599                "Camarilla r4 mismatch at index {}, expected {}, got {}",
2600                i,
2601                exp,
2602                val
2603            );
2604        }
2605        Ok(())
2606    }
2607
2608    fn check_pivot_nan_values(
2609        test_name: &str,
2610        kernel: Kernel,
2611    ) -> Result<(), Box<dyn std::error::Error>> {
2612        skip_if_unsupported!(kernel, test_name);
2613        let high = [10.0, f64::NAN, 30.0];
2614        let low = [9.0, 8.5, f64::NAN];
2615        let close = [9.5, 9.0, 29.0];
2616        let open = [9.1, 8.8, 28.5];
2617
2618        let params = PivotParams { mode: Some(3) };
2619        let input = PivotInput::from_slices(&high, &low, &close, &open, params);
2620        let result = pivot_with_kernel(&input, kernel)?;
2621        assert_eq!(result.pp.len(), high.len());
2622        Ok(())
2623    }
2624
2625    fn check_pivot_no_data(
2626        test_name: &str,
2627        kernel: Kernel,
2628    ) -> Result<(), Box<dyn std::error::Error>> {
2629        skip_if_unsupported!(kernel, test_name);
2630        let high: [f64; 0] = [];
2631        let low: [f64; 0] = [];
2632        let close: [f64; 0] = [];
2633        let open: [f64; 0] = [];
2634        let params = PivotParams { mode: Some(3) };
2635        let input = PivotInput::from_slices(&high, &low, &close, &open, params);
2636        let result = pivot_with_kernel(&input, kernel);
2637        assert!(result.is_err());
2638        if let Err(e) = result {
2639            assert!(
2640                e.to_string().contains("One or more required fields"),
2641                "Expected 'EmptyData' error, got: {}",
2642                e
2643            );
2644        }
2645        Ok(())
2646    }
2647
2648    fn check_pivot_all_nan(
2649        test_name: &str,
2650        kernel: Kernel,
2651    ) -> Result<(), Box<dyn std::error::Error>> {
2652        skip_if_unsupported!(kernel, test_name);
2653        let high = [f64::NAN, f64::NAN];
2654        let low = [f64::NAN, f64::NAN];
2655        let close = [f64::NAN, f64::NAN];
2656        let open = [f64::NAN, f64::NAN];
2657        let params = PivotParams { mode: Some(3) };
2658        let input = PivotInput::from_slices(&high, &low, &close, &open, params);
2659        let result = pivot_with_kernel(&input, kernel);
2660        assert!(result.is_err());
2661        if let Err(e) = result {
2662            assert!(
2663                e.to_string().contains("All values are NaN"),
2664                "Expected 'AllValuesNaN' error, got: {}",
2665                e
2666            );
2667        }
2668        Ok(())
2669    }
2670
2671    fn check_pivot_fibonacci_mode(
2672        test_name: &str,
2673        kernel: Kernel,
2674    ) -> Result<(), Box<dyn std::error::Error>> {
2675        skip_if_unsupported!(kernel, test_name);
2676        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2677        let candles = read_candles_from_csv(file)?;
2678        let params = PivotParams { mode: Some(1) };
2679        let input = PivotInput::from_candles(&candles, params);
2680        let output = pivot_with_kernel(&input, kernel)?;
2681        assert_eq!(output.r3.len(), candles.close.len());
2682        Ok(())
2683    }
2684
2685    fn check_pivot_standard_mode(
2686        test_name: &str,
2687        kernel: Kernel,
2688    ) -> Result<(), Box<dyn std::error::Error>> {
2689        skip_if_unsupported!(kernel, test_name);
2690        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2691        let candles = read_candles_from_csv(file)?;
2692        let params = PivotParams { mode: Some(0) };
2693        let input = PivotInput::from_candles(&candles, params);
2694        let output = pivot_with_kernel(&input, kernel)?;
2695        assert_eq!(output.r2.len(), candles.close.len());
2696        Ok(())
2697    }
2698
2699    fn check_pivot_demark_mode(
2700        test_name: &str,
2701        kernel: Kernel,
2702    ) -> Result<(), Box<dyn std::error::Error>> {
2703        skip_if_unsupported!(kernel, test_name);
2704        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2705        let candles = read_candles_from_csv(file)?;
2706        let params = PivotParams { mode: Some(2) };
2707        let input = PivotInput::from_candles(&candles, params);
2708        let output = pivot_with_kernel(&input, kernel)?;
2709        assert_eq!(output.r1.len(), candles.close.len());
2710        Ok(())
2711    }
2712
2713    fn check_pivot_woodie_mode(
2714        test_name: &str,
2715        kernel: Kernel,
2716    ) -> Result<(), Box<dyn std::error::Error>> {
2717        skip_if_unsupported!(kernel, test_name);
2718        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
2719        let candles = read_candles_from_csv(file)?;
2720        let params = PivotParams { mode: Some(4) };
2721        let input = PivotInput::from_candles(&candles, params);
2722        let output = pivot_with_kernel(&input, kernel)?;
2723        assert_eq!(output.r4.len(), candles.close.len());
2724        Ok(())
2725    }
2726
2727    #[cfg(feature = "proptest")]
2728    #[allow(clippy::float_cmp)]
2729    fn check_pivot_property(
2730        test_name: &str,
2731        kernel: Kernel,
2732    ) -> Result<(), Box<dyn std::error::Error>> {
2733        use proptest::prelude::*;
2734        skip_if_unsupported!(kernel, test_name);
2735
2736        let strat = (10usize..=200).prop_flat_map(|len| {
2737            prop_oneof![
2738                prop::collection::vec(
2739                    (100f64..10000f64).prop_filter("finite", |x| x.is_finite()),
2740                    len,
2741                )
2742                .prop_flat_map(move |base_prices| {
2743                    let ohlc_strat = prop::collection::vec(
2744                        (0f64..1f64, 0f64..1f64, 0f64..1f64, 0f64..1f64),
2745                        len,
2746                    );
2747
2748                    (ohlc_strat, 0usize..=4).prop_map(move |(factors, mode)| {
2749                        let mut high_data = Vec::with_capacity(len);
2750                        let mut low_data = Vec::with_capacity(len);
2751                        let mut close_data = Vec::with_capacity(len);
2752                        let mut open_data = Vec::with_capacity(len);
2753
2754                        for (i, base) in base_prices.iter().enumerate() {
2755                            let (high_factor, low_factor, close_factor, open_factor) = factors[i];
2756
2757                            let range = base * 0.1;
2758                            let low = base - range * low_factor;
2759                            let high = base + range * high_factor;
2760                            let open = low + (high - low) * open_factor;
2761                            let close = low + (high - low) * close_factor;
2762
2763                            high_data.push(high);
2764                            low_data.push(low);
2765                            open_data.push(open);
2766                            close_data.push(close);
2767                        }
2768
2769                        (high_data, low_data, close_data, open_data, mode)
2770                    })
2771                }),
2772                (100f64..1000f64, 0usize..=4).prop_map(move |(price, mode)| {
2773                    let data = vec![price; len];
2774                    (data.clone(), data.clone(), data.clone(), data, mode)
2775                }),
2776                (100f64..1000f64, 0usize..=4).prop_map(move |(base, mode)| {
2777                    let mut high_data = Vec::with_capacity(len);
2778                    let mut low_data = Vec::with_capacity(len);
2779                    let mut close_data = Vec::with_capacity(len);
2780                    let mut open_data = Vec::with_capacity(len);
2781
2782                    for _ in 0..len {
2783                        let epsilon = 1e-10;
2784                        let low = base;
2785                        let high = base + epsilon;
2786                        let open = base + epsilon * 0.3;
2787                        let close = base + epsilon * 0.7;
2788
2789                        high_data.push(high);
2790                        low_data.push(low);
2791                        open_data.push(open);
2792                        close_data.push(close);
2793                    }
2794
2795                    (high_data, low_data, close_data, open_data, mode)
2796                }),
2797            ]
2798        });
2799
2800        proptest::test_runner::TestRunner::default().run(
2801            &strat,
2802            |(high, low, close, open, mode)| {
2803                let params = PivotParams { mode: Some(mode) };
2804                let input = PivotInput::from_slices(&high, &low, &close, &open, params);
2805
2806                let output = pivot_with_kernel(&input, kernel)?;
2807                let ref_output = pivot_with_kernel(&input, Kernel::Scalar)?;
2808
2809                prop_assert_eq!(output.pp.len(), high.len());
2810                prop_assert_eq!(output.r1.len(), high.len());
2811                prop_assert_eq!(output.s1.len(), high.len());
2812
2813                for i in 0..high.len() {
2814                    let h = high[i];
2815                    let l = low[i];
2816                    let c = close[i];
2817                    let o = open[i];
2818
2819                    if h.is_nan() || l.is_nan() || c.is_nan() || o.is_nan() {
2820                        continue;
2821                    }
2822
2823                    let pp = output.pp[i];
2824                    let r4 = output.r4[i];
2825                    let r3 = output.r3[i];
2826                    let r2 = output.r2[i];
2827                    let r1 = output.r1[i];
2828                    let s1 = output.s1[i];
2829                    let s2 = output.s2[i];
2830                    let s3 = output.s3[i];
2831                    let s4 = output.s4[i];
2832
2833                    let tolerance = 1e-9;
2834                    let range = h - l;
2835
2836                    match mode {
2837                        0 => {
2838                            let expected_pp = (h + l + c) / 3.0;
2839                            prop_assert!(
2840                                (pp - expected_pp).abs() < tolerance,
2841                                "Standard PP at {}: {} vs {}",
2842                                i,
2843                                pp,
2844                                expected_pp
2845                            );
2846
2847                            let expected_r1 = 2.0 * pp - l;
2848                            prop_assert!(
2849                                (r1 - expected_r1).abs() < tolerance,
2850                                "Standard R1 at {}: {} vs {}",
2851                                i,
2852                                r1,
2853                                expected_r1
2854                            );
2855
2856                            let expected_r2 = pp + range;
2857                            prop_assert!(
2858                                (r2 - expected_r2).abs() < tolerance,
2859                                "Standard R2 at {}: {} vs {}",
2860                                i,
2861                                r2,
2862                                expected_r2
2863                            );
2864
2865                            let expected_s1 = 2.0 * pp - h;
2866                            prop_assert!(
2867                                (s1 - expected_s1).abs() < tolerance,
2868                                "Standard S1 at {}: {} vs {}",
2869                                i,
2870                                s1,
2871                                expected_s1
2872                            );
2873
2874                            let expected_s2 = pp - range;
2875                            prop_assert!(
2876                                (s2 - expected_s2).abs() < tolerance,
2877                                "Standard S2 at {}: {} vs {}",
2878                                i,
2879                                s2,
2880                                expected_s2
2881                            );
2882
2883                            prop_assert!(r3.is_nan(), "Standard R3 should be NaN at {}", i);
2884                            prop_assert!(r4.is_nan(), "Standard R4 should be NaN at {}", i);
2885                            prop_assert!(s3.is_nan(), "Standard S3 should be NaN at {}", i);
2886                            prop_assert!(s4.is_nan(), "Standard S4 should be NaN at {}", i);
2887
2888                            prop_assert!(s2 <= s1 + tolerance, "S2 > S1 at {}", i);
2889                            prop_assert!(s1 <= pp + tolerance, "S1 > PP at {}", i);
2890                            prop_assert!(pp <= r1 + tolerance, "PP > R1 at {}", i);
2891                            prop_assert!(r1 <= r2 + tolerance, "R1 > R2 at {}", i);
2892                        }
2893                        1 => {
2894                            let expected_pp = (h + l + c) / 3.0;
2895                            prop_assert!(
2896                                (pp - expected_pp).abs() < tolerance,
2897                                "Fibonacci PP at {}: {} vs {}",
2898                                i,
2899                                pp,
2900                                expected_pp
2901                            );
2902
2903                            let expected_r1 = pp + 0.382 * range;
2904                            let expected_r2 = pp + 0.618 * range;
2905                            let expected_r3 = pp + 1.0 * range;
2906                            let expected_s1 = pp - 0.382 * range;
2907                            let expected_s2 = pp - 0.618 * range;
2908                            let expected_s3 = pp - 1.0 * range;
2909
2910                            prop_assert!(
2911                                (r1 - expected_r1).abs() < tolerance,
2912                                "Fibonacci R1 at {}: {} vs {}",
2913                                i,
2914                                r1,
2915                                expected_r1
2916                            );
2917                            prop_assert!(
2918                                (r2 - expected_r2).abs() < tolerance,
2919                                "Fibonacci R2 at {}: {} vs {}",
2920                                i,
2921                                r2,
2922                                expected_r2
2923                            );
2924                            prop_assert!(
2925                                (r3 - expected_r3).abs() < tolerance,
2926                                "Fibonacci R3 at {}: {} vs {}",
2927                                i,
2928                                r3,
2929                                expected_r3
2930                            );
2931                            prop_assert!(
2932                                (s1 - expected_s1).abs() < tolerance,
2933                                "Fibonacci S1 at {}: {} vs {}",
2934                                i,
2935                                s1,
2936                                expected_s1
2937                            );
2938                            prop_assert!(
2939                                (s2 - expected_s2).abs() < tolerance,
2940                                "Fibonacci S2 at {}: {} vs {}",
2941                                i,
2942                                s2,
2943                                expected_s2
2944                            );
2945                            prop_assert!(
2946                                (s3 - expected_s3).abs() < tolerance,
2947                                "Fibonacci S3 at {}: {} vs {}",
2948                                i,
2949                                s3,
2950                                expected_s3
2951                            );
2952
2953                            prop_assert!(r4.is_nan(), "Fibonacci R4 should be NaN at {}", i);
2954                            prop_assert!(s4.is_nan(), "Fibonacci S4 should be NaN at {}", i);
2955
2956                            prop_assert!(s3 <= s2 + tolerance, "S3 > S2 at {}", i);
2957                            prop_assert!(s2 <= s1 + tolerance, "S2 > S1 at {}", i);
2958                            prop_assert!(s1 <= pp + tolerance, "S1 > PP at {}", i);
2959                            prop_assert!(pp <= r1 + tolerance, "PP > R1 at {}", i);
2960                            prop_assert!(r1 <= r2 + tolerance, "R1 > R2 at {}", i);
2961                            prop_assert!(r2 <= r3 + tolerance, "R2 > R3 at {}", i);
2962                        }
2963                        2 => {
2964                            let expected_pp = if c < o {
2965                                (h + 2.0 * l + c) / 4.0
2966                            } else if c > o {
2967                                (2.0 * h + l + c) / 4.0
2968                            } else {
2969                                (h + l + 2.0 * c) / 4.0
2970                            };
2971                            prop_assert!(
2972                                (pp - expected_pp).abs() < tolerance,
2973                                "Demark PP at {}: {} vs {}",
2974                                i,
2975                                pp,
2976                                expected_pp
2977                            );
2978
2979                            let expected_r1 = if c < o {
2980                                (h + 2.0 * l + c) / 2.0 - l
2981                            } else if c > o {
2982                                (2.0 * h + l + c) / 2.0 - l
2983                            } else {
2984                                (h + l + 2.0 * c) / 2.0 - l
2985                            };
2986                            let expected_s1 = if c < o {
2987                                (h + 2.0 * l + c) / 2.0 - h
2988                            } else if c > o {
2989                                (2.0 * h + l + c) / 2.0 - h
2990                            } else {
2991                                (h + l + 2.0 * c) / 2.0 - h
2992                            };
2993
2994                            prop_assert!(
2995                                (r1 - expected_r1).abs() < tolerance,
2996                                "Demark R1 at {}: {} vs {}",
2997                                i,
2998                                r1,
2999                                expected_r1
3000                            );
3001                            prop_assert!(
3002                                (s1 - expected_s1).abs() < tolerance,
3003                                "Demark S1 at {}: {} vs {}",
3004                                i,
3005                                s1,
3006                                expected_s1
3007                            );
3008
3009                            prop_assert!(r2.is_nan(), "Demark R2 should be NaN at {}", i);
3010                            prop_assert!(r3.is_nan(), "Demark R3 should be NaN at {}", i);
3011                            prop_assert!(r4.is_nan(), "Demark R4 should be NaN at {}", i);
3012                            prop_assert!(s2.is_nan(), "Demark S2 should be NaN at {}", i);
3013                            prop_assert!(s3.is_nan(), "Demark S3 should be NaN at {}", i);
3014                            prop_assert!(s4.is_nan(), "Demark S4 should be NaN at {}", i);
3015                        }
3016                        3 => {
3017                            let expected_pp = (h + l + c) / 3.0;
3018                            prop_assert!(
3019                                (pp - expected_pp).abs() < tolerance,
3020                                "Camarilla PP at {}: {} vs {}",
3021                                i,
3022                                pp,
3023                                expected_pp
3024                            );
3025
3026                            let expected_r4 = 0.55 * range + c;
3027                            let expected_r3 = 0.275 * range + c;
3028                            let expected_r2 = 0.183 * range + c;
3029                            let expected_r1 = 0.0916 * range + c;
3030                            let expected_s1 = c - 0.0916 * range;
3031                            let expected_s2 = c - 0.183 * range;
3032                            let expected_s3 = c - 0.275 * range;
3033                            let expected_s4 = c - 0.55 * range;
3034
3035                            prop_assert!(
3036                                (r4 - expected_r4).abs() < tolerance,
3037                                "Camarilla R4 at {}: {} vs {}",
3038                                i,
3039                                r4,
3040                                expected_r4
3041                            );
3042                            prop_assert!(
3043                                (r3 - expected_r3).abs() < tolerance,
3044                                "Camarilla R3 at {}: {} vs {}",
3045                                i,
3046                                r3,
3047                                expected_r3
3048                            );
3049                            prop_assert!(
3050                                (r2 - expected_r2).abs() < tolerance,
3051                                "Camarilla R2 at {}: {} vs {}",
3052                                i,
3053                                r2,
3054                                expected_r2
3055                            );
3056                            prop_assert!(
3057                                (r1 - expected_r1).abs() < tolerance,
3058                                "Camarilla R1 at {}: {} vs {}",
3059                                i,
3060                                r1,
3061                                expected_r1
3062                            );
3063                            prop_assert!(
3064                                (s1 - expected_s1).abs() < tolerance,
3065                                "Camarilla S1 at {}: {} vs {}",
3066                                i,
3067                                s1,
3068                                expected_s1
3069                            );
3070                            prop_assert!(
3071                                (s2 - expected_s2).abs() < tolerance,
3072                                "Camarilla S2 at {}: {} vs {}",
3073                                i,
3074                                s2,
3075                                expected_s2
3076                            );
3077                            prop_assert!(
3078                                (s3 - expected_s3).abs() < tolerance,
3079                                "Camarilla S3 at {}: {} vs {}",
3080                                i,
3081                                s3,
3082                                expected_s3
3083                            );
3084                            prop_assert!(
3085                                (s4 - expected_s4).abs() < tolerance,
3086                                "Camarilla S4 at {}: {} vs {}",
3087                                i,
3088                                s4,
3089                                expected_s4
3090                            );
3091
3092                            prop_assert!(s4 <= s3 + tolerance, "S4 > S3 at {}", i);
3093                            prop_assert!(s3 <= s2 + tolerance, "S3 > S2 at {}", i);
3094                            prop_assert!(s2 <= s1 + tolerance, "S2 > S1 at {}", i);
3095                            prop_assert!(r1 <= r2 + tolerance, "R1 > R2 at {}", i);
3096                            prop_assert!(r2 <= r3 + tolerance, "R2 > R3 at {}", i);
3097                            prop_assert!(r3 <= r4 + tolerance, "R3 > R4 at {}", i);
3098                        }
3099                        4 => {
3100                            let expected_pp = (h + l + 2.0 * o) / 4.0;
3101                            prop_assert!(
3102                                (pp - expected_pp).abs() < tolerance,
3103                                "Woodie PP at {}: {} vs {}",
3104                                i,
3105                                pp,
3106                                expected_pp
3107                            );
3108
3109                            let expected_r1 = 2.0 * pp - l;
3110                            let expected_r2 = pp + range;
3111                            let expected_r3 = h + 2.0 * (pp - l);
3112                            let expected_r4 = expected_r3 + range;
3113                            let expected_s1 = 2.0 * pp - h;
3114                            let expected_s2 = pp - range;
3115                            let expected_s3 = l - 2.0 * (h - pp);
3116                            let expected_s4 = expected_s3 - range;
3117
3118                            prop_assert!(
3119                                (r1 - expected_r1).abs() < tolerance,
3120                                "Woodie R1 at {}: {} vs {}",
3121                                i,
3122                                r1,
3123                                expected_r1
3124                            );
3125                            prop_assert!(
3126                                (r2 - expected_r2).abs() < tolerance,
3127                                "Woodie R2 at {}: {} vs {}",
3128                                i,
3129                                r2,
3130                                expected_r2
3131                            );
3132                            prop_assert!(
3133                                (r3 - expected_r3).abs() < tolerance,
3134                                "Woodie R3 at {}: {} vs {}",
3135                                i,
3136                                r3,
3137                                expected_r3
3138                            );
3139                            prop_assert!(
3140                                (r4 - expected_r4).abs() < tolerance,
3141                                "Woodie R4 at {}: {} vs {}",
3142                                i,
3143                                r4,
3144                                expected_r4
3145                            );
3146                            prop_assert!(
3147                                (s1 - expected_s1).abs() < tolerance,
3148                                "Woodie S1 at {}: {} vs {}",
3149                                i,
3150                                s1,
3151                                expected_s1
3152                            );
3153                            prop_assert!(
3154                                (s2 - expected_s2).abs() < tolerance,
3155                                "Woodie S2 at {}: {} vs {}",
3156                                i,
3157                                s2,
3158                                expected_s2
3159                            );
3160                            prop_assert!(
3161                                (s3 - expected_s3).abs() < tolerance,
3162                                "Woodie S3 at {}: {} vs {}",
3163                                i,
3164                                s3,
3165                                expected_s3
3166                            );
3167                            prop_assert!(
3168                                (s4 - expected_s4).abs() < tolerance,
3169                                "Woodie S4 at {}: {} vs {}",
3170                                i,
3171                                s4,
3172                                expected_s4
3173                            );
3174
3175                            prop_assert!(s4 <= s3 + tolerance, "S4 > S3 at {}", i);
3176                            prop_assert!(s3 <= s2 + tolerance, "S3 > S2 at {}", i);
3177                            prop_assert!(s2 <= s1 + tolerance, "S2 > S1 at {}", i);
3178                            prop_assert!(r1 <= r2 + tolerance, "R1 > R2 at {}", i);
3179                            prop_assert!(r2 <= r3 + tolerance, "R2 > R3 at {}", i);
3180                            prop_assert!(r3 <= r4 + tolerance, "R3 > R4 at {}", i);
3181                        }
3182                        _ => {}
3183                    }
3184
3185                    prop_assert!(
3186                        (pp - ref_output.pp[i]).abs() < tolerance,
3187                        "PP kernel mismatch at {}",
3188                        i
3189                    );
3190                    prop_assert!(
3191                        (r1 - ref_output.r1[i]).abs() < tolerance
3192                            || (r1.is_nan() && ref_output.r1[i].is_nan()),
3193                        "R1 kernel mismatch at {}",
3194                        i
3195                    );
3196                    prop_assert!(
3197                        (s1 - ref_output.s1[i]).abs() < tolerance
3198                            || (s1.is_nan() && ref_output.s1[i].is_nan()),
3199                        "S1 kernel mismatch at {}",
3200                        i
3201                    );
3202
3203                    #[cfg(debug_assertions)]
3204                    {
3205                        let check_poison = |val: f64, name: &str| {
3206                            if !val.is_nan() {
3207                                let bits = val.to_bits();
3208                                prop_assert_ne!(
3209                                    bits,
3210                                    0x11111111_11111111,
3211                                    "{} poison at {}",
3212                                    name,
3213                                    i
3214                                );
3215                                prop_assert_ne!(
3216                                    bits,
3217                                    0x22222222_22222222,
3218                                    "{} poison at {}",
3219                                    name,
3220                                    i
3221                                );
3222                                prop_assert_ne!(
3223                                    bits,
3224                                    0x33333333_33333333,
3225                                    "{} poison at {}",
3226                                    name,
3227                                    i
3228                                );
3229                            }
3230                            Ok(())
3231                        };
3232
3233                        check_poison(pp, "PP")?;
3234                        check_poison(r4, "R4")?;
3235                        check_poison(r3, "R3")?;
3236                        check_poison(r2, "R2")?;
3237                        check_poison(r1, "R1")?;
3238                        check_poison(s1, "S1")?;
3239                        check_poison(s2, "S2")?;
3240                        check_poison(s3, "S3")?;
3241                        check_poison(s4, "S4")?;
3242                    }
3243                }
3244
3245                Ok(())
3246            },
3247        )?;
3248
3249        Ok(())
3250    }
3251
3252    #[cfg(debug_assertions)]
3253    fn check_pivot_no_poison(
3254        test_name: &str,
3255        kernel: Kernel,
3256    ) -> Result<(), Box<dyn std::error::Error>> {
3257        skip_if_unsupported!(kernel, test_name);
3258
3259        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3260        let candles = read_candles_from_csv(file_path)?;
3261
3262        let test_params = vec![
3263            PivotParams::default(),
3264            PivotParams { mode: Some(0) },
3265            PivotParams { mode: Some(1) },
3266            PivotParams { mode: Some(2) },
3267            PivotParams { mode: Some(3) },
3268            PivotParams { mode: Some(4) },
3269        ];
3270
3271        for (param_idx, params) in test_params.iter().enumerate() {
3272            let input = PivotInput::from_candles(&candles, params.clone());
3273            let output = pivot_with_kernel(&input, kernel)?;
3274
3275            let arrays = vec![
3276                ("r4", &output.r4),
3277                ("r3", &output.r3),
3278                ("r2", &output.r2),
3279                ("r1", &output.r1),
3280                ("pp", &output.pp),
3281                ("s1", &output.s1),
3282                ("s2", &output.s2),
3283                ("s3", &output.s3),
3284                ("s4", &output.s4),
3285            ];
3286
3287            for (array_name, values) in arrays {
3288                for (i, &val) in values.iter().enumerate() {
3289                    if val.is_nan() {
3290                        continue;
3291                    }
3292
3293                    let bits = val.to_bits();
3294
3295                    if bits == 0x11111111_11111111 {
3296                        panic!(
3297							"[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
3298							 in array {} with params: {:?} (param set {})",
3299							test_name, val, bits, i, array_name, params, param_idx
3300						);
3301                    }
3302
3303                    if bits == 0x22222222_22222222 {
3304                        panic!(
3305							"[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
3306							 in array {} with params: {:?} (param set {})",
3307							test_name, val, bits, i, array_name, params, param_idx
3308						);
3309                    }
3310
3311                    if bits == 0x33333333_33333333 {
3312                        panic!(
3313							"[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
3314							 in array {} with params: {:?} (param set {})",
3315							test_name, val, bits, i, array_name, params, param_idx
3316						);
3317                    }
3318                }
3319            }
3320        }
3321
3322        Ok(())
3323    }
3324
3325    #[cfg(not(debug_assertions))]
3326    fn check_pivot_no_poison(
3327        _test_name: &str,
3328        _kernel: Kernel,
3329    ) -> Result<(), Box<dyn std::error::Error>> {
3330        Ok(())
3331    }
3332
3333    fn check_pivot_batch_default_row(
3334        test_name: &str,
3335        kernel: Kernel,
3336    ) -> Result<(), Box<dyn std::error::Error>> {
3337        skip_if_unsupported!(kernel, test_name);
3338        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3339        let candles = read_candles_from_csv(file)?;
3340        let output = PivotBatchBuilder::new()
3341            .kernel(kernel)
3342            .apply_candles(&candles)?;
3343        let default = PivotParams::default();
3344        let def_idx = output
3345            .combos
3346            .iter()
3347            .position(|p| p.mode == default.mode)
3348            .expect("default row missing");
3349        for arr in &output.levels[def_idx] {
3350            assert_eq!(arr.len(), candles.close.len());
3351        }
3352        Ok(())
3353    }
3354
3355    macro_rules! generate_all_pivot_tests {
3356        ($($test_fn:ident),*) => {
3357            paste! {
3358                $(
3359                    #[test]
3360                    fn [<$test_fn _scalar>]() { let _ = $test_fn(stringify!([<$test_fn _scalar>]), Kernel::Scalar); }
3361                )*
3362                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3363                $(
3364                    #[test]
3365                    fn [<$test_fn _avx2>]() { let _ = $test_fn(stringify!([<$test_fn _avx2>]), Kernel::Avx2); }
3366                    #[test]
3367                    fn [<$test_fn _avx512>]() { let _ = $test_fn(stringify!([<$test_fn _avx512>]), Kernel::Avx512); }
3368                )*
3369                $(
3370                    #[test]
3371                    fn [<$test_fn _auto_detect>]() { let _ = $test_fn(stringify!([<$test_fn _auto_detect>]), Kernel::Auto); }
3372                )*
3373            }
3374        }
3375    }
3376
3377    generate_all_pivot_tests!(
3378        check_pivot_default_mode_camarilla,
3379        check_pivot_nan_values,
3380        check_pivot_no_data,
3381        check_pivot_all_nan,
3382        check_pivot_fibonacci_mode,
3383        check_pivot_standard_mode,
3384        check_pivot_demark_mode,
3385        check_pivot_woodie_mode,
3386        check_pivot_batch_default_row,
3387        check_pivot_no_poison
3388    );
3389
3390    #[cfg(feature = "proptest")]
3391    generate_all_pivot_tests!(check_pivot_property);
3392
3393    fn check_batch_default_row(
3394        test: &str,
3395        kernel: Kernel,
3396    ) -> Result<(), Box<dyn std::error::Error>> {
3397        skip_if_unsupported!(kernel, test);
3398
3399        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3400        let candles = read_candles_from_csv(file)?;
3401
3402        let output = PivotBatchBuilder::new()
3403            .kernel(kernel)
3404            .apply_candles(&candles)?;
3405
3406        let def = PivotParams::default();
3407        let row = output
3408            .combos
3409            .iter()
3410            .position(|p| p.mode == def.mode)
3411            .expect("default row missing");
3412        let levels = &output.levels[row];
3413
3414        for arr in levels.iter() {
3415            assert_eq!(arr.len(), candles.close.len());
3416        }
3417
3418        let expected_r4 = [59466.5, 59357.55, 59243.6, 59334.85, 59170.35];
3419        let r4 = &levels[0];
3420        let last_five_r4 = &r4[r4.len().saturating_sub(5)..];
3421        for (i, &val) in last_five_r4.iter().enumerate() {
3422            let exp = expected_r4[i];
3423            assert!(
3424                (val - exp).abs() < 1e-1,
3425                "[{test}] Camarilla r4 mismatch at idx {i}: {val} vs {exp:?}"
3426            );
3427        }
3428        Ok(())
3429    }
3430
3431    #[cfg(debug_assertions)]
3432    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn std::error::Error>> {
3433        skip_if_unsupported!(kernel, test);
3434
3435        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3436        let c = read_candles_from_csv(file)?;
3437
3438        let test_configs = vec![
3439            (0, 2, 1),
3440            (0, 4, 1),
3441            (0, 4, 2),
3442            (1, 3, 1),
3443            (3, 4, 1),
3444            (2, 2, 1),
3445            (0, 0, 1),
3446        ];
3447
3448        for (cfg_idx, &(mode_start, mode_end, mode_step)) in test_configs.iter().enumerate() {
3449            let output = PivotBatchBuilder::new()
3450                .kernel(kernel)
3451                .mode_range(mode_start, mode_end, mode_step)
3452                .apply_candles(&c)?;
3453
3454            for (row_idx, levels) in output.levels.iter().enumerate() {
3455                let combo = &output.combos[row_idx];
3456
3457                for (level_idx, level_array) in levels.iter().enumerate() {
3458                    let level_name = match level_idx {
3459                        0 => "r4",
3460                        1 => "r3",
3461                        2 => "r2",
3462                        3 => "r1",
3463                        4 => "pp",
3464                        5 => "s1",
3465                        6 => "s2",
3466                        7 => "s3",
3467                        8 => "s4",
3468                        _ => "unknown",
3469                    };
3470
3471                    for (col, &val) in level_array.iter().enumerate() {
3472                        if val.is_nan() {
3473                            continue;
3474                        }
3475
3476                        let bits = val.to_bits();
3477
3478                        if bits == 0x11111111_11111111 {
3479                            panic!(
3480								"[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
3481								 at row {} col {} in array {} with params: {:?}",
3482								test, cfg_idx, val, bits, row_idx, col, level_name, combo
3483							);
3484                        }
3485
3486                        if bits == 0x22222222_22222222 {
3487                            panic!(
3488								"[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
3489								 at row {} col {} in array {} with params: {:?}",
3490								test, cfg_idx, val, bits, row_idx, col, level_name, combo
3491							);
3492                        }
3493
3494                        if bits == 0x33333333_33333333 {
3495                            panic!(
3496								"[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
3497								 at row {} col {} in array {} with params: {:?}",
3498								test, cfg_idx, val, bits, row_idx, col, level_name, combo
3499							);
3500                        }
3501                    }
3502                }
3503            }
3504        }
3505
3506        Ok(())
3507    }
3508
3509    #[cfg(not(debug_assertions))]
3510    fn check_batch_no_poison(
3511        _test: &str,
3512        _kernel: Kernel,
3513    ) -> Result<(), Box<dyn std::error::Error>> {
3514        Ok(())
3515    }
3516
3517    macro_rules! gen_batch_tests {
3518        ($fn_name:ident) => {
3519            paste! {
3520                #[test] fn [<$fn_name _scalar>]()      {
3521                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
3522                }
3523                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3524                #[test] fn [<$fn_name _avx2>]()        {
3525                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
3526                }
3527                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
3528                #[test] fn [<$fn_name _avx512>]()      {
3529                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
3530                }
3531                #[test] fn [<$fn_name _auto_detect>]() {
3532                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
3533                }
3534            }
3535        };
3536    }
3537
3538    gen_batch_tests!(check_batch_default_row);
3539    gen_batch_tests!(check_batch_no_poison);
3540
3541    #[test]
3542    fn test_pivot_into_matches_api() -> Result<(), Box<dyn std::error::Error>> {
3543        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
3544        let candles = read_candles_from_csv(file)?;
3545        let params = PivotParams::default();
3546        let input = PivotInput::from_candles(&candles, params);
3547
3548        let base = pivot(&input)?;
3549
3550        let len = candles.close.len();
3551
3552        let mut r4 = vec![0.0; len];
3553        let mut r3 = vec![0.0; len];
3554        let mut r2 = vec![0.0; len];
3555        let mut r1 = vec![0.0; len];
3556        let mut pp = vec![0.0; len];
3557        let mut s1 = vec![0.0; len];
3558        let mut s2 = vec![0.0; len];
3559        let mut s3 = vec![0.0; len];
3560        let mut s4 = vec![0.0; len];
3561
3562        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
3563        {
3564            pivot_into(
3565                &input, &mut r4, &mut r3, &mut r2, &mut r1, &mut pp, &mut s1, &mut s2, &mut s3,
3566                &mut s4,
3567            )?;
3568
3569            assert_eq!(r4.len(), base.r4.len());
3570            assert_eq!(r3.len(), base.r3.len());
3571            assert_eq!(r2.len(), base.r2.len());
3572            assert_eq!(r1.len(), base.r1.len());
3573            assert_eq!(pp.len(), base.pp.len());
3574            assert_eq!(s1.len(), base.s1.len());
3575            assert_eq!(s2.len(), base.s2.len());
3576            assert_eq!(s3.len(), base.s3.len());
3577            assert_eq!(s4.len(), base.s4.len());
3578
3579            fn eq_or_both_nan(a: f64, b: f64) -> bool {
3580                (a.is_nan() && b.is_nan()) || (a == b)
3581            }
3582
3583            for i in 0..len {
3584                assert!(eq_or_both_nan(r4[i], base.r4[i]), "r4 mismatch at {i}");
3585                assert!(eq_or_both_nan(r3[i], base.r3[i]), "r3 mismatch at {i}");
3586                assert!(eq_or_both_nan(r2[i], base.r2[i]), "r2 mismatch at {i}");
3587                assert!(eq_or_both_nan(r1[i], base.r1[i]), "r1 mismatch at {i}");
3588                assert!(eq_or_both_nan(pp[i], base.pp[i]), "pp mismatch at {i}");
3589                assert!(eq_or_both_nan(s1[i], base.s1[i]), "s1 mismatch at {i}");
3590                assert!(eq_or_both_nan(s2[i], base.s2[i]), "s2 mismatch at {i}");
3591                assert!(eq_or_both_nan(s3[i], base.s3[i]), "s3 mismatch at {i}");
3592                assert!(eq_or_both_nan(s4[i], base.s4[i]), "s4 mismatch at {i}");
3593            }
3594        }
3595
3596        Ok(())
3597    }
3598}