Skip to main content

vector_ta/indicators/
kurtosis.rs

1#[cfg(all(feature = "python", feature = "cuda"))]
2use crate::utilities::dlpack_cuda::{make_device_array_py, DeviceArrayF32Py};
3#[cfg(feature = "python")]
4use crate::utilities::kernel_validation::validate_kernel;
5#[cfg(all(feature = "python", feature = "cuda"))]
6use numpy::PyUntypedArrayMethods;
7#[cfg(feature = "python")]
8use numpy::{IntoPyArray, PyArray1, PyArrayMethods, PyReadonlyArray1};
9#[cfg(feature = "python")]
10use pyo3::exceptions::PyValueError;
11#[cfg(feature = "python")]
12use pyo3::prelude::*;
13#[cfg(feature = "python")]
14use pyo3::types::PyDict;
15#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
16use serde::{Deserialize, Serialize};
17#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
18use wasm_bindgen::prelude::*;
19
20use crate::utilities::data_loader::{source_type, Candles};
21use crate::utilities::enums::Kernel;
22use crate::utilities::helpers::{
23    alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
24    make_uninit_matrix,
25};
26use aligned_vec::{AVec, CACHELINE_ALIGN};
27#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
28use core::arch::x86_64::*;
29#[cfg(not(target_arch = "wasm32"))]
30use rayon::prelude::*;
31use std::convert::AsRef;
32use std::error::Error;
33#[cfg(all(feature = "python", feature = "cuda"))]
34use std::sync::Arc;
35use thiserror::Error;
36
37impl<'a> AsRef<[f64]> for KurtosisInput<'a> {
38    #[inline(always)]
39    fn as_ref(&self) -> &[f64] {
40        match &self.data {
41            KurtosisData::Slice(slice) => slice,
42            KurtosisData::Candles { candles, source } => source_type(candles, source),
43        }
44    }
45}
46
47#[derive(Debug, Clone)]
48pub enum KurtosisData<'a> {
49    Candles {
50        candles: &'a Candles,
51        source: &'a str,
52    },
53    Slice(&'a [f64]),
54}
55
56#[derive(Debug, Clone)]
57pub struct KurtosisOutput {
58    pub values: Vec<f64>,
59}
60
61#[derive(Debug, Clone)]
62#[cfg_attr(
63    all(target_arch = "wasm32", feature = "wasm"),
64    derive(Serialize, Deserialize)
65)]
66pub struct KurtosisParams {
67    pub period: Option<usize>,
68}
69
70impl Default for KurtosisParams {
71    fn default() -> Self {
72        Self { period: Some(5) }
73    }
74}
75
76#[derive(Debug, Clone)]
77pub struct KurtosisInput<'a> {
78    pub data: KurtosisData<'a>,
79    pub params: KurtosisParams,
80}
81
82impl<'a> KurtosisInput<'a> {
83    #[inline]
84    pub fn from_candles(c: &'a Candles, s: &'a str, p: KurtosisParams) -> Self {
85        Self {
86            data: KurtosisData::Candles {
87                candles: c,
88                source: s,
89            },
90            params: p,
91        }
92    }
93    #[inline]
94    pub fn from_slice(sl: &'a [f64], p: KurtosisParams) -> Self {
95        Self {
96            data: KurtosisData::Slice(sl),
97            params: p,
98        }
99    }
100    #[inline]
101    pub fn with_default_candles(c: &'a Candles) -> Self {
102        Self::from_candles(c, "hl2", KurtosisParams::default())
103    }
104    #[inline]
105    pub fn get_period(&self) -> usize {
106        self.params.period.unwrap_or(5)
107    }
108}
109
110#[derive(Copy, Clone, Debug)]
111pub struct KurtosisBuilder {
112    period: Option<usize>,
113    kernel: Kernel,
114}
115
116impl Default for KurtosisBuilder {
117    fn default() -> Self {
118        Self {
119            period: None,
120            kernel: Kernel::Auto,
121        }
122    }
123}
124
125impl KurtosisBuilder {
126    #[inline(always)]
127    pub fn new() -> Self {
128        Self::default()
129    }
130    #[inline(always)]
131    pub fn period(mut self, n: usize) -> Self {
132        self.period = Some(n);
133        self
134    }
135    #[inline(always)]
136    pub fn kernel(mut self, k: Kernel) -> Self {
137        self.kernel = k;
138        self
139    }
140    #[inline(always)]
141    pub fn apply(self, c: &Candles) -> Result<KurtosisOutput, KurtosisError> {
142        let p = KurtosisParams {
143            period: self.period,
144        };
145        let i = KurtosisInput::from_candles(c, "hl2", p);
146        kurtosis_with_kernel(&i, self.kernel)
147    }
148    #[inline(always)]
149    pub fn apply_slice(self, d: &[f64]) -> Result<KurtosisOutput, KurtosisError> {
150        let p = KurtosisParams {
151            period: self.period,
152        };
153        let i = KurtosisInput::from_slice(d, p);
154        kurtosis_with_kernel(&i, self.kernel)
155    }
156    #[inline(always)]
157    pub fn into_stream(self) -> Result<KurtosisStream, KurtosisError> {
158        let p = KurtosisParams {
159            period: self.period,
160        };
161        KurtosisStream::try_new(p)
162    }
163}
164
165#[derive(Debug, Error)]
166pub enum KurtosisError {
167    #[error("kurtosis: Input data slice is empty.")]
168    EmptyInputData,
169    #[error("kurtosis: All values are NaN.")]
170    AllValuesNaN,
171    #[error("kurtosis: Invalid period: period = {period}, data length = {data_len}")]
172    InvalidPeriod { period: usize, data_len: usize },
173    #[error("kurtosis: Not enough valid data: needed = {needed}, valid = {valid}")]
174    NotEnoughValidData { needed: usize, valid: usize },
175    #[error("kurtosis: Output length mismatch: expected {expected}, got {got}")]
176    OutputLengthMismatch { expected: usize, got: usize },
177    #[error("kurtosis: Invalid range: start={start}, end={end}, step={step}")]
178    InvalidRange {
179        start: String,
180        end: String,
181        step: String,
182    },
183    #[error("kurtosis: Invalid kernel for batch: {0:?}")]
184    InvalidKernelForBatch(Kernel),
185    #[error("kurtosis: Invalid period (zero or missing).")]
186    ZeroOrMissingPeriod,
187}
188
189#[inline]
190pub fn kurtosis(input: &KurtosisInput) -> Result<KurtosisOutput, KurtosisError> {
191    kurtosis_with_kernel(input, Kernel::Auto)
192}
193
194pub fn kurtosis_with_kernel(
195    input: &KurtosisInput,
196    kernel: Kernel,
197) -> Result<KurtosisOutput, KurtosisError> {
198    let data: &[f64] = input.as_ref();
199
200    if data.is_empty() {
201        return Err(KurtosisError::EmptyInputData);
202    }
203
204    let first = data
205        .iter()
206        .position(|x| !x.is_nan())
207        .ok_or(KurtosisError::AllValuesNaN)?;
208
209    let len = data.len();
210    let period = input.get_period();
211
212    if period == 0 || period > len {
213        return Err(KurtosisError::InvalidPeriod {
214            period,
215            data_len: len,
216        });
217    }
218    if (len - first) < period {
219        return Err(KurtosisError::NotEnoughValidData {
220            needed: period,
221            valid: len - first,
222        });
223    }
224
225    let mut out = alloc_with_nan_prefix(len, first + period - 1);
226
227    let chosen = match kernel {
228        Kernel::Auto => Kernel::Scalar,
229        other => other,
230    };
231
232    unsafe {
233        match chosen {
234            Kernel::Scalar | Kernel::ScalarBatch => kurtosis_scalar(data, period, first, &mut out),
235            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
236            Kernel::Avx2 | Kernel::Avx2Batch => kurtosis_avx2(data, period, first, &mut out),
237            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
238            Kernel::Avx512 | Kernel::Avx512Batch => kurtosis_avx512(data, period, first, &mut out),
239            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
240            Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
241                kurtosis_scalar(data, period, first, &mut out)
242            }
243            _ => unreachable!(),
244        }
245    }
246    Ok(KurtosisOutput { values: out })
247}
248
249#[inline]
250pub fn kurtosis_into_slice(
251    dst: &mut [f64],
252    input: &KurtosisInput,
253    kernel: Kernel,
254) -> Result<(), KurtosisError> {
255    let data: &[f64] = input.as_ref();
256
257    if data.is_empty() {
258        return Err(KurtosisError::EmptyInputData);
259    }
260
261    let first = data
262        .iter()
263        .position(|x| !x.is_nan())
264        .ok_or(KurtosisError::AllValuesNaN)?;
265
266    let len = data.len();
267    let period = input.get_period();
268
269    if period == 0 || period > len {
270        return Err(KurtosisError::InvalidPeriod {
271            period,
272            data_len: len,
273        });
274    }
275    if (len - first) < period {
276        return Err(KurtosisError::NotEnoughValidData {
277            needed: period,
278            valid: len - first,
279        });
280    }
281    if dst.len() != data.len() {
282        return Err(KurtosisError::OutputLengthMismatch {
283            expected: data.len(),
284            got: dst.len(),
285        });
286    }
287
288    let warmup_end = first + period - 1;
289    for v in &mut dst[..warmup_end] {
290        *v = f64::NAN;
291    }
292
293    let chosen = match kernel {
294        Kernel::Auto => Kernel::Scalar,
295        other => other,
296    };
297
298    unsafe {
299        match chosen {
300            Kernel::Scalar | Kernel::ScalarBatch => kurtosis_scalar(data, period, first, dst),
301            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
302            Kernel::Avx2 | Kernel::Avx2Batch => kurtosis_avx2(data, period, first, dst),
303            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
304            Kernel::Avx512 | Kernel::Avx512Batch => kurtosis_avx512(data, period, first, dst),
305            #[cfg(not(all(feature = "nightly-avx", target_arch = "x86_64")))]
306            Kernel::Avx2 | Kernel::Avx2Batch | Kernel::Avx512 | Kernel::Avx512Batch => {
307                kurtosis_scalar(data, period, first, dst)
308            }
309            _ => unreachable!(),
310        }
311    }
312
313    Ok(())
314}
315
316#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
317#[inline]
318pub fn kurtosis_into(input: &KurtosisInput, out: &mut [f64]) -> Result<(), KurtosisError> {
319    kurtosis_into_slice(out, input, Kernel::Auto)?;
320
321    let data: &[f64] = input.as_ref();
322    let first = data
323        .iter()
324        .position(|x| !x.is_nan())
325        .ok_or(KurtosisError::AllValuesNaN)?;
326    let warmup_end = first + input.get_period() - 1;
327    let qnan = f64::from_bits(0x7ff8_0000_0000_0000);
328    let warm = warmup_end.min(out.len());
329    for v in &mut out[..warm] {
330        *v = qnan;
331    }
332
333    Ok(())
334}
335
336#[inline]
337pub fn kurtosis_scalar(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
338    for i in (first + period - 1)..data.len() {
339        let start = i + 1 - period;
340        let window = &data[start..start + period];
341
342        let mut has_nan = false;
343        let mut sum = 0.0f64;
344        for &v in window {
345            if v.is_nan() {
346                has_nan = true;
347                break;
348            }
349            sum += v;
350        }
351        if has_nan {
352            out[i] = f64::NAN;
353            continue;
354        }
355        let n = period as f64;
356        let mean = sum / n;
357        let mut m2 = 0.0;
358        let mut m4 = 0.0;
359        for &val in window {
360            let diff = val - mean;
361            let d2 = diff * diff;
362            m2 += d2;
363            m4 += d2 * d2;
364        }
365        m2 /= n;
366        m4 /= n;
367
368        if m2.abs() < f64::EPSILON {
369            out[i] = f64::NAN;
370        } else {
371            out[i] = (m4 / (m2 * m2)) - 3.0;
372        }
373    }
374}
375
376#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
377#[inline]
378pub fn kurtosis_avx512(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
379    if period <= 32 {
380        unsafe { kurtosis_avx512_short(data, period, first, out) }
381    } else {
382        unsafe { kurtosis_avx512_long(data, period, first, out) }
383    }
384}
385
386#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
387#[inline]
388pub fn kurtosis_avx2(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
389    unsafe { kurtosis_scalar(data, period, first, out) }
390}
391
392#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
393#[inline]
394pub unsafe fn kurtosis_avx512_short(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
395    kurtosis_scalar(data, period, first, out)
396}
397
398#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
399#[inline]
400pub unsafe fn kurtosis_avx512_long(data: &[f64], period: usize, first: usize, out: &mut [f64]) {
401    kurtosis_scalar(data, period, first, out)
402}
403
404#[inline(always)]
405pub fn kurtosis_batch_with_kernel(
406    data: &[f64],
407    sweep: &KurtosisBatchRange,
408    k: Kernel,
409) -> Result<KurtosisBatchOutput, KurtosisError> {
410    let kernel = match k {
411        Kernel::Auto => Kernel::ScalarBatch,
412        other if other.is_batch() => other,
413        other => return Err(KurtosisError::InvalidKernelForBatch(other)),
414    };
415    let simd = match kernel {
416        Kernel::Avx512Batch => Kernel::Avx512,
417        Kernel::Avx2Batch => Kernel::Avx2,
418        Kernel::ScalarBatch => Kernel::Scalar,
419        _ => unreachable!(),
420    };
421    kurtosis_batch_par_slice(data, sweep, simd)
422}
423
424#[derive(Clone, Debug)]
425pub struct KurtosisBatchRange {
426    pub period: (usize, usize, usize),
427}
428
429impl Default for KurtosisBatchRange {
430    fn default() -> Self {
431        Self {
432            period: (5, 254, 1),
433        }
434    }
435}
436
437#[derive(Clone, Debug, Default)]
438pub struct KurtosisBatchBuilder {
439    range: KurtosisBatchRange,
440    kernel: Kernel,
441}
442
443impl KurtosisBatchBuilder {
444    pub fn new() -> Self {
445        Self::default()
446    }
447    pub fn kernel(mut self, k: Kernel) -> Self {
448        self.kernel = k;
449        self
450    }
451    #[inline]
452    pub fn period_range(mut self, start: usize, end: usize, step: usize) -> Self {
453        self.range.period = (start, end, step);
454        self
455    }
456    #[inline]
457    pub fn period_static(mut self, p: usize) -> Self {
458        self.range.period = (p, p, 0);
459        self
460    }
461    pub fn apply_slice(self, data: &[f64]) -> Result<KurtosisBatchOutput, KurtosisError> {
462        kurtosis_batch_with_kernel(data, &self.range, self.kernel)
463    }
464    pub fn with_default_slice(
465        data: &[f64],
466        k: Kernel,
467    ) -> Result<KurtosisBatchOutput, KurtosisError> {
468        KurtosisBatchBuilder::new().kernel(k).apply_slice(data)
469    }
470    pub fn apply_candles(
471        self,
472        c: &Candles,
473        src: &str,
474    ) -> Result<KurtosisBatchOutput, KurtosisError> {
475        let slice = source_type(c, src);
476        self.apply_slice(slice)
477    }
478    pub fn with_default_candles(c: &Candles) -> Result<KurtosisBatchOutput, KurtosisError> {
479        KurtosisBatchBuilder::new()
480            .kernel(Kernel::Auto)
481            .apply_candles(c, "hl2")
482    }
483}
484
485#[derive(Clone, Debug)]
486pub struct KurtosisBatchOutput {
487    pub values: Vec<f64>,
488    pub combos: Vec<KurtosisParams>,
489    pub rows: usize,
490    pub cols: usize,
491}
492
493impl KurtosisBatchOutput {
494    pub fn row_for_params(&self, p: &KurtosisParams) -> Option<usize> {
495        self.combos
496            .iter()
497            .position(|c| c.period.unwrap_or(5) == p.period.unwrap_or(5))
498    }
499    pub fn values_for(&self, p: &KurtosisParams) -> Option<&[f64]> {
500        self.row_for_params(p).map(|row| {
501            let start = row * self.cols;
502            &self.values[start..start + self.cols]
503        })
504    }
505}
506
507#[inline(always)]
508fn expand_grid(r: &KurtosisBatchRange) -> Result<Vec<KurtosisParams>, KurtosisError> {
509    fn axis_usize((start, end, step): (usize, usize, usize)) -> Result<Vec<usize>, KurtosisError> {
510        if step == 0 || start == end {
511            return Ok(vec![start]);
512        }
513        if start < end {
514            return Ok((start..=end).step_by(step.max(1)).collect());
515        }
516        let mut v = Vec::new();
517        let mut x = start as isize;
518        let end_i = end as isize;
519        let st = (step as isize).max(1);
520        while x >= end_i {
521            v.push(x as usize);
522            x -= st;
523        }
524        if v.is_empty() {
525            return Err(KurtosisError::InvalidRange {
526                start: start.to_string(),
527                end: end.to_string(),
528                step: step.to_string(),
529            });
530        }
531        Ok(v)
532    }
533    let periods = axis_usize(r.period)?;
534    let mut out = Vec::with_capacity(periods.len());
535    for &p in &periods {
536        out.push(KurtosisParams { period: Some(p) });
537    }
538    Ok(out)
539}
540
541#[inline(always)]
542pub fn kurtosis_batch_slice(
543    data: &[f64],
544    sweep: &KurtosisBatchRange,
545    kern: Kernel,
546) -> Result<KurtosisBatchOutput, KurtosisError> {
547    kurtosis_batch_inner(data, sweep, kern, false)
548}
549
550#[inline(always)]
551pub fn kurtosis_batch_par_slice(
552    data: &[f64],
553    sweep: &KurtosisBatchRange,
554    kern: Kernel,
555) -> Result<KurtosisBatchOutput, KurtosisError> {
556    kurtosis_batch_inner(data, sweep, kern, true)
557}
558
559#[inline(always)]
560fn kurtosis_batch_inner(
561    data: &[f64],
562    sweep: &KurtosisBatchRange,
563    kern: Kernel,
564    parallel: bool,
565) -> Result<KurtosisBatchOutput, KurtosisError> {
566    let combos = expand_grid(sweep)?;
567    if combos.is_empty() {
568        return Err(KurtosisError::InvalidRange {
569            start: "range".into(),
570            end: "range".into(),
571            step: "empty".into(),
572        });
573    }
574
575    let first = data
576        .iter()
577        .position(|x| !x.is_nan())
578        .ok_or(KurtosisError::AllValuesNaN)?;
579    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
580    if data.len() - first < max_p {
581        return Err(KurtosisError::NotEnoughValidData {
582            needed: max_p,
583            valid: data.len() - first,
584        });
585    }
586
587    let rows = combos.len();
588    let cols = data.len();
589    let _ = rows
590        .checked_mul(cols)
591        .ok_or_else(|| KurtosisError::InvalidRange {
592            start: rows.to_string(),
593            end: cols.to_string(),
594            step: "rows*cols".into(),
595        })?;
596    let mut values_mu = make_uninit_matrix(rows, cols);
597
598    let warmup_periods: Vec<usize> = combos
599        .iter()
600        .map(|c| first + c.period.unwrap() - 1)
601        .collect();
602    init_matrix_prefixes(&mut values_mu, cols, &warmup_periods);
603
604    let mut values_guard = core::mem::ManuallyDrop::new(values_mu);
605    let values: &mut [f64] = unsafe {
606        core::slice::from_raw_parts_mut(values_guard.as_mut_ptr() as *mut f64, values_guard.len())
607    };
608
609    let do_row = |row: usize, out_row: &mut [f64]| unsafe {
610        let period = combos[row].period.unwrap();
611        match kern {
612            Kernel::Scalar => kurtosis_row_scalar(data, first, period, out_row),
613            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
614            Kernel::Avx2 => kurtosis_row_avx2(data, first, period, out_row),
615            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
616            Kernel::Avx512 => kurtosis_row_avx512(data, first, period, out_row),
617            _ => unreachable!(),
618        }
619    };
620
621    if parallel {
622        #[cfg(not(target_arch = "wasm32"))]
623        {
624            values
625                .par_chunks_mut(cols)
626                .enumerate()
627                .for_each(|(row, slice)| do_row(row, slice));
628        }
629
630        #[cfg(target_arch = "wasm32")]
631        {
632            for (row, slice) in values.chunks_mut(cols).enumerate() {
633                do_row(row, slice);
634            }
635        }
636    } else {
637        for (row, slice) in values.chunks_mut(cols).enumerate() {
638            do_row(row, slice);
639        }
640    }
641
642    let values_vec = unsafe {
643        Vec::from_raw_parts(
644            values_guard.as_mut_ptr() as *mut f64,
645            values_guard.len(),
646            values_guard.capacity(),
647        )
648    };
649
650    Ok(KurtosisBatchOutput {
651        values: values_vec,
652        combos,
653        rows,
654        cols,
655    })
656}
657
658#[inline(always)]
659fn kurtosis_batch_inner_into(
660    data: &[f64],
661    sweep: &KurtosisBatchRange,
662    kern: Kernel,
663    parallel: bool,
664    out: &mut [f64],
665) -> Result<Vec<KurtosisParams>, KurtosisError> {
666    let combos = expand_grid(sweep)?;
667    if combos.is_empty() {
668        return Err(KurtosisError::InvalidRange {
669            start: "range".into(),
670            end: "range".into(),
671            step: "empty".into(),
672        });
673    }
674
675    let first = data
676        .iter()
677        .position(|x| !x.is_nan())
678        .ok_or(KurtosisError::AllValuesNaN)?;
679    let max_p = combos.iter().map(|c| c.period.unwrap()).max().unwrap();
680    if data.len() - first < max_p {
681        return Err(KurtosisError::NotEnoughValidData {
682            needed: max_p,
683            valid: data.len() - first,
684        });
685    }
686
687    let rows = combos.len();
688    let cols = data.len();
689    let expected = rows
690        .checked_mul(cols)
691        .ok_or_else(|| KurtosisError::InvalidRange {
692            start: rows.to_string(),
693            end: cols.to_string(),
694            step: "rows*cols".into(),
695        })?;
696    if out.len() != expected {
697        return Err(KurtosisError::OutputLengthMismatch {
698            expected,
699            got: out.len(),
700        });
701    }
702
703    for (row, combo) in combos.iter().enumerate() {
704        let period = combo.period.unwrap();
705        let warmup = first + period - 1;
706        let row_start = row * cols;
707        for i in 0..warmup.min(cols) {
708            out[row_start + i] = f64::NAN;
709        }
710    }
711
712    let out_uninit = unsafe {
713        std::slice::from_raw_parts_mut(
714            out.as_mut_ptr() as *mut std::mem::MaybeUninit<f64>,
715            out.len(),
716        )
717    };
718
719    let do_row = |row: usize, dst_mu: &mut [std::mem::MaybeUninit<f64>]| unsafe {
720        let period = combos[row].period.unwrap();
721        let dst = core::slice::from_raw_parts_mut(dst_mu.as_mut_ptr() as *mut f64, dst_mu.len());
722
723        match kern {
724            Kernel::Scalar => kurtosis_row_scalar(data, first, period, dst),
725            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
726            Kernel::Avx2 => kurtosis_row_avx2(data, first, period, dst),
727            #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
728            Kernel::Avx512 => kurtosis_row_avx512(data, first, period, dst),
729            _ => unreachable!(),
730        }
731    };
732
733    if parallel {
734        #[cfg(not(target_arch = "wasm32"))]
735        {
736            out_uninit
737                .par_chunks_mut(cols)
738                .enumerate()
739                .for_each(|(row, slice)| do_row(row, slice));
740        }
741
742        #[cfg(target_arch = "wasm32")]
743        {
744            for (row, slice) in out_uninit.chunks_mut(cols).enumerate() {
745                do_row(row, slice);
746            }
747        }
748    } else {
749        for (row, slice) in out_uninit.chunks_mut(cols).enumerate() {
750            do_row(row, slice);
751        }
752    }
753
754    Ok(combos)
755}
756
757#[inline(always)]
758unsafe fn kurtosis_row_scalar(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
759    kurtosis_scalar(data, period, first, out)
760}
761
762#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
763#[inline(always)]
764unsafe fn kurtosis_row_avx2(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
765    kurtosis_scalar(data, period, first, out)
766}
767
768#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
769#[inline(always)]
770unsafe fn kurtosis_row_avx512(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
771    kurtosis_avx512(data, period, first, out)
772}
773
774#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
775#[inline(always)]
776unsafe fn kurtosis_row_avx512_short(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
777    kurtosis_avx512_short(data, period, first, out)
778}
779
780#[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
781#[inline(always)]
782unsafe fn kurtosis_row_avx512_long(data: &[f64], first: usize, period: usize, out: &mut [f64]) {
783    kurtosis_avx512_long(data, period, first, out)
784}
785
786#[derive(Debug, Clone)]
787pub struct KurtosisStream {
788    period: usize,
789    buffer: Vec<f64>,
790    head: usize,
791    filled: bool,
792
793    nan_count: usize,
794    mean: f64,
795    c2: f64,
796    c3: f64,
797    c4: f64,
798    inv_n: f64,
799    moments_valid: bool,
800    rebuild_ctr: usize,
801}
802
803impl KurtosisStream {
804    pub fn try_new(params: KurtosisParams) -> Result<Self, KurtosisError> {
805        let period = params.period.unwrap_or(5);
806        if period == 0 {
807            return Err(KurtosisError::InvalidPeriod {
808                period,
809                data_len: 0,
810            });
811        }
812        Ok(Self {
813            period,
814            buffer: vec![f64::NAN; period],
815            head: 0,
816            filled: false,
817            nan_count: 0,
818            mean: 0.0,
819            c2: 0.0,
820            c3: 0.0,
821            c4: 0.0,
822            inv_n: 1.0 / (period as f64),
823            moments_valid: false,
824            rebuild_ctr: 0,
825        })
826    }
827
828    #[inline(always)]
829    pub fn update(&mut self, value: f64) -> Option<f64> {
830        let old = self.buffer[self.head];
831        self.buffer[self.head] = value;
832        self.head += 1;
833        if self.head == self.period {
834            self.head = 0;
835            if !self.filled {
836                self.filled = true;
837                self.nan_count = self.buffer.iter().filter(|v| v.is_nan()).count();
838                if self.nan_count > 0 {
839                    self.moments_valid = false;
840                    return Some(f64::NAN);
841                }
842                self.recompute_moments_from_ring();
843                return Some(self.finalize_kurtosis());
844            }
845        }
846
847        if !self.filled {
848            return None;
849        }
850
851        if old.is_nan() {
852            self.nan_count = self.nan_count.saturating_sub(1);
853        }
854        if value.is_nan() {
855            self.nan_count += 1;
856        }
857
858        if self.nan_count > 0 {
859            self.moments_valid = false;
860            return Some(f64::NAN);
861        }
862
863        if !self.moments_valid {
864            self.recompute_moments_from_ring();
865            return Some(self.finalize_kurtosis());
866        }
867
868        self.rebuild_ctr += 1;
869        if self.rebuild_ctr >= 1 {
870            self.recompute_moments_from_ring();
871            return Some(self.finalize_kurtosis());
872        }
873
874        let n = self.period as f64;
875        let diff = value - old;
876        let d = diff * self.inv_n;
877        let mu_new = self.mean + d;
878
879        let diff2 = diff * diff;
880        let d2 = d * d;
881        let inv_n2 = self.inv_n * self.inv_n;
882        let inv_n3 = inv_n2 * self.inv_n;
883        let d3 = diff * diff2 * inv_n2;
884        let d4 = diff2 * diff2 * inv_n3;
885
886        let c2s = self.c2 + diff2 * self.inv_n;
887        let c3s = self.c3 - 3.0 * d * self.c2 - d3;
888        let c4s = self.c4 - 4.0 * d * self.c3 + 6.0 * d2 * self.c2 + d4;
889
890        let dy = old - mu_new;
891        let dx = value - mu_new;
892        let dy2 = dy * dy;
893        let dx2 = dx * dx;
894
895        self.c2 = c2s - dy2 + dx2;
896        self.c3 = c3s - dy * dy2 + dx * dx2;
897        self.c4 = c4s - (dy2 * dy2) + (dx2 * dx2);
898        self.mean = mu_new;
899
900        Some(self.finalize_kurtosis())
901    }
902
903    #[inline(always)]
904    fn finalize_kurtosis(&self) -> f64 {
905        let c2 = self.c2;
906        let c4 = self.c4;
907        let n = self.period as f64;
908        if c2.abs() < f64::EPSILON * n {
909            return f64::NAN;
910        }
911        (c4 * n) / (c2 * c2) - 3.0
912    }
913
914    #[inline(always)]
915    fn recompute_moments_from_ring(&mut self) {
916        debug_assert!(self.nan_count == 0);
917
918        let n = self.period as f64;
919
920        let mut sum = 0.0;
921        for k in 0..self.period {
922            let idx = (self.head + k) % self.period;
923            sum += self.buffer[idx];
924        }
925        let mean = sum / n;
926
927        let mut c2 = 0.0;
928        let mut c3 = 0.0;
929        let mut c4 = 0.0;
930        for k in 0..self.period {
931            let idx = (self.head + k) % self.period;
932            let v = self.buffer[idx];
933            let d = v - mean;
934            let d2 = d * d;
935            c2 += d2;
936            c3 += d * d2;
937            c4 += d2 * d2;
938        }
939        self.mean = mean;
940        self.c2 = c2;
941        self.c3 = c3;
942        self.c4 = c4;
943        self.moments_valid = true;
944        self.rebuild_ctr = 0;
945    }
946}
947
948#[cfg(test)]
949mod tests {
950    use super::*;
951    use crate::skip_if_unsupported;
952    use crate::utilities::data_loader::read_candles_from_csv;
953
954    fn check_kurtosis_partial_params(
955        test_name: &str,
956        kernel: Kernel,
957    ) -> Result<(), Box<dyn Error>> {
958        skip_if_unsupported!(kernel, test_name);
959        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
960        let candles = read_candles_from_csv(file_path)?;
961
962        let default_params = KurtosisParams { period: None };
963        let input = KurtosisInput::from_candles(&candles, "close", default_params);
964        let output = kurtosis_with_kernel(&input, kernel)?;
965        assert_eq!(output.values.len(), candles.close.len());
966
967        Ok(())
968    }
969
970    #[test]
971    fn test_kurtosis_into_matches_api() -> Result<(), Box<dyn Error>> {
972        let len = 256usize;
973        let mut data = Vec::with_capacity(len);
974        for i in 0..len {
975            let x = i as f64;
976
977            let v = (x * 0.01).sin() * 2.0 + (x * 0.007).cos() * 0.5 + x * 1e-3;
978            data.push(v);
979        }
980
981        let input = KurtosisInput::from_slice(&data, KurtosisParams::default());
982
983        let baseline = kurtosis(&input)?.values;
984
985        let mut out = vec![0.0f64; len];
986        #[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
987        {
988            kurtosis_into(&input, &mut out)?;
989        }
990        #[cfg(all(target_arch = "wasm32", feature = "wasm"))]
991        {
992            kurtosis_into_slice(&mut out, &input, Kernel::Auto)?;
993        }
994
995        assert_eq!(baseline.len(), out.len());
996
997        fn eq_or_both_nan(a: f64, b: f64) -> bool {
998            (a.is_nan() && b.is_nan()) || (a - b).abs() <= 1e-12
999        }
1000
1001        for i in 0..len {
1002            assert!(
1003                eq_or_both_nan(baseline[i], out[i]),
1004                "mismatch at index {}: baseline={}, into={}",
1005                i,
1006                baseline[i],
1007                out[i]
1008            );
1009        }
1010
1011        Ok(())
1012    }
1013
1014    fn check_kurtosis_accuracy(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1015        skip_if_unsupported!(kernel, test_name);
1016        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1017        let candles = read_candles_from_csv(file_path)?;
1018
1019        let input = KurtosisInput::from_candles(&candles, "hl2", KurtosisParams::default());
1020        let result = kurtosis_with_kernel(&input, kernel)?;
1021        let expected_last_five = [
1022            -0.5438903789933454,
1023            -1.6848139264816433,
1024            -1.6331336745945797,
1025            -0.6130805596586351,
1026            -0.027802601135927585,
1027        ];
1028        let start = result.values.len().saturating_sub(5);
1029        for (i, &val) in result.values[start..].iter().enumerate() {
1030            let diff = (val - expected_last_five[i]).abs();
1031            assert!(
1032                diff < 1e-6,
1033                "[{}] KURTOSIS {:?} mismatch at idx {}: got {}, expected {}",
1034                test_name,
1035                kernel,
1036                i,
1037                val,
1038                expected_last_five[i]
1039            );
1040        }
1041        Ok(())
1042    }
1043
1044    fn check_kurtosis_default_candles(
1045        test_name: &str,
1046        kernel: Kernel,
1047    ) -> Result<(), Box<dyn Error>> {
1048        skip_if_unsupported!(kernel, test_name);
1049        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1050        let candles = read_candles_from_csv(file_path)?;
1051
1052        let input = KurtosisInput::with_default_candles(&candles);
1053        match input.data {
1054            KurtosisData::Candles { source, .. } => assert_eq!(source, "hl2"),
1055            _ => panic!("Expected KurtosisData::Candles"),
1056        }
1057        let output = kurtosis_with_kernel(&input, kernel)?;
1058        assert_eq!(output.values.len(), candles.close.len());
1059
1060        Ok(())
1061    }
1062
1063    fn check_kurtosis_zero_period(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1064        skip_if_unsupported!(kernel, test_name);
1065        let input_data = [10.0, 20.0, 30.0];
1066        let params = KurtosisParams { period: Some(0) };
1067        let input = KurtosisInput::from_slice(&input_data, params);
1068        let res = kurtosis_with_kernel(&input, kernel);
1069        assert!(
1070            res.is_err(),
1071            "[{}] KURTOSIS should fail with zero period",
1072            test_name
1073        );
1074        Ok(())
1075    }
1076
1077    fn check_kurtosis_period_exceeds_length(
1078        test_name: &str,
1079        kernel: Kernel,
1080    ) -> Result<(), Box<dyn Error>> {
1081        skip_if_unsupported!(kernel, test_name);
1082        let data_small = [10.0, 20.0, 30.0];
1083        let params = KurtosisParams { period: Some(10) };
1084        let input = KurtosisInput::from_slice(&data_small, params);
1085        let res = kurtosis_with_kernel(&input, kernel);
1086        assert!(
1087            res.is_err(),
1088            "[{}] KURTOSIS should fail with period exceeding length",
1089            test_name
1090        );
1091        Ok(())
1092    }
1093
1094    fn check_kurtosis_very_small_dataset(
1095        test_name: &str,
1096        kernel: Kernel,
1097    ) -> Result<(), Box<dyn Error>> {
1098        skip_if_unsupported!(kernel, test_name);
1099        let single_point = [42.0];
1100        let params = KurtosisParams { period: Some(5) };
1101        let input = KurtosisInput::from_slice(&single_point, params);
1102        let res = kurtosis_with_kernel(&input, kernel);
1103        assert!(
1104            res.is_err(),
1105            "[{}] KURTOSIS should fail with insufficient data",
1106            test_name
1107        );
1108        Ok(())
1109    }
1110
1111    fn check_kurtosis_reinput(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1112        skip_if_unsupported!(kernel, test_name);
1113        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1114        let candles = read_candles_from_csv(file_path)?;
1115
1116        let first_params = KurtosisParams { period: Some(5) };
1117        let first_input = KurtosisInput::from_candles(&candles, "close", first_params);
1118        let first_result = kurtosis_with_kernel(&first_input, kernel)?;
1119
1120        let second_params = KurtosisParams { period: Some(5) };
1121        let second_input = KurtosisInput::from_slice(&first_result.values, second_params);
1122        let second_result = kurtosis_with_kernel(&second_input, kernel)?;
1123
1124        assert_eq!(second_result.values.len(), first_result.values.len());
1125        Ok(())
1126    }
1127
1128    fn check_kurtosis_nan_handling(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1129        skip_if_unsupported!(kernel, test_name);
1130        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1131        let candles = read_candles_from_csv(file_path)?;
1132
1133        let input =
1134            KurtosisInput::from_candles(&candles, "close", KurtosisParams { period: Some(5) });
1135        let res = kurtosis_with_kernel(&input, kernel)?;
1136        assert_eq!(res.values.len(), candles.close.len());
1137        if res.values.len() > 20 {
1138            for (i, &val) in res.values[20..].iter().enumerate() {
1139                assert!(
1140                    !val.is_nan(),
1141                    "[{}] Found unexpected NaN at out-index {}",
1142                    test_name,
1143                    20 + i
1144                );
1145            }
1146        }
1147        Ok(())
1148    }
1149
1150    fn check_kurtosis_streaming(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1151        skip_if_unsupported!(kernel, test_name);
1152        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1153        let candles = read_candles_from_csv(file_path)?;
1154
1155        let period = 5;
1156
1157        let input = KurtosisInput::from_candles(
1158            &candles,
1159            "close",
1160            KurtosisParams {
1161                period: Some(period),
1162            },
1163        );
1164        let batch_output = kurtosis_with_kernel(&input, kernel)?.values;
1165
1166        let mut stream = KurtosisStream::try_new(KurtosisParams {
1167            period: Some(period),
1168        })?;
1169        let mut stream_values = Vec::with_capacity(candles.close.len());
1170        for &price in &candles.close {
1171            match stream.update(price) {
1172                Some(kurtosis_val) => stream_values.push(kurtosis_val),
1173                None => stream_values.push(f64::NAN),
1174            }
1175        }
1176
1177        assert_eq!(batch_output.len(), stream_values.len());
1178        for (i, (&b, &s)) in batch_output.iter().zip(stream_values.iter()).enumerate() {
1179            if b.is_nan() && s.is_nan() {
1180                continue;
1181            }
1182            let diff = (b - s).abs();
1183            assert!(
1184                diff < 1e-9,
1185                "[{}] KURTOSIS streaming f64 mismatch at idx {}: batch={}, stream={}, diff={}",
1186                test_name,
1187                i,
1188                b,
1189                s,
1190                diff
1191            );
1192        }
1193        Ok(())
1194    }
1195
1196    #[cfg(debug_assertions)]
1197    fn check_kurtosis_no_poison(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1198        skip_if_unsupported!(kernel, test_name);
1199
1200        let file_path = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1201        let candles = read_candles_from_csv(file_path)?;
1202
1203        let test_params = vec![
1204            KurtosisParams::default(),
1205            KurtosisParams { period: Some(2) },
1206            KurtosisParams { period: Some(3) },
1207            KurtosisParams { period: Some(4) },
1208            KurtosisParams { period: Some(5) },
1209            KurtosisParams { period: Some(7) },
1210            KurtosisParams { period: Some(10) },
1211            KurtosisParams { period: Some(14) },
1212            KurtosisParams { period: Some(20) },
1213            KurtosisParams { period: Some(30) },
1214            KurtosisParams { period: Some(50) },
1215            KurtosisParams { period: Some(100) },
1216            KurtosisParams { period: Some(200) },
1217            KurtosisParams { period: Some(6) },
1218            KurtosisParams { period: Some(25) },
1219            KurtosisParams { period: Some(75) },
1220        ];
1221
1222        for (param_idx, params) in test_params.iter().enumerate() {
1223            let input = KurtosisInput::from_candles(&candles, "close", params.clone());
1224            let output = kurtosis_with_kernel(&input, kernel)?;
1225
1226            for (i, &val) in output.values.iter().enumerate() {
1227                if val.is_nan() {
1228                    continue;
1229                }
1230
1231                let bits = val.to_bits();
1232
1233                if bits == 0x11111111_11111111 {
1234                    panic!(
1235                        "[{}] Found alloc_with_nan_prefix poison value {} (0x{:016X}) at index {} \
1236						 with params: period={} (param set {})",
1237                        test_name,
1238                        val,
1239                        bits,
1240                        i,
1241                        params.period.unwrap_or(5),
1242                        param_idx
1243                    );
1244                }
1245
1246                if bits == 0x22222222_22222222 {
1247                    panic!(
1248                        "[{}] Found init_matrix_prefixes poison value {} (0x{:016X}) at index {} \
1249						 with params: period={} (param set {})",
1250                        test_name,
1251                        val,
1252                        bits,
1253                        i,
1254                        params.period.unwrap_or(5),
1255                        param_idx
1256                    );
1257                }
1258
1259                if bits == 0x33333333_33333333 {
1260                    panic!(
1261                        "[{}] Found make_uninit_matrix poison value {} (0x{:016X}) at index {} \
1262						 with params: period={} (param set {})",
1263                        test_name,
1264                        val,
1265                        bits,
1266                        i,
1267                        params.period.unwrap_or(5),
1268                        param_idx
1269                    );
1270                }
1271            }
1272        }
1273
1274        Ok(())
1275    }
1276
1277    #[cfg(not(debug_assertions))]
1278    fn check_kurtosis_no_poison(_test_name: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1279        Ok(())
1280    }
1281
1282    macro_rules! generate_all_kurtosis_tests {
1283        ($($test_fn:ident),*) => {
1284            paste::paste! {
1285                $(
1286                    #[test]
1287                    fn [<$test_fn _scalar_f64>]() {
1288                        let _ = $test_fn(stringify!([<$test_fn _scalar_f64>]), Kernel::Scalar);
1289                    }
1290                )*
1291                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1292                $(
1293                    #[test]
1294                    fn [<$test_fn _avx2_f64>]() {
1295                        let _ = $test_fn(stringify!([<$test_fn _avx2_f64>]), Kernel::Avx2);
1296                    }
1297                    #[test]
1298                    fn [<$test_fn _avx512_f64>]() {
1299                        let _ = $test_fn(stringify!([<$test_fn _avx512_f64>]), Kernel::Avx512);
1300                    }
1301                )*
1302            }
1303        }
1304    }
1305
1306    #[cfg(feature = "proptest")]
1307    fn check_kurtosis_property(test_name: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1308        use proptest::prelude::*;
1309        skip_if_unsupported!(kernel, test_name);
1310
1311        let strat = (2usize..=50)
1312            .prop_flat_map(|period| {
1313                let min_len = period * 2;
1314                let max_len = 400.min(period * 20);
1315                (min_len..=max_len, Just(period))
1316            })
1317            .prop_flat_map(|(len, period)| {
1318                (
1319                    proptest::collection::vec(
1320                        (50.0f64..150.0f64).prop_flat_map(move |base| {
1321                            let trend = (-0.01f64..0.01f64);
1322                            trend.prop_flat_map(move |t| {
1323                                let noise = (-2.0f64..2.0f64);
1324                                noise.prop_map(move |n| base * (1.0 + t) + n)
1325                            })
1326                        }),
1327                        len,
1328                    ),
1329                    Just(period),
1330                )
1331            });
1332
1333        proptest::test_runner::TestRunner::default().run(&strat, |(data, period)| {
1334            let params = KurtosisParams {
1335                period: Some(period),
1336            };
1337            let input = KurtosisInput::from_slice(&data, params.clone());
1338
1339            let result = kurtosis_with_kernel(&input, kernel)?;
1340            let scalar_result = kurtosis_with_kernel(&input, Kernel::Scalar)?;
1341
1342            prop_assert_eq!(result.values.len(), data.len(), "Output length mismatch");
1343
1344            let warmup_end = period - 1;
1345            for i in 0..warmup_end.min(data.len()) {
1346                prop_assert!(
1347                    result.values[i].is_nan(),
1348                    "Expected NaN during warmup at index {}",
1349                    i
1350                );
1351            }
1352
1353            for i in warmup_end..data.len() {
1354                let window_start = i + 1 - period;
1355                let window = &data[window_start..=i];
1356                let has_nan = window.iter().any(|x| x.is_nan());
1357
1358                if has_nan {
1359                    prop_assert!(
1360                        result.values[i].is_nan(),
1361                        "Expected NaN when window contains NaN at index {}",
1362                        i
1363                    );
1364                } else {
1365                    prop_assert!(
1366                        result.values[i].is_finite() || result.values[i].is_nan(),
1367                        "Expected finite or NaN value at index {}, got {}",
1368                        i,
1369                        result.values[i]
1370                    );
1371                }
1372            }
1373
1374            for i in warmup_end..data.len() {
1375                let val = result.values[i];
1376                let scalar_val = scalar_result.values[i];
1377
1378                if val.is_nan() && scalar_val.is_nan() {
1379                    continue;
1380                }
1381
1382                if val.is_finite() && scalar_val.is_finite() {
1383                    let val_bits = val.to_bits();
1384                    let scalar_bits = scalar_val.to_bits();
1385                    let ulp_diff = val_bits.abs_diff(scalar_bits);
1386
1387                    prop_assert!(
1388                        (val - scalar_val).abs() <= 1e-9 || ulp_diff <= 5,
1389                        "Kernel mismatch at index {}: {} vs {} (ULP diff: {})",
1390                        i,
1391                        val,
1392                        scalar_val,
1393                        ulp_diff
1394                    );
1395                } else {
1396                    prop_assert_eq!(
1397                        val.is_nan(),
1398                        scalar_val.is_nan(),
1399                        "NaN mismatch at index {}",
1400                        i
1401                    );
1402                }
1403            }
1404
1405            let constant_data = vec![42.0; data.len()];
1406            let constant_input = KurtosisInput::from_slice(&constant_data, params.clone());
1407            let constant_result = kurtosis_with_kernel(&constant_input, kernel)?;
1408
1409            for i in warmup_end..constant_data.len() {
1410                prop_assert!(
1411                    constant_result.values[i].is_nan(),
1412                    "Expected NaN for constant values at index {}, got {}",
1413                    i,
1414                    constant_result.values[i]
1415                );
1416            }
1417
1418            if period >= 30 && data.len() >= 100 {
1419                let stable_start = data.len() / 4;
1420                let stable_end = data.len() * 3 / 4;
1421                let stable_kurtosis: Vec<f64> = result.values[stable_start..stable_end]
1422                    .iter()
1423                    .filter(|x| x.is_finite())
1424                    .copied()
1425                    .collect();
1426
1427                if stable_kurtosis.len() > 10 {
1428                    let mean_kurtosis =
1429                        stable_kurtosis.iter().sum::<f64>() / stable_kurtosis.len() as f64;
1430
1431                    prop_assert!(
1432							mean_kurtosis >= -0.5 && mean_kurtosis <= 0.5,
1433							"Mean kurtosis {} outside expected range [-0.5, 0.5] for pseudo-normal data", mean_kurtosis
1434						);
1435                }
1436            }
1437
1438            for i in warmup_end..data.len() {
1439                if result.values[i].is_finite() {
1440                    prop_assert!(
1441                        result.values[i] >= -2.0 - 1e-10,
1442                        "Kurtosis {} at index {} violates theoretical minimum of -2.0",
1443                        result.values[i],
1444                        i
1445                    );
1446                }
1447            }
1448
1449            if data.len() > period * 2 && period >= 3 {
1450                let mut outlier_data = data.clone();
1451                let mid = data.len() / 2;
1452                if mid >= period {
1453                    let window_start = mid - period + 1;
1454                    let window = &data[window_start..=mid];
1455                    let mean = window.iter().sum::<f64>() / period as f64;
1456                    let variance =
1457                        window.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / period as f64;
1458                    let std_dev = variance.sqrt();
1459
1460                    outlier_data[mid] = mean + std_dev * 10.0;
1461
1462                    let outlier_input = KurtosisInput::from_slice(&outlier_data, params.clone());
1463                    let outlier_result = kurtosis_with_kernel(&outlier_input, kernel)?;
1464
1465                    if result.values[mid].is_finite()
1466                        && outlier_result.values[mid].is_finite()
1467                        && std_dev > 0.01
1468                    {
1469                        prop_assert!(
1470                            outlier_result.values[mid] > result.values[mid],
1471                            "Outlier should increase kurtosis: original {}, with outlier {}",
1472                            result.values[mid],
1473                            outlier_result.values[mid]
1474                        );
1475
1476                        let kurtosis_increase = outlier_result.values[mid] - result.values[mid];
1477                        prop_assert!(
1478								kurtosis_increase > 0.5,
1479								"Outlier should substantially increase kurtosis: increase of {} is too small",
1480								kurtosis_increase
1481							);
1482                    }
1483                }
1484            }
1485
1486            if period >= 4 {
1487                let uniform_data: Vec<f64> = (0..data.len())
1488                    .map(|i| {
1489                        let base = (i / period) as f64 * 10.0;
1490
1491                        base + ((i % period) as f64) * 0.001
1492                    })
1493                    .collect();
1494
1495                let uniform_input = KurtosisInput::from_slice(&uniform_data, params.clone());
1496                let uniform_result = kurtosis_with_kernel(&uniform_input, kernel)?;
1497
1498                let check_start = warmup_end + period;
1499                let check_end = (check_start + 5).min(uniform_data.len());
1500
1501                for i in check_start..check_end {
1502                    if uniform_result.values[i].is_finite() {
1503                        prop_assert!(
1504								uniform_result.values[i] < 0.0,
1505								"Nearly uniform distribution should have negative excess kurtosis at index {}, got {}",
1506								i, uniform_result.values[i]
1507							);
1508                    }
1509                }
1510            }
1511
1512            Ok(())
1513        })?;
1514
1515        Ok(())
1516    }
1517
1518    generate_all_kurtosis_tests!(
1519        check_kurtosis_partial_params,
1520        check_kurtosis_accuracy,
1521        check_kurtosis_default_candles,
1522        check_kurtosis_zero_period,
1523        check_kurtosis_period_exceeds_length,
1524        check_kurtosis_very_small_dataset,
1525        check_kurtosis_reinput,
1526        check_kurtosis_nan_handling,
1527        check_kurtosis_streaming,
1528        check_kurtosis_no_poison
1529    );
1530
1531    #[cfg(feature = "proptest")]
1532    generate_all_kurtosis_tests!(check_kurtosis_property);
1533
1534    fn check_batch_default_row(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1535        skip_if_unsupported!(kernel, test);
1536
1537        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1538        let c = read_candles_from_csv(file)?;
1539
1540        let output = KurtosisBatchBuilder::new()
1541            .kernel(kernel)
1542            .apply_candles(&c, "hl2")?;
1543
1544        let def = KurtosisParams::default();
1545        let row = output.values_for(&def).expect("default row missing");
1546
1547        assert_eq!(row.len(), c.close.len());
1548
1549        let expected = [
1550            -0.5438903789933454,
1551            -1.6848139264816433,
1552            -1.6331336745945797,
1553            -0.6130805596586351,
1554            -0.027802601135927585,
1555        ];
1556        let start = row.len() - 5;
1557        for (i, &v) in row[start..].iter().enumerate() {
1558            assert!(
1559                (v - expected[i]).abs() < 1e-6,
1560                "[{test}] default-row mismatch at idx {i}: {v} vs {expected:?}"
1561            );
1562        }
1563        Ok(())
1564    }
1565
1566    #[cfg(debug_assertions)]
1567    fn check_batch_no_poison(test: &str, kernel: Kernel) -> Result<(), Box<dyn Error>> {
1568        skip_if_unsupported!(kernel, test);
1569
1570        let file = "src/data/2018-09-01-2024-Bitfinex_Spot-4h.csv";
1571        let c = read_candles_from_csv(file)?;
1572        let data = source_type(&c, "close");
1573
1574        let test_configs = vec![
1575            (2, 10, 2),
1576            (5, 25, 5),
1577            (30, 60, 15),
1578            (2, 5, 1),
1579            (10, 20, 2),
1580            (20, 50, 10),
1581            (5, 5, 0),
1582        ];
1583
1584        for (cfg_idx, &(p_start, p_end, p_step)) in test_configs.iter().enumerate() {
1585            let output = KurtosisBatchBuilder::new()
1586                .kernel(kernel)
1587                .period_range(p_start, p_end, p_step)
1588                .apply_slice(data)?;
1589
1590            for (idx, &val) in output.values.iter().enumerate() {
1591                if val.is_nan() {
1592                    continue;
1593                }
1594
1595                let bits = val.to_bits();
1596                let row = idx / output.cols;
1597                let col = idx % output.cols;
1598                let combo = &output.combos[row];
1599
1600                if bits == 0x11111111_11111111 {
1601                    panic!(
1602                        "[{}] Config {}: Found alloc_with_nan_prefix poison value {} (0x{:016X}) \
1603						 at row {} col {} (flat index {}) with params: period={}",
1604                        test,
1605                        cfg_idx,
1606                        val,
1607                        bits,
1608                        row,
1609                        col,
1610                        idx,
1611                        combo.period.unwrap_or(5)
1612                    );
1613                }
1614
1615                if bits == 0x22222222_22222222 {
1616                    panic!(
1617                        "[{}] Config {}: Found init_matrix_prefixes poison value {} (0x{:016X}) \
1618						 at row {} col {} (flat index {}) with params: period={}",
1619                        test,
1620                        cfg_idx,
1621                        val,
1622                        bits,
1623                        row,
1624                        col,
1625                        idx,
1626                        combo.period.unwrap_or(5)
1627                    );
1628                }
1629
1630                if bits == 0x33333333_33333333 {
1631                    panic!(
1632                        "[{}] Config {}: Found make_uninit_matrix poison value {} (0x{:016X}) \
1633						 at row {} col {} (flat index {}) with params: period={}",
1634                        test,
1635                        cfg_idx,
1636                        val,
1637                        bits,
1638                        row,
1639                        col,
1640                        idx,
1641                        combo.period.unwrap_or(5)
1642                    );
1643                }
1644            }
1645        }
1646
1647        Ok(())
1648    }
1649
1650    #[cfg(not(debug_assertions))]
1651    fn check_batch_no_poison(_test: &str, _kernel: Kernel) -> Result<(), Box<dyn Error>> {
1652        Ok(())
1653    }
1654
1655    macro_rules! gen_batch_tests {
1656        ($fn_name:ident) => {
1657            paste::paste! {
1658                #[test] fn [<$fn_name _scalar>]()      {
1659                    let _ = $fn_name(stringify!([<$fn_name _scalar>]), Kernel::ScalarBatch);
1660                }
1661                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1662                #[test] fn [<$fn_name _avx2>]()        {
1663                    let _ = $fn_name(stringify!([<$fn_name _avx2>]), Kernel::Avx2Batch);
1664                }
1665                #[cfg(all(feature = "nightly-avx", target_arch = "x86_64"))]
1666                #[test] fn [<$fn_name _avx512>]()      {
1667                    let _ = $fn_name(stringify!([<$fn_name _avx512>]), Kernel::Avx512Batch);
1668                }
1669                #[test] fn [<$fn_name _auto_detect>]() {
1670                    let _ = $fn_name(stringify!([<$fn_name _auto_detect>]), Kernel::Auto);
1671                }
1672            }
1673        };
1674    }
1675    gen_batch_tests!(check_batch_default_row);
1676    gen_batch_tests!(check_batch_no_poison);
1677}
1678
1679#[cfg(feature = "python")]
1680#[pyfunction(name = "kurtosis")]
1681#[pyo3(signature = (data, period, kernel=None))]
1682pub fn kurtosis_py<'py>(
1683    py: Python<'py>,
1684    data: PyReadonlyArray1<'py, f64>,
1685    period: usize,
1686    kernel: Option<&str>,
1687) -> PyResult<Bound<'py, PyArray1<f64>>> {
1688    use numpy::{IntoPyArray, PyArrayMethods};
1689
1690    let slice_in = data.as_slice()?;
1691    let kern = validate_kernel(kernel, false)?;
1692
1693    let params = KurtosisParams {
1694        period: Some(period),
1695    };
1696    let input = KurtosisInput::from_slice(slice_in, params);
1697
1698    let result_vec: Vec<f64> = py
1699        .allow_threads(|| kurtosis_with_kernel(&input, kern).map(|o| o.values))
1700        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1701
1702    Ok(result_vec.into_pyarray(py))
1703}
1704
1705#[cfg(feature = "python")]
1706#[pyclass(name = "KurtosisStream")]
1707pub struct KurtosisStreamPy {
1708    stream: KurtosisStream,
1709}
1710
1711#[cfg(feature = "python")]
1712#[pymethods]
1713impl KurtosisStreamPy {
1714    #[new]
1715    fn new(period: usize) -> PyResult<Self> {
1716        let params = KurtosisParams {
1717            period: Some(period),
1718        };
1719        let stream =
1720            KurtosisStream::try_new(params).map_err(|e| PyValueError::new_err(e.to_string()))?;
1721        Ok(KurtosisStreamPy { stream })
1722    }
1723
1724    fn update(&mut self, value: f64) -> Option<f64> {
1725        self.stream.update(value)
1726    }
1727}
1728
1729#[cfg(feature = "python")]
1730#[pyfunction(name = "kurtosis_batch")]
1731#[pyo3(signature = (data, period_range, kernel=None))]
1732pub fn kurtosis_batch_py<'py>(
1733    py: Python<'py>,
1734    data: PyReadonlyArray1<'py, f64>,
1735    period_range: (usize, usize, usize),
1736    kernel: Option<&str>,
1737) -> PyResult<Bound<'py, PyDict>> {
1738    use numpy::{IntoPyArray, PyArray1, PyArrayMethods};
1739    use pyo3::types::PyDict;
1740
1741    let slice_in = data.as_slice()?;
1742    let kern = validate_kernel(kernel, true)?;
1743
1744    let sweep = KurtosisBatchRange {
1745        period: period_range,
1746    };
1747
1748    let combos = expand_grid(&sweep).map_err(|e| PyValueError::new_err(e.to_string()))?;
1749    let rows = combos.len();
1750    let cols = slice_in.len();
1751
1752    let expected = rows
1753        .checked_mul(cols)
1754        .ok_or_else(|| PyValueError::new_err("kurtosis: rows*cols overflow"))?;
1755    let out_arr = unsafe { PyArray1::<f64>::new(py, [expected], false) };
1756    let slice_out = unsafe { out_arr.as_slice_mut()? };
1757
1758    let combos = py
1759        .allow_threads(|| {
1760            let kernel = match kern {
1761                Kernel::Auto => detect_best_batch_kernel(),
1762                k => k,
1763            };
1764            let simd = match kernel {
1765                Kernel::Avx512Batch => Kernel::Avx512,
1766                Kernel::Avx2Batch => Kernel::Avx2,
1767                Kernel::ScalarBatch => Kernel::Scalar,
1768                _ => unreachable!(),
1769            };
1770            kurtosis_batch_inner_into(slice_in, &sweep, simd, true, slice_out)
1771        })
1772        .map_err(|e| PyValueError::new_err(e.to_string()))?;
1773
1774    let dict = PyDict::new(py);
1775    dict.set_item("values", out_arr.reshape((rows, cols))?)?;
1776    dict.set_item(
1777        "periods",
1778        combos
1779            .iter()
1780            .map(|p| p.period.unwrap() as u64)
1781            .collect::<Vec<_>>()
1782            .into_pyarray(py),
1783    )?;
1784
1785    Ok(dict)
1786}
1787
1788#[cfg(all(feature = "python", feature = "cuda"))]
1789#[pyfunction(name = "kurtosis_cuda_batch_dev")]
1790#[pyo3(signature = (data_f32, period_range, device_id=0))]
1791pub fn kurtosis_cuda_batch_dev_py(
1792    py: Python<'_>,
1793    data_f32: numpy::PyReadonlyArray1<'_, f32>,
1794    period_range: (usize, usize, usize),
1795    device_id: usize,
1796) -> PyResult<DeviceArrayF32Py> {
1797    use crate::cuda::cuda_available;
1798    use crate::cuda::kurtosis_wrapper::CudaKurtosis;
1799    if !cuda_available() {
1800        return Err(PyValueError::new_err("CUDA not available"));
1801    }
1802    let slice_in = data_f32.as_slice()?;
1803    let sweep = KurtosisBatchRange {
1804        period: period_range,
1805    };
1806    let inner = py.allow_threads(|| {
1807        let cuda =
1808            CudaKurtosis::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1809        let (dev, _combos) = cuda
1810            .kurtosis_batch_dev(slice_in, &sweep)
1811            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1812        Ok::<_, PyErr>(dev)
1813    })?;
1814    let handle = make_device_array_py(device_id, inner)?;
1815    Ok(handle)
1816}
1817
1818#[cfg(all(feature = "python", feature = "cuda"))]
1819#[pyfunction(name = "kurtosis_cuda_many_series_one_param_dev")]
1820#[pyo3(signature = (data_tm_f32, period, device_id=0))]
1821pub fn kurtosis_cuda_many_series_one_param_dev_py(
1822    py: Python<'_>,
1823    data_tm_f32: numpy::PyReadonlyArray2<'_, f32>,
1824    period: usize,
1825    device_id: usize,
1826) -> PyResult<DeviceArrayF32Py> {
1827    use crate::cuda::cuda_available;
1828    use crate::cuda::kurtosis_wrapper::CudaKurtosis;
1829    if !cuda_available() {
1830        return Err(PyValueError::new_err("CUDA not available"));
1831    }
1832    let shape = data_tm_f32.shape();
1833    if shape.len() != 2 {
1834        return Err(PyValueError::new_err("expected 2D time-major array"));
1835    }
1836    let rows = shape[0];
1837    let cols = shape[1];
1838    let slice_in = data_tm_f32.as_slice()?;
1839    let inner = py.allow_threads(|| {
1840        let cuda =
1841            CudaKurtosis::new(device_id).map_err(|e| PyValueError::new_err(e.to_string()))?;
1842        let dev = cuda
1843            .kurtosis_many_series_one_param_time_major_dev(slice_in, cols, rows, period)
1844            .map_err(|e| PyValueError::new_err(e.to_string()))?;
1845        Ok::<_, PyErr>(dev)
1846    })?;
1847    let handle = make_device_array_py(device_id, inner)?;
1848    Ok(handle)
1849}
1850
1851#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1852#[wasm_bindgen]
1853pub fn kurtosis_js(data: &[f64], period: usize) -> Result<Vec<f64>, JsValue> {
1854    let params = KurtosisParams {
1855        period: Some(period),
1856    };
1857    let input = KurtosisInput::from_slice(data, params);
1858
1859    let mut output = vec![0.0; data.len()];
1860
1861    kurtosis_into_slice(&mut output, &input, Kernel::Auto)
1862        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1863
1864    Ok(output)
1865}
1866
1867#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1868#[wasm_bindgen]
1869pub fn kurtosis_alloc(len: usize) -> *mut f64 {
1870    let mut vec = Vec::<f64>::with_capacity(len);
1871    let ptr = vec.as_mut_ptr();
1872    std::mem::forget(vec);
1873    ptr
1874}
1875
1876#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1877#[wasm_bindgen]
1878pub fn kurtosis_free(ptr: *mut f64, len: usize) {
1879    if !ptr.is_null() {
1880        unsafe {
1881            let _ = Vec::from_raw_parts(ptr, len, len);
1882        }
1883    }
1884}
1885
1886#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1887#[wasm_bindgen]
1888pub fn kurtosis_into(
1889    in_ptr: *const f64,
1890    out_ptr: *mut f64,
1891    len: usize,
1892    period: usize,
1893) -> Result<(), JsValue> {
1894    if in_ptr.is_null() || out_ptr.is_null() {
1895        return Err(JsValue::from_str("null pointer passed to kurtosis_into"));
1896    }
1897
1898    unsafe {
1899        let data = std::slice::from_raw_parts(in_ptr, len);
1900
1901        if period == 0 || period > len {
1902            return Err(JsValue::from_str("Invalid period"));
1903        }
1904
1905        let params = KurtosisParams {
1906            period: Some(period),
1907        };
1908        let input = KurtosisInput::from_slice(data, params);
1909
1910        if in_ptr == out_ptr {
1911            let mut temp = vec![0.0; len];
1912            kurtosis_into_slice(&mut temp, &input, Kernel::Auto)
1913                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1914            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1915            out.copy_from_slice(&temp);
1916        } else {
1917            let out = std::slice::from_raw_parts_mut(out_ptr, len);
1918            kurtosis_into_slice(out, &input, Kernel::Auto)
1919                .map_err(|e| JsValue::from_str(&e.to_string()))?;
1920        }
1921
1922        Ok(())
1923    }
1924}
1925
1926#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1927#[derive(Serialize, Deserialize)]
1928pub struct KurtosisBatchConfig {
1929    pub period_range: (usize, usize, usize),
1930}
1931
1932#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1933#[derive(Serialize, Deserialize)]
1934pub struct KurtosisBatchJsOutput {
1935    pub values: Vec<f64>,
1936    pub combos: Vec<KurtosisParams>,
1937    pub periods: Vec<usize>,
1938    pub rows: usize,
1939    pub cols: usize,
1940}
1941
1942#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1943#[wasm_bindgen(js_name = kurtosis_batch)]
1944pub fn kurtosis_batch_unified_js(data: &[f64], config: JsValue) -> Result<JsValue, JsValue> {
1945    let config: KurtosisBatchConfig = serde_wasm_bindgen::from_value(config)
1946        .map_err(|e| JsValue::from_str(&format!("Invalid config: {}", e)))?;
1947
1948    let sweep = KurtosisBatchRange {
1949        period: config.period_range,
1950    };
1951
1952    let output = kurtosis_batch_with_kernel(data, &sweep, Kernel::Auto)
1953        .map_err(|e| JsValue::from_str(&e.to_string()))?;
1954
1955    let js_output = KurtosisBatchJsOutput {
1956        values: output.values,
1957        periods: output.combos.iter().map(|c| c.period.unwrap()).collect(),
1958        combos: output.combos,
1959        rows: output.rows,
1960        cols: output.cols,
1961    };
1962
1963    serde_wasm_bindgen::to_value(&js_output)
1964        .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
1965}
1966
1967#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1968#[wasm_bindgen]
1969pub fn kurtosis_batch_into(
1970    in_ptr: *const f64,
1971    out_ptr: *mut f64,
1972    len: usize,
1973    period_start: usize,
1974    period_end: usize,
1975    period_step: usize,
1976) -> Result<usize, JsValue> {
1977    if in_ptr.is_null() || out_ptr.is_null() {
1978        return Err(JsValue::from_str(
1979            "null pointer passed to kurtosis_batch_into",
1980        ));
1981    }
1982
1983    unsafe {
1984        let data = std::slice::from_raw_parts(in_ptr, len);
1985
1986        let sweep = KurtosisBatchRange {
1987            period: (period_start, period_end, period_step),
1988        };
1989
1990        let combos = expand_grid(&sweep).map_err(|e| JsValue::from_str(&e.to_string()))?;
1991        let rows = combos.len();
1992        let cols = len;
1993
1994        let expected = rows.checked_mul(cols).ok_or_else(|| {
1995            JsValue::from_str(
1996                &KurtosisError::InvalidRange {
1997                    start: rows.to_string(),
1998                    end: cols.to_string(),
1999                    step: "rows*cols".into(),
2000                }
2001                .to_string(),
2002            )
2003        })?;
2004        let out = std::slice::from_raw_parts_mut(out_ptr, expected);
2005
2006        let kernel = detect_best_kernel();
2007        kurtosis_batch_inner_into(data, &sweep, kernel, false, out)
2008            .map_err(|e| JsValue::from_str(&e.to_string()))?;
2009
2010        Ok(rows)
2011    }
2012}