Skip to main content

vector_ta/indicators/
chandelier_exit.rs

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