Skip to main content

vector_ta/indicators/
kaufmanstop.rs

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