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