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