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