Skip to main content

vector_ta/indicators/
devstop.rs

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