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}