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
10#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
11use serde::{Deserialize, Serialize};
12#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
13use wasm_bindgen::prelude::*;
14
15use crate::utilities::data_loader::Candles;
16use crate::utilities::enums::Kernel;
17use crate::utilities::helpers::{
18 alloc_with_nan_prefix, detect_best_batch_kernel, detect_best_kernel, init_matrix_prefixes,
19 make_uninit_matrix,
20};
21#[cfg(feature = "python")]
22use crate::utilities::kernel_validation::validate_kernel;
23
24#[cfg(not(target_arch = "wasm32"))]
25use rayon::prelude::*;
26use std::collections::VecDeque;
27use std::mem::{ManuallyDrop, MaybeUninit};
28use thiserror::Error;
29
30const DEFAULT_ADAPTIVE_LENGTH: usize = 55;
31const DEFAULT_STC_LENGTH: usize = 12;
32const DEFAULT_SMOOTHING_FACTOR: f64 = 0.45;
33const DEFAULT_FAST_LENGTH: usize = 26;
34const DEFAULT_SLOW_LENGTH: usize = 50;
35const HISTOGRAM_EMA_PERIOD: usize = 9;
36const SCALE_100: f64 = 100.0;
37const CENTER: f64 = 50.0;
38const EPS: f64 = 1.0e-12;
39
40#[derive(Debug, Clone)]
41pub enum AdaptiveSchaffTrendCycleData<'a> {
42 Candles(&'a Candles),
43 Slices {
44 high: &'a [f64],
45 low: &'a [f64],
46 close: &'a [f64],
47 },
48}
49
50#[derive(Debug, Clone)]
51pub struct AdaptiveSchaffTrendCycleOutput {
52 pub stc: Vec<f64>,
53 pub histogram: Vec<f64>,
54}
55
56#[derive(Debug, Clone)]
57#[cfg_attr(
58 all(target_arch = "wasm32", feature = "wasm"),
59 derive(Serialize, Deserialize)
60)]
61pub struct AdaptiveSchaffTrendCycleParams {
62 pub adaptive_length: Option<usize>,
63 pub stc_length: Option<usize>,
64 pub smoothing_factor: Option<f64>,
65 pub fast_length: Option<usize>,
66 pub slow_length: Option<usize>,
67}
68
69impl Default for AdaptiveSchaffTrendCycleParams {
70 fn default() -> Self {
71 Self {
72 adaptive_length: Some(DEFAULT_ADAPTIVE_LENGTH),
73 stc_length: Some(DEFAULT_STC_LENGTH),
74 smoothing_factor: Some(DEFAULT_SMOOTHING_FACTOR),
75 fast_length: Some(DEFAULT_FAST_LENGTH),
76 slow_length: Some(DEFAULT_SLOW_LENGTH),
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
82pub struct AdaptiveSchaffTrendCycleInput<'a> {
83 pub data: AdaptiveSchaffTrendCycleData<'a>,
84 pub params: AdaptiveSchaffTrendCycleParams,
85}
86
87impl<'a> AdaptiveSchaffTrendCycleInput<'a> {
88 #[inline]
89 pub fn from_candles(candles: &'a Candles, params: AdaptiveSchaffTrendCycleParams) -> Self {
90 Self {
91 data: AdaptiveSchaffTrendCycleData::Candles(candles),
92 params,
93 }
94 }
95
96 #[inline]
97 pub fn from_slices(
98 high: &'a [f64],
99 low: &'a [f64],
100 close: &'a [f64],
101 params: AdaptiveSchaffTrendCycleParams,
102 ) -> Self {
103 Self {
104 data: AdaptiveSchaffTrendCycleData::Slices { high, low, close },
105 params,
106 }
107 }
108
109 #[inline]
110 pub fn with_default_candles(candles: &'a Candles) -> Self {
111 Self::from_candles(candles, AdaptiveSchaffTrendCycleParams::default())
112 }
113
114 #[inline]
115 pub fn get_adaptive_length(&self) -> usize {
116 self.params
117 .adaptive_length
118 .unwrap_or(DEFAULT_ADAPTIVE_LENGTH)
119 }
120
121 #[inline]
122 pub fn get_stc_length(&self) -> usize {
123 self.params.stc_length.unwrap_or(DEFAULT_STC_LENGTH)
124 }
125
126 #[inline]
127 pub fn get_smoothing_factor(&self) -> f64 {
128 self.params
129 .smoothing_factor
130 .unwrap_or(DEFAULT_SMOOTHING_FACTOR)
131 }
132
133 #[inline]
134 pub fn get_fast_length(&self) -> usize {
135 self.params.fast_length.unwrap_or(DEFAULT_FAST_LENGTH)
136 }
137
138 #[inline]
139 pub fn get_slow_length(&self) -> usize {
140 self.params.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH)
141 }
142
143 #[inline]
144 pub fn as_refs(&'a self) -> (&'a [f64], &'a [f64], &'a [f64]) {
145 match &self.data {
146 AdaptiveSchaffTrendCycleData::Candles(candles) => (
147 candles.high.as_slice(),
148 candles.low.as_slice(),
149 candles.close.as_slice(),
150 ),
151 AdaptiveSchaffTrendCycleData::Slices { high, low, close } => (*high, *low, *close),
152 }
153 }
154}
155
156#[derive(Clone, Debug)]
157pub struct AdaptiveSchaffTrendCycleBuilder {
158 adaptive_length: Option<usize>,
159 stc_length: Option<usize>,
160 smoothing_factor: Option<f64>,
161 fast_length: Option<usize>,
162 slow_length: Option<usize>,
163 kernel: Kernel,
164}
165
166impl Default for AdaptiveSchaffTrendCycleBuilder {
167 fn default() -> Self {
168 Self {
169 adaptive_length: None,
170 stc_length: None,
171 smoothing_factor: None,
172 fast_length: None,
173 slow_length: None,
174 kernel: Kernel::Auto,
175 }
176 }
177}
178
179impl AdaptiveSchaffTrendCycleBuilder {
180 #[inline]
181 pub fn new() -> Self {
182 Self::default()
183 }
184
185 #[inline]
186 pub fn adaptive_length(mut self, value: usize) -> Self {
187 self.adaptive_length = Some(value);
188 self
189 }
190
191 #[inline]
192 pub fn stc_length(mut self, value: usize) -> Self {
193 self.stc_length = Some(value);
194 self
195 }
196
197 #[inline]
198 pub fn smoothing_factor(mut self, value: f64) -> Self {
199 self.smoothing_factor = Some(value);
200 self
201 }
202
203 #[inline]
204 pub fn fast_length(mut self, value: usize) -> Self {
205 self.fast_length = Some(value);
206 self
207 }
208
209 #[inline]
210 pub fn slow_length(mut self, value: usize) -> Self {
211 self.slow_length = Some(value);
212 self
213 }
214
215 #[inline]
216 pub fn kernel(mut self, value: Kernel) -> Self {
217 self.kernel = value;
218 self
219 }
220
221 #[inline]
222 pub fn apply(
223 self,
224 candles: &Candles,
225 ) -> Result<AdaptiveSchaffTrendCycleOutput, AdaptiveSchaffTrendCycleError> {
226 let input = AdaptiveSchaffTrendCycleInput::from_candles(
227 candles,
228 AdaptiveSchaffTrendCycleParams {
229 adaptive_length: self.adaptive_length,
230 stc_length: self.stc_length,
231 smoothing_factor: self.smoothing_factor,
232 fast_length: self.fast_length,
233 slow_length: self.slow_length,
234 },
235 );
236 adaptive_schaff_trend_cycle_with_kernel(&input, self.kernel)
237 }
238
239 #[inline]
240 pub fn apply_slices(
241 self,
242 high: &[f64],
243 low: &[f64],
244 close: &[f64],
245 ) -> Result<AdaptiveSchaffTrendCycleOutput, AdaptiveSchaffTrendCycleError> {
246 let input = AdaptiveSchaffTrendCycleInput::from_slices(
247 high,
248 low,
249 close,
250 AdaptiveSchaffTrendCycleParams {
251 adaptive_length: self.adaptive_length,
252 stc_length: self.stc_length,
253 smoothing_factor: self.smoothing_factor,
254 fast_length: self.fast_length,
255 slow_length: self.slow_length,
256 },
257 );
258 adaptive_schaff_trend_cycle_with_kernel(&input, self.kernel)
259 }
260
261 #[inline]
262 pub fn into_stream(
263 self,
264 ) -> Result<AdaptiveSchaffTrendCycleStream, AdaptiveSchaffTrendCycleError> {
265 AdaptiveSchaffTrendCycleStream::try_new(AdaptiveSchaffTrendCycleParams {
266 adaptive_length: self.adaptive_length,
267 stc_length: self.stc_length,
268 smoothing_factor: self.smoothing_factor,
269 fast_length: self.fast_length,
270 slow_length: self.slow_length,
271 })
272 }
273}
274
275#[derive(Debug, Error)]
276pub enum AdaptiveSchaffTrendCycleError {
277 #[error("adaptive_schaff_trend_cycle: Empty input data.")]
278 EmptyInputData,
279 #[error(
280 "adaptive_schaff_trend_cycle: Input length mismatch: high={high}, low={low}, close={close}"
281 )]
282 DataLengthMismatch {
283 high: usize,
284 low: usize,
285 close: usize,
286 },
287 #[error("adaptive_schaff_trend_cycle: All input values are invalid.")]
288 AllValuesNaN,
289 #[error(
290 "adaptive_schaff_trend_cycle: Invalid adaptive_length: adaptive_length = {adaptive_length}, data length = {data_len}"
291 )]
292 InvalidAdaptiveLength {
293 adaptive_length: usize,
294 data_len: usize,
295 },
296 #[error(
297 "adaptive_schaff_trend_cycle: Invalid stc_length: stc_length = {stc_length}, data length = {data_len}"
298 )]
299 InvalidStcLength { stc_length: usize, data_len: usize },
300 #[error("adaptive_schaff_trend_cycle: Invalid smoothing_factor: {smoothing_factor}")]
301 InvalidSmoothingFactor { smoothing_factor: f64 },
302 #[error("adaptive_schaff_trend_cycle: Invalid fast_length: {fast_length}")]
303 InvalidFastLength { fast_length: usize },
304 #[error("adaptive_schaff_trend_cycle: Invalid slow_length: {slow_length}")]
305 InvalidSlowLength { slow_length: usize },
306 #[error(
307 "adaptive_schaff_trend_cycle: Not enough valid data: needed = {needed}, valid = {valid}"
308 )]
309 NotEnoughValidData { needed: usize, valid: usize },
310 #[error("adaptive_schaff_trend_cycle: Output length mismatch: expected={expected}, got={got}")]
311 OutputLengthMismatch { expected: usize, got: usize },
312 #[error("adaptive_schaff_trend_cycle: Invalid range: start={start}, end={end}, step={step}")]
313 InvalidRange {
314 start: String,
315 end: String,
316 step: String,
317 },
318 #[error(
319 "adaptive_schaff_trend_cycle: Invalid float range: start={start}, end={end}, step={step}"
320 )]
321 InvalidFloatRange { start: f64, end: f64, step: f64 },
322 #[error("adaptive_schaff_trend_cycle: Invalid kernel for batch: {0:?}")]
323 InvalidKernelForBatch(Kernel),
324}
325
326#[inline(always)]
327fn valid_bar(high: f64, low: f64, close: f64) -> bool {
328 high.is_finite() && low.is_finite() && close.is_finite() && high >= low
329}
330
331#[inline(always)]
332fn first_valid_bar(high: &[f64], low: &[f64], close: &[f64]) -> Option<usize> {
333 (0..close.len()).find(|&i| valid_bar(high[i], low[i], close[i]))
334}
335
336#[inline(always)]
337fn normalize_kernel(kernel: Kernel) -> Kernel {
338 match kernel {
339 Kernel::Auto => detect_best_kernel(),
340 other if other.is_batch() => other.to_non_batch(),
341 other => other,
342 }
343}
344
345#[inline(always)]
346fn validate_lengths(
347 high: &[f64],
348 low: &[f64],
349 close: &[f64],
350) -> Result<(), AdaptiveSchaffTrendCycleError> {
351 if high.is_empty() || low.is_empty() || close.is_empty() {
352 return Err(AdaptiveSchaffTrendCycleError::EmptyInputData);
353 }
354 if high.len() != low.len() || low.len() != close.len() {
355 return Err(AdaptiveSchaffTrendCycleError::DataLengthMismatch {
356 high: high.len(),
357 low: low.len(),
358 close: close.len(),
359 });
360 }
361 Ok(())
362}
363
364#[inline(always)]
365fn validate_params(
366 adaptive_length: usize,
367 stc_length: usize,
368 smoothing_factor: f64,
369 fast_length: usize,
370 slow_length: usize,
371 len: usize,
372) -> Result<(), AdaptiveSchaffTrendCycleError> {
373 if adaptive_length == 0 || adaptive_length > len {
374 return Err(AdaptiveSchaffTrendCycleError::InvalidAdaptiveLength {
375 adaptive_length,
376 data_len: len,
377 });
378 }
379 if stc_length == 0 || stc_length > len {
380 return Err(AdaptiveSchaffTrendCycleError::InvalidStcLength {
381 stc_length,
382 data_len: len,
383 });
384 }
385 if !smoothing_factor.is_finite()
386 || !(0.0..=1.0).contains(&smoothing_factor)
387 || smoothing_factor <= 0.0
388 {
389 return Err(AdaptiveSchaffTrendCycleError::InvalidSmoothingFactor { smoothing_factor });
390 }
391 if fast_length == 0 {
392 return Err(AdaptiveSchaffTrendCycleError::InvalidFastLength { fast_length });
393 }
394 if slow_length == 0 {
395 return Err(AdaptiveSchaffTrendCycleError::InvalidSlowLength { slow_length });
396 }
397 Ok(())
398}
399
400#[derive(Clone, Debug)]
401struct EmaState {
402 alpha: f64,
403 initialized: bool,
404 value: f64,
405}
406
407impl EmaState {
408 #[inline]
409 fn new(period: usize) -> Self {
410 Self {
411 alpha: 2.0 / (period as f64 + 1.0),
412 initialized: false,
413 value: f64::NAN,
414 }
415 }
416
417 #[inline]
418 fn reset(&mut self) {
419 self.initialized = false;
420 self.value = f64::NAN;
421 }
422
423 #[inline]
424 fn update(&mut self, value: f64) -> f64 {
425 if !self.initialized {
426 self.value = value;
427 self.initialized = true;
428 } else {
429 self.value += self.alpha * (value - self.value);
430 }
431 self.value
432 }
433}
434
435#[derive(Clone, Debug)]
436struct RollingCorrelationTime {
437 period: usize,
438 values: VecDeque<f64>,
439 sum_x: f64,
440 sum_x2: f64,
441 sum_xy: f64,
442 sum_y: f64,
443 n_sum_y2_minus_sum_y_sq: f64,
444}
445
446impl RollingCorrelationTime {
447 #[inline]
448 fn new(period: usize) -> Self {
449 let n = period as f64;
450 let sum_y = n * (n - 1.0) * 0.5;
451 let sum_y2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
452 Self {
453 period,
454 values: VecDeque::with_capacity(period),
455 sum_x: 0.0,
456 sum_x2: 0.0,
457 sum_xy: 0.0,
458 sum_y,
459 n_sum_y2_minus_sum_y_sq: n * sum_y2 - sum_y * sum_y,
460 }
461 }
462
463 #[inline]
464 fn reset(&mut self) {
465 self.values.clear();
466 self.sum_x = 0.0;
467 self.sum_x2 = 0.0;
468 self.sum_xy = 0.0;
469 }
470
471 #[inline]
472 fn update(&mut self, value: f64) -> Option<f64> {
473 if self.values.len() < self.period {
474 let idx = self.values.len() as f64;
475 self.values.push_back(value);
476 self.sum_x += value;
477 self.sum_x2 += value * value;
478 self.sum_xy += idx * value;
479 if self.values.len() == self.period {
480 return Some(self.compute());
481 }
482 return None;
483 }
484
485 let old_sum_x = self.sum_x;
486 let old_first = self.values.pop_front().unwrap_or(0.0);
487 self.values.push_back(value);
488 self.sum_x = old_sum_x - old_first + value;
489 self.sum_x2 = self.sum_x2 - old_first * old_first + value * value;
490 self.sum_xy = self.sum_xy - (old_sum_x - old_first) + (self.period as f64 - 1.0) * value;
491 Some(self.compute())
492 }
493
494 #[inline]
495 fn compute(&self) -> f64 {
496 if self.period <= 1 {
497 return 0.0;
498 }
499
500 let n = self.period as f64;
501 let numerator = n * self.sum_xy - self.sum_x * self.sum_y;
502 let denom_x = n * self.sum_x2 - self.sum_x * self.sum_x;
503 if denom_x <= EPS || self.n_sum_y2_minus_sum_y_sq <= EPS {
504 return 0.0;
505 }
506
507 let corr = numerator / (denom_x * self.n_sum_y2_minus_sum_y_sq).sqrt();
508 corr.clamp(-1.0, 1.0)
509 }
510}
511
512#[derive(Clone, Debug)]
513struct RollingMinMax {
514 period: usize,
515 next_index: usize,
516 min_q: VecDeque<(usize, f64)>,
517 max_q: VecDeque<(usize, f64)>,
518}
519
520impl RollingMinMax {
521 #[inline]
522 fn new(period: usize) -> Self {
523 Self {
524 period,
525 next_index: 0,
526 min_q: VecDeque::with_capacity(period),
527 max_q: VecDeque::with_capacity(period),
528 }
529 }
530
531 #[inline]
532 fn reset(&mut self) {
533 self.next_index = 0;
534 self.min_q.clear();
535 self.max_q.clear();
536 }
537
538 #[inline]
539 fn update(&mut self, value: f64) -> Option<(f64, f64)> {
540 let idx = self.next_index;
541 self.next_index += 1;
542
543 while let Some((_, back)) = self.min_q.back() {
544 if *back <= value {
545 break;
546 }
547 self.min_q.pop_back();
548 }
549 self.min_q.push_back((idx, value));
550
551 while let Some((_, back)) = self.max_q.back() {
552 if *back >= value {
553 break;
554 }
555 self.max_q.pop_back();
556 }
557 self.max_q.push_back((idx, value));
558
559 let window_start = idx.saturating_add(1).saturating_sub(self.period);
560 while let Some((front_idx, _)) = self.min_q.front() {
561 if *front_idx >= window_start {
562 break;
563 }
564 self.min_q.pop_front();
565 }
566 while let Some((front_idx, _)) = self.max_q.front() {
567 if *front_idx >= window_start {
568 break;
569 }
570 self.max_q.pop_front();
571 }
572
573 if idx + 1 < self.period {
574 return None;
575 }
576
577 Some((
578 self.min_q.front().map(|(_, value)| *value).unwrap_or(value),
579 self.max_q.front().map(|(_, value)| *value).unwrap_or(value),
580 ))
581 }
582}
583
584#[derive(Clone, Debug)]
585struct AdaptiveSchaffTrendCycleCore {
586 smoothing_factor: f64,
587 fast_alpha: f64,
588 slow_alpha: f64,
589 correlation: RollingCorrelationTime,
590 macd_window: RollingMinMax,
591 smoothed_window: RollingMinMax,
592 range_ema: EmaState,
593 histogram_ema: EmaState,
594 prev_close: f64,
595 macd_prev1: f64,
596 macd_prev2: f64,
597 normalized_prev: f64,
598 smoothed_macd_prev: f64,
599 smoothed_macd_initialized: bool,
600 smoothed_normalized_prev: f64,
601 stc_prev: f64,
602 stc_initialized: bool,
603}
604
605impl AdaptiveSchaffTrendCycleCore {
606 #[inline]
607 fn new(
608 adaptive_length: usize,
609 stc_length: usize,
610 smoothing_factor: f64,
611 fast_length: usize,
612 slow_length: usize,
613 ) -> Self {
614 Self {
615 smoothing_factor,
616 fast_alpha: 2.0 / (fast_length as f64 + 1.0),
617 slow_alpha: 2.0 / (slow_length as f64 + 1.0),
618 correlation: RollingCorrelationTime::new(adaptive_length),
619 macd_window: RollingMinMax::new(stc_length),
620 smoothed_window: RollingMinMax::new(stc_length),
621 range_ema: EmaState::new(slow_length),
622 histogram_ema: EmaState::new(HISTOGRAM_EMA_PERIOD),
623 prev_close: f64::NAN,
624 macd_prev1: 0.0,
625 macd_prev2: 0.0,
626 normalized_prev: 0.0,
627 smoothed_macd_prev: 0.0,
628 smoothed_macd_initialized: false,
629 smoothed_normalized_prev: 0.0,
630 stc_prev: 0.0,
631 stc_initialized: false,
632 }
633 }
634
635 #[inline]
636 fn reset(&mut self) {
637 self.correlation.reset();
638 self.macd_window.reset();
639 self.smoothed_window.reset();
640 self.range_ema.reset();
641 self.histogram_ema.reset();
642 self.prev_close = f64::NAN;
643 self.macd_prev1 = 0.0;
644 self.macd_prev2 = 0.0;
645 self.normalized_prev = 0.0;
646 self.smoothed_macd_prev = 0.0;
647 self.smoothed_macd_initialized = false;
648 self.smoothed_normalized_prev = 0.0;
649 self.stc_prev = 0.0;
650 self.stc_initialized = false;
651 }
652
653 #[inline]
654 fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
655 if !valid_bar(high, low, close) {
656 self.reset();
657 return None;
658 }
659
660 let range_ema = self.range_ema.update(high - low);
661 let correlation = self.correlation.update(close);
662 let prev_close = self.prev_close;
663 self.prev_close = close;
664
665 let Some(corr) = correlation else {
666 return Some((f64::NAN, f64::NAN));
667 };
668
669 let delta = if prev_close.is_finite() {
670 close - prev_close
671 } else {
672 0.0
673 };
674 let r2 = 0.5 * corr * corr + 0.5;
675 let k = r2 * ((1.0 - self.fast_alpha) * (1.0 - self.slow_alpha))
676 + (1.0 - r2) * ((1.0 - self.fast_alpha) / (1.0 - self.slow_alpha));
677 let macd = delta * (self.fast_alpha - self.slow_alpha)
678 + (2.0 - self.fast_alpha - self.slow_alpha) * self.macd_prev1
679 - k * self.macd_prev2;
680 self.macd_prev2 = self.macd_prev1;
681 self.macd_prev1 = macd;
682
683 let histogram = if range_ema.abs() > EPS {
684 let normalized_macd = macd / range_ema * SCALE_100;
685 let histogram_ema = self.histogram_ema.update(normalized_macd);
686 (normalized_macd - histogram_ema) * 0.5
687 } else {
688 f64::NAN
689 };
690
691 let Some((macd_min, macd_max)) = self.macd_window.update(macd) else {
692 return Some((f64::NAN, histogram));
693 };
694 let macd_span = macd_max - macd_min;
695 let normalized = if macd_span > EPS {
696 (macd - macd_min) / macd_span * SCALE_100
697 } else {
698 self.normalized_prev
699 };
700 self.normalized_prev = normalized;
701
702 let smoothed_macd = if !self.smoothed_macd_initialized {
703 self.smoothed_macd_initialized = true;
704 normalized
705 } else {
706 self.smoothed_macd_prev + self.smoothing_factor * (normalized - self.smoothed_macd_prev)
707 };
708 self.smoothed_macd_prev = smoothed_macd;
709
710 let Some((smoothed_min, smoothed_max)) = self.smoothed_window.update(smoothed_macd) else {
711 return Some((f64::NAN, histogram));
712 };
713 let smoothed_span = smoothed_max - smoothed_min;
714 let smoothed_normalized = if smoothed_span > EPS {
715 (smoothed_macd - smoothed_min) / smoothed_span * SCALE_100
716 } else {
717 self.smoothed_normalized_prev
718 };
719 self.smoothed_normalized_prev = smoothed_normalized;
720
721 let stc_raw = if !self.stc_initialized {
722 self.stc_initialized = true;
723 smoothed_normalized
724 } else {
725 self.stc_prev + self.smoothing_factor * (smoothed_normalized - self.stc_prev)
726 };
727 self.stc_prev = stc_raw;
728
729 Some((stc_raw - CENTER, histogram))
730 }
731}
732
733#[inline]
734fn adaptive_schaff_trend_cycle_row_scalar(
735 high: &[f64],
736 low: &[f64],
737 close: &[f64],
738 adaptive_length: usize,
739 stc_length: usize,
740 smoothing_factor: f64,
741 fast_length: usize,
742 slow_length: usize,
743 out_stc: &mut [f64],
744 out_histogram: &mut [f64],
745) {
746 let mut core = AdaptiveSchaffTrendCycleCore::new(
747 adaptive_length,
748 stc_length,
749 smoothing_factor,
750 fast_length,
751 slow_length,
752 );
753
754 for i in 0..close.len() {
755 match core.update(high[i], low[i], close[i]) {
756 Some((stc, histogram)) => {
757 out_stc[i] = stc;
758 out_histogram[i] = histogram;
759 }
760 None => {
761 out_stc[i] = f64::NAN;
762 out_histogram[i] = f64::NAN;
763 }
764 }
765 }
766}
767
768#[inline]
769pub fn adaptive_schaff_trend_cycle(
770 input: &AdaptiveSchaffTrendCycleInput,
771) -> Result<AdaptiveSchaffTrendCycleOutput, AdaptiveSchaffTrendCycleError> {
772 adaptive_schaff_trend_cycle_with_kernel(input, Kernel::Auto)
773}
774
775#[inline]
776pub fn adaptive_schaff_trend_cycle_with_kernel(
777 input: &AdaptiveSchaffTrendCycleInput,
778 kernel: Kernel,
779) -> Result<AdaptiveSchaffTrendCycleOutput, AdaptiveSchaffTrendCycleError> {
780 let (high, low, close) = input.as_refs();
781 validate_lengths(high, low, close)?;
782
783 let adaptive_length = input.get_adaptive_length();
784 let stc_length = input.get_stc_length();
785 let smoothing_factor = input.get_smoothing_factor();
786 let fast_length = input.get_fast_length();
787 let slow_length = input.get_slow_length();
788 validate_params(
789 adaptive_length,
790 stc_length,
791 smoothing_factor,
792 fast_length,
793 slow_length,
794 close.len(),
795 )?;
796
797 let first_valid =
798 first_valid_bar(high, low, close).ok_or(AdaptiveSchaffTrendCycleError::AllValuesNaN)?;
799 let valid = close.len().saturating_sub(first_valid);
800 let needed = adaptive_length.max(stc_length);
801 if valid < needed {
802 return Err(AdaptiveSchaffTrendCycleError::NotEnoughValidData { needed, valid });
803 }
804
805 let _kernel = normalize_kernel(kernel);
806 let len = close.len();
807 let mut stc = alloc_with_nan_prefix(len, first_valid);
808 let mut histogram = alloc_with_nan_prefix(len, first_valid);
809
810 adaptive_schaff_trend_cycle_row_scalar(
811 high,
812 low,
813 close,
814 adaptive_length,
815 stc_length,
816 smoothing_factor,
817 fast_length,
818 slow_length,
819 &mut stc,
820 &mut histogram,
821 );
822
823 Ok(AdaptiveSchaffTrendCycleOutput { stc, histogram })
824}
825
826#[inline]
827pub fn adaptive_schaff_trend_cycle_into_slice(
828 out_stc: &mut [f64],
829 out_histogram: &mut [f64],
830 input: &AdaptiveSchaffTrendCycleInput,
831 kernel: Kernel,
832) -> Result<(), AdaptiveSchaffTrendCycleError> {
833 let (high, low, close) = input.as_refs();
834 validate_lengths(high, low, close)?;
835 let len = close.len();
836 if out_stc.len() != len || out_histogram.len() != len {
837 return Err(AdaptiveSchaffTrendCycleError::OutputLengthMismatch {
838 expected: len,
839 got: out_stc.len().max(out_histogram.len()),
840 });
841 }
842
843 let adaptive_length = input.get_adaptive_length();
844 let stc_length = input.get_stc_length();
845 let smoothing_factor = input.get_smoothing_factor();
846 let fast_length = input.get_fast_length();
847 let slow_length = input.get_slow_length();
848 validate_params(
849 adaptive_length,
850 stc_length,
851 smoothing_factor,
852 fast_length,
853 slow_length,
854 len,
855 )?;
856
857 let _kernel = normalize_kernel(kernel);
858 adaptive_schaff_trend_cycle_row_scalar(
859 high,
860 low,
861 close,
862 adaptive_length,
863 stc_length,
864 smoothing_factor,
865 fast_length,
866 slow_length,
867 out_stc,
868 out_histogram,
869 );
870 Ok(())
871}
872
873#[cfg(not(all(target_arch = "wasm32", feature = "wasm")))]
874#[inline]
875pub fn adaptive_schaff_trend_cycle_into(
876 input: &AdaptiveSchaffTrendCycleInput,
877 out_stc: &mut [f64],
878 out_histogram: &mut [f64],
879) -> Result<(), AdaptiveSchaffTrendCycleError> {
880 adaptive_schaff_trend_cycle_into_slice(out_stc, out_histogram, input, Kernel::Auto)
881}
882
883#[derive(Clone, Debug)]
884pub struct AdaptiveSchaffTrendCycleStream {
885 core: AdaptiveSchaffTrendCycleCore,
886}
887
888impl AdaptiveSchaffTrendCycleStream {
889 #[inline]
890 pub fn try_new(
891 params: AdaptiveSchaffTrendCycleParams,
892 ) -> Result<Self, AdaptiveSchaffTrendCycleError> {
893 let adaptive_length = params.adaptive_length.unwrap_or(DEFAULT_ADAPTIVE_LENGTH);
894 let stc_length = params.stc_length.unwrap_or(DEFAULT_STC_LENGTH);
895 let smoothing_factor = params.smoothing_factor.unwrap_or(DEFAULT_SMOOTHING_FACTOR);
896 let fast_length = params.fast_length.unwrap_or(DEFAULT_FAST_LENGTH);
897 let slow_length = params.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH);
898 validate_params(
899 adaptive_length,
900 stc_length,
901 smoothing_factor,
902 fast_length,
903 slow_length,
904 usize::MAX,
905 )?;
906 Ok(Self {
907 core: AdaptiveSchaffTrendCycleCore::new(
908 adaptive_length,
909 stc_length,
910 smoothing_factor,
911 fast_length,
912 slow_length,
913 ),
914 })
915 }
916
917 #[inline]
918 pub fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
919 self.core.update(high, low, close)
920 }
921}
922
923#[derive(Clone, Debug)]
924pub struct AdaptiveSchaffTrendCycleBatchRange {
925 pub adaptive_length: (usize, usize, usize),
926 pub stc_length: (usize, usize, usize),
927 pub smoothing_factor: (f64, f64, f64),
928 pub fast_length: (usize, usize, usize),
929 pub slow_length: (usize, usize, usize),
930}
931
932impl Default for AdaptiveSchaffTrendCycleBatchRange {
933 fn default() -> Self {
934 Self {
935 adaptive_length: (DEFAULT_ADAPTIVE_LENGTH, DEFAULT_ADAPTIVE_LENGTH, 0),
936 stc_length: (DEFAULT_STC_LENGTH, DEFAULT_STC_LENGTH, 0),
937 smoothing_factor: (DEFAULT_SMOOTHING_FACTOR, DEFAULT_SMOOTHING_FACTOR, 0.0),
938 fast_length: (DEFAULT_FAST_LENGTH, DEFAULT_FAST_LENGTH, 0),
939 slow_length: (DEFAULT_SLOW_LENGTH, DEFAULT_SLOW_LENGTH, 0),
940 }
941 }
942}
943
944#[derive(Clone, Debug)]
945pub struct AdaptiveSchaffTrendCycleBatchOutput {
946 pub stc: Vec<f64>,
947 pub histogram: Vec<f64>,
948 pub combos: Vec<AdaptiveSchaffTrendCycleParams>,
949 pub rows: usize,
950 pub cols: usize,
951}
952
953#[derive(Clone, Debug)]
954pub struct AdaptiveSchaffTrendCycleBatchBuilder {
955 range: AdaptiveSchaffTrendCycleBatchRange,
956 kernel: Kernel,
957}
958
959impl Default for AdaptiveSchaffTrendCycleBatchBuilder {
960 fn default() -> Self {
961 Self {
962 range: AdaptiveSchaffTrendCycleBatchRange::default(),
963 kernel: Kernel::Auto,
964 }
965 }
966}
967
968impl AdaptiveSchaffTrendCycleBatchBuilder {
969 #[inline]
970 pub fn new() -> Self {
971 Self::default()
972 }
973
974 #[inline]
975 pub fn adaptive_length_range(mut self, value: (usize, usize, usize)) -> Self {
976 self.range.adaptive_length = value;
977 self
978 }
979
980 #[inline]
981 pub fn stc_length_range(mut self, value: (usize, usize, usize)) -> Self {
982 self.range.stc_length = value;
983 self
984 }
985
986 #[inline]
987 pub fn smoothing_factor_range(mut self, value: (f64, f64, f64)) -> Self {
988 self.range.smoothing_factor = value;
989 self
990 }
991
992 #[inline]
993 pub fn fast_length_range(mut self, value: (usize, usize, usize)) -> Self {
994 self.range.fast_length = value;
995 self
996 }
997
998 #[inline]
999 pub fn slow_length_range(mut self, value: (usize, usize, usize)) -> Self {
1000 self.range.slow_length = value;
1001 self
1002 }
1003
1004 #[inline]
1005 pub fn kernel(mut self, value: Kernel) -> Self {
1006 self.kernel = value;
1007 self
1008 }
1009
1010 #[inline]
1011 pub fn apply_slices(
1012 self,
1013 high: &[f64],
1014 low: &[f64],
1015 close: &[f64],
1016 ) -> Result<AdaptiveSchaffTrendCycleBatchOutput, AdaptiveSchaffTrendCycleError> {
1017 adaptive_schaff_trend_cycle_batch_with_kernel(high, low, close, &self.range, self.kernel)
1018 }
1019
1020 #[inline]
1021 pub fn apply(
1022 self,
1023 candles: &Candles,
1024 ) -> Result<AdaptiveSchaffTrendCycleBatchOutput, AdaptiveSchaffTrendCycleError> {
1025 adaptive_schaff_trend_cycle_batch_with_kernel(
1026 &candles.high,
1027 &candles.low,
1028 &candles.close,
1029 &self.range,
1030 self.kernel,
1031 )
1032 }
1033}
1034
1035pub fn expand_grid_adaptive_schaff_trend_cycle(
1036 range: &AdaptiveSchaffTrendCycleBatchRange,
1037) -> Result<Vec<AdaptiveSchaffTrendCycleParams>, AdaptiveSchaffTrendCycleError> {
1038 fn axis_usize(
1039 (start, end, step): (usize, usize, usize),
1040 ) -> Result<Vec<usize>, AdaptiveSchaffTrendCycleError> {
1041 if step == 0 || start == end {
1042 return Ok(vec![start]);
1043 }
1044
1045 let mut out = Vec::new();
1046 if start <= end {
1047 let mut x = start;
1048 while x <= end {
1049 out.push(x);
1050 x = x.saturating_add(step);
1051 if step == 0 {
1052 break;
1053 }
1054 }
1055 } else {
1056 let mut x = start;
1057 while x >= end {
1058 out.push(x);
1059 let next = x.saturating_sub(step);
1060 if next == x {
1061 break;
1062 }
1063 x = next;
1064 if x < end {
1065 break;
1066 }
1067 }
1068 }
1069
1070 if out.is_empty() {
1071 return Err(AdaptiveSchaffTrendCycleError::InvalidRange {
1072 start: start.to_string(),
1073 end: end.to_string(),
1074 step: step.to_string(),
1075 });
1076 }
1077 Ok(out)
1078 }
1079
1080 fn axis_f64(
1081 (start, end, step): (f64, f64, f64),
1082 ) -> Result<Vec<f64>, AdaptiveSchaffTrendCycleError> {
1083 if !start.is_finite() || !end.is_finite() || !step.is_finite() {
1084 return Err(AdaptiveSchaffTrendCycleError::InvalidFloatRange { start, end, step });
1085 }
1086 if step.abs() < EPS || (start - end).abs() < EPS {
1087 return Ok(vec![start]);
1088 }
1089
1090 let step = step.abs();
1091 let mut out = Vec::new();
1092 if start <= end {
1093 let mut x = start;
1094 while x <= end + EPS {
1095 out.push(x);
1096 x += step;
1097 }
1098 } else {
1099 let mut x = start;
1100 while x + EPS >= end {
1101 out.push(x);
1102 x -= step;
1103 }
1104 }
1105
1106 if out.is_empty() {
1107 return Err(AdaptiveSchaffTrendCycleError::InvalidFloatRange { start, end, step });
1108 }
1109 Ok(out)
1110 }
1111
1112 let adaptive_lengths = axis_usize(range.adaptive_length)?;
1113 let stc_lengths = axis_usize(range.stc_length)?;
1114 let smoothing_factors = axis_f64(range.smoothing_factor)?;
1115 let fast_lengths = axis_usize(range.fast_length)?;
1116 let slow_lengths = axis_usize(range.slow_length)?;
1117
1118 let cap = adaptive_lengths
1119 .len()
1120 .checked_mul(stc_lengths.len())
1121 .and_then(|value| value.checked_mul(smoothing_factors.len()))
1122 .and_then(|value| value.checked_mul(fast_lengths.len()))
1123 .and_then(|value| value.checked_mul(slow_lengths.len()))
1124 .ok_or(AdaptiveSchaffTrendCycleError::InvalidRange {
1125 start: range.adaptive_length.0.to_string(),
1126 end: range.adaptive_length.1.to_string(),
1127 step: range.adaptive_length.2.to_string(),
1128 })?;
1129
1130 let mut out = Vec::with_capacity(cap);
1131 for &adaptive_length in &adaptive_lengths {
1132 for &stc_length in &stc_lengths {
1133 for &smoothing_factor in &smoothing_factors {
1134 for &fast_length in &fast_lengths {
1135 for &slow_length in &slow_lengths {
1136 out.push(AdaptiveSchaffTrendCycleParams {
1137 adaptive_length: Some(adaptive_length),
1138 stc_length: Some(stc_length),
1139 smoothing_factor: Some(smoothing_factor),
1140 fast_length: Some(fast_length),
1141 slow_length: Some(slow_length),
1142 });
1143 }
1144 }
1145 }
1146 }
1147 }
1148 Ok(out)
1149}
1150
1151#[inline]
1152pub fn adaptive_schaff_trend_cycle_batch_with_kernel(
1153 high: &[f64],
1154 low: &[f64],
1155 close: &[f64],
1156 sweep: &AdaptiveSchaffTrendCycleBatchRange,
1157 kernel: Kernel,
1158) -> Result<AdaptiveSchaffTrendCycleBatchOutput, AdaptiveSchaffTrendCycleError> {
1159 let batch_kernel = match kernel {
1160 Kernel::Auto => detect_best_batch_kernel(),
1161 other if other.is_batch() => other,
1162 other => return Err(AdaptiveSchaffTrendCycleError::InvalidKernelForBatch(other)),
1163 };
1164 adaptive_schaff_trend_cycle_batch_par_slice(
1165 high,
1166 low,
1167 close,
1168 sweep,
1169 batch_kernel.to_non_batch(),
1170 )
1171}
1172
1173#[inline]
1174pub fn adaptive_schaff_trend_cycle_batch_slice(
1175 high: &[f64],
1176 low: &[f64],
1177 close: &[f64],
1178 sweep: &AdaptiveSchaffTrendCycleBatchRange,
1179 kernel: Kernel,
1180) -> Result<AdaptiveSchaffTrendCycleBatchOutput, AdaptiveSchaffTrendCycleError> {
1181 adaptive_schaff_trend_cycle_batch_inner(high, low, close, sweep, kernel, false)
1182}
1183
1184#[inline]
1185pub fn adaptive_schaff_trend_cycle_batch_par_slice(
1186 high: &[f64],
1187 low: &[f64],
1188 close: &[f64],
1189 sweep: &AdaptiveSchaffTrendCycleBatchRange,
1190 kernel: Kernel,
1191) -> Result<AdaptiveSchaffTrendCycleBatchOutput, AdaptiveSchaffTrendCycleError> {
1192 adaptive_schaff_trend_cycle_batch_inner(high, low, close, sweep, kernel, true)
1193}
1194
1195fn adaptive_schaff_trend_cycle_batch_inner(
1196 high: &[f64],
1197 low: &[f64],
1198 close: &[f64],
1199 sweep: &AdaptiveSchaffTrendCycleBatchRange,
1200 _kernel: Kernel,
1201 parallel: bool,
1202) -> Result<AdaptiveSchaffTrendCycleBatchOutput, AdaptiveSchaffTrendCycleError> {
1203 validate_lengths(high, low, close)?;
1204 let combos = expand_grid_adaptive_schaff_trend_cycle(sweep)?;
1205 for params in &combos {
1206 validate_params(
1207 params.adaptive_length.unwrap_or(DEFAULT_ADAPTIVE_LENGTH),
1208 params.stc_length.unwrap_or(DEFAULT_STC_LENGTH),
1209 params.smoothing_factor.unwrap_or(DEFAULT_SMOOTHING_FACTOR),
1210 params.fast_length.unwrap_or(DEFAULT_FAST_LENGTH),
1211 params.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH),
1212 close.len(),
1213 )?;
1214 }
1215
1216 let first_valid =
1217 first_valid_bar(high, low, close).ok_or(AdaptiveSchaffTrendCycleError::AllValuesNaN)?;
1218 let rows = combos.len();
1219 let cols = close.len();
1220 let total =
1221 rows.checked_mul(cols)
1222 .ok_or(AdaptiveSchaffTrendCycleError::OutputLengthMismatch {
1223 expected: usize::MAX,
1224 got: 0,
1225 })?;
1226
1227 let mut stc_matrix = make_uninit_matrix(rows, cols);
1228 let mut histogram_matrix = make_uninit_matrix(rows, cols);
1229 let warmups = vec![first_valid; rows];
1230 init_matrix_prefixes(&mut stc_matrix, cols, &warmups);
1231 init_matrix_prefixes(&mut histogram_matrix, cols, &warmups);
1232
1233 let mut stc_guard = ManuallyDrop::new(stc_matrix);
1234 let mut histogram_guard = ManuallyDrop::new(histogram_matrix);
1235
1236 let stc_mu: &mut [MaybeUninit<f64>] =
1237 unsafe { std::slice::from_raw_parts_mut(stc_guard.as_mut_ptr(), stc_guard.len()) };
1238 let histogram_mu: &mut [MaybeUninit<f64>] = unsafe {
1239 std::slice::from_raw_parts_mut(histogram_guard.as_mut_ptr(), histogram_guard.len())
1240 };
1241
1242 let do_row = |row: usize,
1243 row_stc: &mut [MaybeUninit<f64>],
1244 row_histogram: &mut [MaybeUninit<f64>]| {
1245 let params = &combos[row];
1246 let dst_stc =
1247 unsafe { std::slice::from_raw_parts_mut(row_stc.as_mut_ptr() as *mut f64, cols) };
1248 let dst_histogram =
1249 unsafe { std::slice::from_raw_parts_mut(row_histogram.as_mut_ptr() as *mut f64, cols) };
1250 adaptive_schaff_trend_cycle_row_scalar(
1251 high,
1252 low,
1253 close,
1254 params.adaptive_length.unwrap_or(DEFAULT_ADAPTIVE_LENGTH),
1255 params.stc_length.unwrap_or(DEFAULT_STC_LENGTH),
1256 params.smoothing_factor.unwrap_or(DEFAULT_SMOOTHING_FACTOR),
1257 params.fast_length.unwrap_or(DEFAULT_FAST_LENGTH),
1258 params.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH),
1259 dst_stc,
1260 dst_histogram,
1261 );
1262 };
1263
1264 if parallel {
1265 #[cfg(not(target_arch = "wasm32"))]
1266 stc_mu
1267 .par_chunks_mut(cols)
1268 .zip(histogram_mu.par_chunks_mut(cols))
1269 .enumerate()
1270 .for_each(|(row, (row_stc, row_histogram))| do_row(row, row_stc, row_histogram));
1271
1272 #[cfg(target_arch = "wasm32")]
1273 for (row, (row_stc, row_histogram)) in stc_mu
1274 .chunks_mut(cols)
1275 .zip(histogram_mu.chunks_mut(cols))
1276 .enumerate()
1277 {
1278 do_row(row, row_stc, row_histogram);
1279 }
1280 } else {
1281 for (row, (row_stc, row_histogram)) in stc_mu
1282 .chunks_mut(cols)
1283 .zip(histogram_mu.chunks_mut(cols))
1284 .enumerate()
1285 {
1286 do_row(row, row_stc, row_histogram);
1287 }
1288 }
1289
1290 let stc = unsafe {
1291 Vec::from_raw_parts(
1292 stc_guard.as_mut_ptr() as *mut f64,
1293 total,
1294 stc_guard.capacity(),
1295 )
1296 };
1297 let histogram = unsafe {
1298 Vec::from_raw_parts(
1299 histogram_guard.as_mut_ptr() as *mut f64,
1300 total,
1301 histogram_guard.capacity(),
1302 )
1303 };
1304
1305 Ok(AdaptiveSchaffTrendCycleBatchOutput {
1306 stc,
1307 histogram,
1308 combos,
1309 rows,
1310 cols,
1311 })
1312}
1313
1314fn adaptive_schaff_trend_cycle_batch_inner_into(
1315 high: &[f64],
1316 low: &[f64],
1317 close: &[f64],
1318 sweep: &AdaptiveSchaffTrendCycleBatchRange,
1319 kernel: Kernel,
1320 parallel: bool,
1321 out_stc: &mut [f64],
1322 out_histogram: &mut [f64],
1323) -> Result<Vec<AdaptiveSchaffTrendCycleParams>, AdaptiveSchaffTrendCycleError> {
1324 validate_lengths(high, low, close)?;
1325 let combos = expand_grid_adaptive_schaff_trend_cycle(sweep)?;
1326 for params in &combos {
1327 validate_params(
1328 params.adaptive_length.unwrap_or(DEFAULT_ADAPTIVE_LENGTH),
1329 params.stc_length.unwrap_or(DEFAULT_STC_LENGTH),
1330 params.smoothing_factor.unwrap_or(DEFAULT_SMOOTHING_FACTOR),
1331 params.fast_length.unwrap_or(DEFAULT_FAST_LENGTH),
1332 params.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH),
1333 close.len(),
1334 )?;
1335 }
1336
1337 let rows = combos.len();
1338 let cols = close.len();
1339 let total =
1340 rows.checked_mul(cols)
1341 .ok_or(AdaptiveSchaffTrendCycleError::OutputLengthMismatch {
1342 expected: usize::MAX,
1343 got: 0,
1344 })?;
1345 if out_stc.len() != total || out_histogram.len() != total {
1346 return Err(AdaptiveSchaffTrendCycleError::OutputLengthMismatch {
1347 expected: total,
1348 got: out_stc.len().max(out_histogram.len()),
1349 });
1350 }
1351
1352 let _kernel = kernel;
1353 let do_row = |row: usize, dst_stc: &mut [f64], dst_histogram: &mut [f64]| {
1354 let params = &combos[row];
1355 adaptive_schaff_trend_cycle_row_scalar(
1356 high,
1357 low,
1358 close,
1359 params.adaptive_length.unwrap_or(DEFAULT_ADAPTIVE_LENGTH),
1360 params.stc_length.unwrap_or(DEFAULT_STC_LENGTH),
1361 params.smoothing_factor.unwrap_or(DEFAULT_SMOOTHING_FACTOR),
1362 params.fast_length.unwrap_or(DEFAULT_FAST_LENGTH),
1363 params.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH),
1364 dst_stc,
1365 dst_histogram,
1366 );
1367 };
1368
1369 if parallel {
1370 #[cfg(not(target_arch = "wasm32"))]
1371 out_stc
1372 .par_chunks_mut(cols)
1373 .zip(out_histogram.par_chunks_mut(cols))
1374 .enumerate()
1375 .for_each(|(row, (dst_stc, dst_histogram))| do_row(row, dst_stc, dst_histogram));
1376
1377 #[cfg(target_arch = "wasm32")]
1378 for (row, (dst_stc, dst_histogram)) in out_stc
1379 .chunks_mut(cols)
1380 .zip(out_histogram.chunks_mut(cols))
1381 .enumerate()
1382 {
1383 do_row(row, dst_stc, dst_histogram);
1384 }
1385 } else {
1386 for (row, (dst_stc, dst_histogram)) in out_stc
1387 .chunks_mut(cols)
1388 .zip(out_histogram.chunks_mut(cols))
1389 .enumerate()
1390 {
1391 do_row(row, dst_stc, dst_histogram);
1392 }
1393 }
1394
1395 Ok(combos)
1396}
1397
1398#[cfg(feature = "python")]
1399#[pyfunction(name = "adaptive_schaff_trend_cycle")]
1400#[pyo3(signature = (high, low, close, adaptive_length=DEFAULT_ADAPTIVE_LENGTH, stc_length=DEFAULT_STC_LENGTH, smoothing_factor=DEFAULT_SMOOTHING_FACTOR, fast_length=DEFAULT_FAST_LENGTH, slow_length=DEFAULT_SLOW_LENGTH, kernel=None))]
1401pub fn adaptive_schaff_trend_cycle_py<'py>(
1402 py: Python<'py>,
1403 high: PyReadonlyArray1<'py, f64>,
1404 low: PyReadonlyArray1<'py, f64>,
1405 close: PyReadonlyArray1<'py, f64>,
1406 adaptive_length: usize,
1407 stc_length: usize,
1408 smoothing_factor: f64,
1409 fast_length: usize,
1410 slow_length: usize,
1411 kernel: Option<&str>,
1412) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
1413 let high = high.as_slice()?;
1414 let low = low.as_slice()?;
1415 let close = close.as_slice()?;
1416 let input = AdaptiveSchaffTrendCycleInput::from_slices(
1417 high,
1418 low,
1419 close,
1420 AdaptiveSchaffTrendCycleParams {
1421 adaptive_length: Some(adaptive_length),
1422 stc_length: Some(stc_length),
1423 smoothing_factor: Some(smoothing_factor),
1424 fast_length: Some(fast_length),
1425 slow_length: Some(slow_length),
1426 },
1427 );
1428 let kernel = validate_kernel(kernel, false)?;
1429 let out = py
1430 .allow_threads(|| adaptive_schaff_trend_cycle_with_kernel(&input, kernel))
1431 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1432 Ok((out.stc.into_pyarray(py), out.histogram.into_pyarray(py)))
1433}
1434
1435#[cfg(feature = "python")]
1436#[pyclass(name = "AdaptiveSchaffTrendCycleStream")]
1437pub struct AdaptiveSchaffTrendCycleStreamPy {
1438 stream: AdaptiveSchaffTrendCycleStream,
1439}
1440
1441#[cfg(feature = "python")]
1442#[pymethods]
1443impl AdaptiveSchaffTrendCycleStreamPy {
1444 #[new]
1445 #[pyo3(signature = (adaptive_length=DEFAULT_ADAPTIVE_LENGTH, stc_length=DEFAULT_STC_LENGTH, smoothing_factor=DEFAULT_SMOOTHING_FACTOR, fast_length=DEFAULT_FAST_LENGTH, slow_length=DEFAULT_SLOW_LENGTH))]
1446 fn new(
1447 adaptive_length: usize,
1448 stc_length: usize,
1449 smoothing_factor: f64,
1450 fast_length: usize,
1451 slow_length: usize,
1452 ) -> PyResult<Self> {
1453 let stream = AdaptiveSchaffTrendCycleStream::try_new(AdaptiveSchaffTrendCycleParams {
1454 adaptive_length: Some(adaptive_length),
1455 stc_length: Some(stc_length),
1456 smoothing_factor: Some(smoothing_factor),
1457 fast_length: Some(fast_length),
1458 slow_length: Some(slow_length),
1459 })
1460 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1461 Ok(Self { stream })
1462 }
1463
1464 fn update(&mut self, high: f64, low: f64, close: f64) -> Option<(f64, f64)> {
1465 self.stream.update(high, low, close)
1466 }
1467}
1468
1469#[cfg(feature = "python")]
1470#[pyfunction(name = "adaptive_schaff_trend_cycle_batch")]
1471#[pyo3(signature = (high, low, close, adaptive_length_range, stc_length_range, smoothing_factor_range, fast_length_range, slow_length_range, kernel=None))]
1472pub fn adaptive_schaff_trend_cycle_batch_py<'py>(
1473 py: Python<'py>,
1474 high: PyReadonlyArray1<'py, f64>,
1475 low: PyReadonlyArray1<'py, f64>,
1476 close: PyReadonlyArray1<'py, f64>,
1477 adaptive_length_range: (usize, usize, usize),
1478 stc_length_range: (usize, usize, usize),
1479 smoothing_factor_range: (f64, f64, f64),
1480 fast_length_range: (usize, usize, usize),
1481 slow_length_range: (usize, usize, usize),
1482 kernel: Option<&str>,
1483) -> PyResult<Bound<'py, PyDict>> {
1484 let high = high.as_slice()?;
1485 let low = low.as_slice()?;
1486 let close = close.as_slice()?;
1487 let sweep = AdaptiveSchaffTrendCycleBatchRange {
1488 adaptive_length: adaptive_length_range,
1489 stc_length: stc_length_range,
1490 smoothing_factor: smoothing_factor_range,
1491 fast_length: fast_length_range,
1492 slow_length: slow_length_range,
1493 };
1494 let combos = expand_grid_adaptive_schaff_trend_cycle(&sweep)
1495 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1496 let rows = combos.len();
1497 let cols = close.len();
1498 let total = rows
1499 .checked_mul(cols)
1500 .ok_or_else(|| PyValueError::new_err("rows*cols overflow"))?;
1501 let stc_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1502 let histogram_arr = unsafe { PyArray1::<f64>::new(py, [total], false) };
1503 let out_stc = unsafe { stc_arr.as_slice_mut()? };
1504 let out_histogram = unsafe { histogram_arr.as_slice_mut()? };
1505 let kernel = validate_kernel(kernel, true)?;
1506
1507 py.allow_threads(|| {
1508 let batch_kernel = match kernel {
1509 Kernel::Auto => detect_best_batch_kernel(),
1510 other => other,
1511 };
1512 adaptive_schaff_trend_cycle_batch_inner_into(
1513 high,
1514 low,
1515 close,
1516 &sweep,
1517 batch_kernel.to_non_batch(),
1518 true,
1519 out_stc,
1520 out_histogram,
1521 )
1522 })
1523 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1524
1525 let adaptive_lengths: Vec<u64> = combos
1526 .iter()
1527 .map(|params| params.adaptive_length.unwrap_or(DEFAULT_ADAPTIVE_LENGTH) as u64)
1528 .collect();
1529 let stc_lengths: Vec<u64> = combos
1530 .iter()
1531 .map(|params| params.stc_length.unwrap_or(DEFAULT_STC_LENGTH) as u64)
1532 .collect();
1533 let smoothing_factors: Vec<f64> = combos
1534 .iter()
1535 .map(|params| params.smoothing_factor.unwrap_or(DEFAULT_SMOOTHING_FACTOR))
1536 .collect();
1537 let fast_lengths: Vec<u64> = combos
1538 .iter()
1539 .map(|params| params.fast_length.unwrap_or(DEFAULT_FAST_LENGTH) as u64)
1540 .collect();
1541 let slow_lengths: Vec<u64> = combos
1542 .iter()
1543 .map(|params| params.slow_length.unwrap_or(DEFAULT_SLOW_LENGTH) as u64)
1544 .collect();
1545
1546 let dict = PyDict::new(py);
1547 dict.set_item("stc", stc_arr.reshape((rows, cols))?)?;
1548 dict.set_item("histogram", histogram_arr.reshape((rows, cols))?)?;
1549 dict.set_item("rows", rows)?;
1550 dict.set_item("cols", cols)?;
1551 dict.set_item("adaptive_lengths", adaptive_lengths.into_pyarray(py))?;
1552 dict.set_item("stc_lengths", stc_lengths.into_pyarray(py))?;
1553 dict.set_item("smoothing_factors", smoothing_factors.into_pyarray(py))?;
1554 dict.set_item("fast_lengths", fast_lengths.into_pyarray(py))?;
1555 dict.set_item("slow_lengths", slow_lengths.into_pyarray(py))?;
1556 Ok(dict)
1557}
1558
1559#[cfg(feature = "python")]
1560pub fn register_adaptive_schaff_trend_cycle_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
1561 m.add_function(wrap_pyfunction!(adaptive_schaff_trend_cycle_py, m)?)?;
1562 m.add_function(wrap_pyfunction!(adaptive_schaff_trend_cycle_batch_py, m)?)?;
1563 m.add_class::<AdaptiveSchaffTrendCycleStreamPy>()?;
1564 Ok(())
1565}
1566
1567#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1568#[derive(Debug, Clone, Serialize, Deserialize)]
1569struct AdaptiveSchaffTrendCycleJsOutput {
1570 stc: Vec<f64>,
1571 histogram: Vec<f64>,
1572}
1573
1574#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1575#[derive(Debug, Clone, Serialize, Deserialize)]
1576struct AdaptiveSchaffTrendCycleBatchConfig {
1577 adaptive_length_range: Vec<usize>,
1578 stc_length_range: Vec<usize>,
1579 smoothing_factor_range: Vec<f64>,
1580 fast_length_range: Vec<usize>,
1581 slow_length_range: Vec<usize>,
1582}
1583
1584#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1585#[derive(Debug, Clone, Serialize, Deserialize)]
1586struct AdaptiveSchaffTrendCycleBatchJsOutput {
1587 stc: Vec<f64>,
1588 histogram: Vec<f64>,
1589 rows: usize,
1590 cols: usize,
1591 combos: Vec<AdaptiveSchaffTrendCycleParams>,
1592}
1593
1594#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1595#[wasm_bindgen(js_name = "adaptive_schaff_trend_cycle")]
1596pub fn adaptive_schaff_trend_cycle_js(
1597 high: &[f64],
1598 low: &[f64],
1599 close: &[f64],
1600 adaptive_length: usize,
1601 stc_length: usize,
1602 smoothing_factor: f64,
1603 fast_length: usize,
1604 slow_length: usize,
1605) -> Result<JsValue, JsValue> {
1606 let input = AdaptiveSchaffTrendCycleInput::from_slices(
1607 high,
1608 low,
1609 close,
1610 AdaptiveSchaffTrendCycleParams {
1611 adaptive_length: Some(adaptive_length),
1612 stc_length: Some(stc_length),
1613 smoothing_factor: Some(smoothing_factor),
1614 fast_length: Some(fast_length),
1615 slow_length: Some(slow_length),
1616 },
1617 );
1618 let out = adaptive_schaff_trend_cycle(&input).map_err(|e| JsValue::from_str(&e.to_string()))?;
1619 serde_wasm_bindgen::to_value(&AdaptiveSchaffTrendCycleJsOutput {
1620 stc: out.stc,
1621 histogram: out.histogram,
1622 })
1623 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1624}
1625
1626#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1627#[wasm_bindgen]
1628pub fn adaptive_schaff_trend_cycle_into(
1629 high_ptr: *const f64,
1630 low_ptr: *const f64,
1631 close_ptr: *const f64,
1632 out_ptr: *mut f64,
1633 len: usize,
1634 adaptive_length: usize,
1635 stc_length: usize,
1636 smoothing_factor: f64,
1637 fast_length: usize,
1638 slow_length: usize,
1639) -> Result<(), JsValue> {
1640 if high_ptr.is_null() || low_ptr.is_null() || close_ptr.is_null() || out_ptr.is_null() {
1641 return Err(JsValue::from_str(
1642 "null pointer passed to adaptive_schaff_trend_cycle_into",
1643 ));
1644 }
1645
1646 unsafe {
1647 let high = std::slice::from_raw_parts(high_ptr, len);
1648 let low = std::slice::from_raw_parts(low_ptr, len);
1649 let close = std::slice::from_raw_parts(close_ptr, len);
1650 let out = std::slice::from_raw_parts_mut(out_ptr, len * 2);
1651 let (out_stc, out_histogram) = out.split_at_mut(len);
1652 let input = AdaptiveSchaffTrendCycleInput::from_slices(
1653 high,
1654 low,
1655 close,
1656 AdaptiveSchaffTrendCycleParams {
1657 adaptive_length: Some(adaptive_length),
1658 stc_length: Some(stc_length),
1659 smoothing_factor: Some(smoothing_factor),
1660 fast_length: Some(fast_length),
1661 slow_length: Some(slow_length),
1662 },
1663 );
1664 adaptive_schaff_trend_cycle_into_slice(out_stc, out_histogram, &input, Kernel::Auto)
1665 .map_err(|e| JsValue::from_str(&e.to_string()))
1666 }
1667}
1668
1669#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1670#[wasm_bindgen(js_name = "adaptive_schaff_trend_cycle_into_host")]
1671pub fn adaptive_schaff_trend_cycle_into_host(
1672 high: &[f64],
1673 low: &[f64],
1674 close: &[f64],
1675 out_ptr: *mut f64,
1676 adaptive_length: usize,
1677 stc_length: usize,
1678 smoothing_factor: f64,
1679 fast_length: usize,
1680 slow_length: usize,
1681) -> Result<(), JsValue> {
1682 if out_ptr.is_null() {
1683 return Err(JsValue::from_str(
1684 "null pointer passed to adaptive_schaff_trend_cycle_into_host",
1685 ));
1686 }
1687
1688 unsafe {
1689 let out = std::slice::from_raw_parts_mut(out_ptr, close.len() * 2);
1690 let (out_stc, out_histogram) = out.split_at_mut(close.len());
1691 let input = AdaptiveSchaffTrendCycleInput::from_slices(
1692 high,
1693 low,
1694 close,
1695 AdaptiveSchaffTrendCycleParams {
1696 adaptive_length: Some(adaptive_length),
1697 stc_length: Some(stc_length),
1698 smoothing_factor: Some(smoothing_factor),
1699 fast_length: Some(fast_length),
1700 slow_length: Some(slow_length),
1701 },
1702 );
1703 adaptive_schaff_trend_cycle_into_slice(out_stc, out_histogram, &input, Kernel::Auto)
1704 .map_err(|e| JsValue::from_str(&e.to_string()))
1705 }
1706}
1707
1708#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1709#[wasm_bindgen]
1710pub fn adaptive_schaff_trend_cycle_alloc(len: usize) -> *mut f64 {
1711 let mut buf = vec![0.0_f64; len * 2];
1712 let ptr = buf.as_mut_ptr();
1713 std::mem::forget(buf);
1714 ptr
1715}
1716
1717#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1718#[wasm_bindgen]
1719pub fn adaptive_schaff_trend_cycle_free(ptr: *mut f64, len: usize) {
1720 if ptr.is_null() {
1721 return;
1722 }
1723 unsafe {
1724 let _ = Vec::from_raw_parts(ptr, len * 2, len * 2);
1725 }
1726}
1727
1728#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1729#[wasm_bindgen(js_name = "adaptive_schaff_trend_cycle_batch")]
1730pub fn adaptive_schaff_trend_cycle_batch_js(
1731 high: &[f64],
1732 low: &[f64],
1733 close: &[f64],
1734 config: JsValue,
1735) -> Result<JsValue, JsValue> {
1736 let config: AdaptiveSchaffTrendCycleBatchConfig = serde_wasm_bindgen::from_value(config)
1737 .map_err(|e| JsValue::from_str(&format!("Invalid config: {e}")))?;
1738 if config.adaptive_length_range.len() != 3
1739 || config.stc_length_range.len() != 3
1740 || config.smoothing_factor_range.len() != 3
1741 || config.fast_length_range.len() != 3
1742 || config.slow_length_range.len() != 3
1743 {
1744 return Err(JsValue::from_str(
1745 "Invalid config: ranges must have exactly 3 elements [start, end, step]",
1746 ));
1747 }
1748
1749 let sweep = AdaptiveSchaffTrendCycleBatchRange {
1750 adaptive_length: (
1751 config.adaptive_length_range[0],
1752 config.adaptive_length_range[1],
1753 config.adaptive_length_range[2],
1754 ),
1755 stc_length: (
1756 config.stc_length_range[0],
1757 config.stc_length_range[1],
1758 config.stc_length_range[2],
1759 ),
1760 smoothing_factor: (
1761 config.smoothing_factor_range[0],
1762 config.smoothing_factor_range[1],
1763 config.smoothing_factor_range[2],
1764 ),
1765 fast_length: (
1766 config.fast_length_range[0],
1767 config.fast_length_range[1],
1768 config.fast_length_range[2],
1769 ),
1770 slow_length: (
1771 config.slow_length_range[0],
1772 config.slow_length_range[1],
1773 config.slow_length_range[2],
1774 ),
1775 };
1776 let batch = adaptive_schaff_trend_cycle_batch_slice(high, low, close, &sweep, Kernel::Scalar)
1777 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1778 serde_wasm_bindgen::to_value(&AdaptiveSchaffTrendCycleBatchJsOutput {
1779 stc: batch.stc,
1780 histogram: batch.histogram,
1781 rows: batch.rows,
1782 cols: batch.cols,
1783 combos: batch.combos,
1784 })
1785 .map_err(|e| JsValue::from_str(&format!("Serialization error: {e}")))
1786}
1787
1788#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
1789#[wasm_bindgen]
1790pub fn adaptive_schaff_trend_cycle_batch_into(
1791 high_ptr: *const f64,
1792 low_ptr: *const f64,
1793 close_ptr: *const f64,
1794 stc_ptr: *mut f64,
1795 histogram_ptr: *mut f64,
1796 len: usize,
1797 adaptive_length_start: usize,
1798 adaptive_length_end: usize,
1799 adaptive_length_step: usize,
1800 stc_length_start: usize,
1801 stc_length_end: usize,
1802 stc_length_step: usize,
1803 smoothing_factor_start: f64,
1804 smoothing_factor_end: f64,
1805 smoothing_factor_step: f64,
1806 fast_length_start: usize,
1807 fast_length_end: usize,
1808 fast_length_step: usize,
1809 slow_length_start: usize,
1810 slow_length_end: usize,
1811 slow_length_step: usize,
1812) -> Result<usize, JsValue> {
1813 if high_ptr.is_null()
1814 || low_ptr.is_null()
1815 || close_ptr.is_null()
1816 || stc_ptr.is_null()
1817 || histogram_ptr.is_null()
1818 {
1819 return Err(JsValue::from_str(
1820 "null pointer passed to adaptive_schaff_trend_cycle_batch_into",
1821 ));
1822 }
1823
1824 unsafe {
1825 let high = std::slice::from_raw_parts(high_ptr, len);
1826 let low = std::slice::from_raw_parts(low_ptr, len);
1827 let close = std::slice::from_raw_parts(close_ptr, len);
1828 let sweep = AdaptiveSchaffTrendCycleBatchRange {
1829 adaptive_length: (
1830 adaptive_length_start,
1831 adaptive_length_end,
1832 adaptive_length_step,
1833 ),
1834 stc_length: (stc_length_start, stc_length_end, stc_length_step),
1835 smoothing_factor: (
1836 smoothing_factor_start,
1837 smoothing_factor_end,
1838 smoothing_factor_step,
1839 ),
1840 fast_length: (fast_length_start, fast_length_end, fast_length_step),
1841 slow_length: (slow_length_start, slow_length_end, slow_length_step),
1842 };
1843 let combos = expand_grid_adaptive_schaff_trend_cycle(&sweep)
1844 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1845 let rows = combos.len();
1846 let total = rows
1847 .checked_mul(len)
1848 .ok_or_else(|| JsValue::from_str("rows*cols overflow"))?;
1849 let out_stc = std::slice::from_raw_parts_mut(stc_ptr, total);
1850 let out_histogram = std::slice::from_raw_parts_mut(histogram_ptr, total);
1851 adaptive_schaff_trend_cycle_batch_inner_into(
1852 high,
1853 low,
1854 close,
1855 &sweep,
1856 Kernel::Scalar,
1857 false,
1858 out_stc,
1859 out_histogram,
1860 )
1861 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1862 Ok(rows)
1863 }
1864}
1865
1866#[cfg(test)]
1867mod tests {
1868 use super::*;
1869 use crate::indicators::dispatch::{
1870 compute_cpu_batch, IndicatorBatchRequest, IndicatorDataRef, IndicatorParamSet, ParamKV,
1871 ParamValue,
1872 };
1873
1874 fn assert_close(a: &[f64], b: &[f64], tol: f64) {
1875 assert_eq!(a.len(), b.len());
1876 for (i, (&lhs, &rhs)) in a.iter().zip(b.iter()).enumerate() {
1877 if lhs.is_nan() || rhs.is_nan() {
1878 assert!(
1879 lhs.is_nan() && rhs.is_nan(),
1880 "nan mismatch at {i}: {lhs} vs {rhs}"
1881 );
1882 } else {
1883 assert!(
1884 (lhs - rhs).abs() <= tol,
1885 "mismatch at {i}: {lhs} vs {rhs} with tol {tol}"
1886 );
1887 }
1888 }
1889 }
1890
1891 fn sample_hlc(len: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
1892 let mut high = Vec::with_capacity(len);
1893 let mut low = Vec::with_capacity(len);
1894 let mut close = Vec::with_capacity(len);
1895 for i in 0..len {
1896 let base = 100.0 + i as f64 * 0.17 + (i as f64 * 0.031).sin() * 2.2;
1897 let spread = 1.1 + (i as f64 * 0.07).cos().abs() * 1.4;
1898 let c = base + (i as f64 * 0.11).sin() * 0.75;
1899 high.push(base + spread);
1900 low.push(base - spread);
1901 close.push(c);
1902 }
1903 (high, low, close)
1904 }
1905
1906 fn check_output_contract(kernel: Kernel) {
1907 let (high, low, close) = sample_hlc(320);
1908 let input = AdaptiveSchaffTrendCycleInput::from_slices(
1909 &high,
1910 &low,
1911 &close,
1912 AdaptiveSchaffTrendCycleParams::default(),
1913 );
1914 let out = adaptive_schaff_trend_cycle_with_kernel(&input, kernel).expect("indicator");
1915 assert_eq!(out.stc.len(), close.len());
1916 assert_eq!(out.histogram.len(), close.len());
1917 assert!(out.stc.iter().any(|v| v.is_finite()));
1918 assert!(out.histogram.iter().any(|v| v.is_finite()));
1919 }
1920
1921 fn check_into_matches_api(kernel: Kernel) {
1922 let (high, low, close) = sample_hlc(240);
1923 let input = AdaptiveSchaffTrendCycleInput::from_slices(
1924 &high,
1925 &low,
1926 &close,
1927 AdaptiveSchaffTrendCycleParams {
1928 adaptive_length: Some(40),
1929 stc_length: Some(10),
1930 smoothing_factor: Some(0.38),
1931 fast_length: Some(20),
1932 slow_length: Some(42),
1933 },
1934 );
1935 let baseline = adaptive_schaff_trend_cycle_with_kernel(&input, kernel).expect("baseline");
1936 let mut stc = vec![0.0; close.len()];
1937 let mut histogram = vec![0.0; close.len()];
1938 adaptive_schaff_trend_cycle_into_slice(&mut stc, &mut histogram, &input, kernel)
1939 .expect("into");
1940 assert_close(&baseline.stc, &stc, 1e-12);
1941 assert_close(&baseline.histogram, &histogram, 1e-12);
1942 }
1943
1944 fn check_stream_matches_batch() {
1945 let (high, low, close) = sample_hlc(260);
1946 let params = AdaptiveSchaffTrendCycleParams {
1947 adaptive_length: Some(34),
1948 stc_length: Some(9),
1949 smoothing_factor: Some(0.5),
1950 fast_length: Some(18),
1951 slow_length: Some(40),
1952 };
1953 let input = AdaptiveSchaffTrendCycleInput::from_slices(&high, &low, &close, params.clone());
1954 let batch = adaptive_schaff_trend_cycle(&input).expect("batch");
1955 let mut stream = AdaptiveSchaffTrendCycleStream::try_new(params).expect("stream");
1956 let mut stc = vec![f64::NAN; close.len()];
1957 let mut histogram = vec![f64::NAN; close.len()];
1958 for i in 0..close.len() {
1959 if let Some((s, h)) = stream.update(high[i], low[i], close[i]) {
1960 stc[i] = s;
1961 histogram[i] = h;
1962 }
1963 }
1964 assert_close(&batch.stc, &stc, 1e-12);
1965 assert_close(&batch.histogram, &histogram, 1e-12);
1966 }
1967
1968 fn check_batch_single_matches_single(kernel: Kernel) {
1969 let (high, low, close) = sample_hlc(180);
1970 let batch = adaptive_schaff_trend_cycle_batch_with_kernel(
1971 &high,
1972 &low,
1973 &close,
1974 &AdaptiveSchaffTrendCycleBatchRange {
1975 adaptive_length: (55, 55, 0),
1976 stc_length: (12, 12, 0),
1977 smoothing_factor: (0.45, 0.45, 0.0),
1978 fast_length: (26, 26, 0),
1979 slow_length: (50, 50, 0),
1980 },
1981 kernel,
1982 )
1983 .expect("batch");
1984 let single = adaptive_schaff_trend_cycle(&AdaptiveSchaffTrendCycleInput::from_slices(
1985 &high,
1986 &low,
1987 &close,
1988 AdaptiveSchaffTrendCycleParams::default(),
1989 ))
1990 .expect("single");
1991 assert_eq!(batch.rows, 1);
1992 assert_eq!(batch.cols, close.len());
1993 assert_close(&batch.stc[..close.len()], &single.stc, 1e-12);
1994 assert_close(&batch.histogram[..close.len()], &single.histogram, 1e-12);
1995 }
1996
1997 #[test]
1998 fn adaptive_schaff_trend_cycle_invalid_params() {
1999 let (high, low, close) = sample_hlc(64);
2000
2001 let err = adaptive_schaff_trend_cycle(&AdaptiveSchaffTrendCycleInput::from_slices(
2002 &high,
2003 &low,
2004 &close,
2005 AdaptiveSchaffTrendCycleParams {
2006 adaptive_length: Some(0),
2007 ..AdaptiveSchaffTrendCycleParams::default()
2008 },
2009 ))
2010 .expect_err("invalid adaptive length");
2011 assert!(matches!(
2012 err,
2013 AdaptiveSchaffTrendCycleError::InvalidAdaptiveLength { .. }
2014 ));
2015
2016 let err = adaptive_schaff_trend_cycle(&AdaptiveSchaffTrendCycleInput::from_slices(
2017 &high,
2018 &low,
2019 &close,
2020 AdaptiveSchaffTrendCycleParams {
2021 smoothing_factor: Some(0.0),
2022 ..AdaptiveSchaffTrendCycleParams::default()
2023 },
2024 ))
2025 .expect_err("invalid smoothing");
2026 assert!(matches!(
2027 err,
2028 AdaptiveSchaffTrendCycleError::InvalidSmoothingFactor { .. }
2029 ));
2030 }
2031
2032 #[test]
2033 fn adaptive_schaff_trend_cycle_output_contract() {
2034 check_output_contract(Kernel::Auto);
2035 check_output_contract(Kernel::Scalar);
2036 }
2037
2038 #[test]
2039 fn adaptive_schaff_trend_cycle_into_matches_api() {
2040 check_into_matches_api(Kernel::Auto);
2041 check_into_matches_api(Kernel::Scalar);
2042 }
2043
2044 #[test]
2045 fn adaptive_schaff_trend_cycle_stream_matches_batch() {
2046 check_stream_matches_batch();
2047 }
2048
2049 #[test]
2050 fn adaptive_schaff_trend_cycle_batch_single_matches_single() {
2051 check_batch_single_matches_single(Kernel::Auto);
2052 }
2053
2054 #[test]
2055 fn adaptive_schaff_trend_cycle_dispatch_matches_direct() {
2056 let (high, low, close) = sample_hlc(160);
2057 let combo = [
2058 ParamKV {
2059 key: "adaptive_length",
2060 value: ParamValue::Int(55),
2061 },
2062 ParamKV {
2063 key: "stc_length",
2064 value: ParamValue::Int(12),
2065 },
2066 ParamKV {
2067 key: "smoothing_factor",
2068 value: ParamValue::Float(0.45),
2069 },
2070 ParamKV {
2071 key: "fast_length",
2072 value: ParamValue::Int(26),
2073 },
2074 ParamKV {
2075 key: "slow_length",
2076 value: ParamValue::Int(50),
2077 },
2078 ];
2079 let combos = [IndicatorParamSet { params: &combo }];
2080 let req = IndicatorBatchRequest {
2081 indicator_id: "adaptive_schaff_trend_cycle",
2082 output_id: Some("stc"),
2083 data: IndicatorDataRef::Ohlc {
2084 open: &close,
2085 high: &high,
2086 low: &low,
2087 close: &close,
2088 },
2089 combos: &combos,
2090 kernel: Kernel::Auto,
2091 };
2092
2093 let batch = compute_cpu_batch(req).expect("dispatch");
2094 assert_eq!(batch.rows, 1);
2095 assert_eq!(batch.cols, close.len());
2096
2097 let direct = adaptive_schaff_trend_cycle(&AdaptiveSchaffTrendCycleInput::from_slices(
2098 &high,
2099 &low,
2100 &close,
2101 AdaptiveSchaffTrendCycleParams::default(),
2102 ))
2103 .expect("direct");
2104 let row = &batch.values_f64.as_ref().expect("f64 output")[0..close.len()];
2105 assert_close(row, &direct.stc, 1e-12);
2106 }
2107}