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